1import type { Request } from "express"; 2import { ApiError } from "./errors.js"; 3 4/** 5 * Authentication context attached to `req.auth` by the global AuthGuard. 6 * `kind: "user"` — JWT bearer (user session). 7 * `kind: "api"` — fk_ API key (workspace-scoped, no user identity). 8 */ 9export interface AuthContext { 10 kind: "user" | "api"; 11 userId?: string; 12 workspaceId?: string; 13 /** Workspace ids the user belongs to (JWT snapshot). */ 14 workspaces?: string[]; 15 /** Session (refresh chain) id, user kind only. */ 16 sessionId?: string; 17 /** API key kind: scopes + key id. */ 18 scopes?: string[]; 19 apiKeyId?: string; 20 /** API key kind: per-key rate limit (req/min) — wins over the env default (docs §11). */ 21 rateLimit?: number | null; 22 /** 23 * TASK-0129: set when the bearer is an OAuth access token 24 * (`type: "oauth"`). The token keeps `kind: "user"` (it carries the 25 * user's identity — `userId` is the user) but is gated by its SCOPE, not 26 * the user's membership: `lib/authorize.ts` branches on `oauthScopes` 27 * present. The target workspace comes from `X-Factory-Workspace` 28 * (docs §8.2), validated against the user's memberships + the scope. 29 */ 30 oauthScopes?: string[]; 31 /** TASK-0129: the client_id of the OAuth app that minted the token. */ 32 oauthClientId?: string; 33} 34 35declare global { 36 // eslint-disable-next-line @typescript-eslint/no-namespace 37 namespace Express { 38 interface Request { 39 id?: string; 40 auth?: AuthContext; 41 /** Exact request bytes (Nest `rawBody: true` / csv-import-body.ts). */ 42 rawBody?: Buffer; 43 } 44 } 45} 46 47/** Require a user (not an API key) — throws FORBIDDEN for keys. 48 * For the USER-IDENTITY-only endpoints (docs/api/authentication.md §7: 49 * `PATCH /users/me`, invitations as a user, personal sessions/ 50 * notifications/orgs/AI conversations) — TASK-0127. */ 51export function requireUser(req: Request): string { 52 const auth = req.auth; 53 if (!auth || auth.kind !== "user" || !auth.userId) { 54 throw ApiError.forbidden("This endpoint requires user credentials (JWT), not an API key.", [ 55 { reason: "requires_user_credentials" } 56 ]); 57 } 58 return auth.userId; 59} 60 61/** 62 * Require an authenticated actor and return the USER id to attribute the 63 * operation to (TASK-0127): 64 * - a user JWT → the user's id; 65 * - an API key → the key's CREATOR's id (the audit trail still records 66 * `actorType: "API_KEY"` via the auth context; `createdBy`-style columns 67 * need a real user row). 68 * 69 * Scope/permission decisions are NOT made here — they are made at the 70 * service's `assertCan`/`assertRole` call, where the actor context applies 71 * the API-key scope rules (workspace binding + READ/WRITE/ADMIN → level) 72 * INDEPENDENTLY of the creator's membership. A READ key on a create route 73 * gets 403 `api_key_scope_write` from the service, never from this helper. 74 */ 75export function requireActor(req: Request): string { 76 const auth = req.auth; 77 if (!auth) throw ApiError.unauthorized("Authentication required.", [{ reason: "token_missing" }]); 78 if (auth.kind === "api") { 79 // The AuthGuard resolved the key's creator (`userId`) on lookup. 80 if (!auth.userId) { 81 throw ApiError.unauthorized("API key has no creator user.", [{ reason: "api_key_invalid" }]); 82 } 83 return auth.userId; 84 } 85 if (!auth.userId) throw ApiError.unauthorized("Token has no subject.", [{ reason: "token_malformed" }]); 86 return auth.userId; 87} 88