import { pathToFileURL } from "node:url"; import { buildRouter, type Router } from "@wrnexus/router"; import { emptyCatalog, mergeCatalogs, type AuthzCatalog, type AuthzModule, type CatalogSource, } from "@wrnexus/authz"; /** Imports one declaration module. Defaults to a raw `import()`; the hot-reload * call site passes `loadModule` instead (see the note below on why). */ export type AuthzImporter = (file: string) => Promise<{ default?: AuthzModule }>; const rawImport: AuthzImporter = (file) => import(pathToFileURL(file).href) as Promise<{ default?: AuthzModule }>; /** * Load and merge every `app/authz/*.ts` declaration. Conflicts throw so a * misconfigured catalog fails the boot rather than silently changing who can * do what. An app with no `app/authz/` directory gets an empty catalog rather * than an error, since not every app uses permissions. * * Accepts either an app directory — the original, standalone shape, still * used by the test suite and by any caller without a router on hand — or an * already-built `Router`. `startServer` passes its own router (built once at * `:315` with the full `componentDirs`/`externalRoutes`/`middlewareFiles` * options) to avoid a second, redundant filesystem scan of the whole `app/` * tree on every dev boot and on every hot reload of an `app/authz/*.ts` file. * * `importModule` defaults to a raw dynamic `import()`, correct for the * initial boot. On a HOT reload, the caller must instead pass `loadModule` * (from `./pipeline.ts`): Bun caches local TS/JS modules by filesystem path * and ignores query strings, so re-`import()`-ing the same absolute path * after an edit silently returns the stale, already-cached module — * `loadModule` is what copies an edited file to a versioned sibling path * specifically to defeat that cache. */ export async function loadAppAuthzCatalog( appDirOrRouter: string | Router, importModule: AuthzImporter = rawImport, ): Promise { const router = typeof appDirOrRouter === "string" ? buildRouter(appDirOrRouter) : appDirOrRouter; if (!router.authz.length) return emptyCatalog(); const sources: CatalogSource[] = []; for (const entry of router.authz) { // buildRouter already skips *.gen.ts, so only real declarations arrive here. // A file that throws on import is intentionally NOT caught here: it is the // same failure class as a genuine conflict (a broken/misconfigured catalog), // and letting it propagate fails the boot loudly instead of silently // producing a partial catalog. Do not "helpfully" wrap this in a try/catch. const imported = await importModule(entry.file); if (!imported.default) { console.warn(`[wrnexus] authz declaration ${entry.file} has no default export; skipping.`); continue; } sources.push({ source: entry.file, module: imported.default }); } return mergeCatalogs(sources); }