00 — Authentication & API keys
Session exchange (/auth) and scoped API-key management (/api-keys).
Both actor kinds send Authorization: Bearer <token>; the server tells
them apart by shape (ADR-0005): a JWT (3 dot-segments) is a user, a
wk_live_<id>.<secret> string is an agent (API key). See the SDK
guide docs/sdk/01-authentication for
the consumer-facing view.
POST /auth/session
Exchange a forge_session cookie value for a short-lived user JWT.
Public — the route has no requireAuth; the cookie value in the body is
the credential.
{ "forge_session": "…" } // required, non-empty string
→ 200 { "user": { "id", "email", "displayName" }, "jwt": "…" }
- The session is validated with forge-auth; the user row is upserted
(created on first login,
email/displayNamerefreshed later). 401 { error: 'unauthorized' }when the value does not resolve to a forge-auth session. A body that fails validation is the validator's standard400.- The JWT's
subis the forge-auth user id. Its lifetime is set inauth/jwt.ts(not re-read here).
The SDK exposes this as exchangeSession().
POST /auth/key
Desktop sign-in: exchange a forge-auth device-flow API key (fk_live_…)
for the same { user, jwt } response. Body { "key": string } (1–512
chars). The key is verified against forge-auth (FORGE_AUTH_URL) for each
app id in FORGE_AUTH_KEY_APP_IDS in order; if none accepts it →
401 { error: 'unauthorized' }. The client keeps the key and re-exchanges
it when the JWT expires, which is also when a revoked key stops working.
Also public (no requireAuth).
/api-keys
| Method | Path | Notes |
|---|---|---|
GET | /api-keys | List the caller's keys (metadata, never the secret) |
POST | /api-keys | Mint a key |
DELETE | /api-keys/:id | Revoke |
User session required — API keys cannot mint or revoke keys
(the route refuses non-user actors by design).
POST body:
{
"name": "my-integration",
"scopes": ["world:read", "world:write"],
"expires_at": "2026-12-20T00:00:00Z" // optional
}
scopesmust come from the catalogue (auth/api-key-scopes.ts);*is never valid for an API key.- Expiry defaults to 90 days; the maximum is 365 days.
- The response contains the secret once (
wk_live_…); only an Argon2id hash is stored.
Scope enforcement
apiKeyScopeGate (mounted first in server.ts) matches the request
path against a rule table and rejects with
403 { error: 'forbidden', missing: ['scope'] }. JWTs and anonymous
health probes skip the gate. Route groups that need specific scopes are
called out on their own pages.