Skip to content

Add API keys and an in-app API reference - #1412

Merged
daveearley merged 1 commit into
developfrom
feature/api-keys
Oct 17, 2026
Merged

daveearley merged 1 commit into
developfrom
feature/api-keys

Conversation

@daveearley

@daveearley daveearley commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

API keys

Organizers can create API keys under Settings → API keys to connect Zapier, CRMs and their own scripts.

  • Model: Sanctum personal access tokens owned by a user and scoped to one account (personal_access_tokens.account_id). A key acts as that member, with their role, grants and permission overrides, and can never do more. Changes to the member's role apply straight away.
  • Access levels: Read and write, or Read-only. Read-only keys can call GET endpoints and exports; other requests get 403 API_KEY_READ_ONLY.
  • Route split: signed-in routes are now auth:api,sanctum (keys allowed) or a session-only auth:api group. The session-only group covers auth, profile and sign-in security, team, account settings, payouts (Stripe Connect, terminal readers, VAT) and key management; keys get 403 API_KEY_NOT_ALLOWED there. ApiKeyRouteExposureTest snapshots that group and fails if any MANAGE_TEAM, MANAGE_ACCOUNT or MANAGE_PAYMENTS action becomes reachable with a key.
  • Auth context: every JWT payload read (account id, impersonation, jti) now goes through AuthUserService, so it works under both guards.
  • Revocation: a key stops working when its owner is deactivated or leaves the account, after a password reset or SSO change (scoped to the affected account), and when the account is deleted. The account's 2FA requirement and pending deletion also block keys.
  • Creating a key: needs a verified email and identity confirmation (password, or SSO re-auth for password-less users), is refused while impersonating, and is capped at 25 active keys per member. Only the SHA-256 hash and the last four characters are stored.
  • Hardening: keys can't add Google Tag Manager containers, and last_used_at is written at most every 5 minutes.
  • Emails: sent on create, when an admin revokes a member's key, and 7 days before expiry (scheduled job). Expired keys are pruned after 30 days.
  • UI: the create modal sets the access level, an expiry from 7 days to 1 year or none, and the identity check. The reveal step shows the key once, with a copy fallback if the browser blocks the clipboard and a warning before closing uncopied. Admins can switch to everyone's keys and revoke them. Expired keys are grouped separately. Works from phone to desktop.

In-app API reference

Settings → API keys → API reference renders the OpenAPI spec inside the app with Scalar, themed to match Hi.Events. Scalar's AI agent, MCP and telemetry are switched off, and it's loaded client-only in its own lazy chunk, so the main bundle doesn't change.

  • Committed spec: the spec is generated and committed at backend/resources/api-reference/openapi.json, and served from a session-only GET /api-reference. Generating it at request time takes about 4s and roughly 214MB, so that was ruled out.
  • Drift check: php artisan api-reference:export regenerates the file, and IntegrationApiReferenceTest fails when it's out of date. Every change to the API surface therefore shows up as a diff in review.
  • What's left out: session-only operations, /auth/* and the licensing endpoints (licensing is now excluded from /docs/api too).
  • Docs intro: covers API keys, an error-code table, rate limits and a backward-compatibility promise for endpoints keys can call. Tags now read naturally ("Capacity Assignments" rather than "CapacityAssignments"). AGENTS.md gains a Public API compatibility section warning about breaking changes.

SSO follow-ups

These were finished after #1411 merged:

  • Domain capture: while a connection enforces SSO, nobody can sign up with an email on its domains. They're sent to SSO sign-in instead.
  • password_reset_allowed: exposed on /users/me, so Security settings stops offering a reset link that would never arrive.
  • 2FA status: required_by_accounts lists only accounts that require 2FA, as it does on develop. The new required_by_sso_accounts lists accounts you own that enforce SSO.

