74 lines
3.5 KiB
TypeScript
74 lines
3.5 KiB
TypeScript
/**
|
|
* 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<PropertyKey, unknown>;
|
|
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;
|
|
}
|