AUTH4D-71 console team screen and invitation acceptance

The developer console gains a Team section (/projects/{tenantId}/team) and the invitation link page /invitations/accept. Both use the AUTH4D-70 management routes (docs/api-spec/auth4d-70.md) through the console backend-for-frontend (BFF) in apps/console/worker/app.ts. The UI lives in apps/console/src/features/team.

BFF forwarding

/api/management/* now also forwards these routes to the API, with the session’s management token. Writes still require the console Origin.

Console path Methods
/api/management/tenants/{tenantId}/members GET
/api/management/tenants/{tenantId}/members/{membershipId} PATCH, DELETE
/api/management/tenants/{tenantId}/invitations GET, POST
/api/management/tenants/{tenantId}/invitations/{invitationId} DELETE
/api/management/invitations/accept POST {"token"}

The console sends DELETE requests with Content-Type: application/json and the body {}.

Accepting an invitation

The invitation email links to {CONSOLE_ORIGIN}/invitations/accept#token=<token>. The page reads the token from the fragment once, then replaces the address with /invitations/accept (history.replaceState), so the token is not left in the address bar or history. The token is kept only in page memory. It is never written to browser storage.

  • Signed in. The page shows the signed-in address. Accept invitation POSTs {"token"} to /api/management/invitations/accept. Use a different account signs out and keeps the token in memory, so the developer can sign in with the invited address.
  • Signed out, or the session expired. Sign in to accept is a top-level form POST to /api/auth/start with the form field invitation_token.

POST /api/auth/start (changed)

When the body is application/x-www-form-urlencoded (at most 1 KiB) and holds an invitation_token that matches ^[A-Za-z0-9_-]{43}$, the BFF stores the token in the encrypted, single-use pending login in the session Durable Object (PendingLogin.invitation_token). Any other body, a missing token and a malformed token are ignored, and sign-in proceeds as before. The token is never added to the authorize URL.

GET /api/auth/callback (changed)

After the session is established, a carried token is POSTed to /management/invitations/accept with the new access token. The callback then redirects to /invitations/accept?result=<code> instead of /. The code holds no secret:

API answer Redirect
200 /invitations/accept?result=accepted&tenant_id={tenantId}
400 invalid_invitation /invitations/accept?result=invalid
409 already_member /invitations/accept?result=already_member
429 /invitations/accept?result=rate_limited
anything else, network error /invitations/accept?result=failed

Sign-in succeeds whatever the invitation result. The page removes the query on load. An invitation that was not consumed (failed, rate_limited) can be retried by opening the email link again.

UI rules

The Team screen mirrors the AUTH4D-70 role rules to hide actions. The API re-checks every change.

Action Owner Admin Viewer
See members yes yes yes
See and revoke invitations yes, any role viewer invitations no
Invite admin or viewer viewer only no
Change a member’s role yes (admin or viewer) no no
Remove a member anyone viewers no
Leave the project yes yes yes

The screen derives the developer’s role from their own row in GET /members. Lists show pending and expired invitations; accepted and revoked ones are hidden.

Error messages

Code Message shown
last_owner A project must keep at least one owner…
membership_changed The role changed while you were editing; the list is refreshed
insufficient_role Your role doesn’t allow this change; the list is refreshed
already_member Shown on the email field of the invite dialog
invitation_pending Shown on the email field: revoke the pending invitation first
invitation_accepted The invitation has already been accepted
email_unavailable Invitation emails can’t be sent right now; no invitation was created
project_suspended The project is suspended
rate_limited Wait a while and try again
invalid_invitation The invitation may have expired, been revoked or used, or was sent to another address; the signed-in address is shown

Tests

  • pnpm exec playwright test -c tests/e2e/auth4d-71.playwright.config.ts (port 5211) covers inviting by keyboard, invite validation and API errors, revoking, a role change, the last-owner error, role-based hiding for admins and viewers, the 360px layout, and acceptance signed in, signed out (through sign-in), invalid or mismatched, already a member and without a token.
  • pnpm test runs tests/integration/auth4d-71.console-invitations.test.ts. It covers the BFF route allowlist, carrying the token through sign-in, the callback result codes, and ignoring malformed tokens.

Source: docs/api-spec/auth4d-71.md