import { AsyncLocalStorage } from "node:async_hooks"; import type { AuthContext } from "./types.js"; /** * Actor context (TASK-0127 Phase 13) — the authenticated actor of the * in-flight request, visible to the service layer WITHOUT threading a new * parameter through every controller → service → assertCan call (81+ * call sites; keeping them untouched is the backward-compat win). * * The global `ActorContextInterceptor` runs every request's handler inside * `ActorContext.run(req.auth, …)`. Authorization helpers (`assertCan`, * `assertRole` in `lib/authorize.ts`) consult the context: * * - `kind: "api"` — the request is an API key. The service's permission * check becomes: (1) the asserted workspace is the key's own workspace, * (2) the key's scopes cover the operation (VIEW → READ, anything else → * WRITE). The resolved `userId` (the key's creator, via `requireUser`) * is NEVER consulted for the membership decision — an API key acts as * the workspace per docs/api/authentication.md §7, and its creator's * role in another workspace must not leak access here. * - `kind: "user"` / absent — the legacy membership path, unchanged. * * Absent context (workers, tests calling services directly) behaves * exactly as before — the ALS is a pure capability addition. */ const storage = new AsyncLocalStorage(); export const ActorContext = { /** Run `fn` with `auth` as the in-flight actor. */ run(auth: AuthContext | undefined, fn: () => T): T { return storage.run(auth, fn); }, /** The in-flight actor (undefined outside an HTTP request). */ get(): AuthContext | undefined { return storage.getStore(); } };