import type { Request } from "express"; import { ApiError } from "./errors.js"; /** * Authentication context attached to `req.auth` by the global AuthGuard. * `kind: "user"` — JWT bearer (user session). * `kind: "api"` — fk_ API key (workspace-scoped, no user identity). */ export interface AuthContext { kind: "user" | "api"; userId?: string; workspaceId?: string; /** Workspace ids the user belongs to (JWT snapshot). */ workspaces?: string[]; /** Session (refresh chain) id, user kind only. */ sessionId?: string; /** API key kind: scopes + key id. */ scopes?: string[]; apiKeyId?: string; /** API key kind: per-key rate limit (req/min) — wins over the env default (docs §11). */ rateLimit?: number | null; /** * TASK-0129: set when the bearer is an OAuth access token * (`type: "oauth"`). The token keeps `kind: "user"` (it carries the * user's identity — `userId` is the user) but is gated by its SCOPE, not * the user's membership: `lib/authorize.ts` branches on `oauthScopes` * present. The target workspace comes from `X-Factory-Workspace` * (docs §8.2), validated against the user's memberships + the scope. */ oauthScopes?: string[]; /** TASK-0129: the client_id of the OAuth app that minted the token. */ oauthClientId?: string; } declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Express { interface Request { id?: string; auth?: AuthContext; /** Exact request bytes (Nest `rawBody: true` / csv-import-body.ts). */ rawBody?: Buffer; } } } /** Require a user (not an API key) — throws FORBIDDEN for keys. * For the USER-IDENTITY-only endpoints (docs/api/authentication.md §7: * `PATCH /users/me`, invitations as a user, personal sessions/ * notifications/orgs/AI conversations) — TASK-0127. */ export function requireUser(req: Request): string { const auth = req.auth; if (!auth || auth.kind !== "user" || !auth.userId) { throw ApiError.forbidden("This endpoint requires user credentials (JWT), not an API key.", [ { reason: "requires_user_credentials" } ]); } return auth.userId; } /** * Require an authenticated actor and return the USER id to attribute the * operation to (TASK-0127): * - a user JWT → the user's id; * - an API key → the key's CREATOR's id (the audit trail still records * `actorType: "API_KEY"` via the auth context; `createdBy`-style columns * need a real user row). * * Scope/permission decisions are NOT made here — they are made at the * service's `assertCan`/`assertRole` call, where the actor context applies * the API-key scope rules (workspace binding + READ/WRITE/ADMIN → level) * INDEPENDENTLY of the creator's membership. A READ key on a create route * gets 403 `api_key_scope_write` from the service, never from this helper. */ export function requireActor(req: Request): string { const auth = req.auth; if (!auth) throw ApiError.unauthorized("Authentication required.", [{ reason: "token_missing" }]); if (auth.kind === "api") { // The AuthGuard resolved the key's creator (`userId`) on lookup. if (!auth.userId) { throw ApiError.unauthorized("API key has no creator user.", [{ reason: "api_key_invalid" }]); } return auth.userId; } if (!auth.userId) throw ApiError.unauthorized("Token has no subject.", [{ reason: "token_malformed" }]); return auth.userId; }