Files
WRNexusJS/packages/authz/src/client.ts
T
Clintchiz 37439188ca
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
fix: share authz catalog across package instances
2026-08-22 23:30:01 +05:30

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;
}