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/startwith the form fieldinvitation_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 testrunstests/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