/** * Per-request context propagated via AsyncLocalStorage. * * Every API request carries an immutable {requestId, portId, userId, * method, path} bag that is available from anywhere in the call stack * without threading it through every function signature. This is what * lets `logger.info(...)` deep inside a service call automatically * stamp the originating request id into the log line, and what lets * `errorResponse` know which request to attach to a persisted * `error_events` row. * * Why ALS over an explicit context arg: 80% of the codebase is already * written; threading `RequestContext` through every helper would touch * hundreds of files and break domain isolation. ALS is the standard * Node-side pattern (Express + Pino + many production services use * the exact same approach). * * Wiring: * - The `withAuth` wrapper in `src/lib/api/helpers.ts` calls * `runWithRequestContext({...}, () => handler(...))` so every code * path inside the request runs inside the ALS frame. * - The pino logger in `src/lib/logger.ts` mixes `getRequestContext()` * into every emitted log line via the `mixin` hook. * - `errorResponse(err)` reads the same context to build the user- * facing error envelope and to persist a row to `error_events`. */ import { AsyncLocalStorage } from 'node:async_hooks'; export interface RequestContext { /** UUID — surfaces in `X-Request-Id` response header + every log line. */ requestId: string; /** Active port for this request (empty string for super-admin pre-port). */ portId: string; /** better-auth user id (empty string for unauthenticated paths). */ userId: string; /** HTTP method — recorded for error_events triage. */ method: string; /** Pathname (no query string) — recorded for error_events triage. */ path: string; /** Wall-clock ms timestamp at request entry. Used for duration metrics. */ startedAt: number; } const store = new AsyncLocalStorage(); /** * Run `fn` inside a request-context frame. Every call within the * resulting callstack — including async work, queue callbacks, and * service-layer DB queries — sees the same context via * `getRequestContext()`. */ export function runWithRequestContext(ctx: RequestContext, fn: () => T): T { return store.run(ctx, fn); } /** * Read the current request context, or `null` when called outside a * request (e.g. queue worker, scheduled job). Callers must handle the * null case — the logger mixin does so gracefully. */ export function getRequestContext(): RequestContext | null { return store.getStore() ?? null; } /** Convenience accessor for the most common field. */ export function getRequestId(): string | null { return store.getStore()?.requestId ?? null; }