Testing

  • Backend: Unit and Feature suites pass (Feature: 1,183). Pint is clean on all changed files, and scramble:analyze is clean.
  • E2E: all of tests/account and tests/auth pass against the dev stack: 51 passed, 6 SSO specs skipped because no mock provider was set. That includes 12 API key specs covering:
    • create, copy, use and revoke
    • read-only access and a wrong password
    • closing without copying, and a blocked clipboard
    • an admin revoking a member's key, a viewer's options, expired keys and the phone layout
    • SSO re-auth with the form draft restored
    • the API reference page
  • SSO re-auth spec: passes locally with the mock IdP configured.
  • Registration spec: the page object now waits for hydration. It was failing about half the time before; it passed 10 of 10 runs after.
  • Production build: builds cleanly. The main bundle is unchanged and Scalar sits in its own lazy chunk.

API keys
- Sanctum personal access tokens owned by a user and scoped to one
  account. A key acts as that member (role, grants, permission
  overrides) and never more. Read-only keys carry only the `read`
  ability and get 403 API_KEY_READ_ONLY on writes.
- Signed-in routes are split into `auth:api,sanctum` (keys allowed) and
  a session-only `auth:api` group: auth, profile and sign-in security,
  team, account settings, payouts and key management, which return 403
  API_KEY_NOT_ALLOWED to keys. ApiKeyRouteExposureTest snapshots it.
- JWT claim reads go through AuthUserService so they work under both
  guards; the access cache keys key requests as api-key:{id}.
- Keys work only while the owner is an ACTIVE member. Deactivation,
  password resets and SSO changes revoke them (scoped to the affected
  account), and account deletion removes them. The account's 2FA
  requirement and pending deletion still apply.
- Creating a key needs a verified email and identity confirmation
  (password or SSO re-auth), is refused while impersonating, and is
  capped at 25 active keys. Only the hash and last four characters
  are stored. Keys can't add Google Tag Manager containers, and
  last_used_at is written at most every five minutes.
- Emails when a key is created, when an admin revokes it, and a week
  before it expires. Expired keys are pruned after 30 days.
- Settings > API keys: create with access level and expiry, copy once
  with a copy-failure fallback, admins see and revoke everyone's keys,
  expired keys grouped. Works from phone to desktop.

In-app API reference
- Settings > API keys > API reference renders a committed, generated
  resources/api-reference/openapi.json with Scalar, branded to match
  the app, with its AI agent, MCP and telemetry switched off.
- `php artisan api-reference:export` regenerates it; a Feature test
  fails when it drifts, so API changes show up in PR diffs. It leaves
  out session-only operations, /auth/* and licensing endpoints.
- The docs intro covers API keys, an error-code table, rate limits and
  backward compatibility. AGENTS.md gains a "Public API compatibility"
  section.

SSO follow-ups
- While a connection enforces SSO, nobody can sign up with an email on
  its domains; they're sent to SSO sign-in instead.
- /users/me exposes password_reset_allowed, and the 2FA status keeps
  "required by the account" apart from "the account enforces SSO".

Also: the registration e2e page object waits for hydration before
filling the form, which fixes an intermittent failure.
@railway-app
railway-app Bot temporarily deployed to Hi.Events / Hi.Events-pr-1412 October 17, 2026 20:43 Destroyed
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ E2E smoke tests: Everything passed, but 1 test only passed on retry.

Mode Result ✅ Passed ❌ Failed ⚠️ Flaky ⏭️ Skipped ⏱️ Duration
Self-hosted ⚠️ Passed (some flaky) 106 0 1 0 3m 28s

⚠️ Flaky tests: failed first, then passed on retry (1)

  • Self-hosted · organizer/organizer-management.spec.ts:7 · organizer management › an organizer creates a second organizer from the global menu

Open the run

@daveearley
daveearley merged commit 1a453e0 into develop Oct 17, 2026
11 of 12 checks passed
@daveearley
daveearley deleted the feature/api-keys branch October 17, 2026 21:03
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 17, 2026

This branch was successfully deployed

No deployments
Hi.Events / Hi.Events-pr-1412 — ce4d5129 Deployed Oct 17, 2026 by railway-app[bot]
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant