WorldForge Studio

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/displayName refreshed later).
  • 401 { error: 'unauthorized' } when the value does not resolve to a forge-auth session. A body that fails validation is the validator's standard 400.
  • The JWT's sub is the forge-auth user id. Its lifetime is set in auth/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

MethodPathNotes
GET/api-keysList the caller's keys (metadata, never the secret)
POST/api-keysMint a key
DELETE/api-keys/:idRevoke

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
}
  • scopes must 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.