/** * A process-wide authorization catalog registry, mirroring `@wrnexus/db`'s * `client.ts` (`setDb`/`getDb`/`hasDb`). It exists for the same reason: app * middleware runs at module-eval time — `app/middleware/*.ts` registers * `authzMiddleware({ catalog, store, ... })` itself, an EAGER call (the same * shape as `logger.ts`'s `export default requestLogger({...})`), and it needs * the merged catalog *then*, before its own module body finishes running. * Passing it through `ctx` does not work at that point, so the framework * loads and merges every `app/authz/*.ts` declaration and stashes it here * before any other module can observe it: * * - dev: `startServer` calls `loadAppAuthzCatalog` + `setAuthzCatalog` * before middleware is resolved. * - prod (the normal `wrnexus build` output): the generated entry statically * imports a small `.authz-setup.ts` module FIRST — before any page, API, * or middleware import — which calls `setAuthzCatalog` at ITS OWN module * scope. ES modules evaluate every static import before the importing * module's body runs, and evaluate sibling imports in declaration order, * so import position is evaluation order: this guarantees the catalog * exists before app middleware's own module body (which may read it * eagerly) ever evaluates. `createProductionHandlers` (`prod.ts`) then * repeats the merge as an idempotent second pass, mainly so a caller who * bypasses the generated entry and invokes it directly still gets a * catalog — for THAT path specifically, an eager module-scope read in * middleware is only safe if the caller sets the catalog before importing * the middleware itself, since no generated `.authz-setup.ts` runs first. * * The framework never installs `authzMiddleware` itself — the app always * chooses its own store and registers the middleware; this registry only * makes the merged catalog reachable when it does. */ import type { AuthzCatalog } from "./types.ts"; interface AuthzCatalogState { catalog?: AuthzCatalog; } const catalogKey = Symbol.for("@wrnexus/authz:catalog:v1"); function state(): AuthzCatalogState { const global = globalThis as Record; return (global[catalogKey] ??= {}) as AuthzCatalogState; } /** Set the process-wide authorization catalog (called by the framework at boot). */ export function setAuthzCatalog(next: AuthzCatalog): AuthzCatalog { state().catalog = next; return next; } /** The process-wide authorization catalog. Throws if it hasn't been set. */ export function getAuthzCatalog(): AuthzCatalog { const catalog = state().catalog; if (!catalog) { throw new Error( "WRN-AUTHZ-SETUP: no authorization catalog is configured. The dev server and " + "`wrnexus build`'s generated production entry both call setAuthzCatalog() before " + "any other module — including your app's middleware — evaluates. If you're seeing " + "this: (a) you're on a custom production entry that calls createProductionHandlers " + "directly instead of the generated one, so you must call setAuthzCatalog(catalog) " + "yourself before importing anything that reads it eagerly; or (b) you're outside " + "the normal boot path entirely (a standalone script or test) and must call " + "setAuthzCatalog(catalog) first.", ); } return catalog; } /** Whether the process-wide authorization catalog has been set. */ export function hasAuthzCatalog(): boolean { return state().catalog !== undefined; }