fix(dev-server): don't clobber a caller-set authz catalog; drop dead RuntimeDeps.authz
createProductionHandlers called setAuthzCatalog unconditionally, so a caller using client.ts's documented escape hatch (setAuthzCatalog(catalog) before importing anything that reads it) had that catalog silently wiped to empty whenever opts.authz was omitted. Now only sets when opts.authz has entries to contribute, or when nothing has been set yet; a non-empty opts.authz still always sets and still throws on a genuine conflict. Also removes RuntimeDeps.authz: nothing read it, and its doc comment described a consumer that doesn't exist. The real wiring is getAuthzCatalog()/setAuthzCatalog(), including the HMR hot-update path, which is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -472,7 +472,6 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
security: opts.security,
|
||||
observability: opts.observability,
|
||||
tenancy: opts.tenancy,
|
||||
authz: authzCatalog,
|
||||
navigation: opts.navigation,
|
||||
clientRuntimes: pluginContributions.clientRuntimes,
|
||||
hub,
|
||||
@@ -636,7 +635,6 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
(file) => loadModule(file) as Promise<{ default?: AuthzModule }>,
|
||||
);
|
||||
setAuthzCatalog(nextAuthzCatalog);
|
||||
runtimeDeps.authz = nextAuthzCatalog;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[wrnexus] authz hot update failed — the PREVIOUS catalog remains authoritative " +
|
||||
|
||||
@@ -38,7 +38,7 @@ import { VALIDATE_RUNTIME } from "@wrnexus/validation";
|
||||
import { I18N_RUNTIME, type ResolvedI18n } from "@wrnexus/i18n";
|
||||
import { setDb, registerLazyDb, getDb, hasDb, migrate } from "@wrnexus/db";
|
||||
import { connectFromConfig } from "@wrnexus/db/connect";
|
||||
import { mergeCatalogs, setAuthzCatalog, type AuthzModule } from "@wrnexus/authz";
|
||||
import { hasAuthzCatalog, mergeCatalogs, setAuthzCatalog, type AuthzModule } from "@wrnexus/authz";
|
||||
import {
|
||||
configureStorage,
|
||||
serveStoredFile,
|
||||
@@ -416,17 +416,25 @@ export function createProductionHandlers(
|
||||
// function was ever called, specifically so a middleware module that reads
|
||||
// getAuthzCatalog() at its own module scope sees a populated catalog — this
|
||||
// function's body runs too late for that (it is reached only once every
|
||||
// OTHER static import, including middleware, has already evaluated). This
|
||||
// pass still runs unconditionally (not skipped when the catalog is already
|
||||
// set) so a direct caller that bypasses the generated entry — and thus
|
||||
// never ran that early pass — still gets a correctly merged, validated
|
||||
// catalog, and so this function's own authorization handling stays fully
|
||||
// testable in isolation.
|
||||
// OTHER static import, including middleware, has already evaluated).
|
||||
//
|
||||
// The merge+validation of opts.authz always runs (a genuine conflict must
|
||||
// still fail the boot loudly, no matter which pass discovers it). But
|
||||
// setAuthzCatalog is only called when this pass actually has something to
|
||||
// contribute, OR when nothing has been set yet: client.ts documents an
|
||||
// escape hatch where a direct caller of createProductionHandlers may call
|
||||
// setAuthzCatalog(catalog) itself before importing anything that reads it,
|
||||
// specifically for a custom entry that never ran the generated
|
||||
// `.authz-setup.ts` pass. Calling setAuthzCatalog unconditionally here would
|
||||
// clobber that caller's catalog with an empty one whenever opts.authz is
|
||||
// omitted — silently deleting every permission the app declared.
|
||||
for (const missing of (opts.authz ?? []).filter((entry) => !entry.module)) {
|
||||
console.warn(`[wrnexus] authz declaration ${missing.source} has no default export; skipping.`);
|
||||
}
|
||||
const authzCatalog = mergeCatalogs(resolveAuthzSources(opts.authz ?? []));
|
||||
setAuthzCatalog(authzCatalog);
|
||||
const mergedAuthzCatalog = mergeCatalogs(resolveAuthzSources(opts.authz ?? []));
|
||||
if ((opts.authz?.length ?? 0) > 0 || !hasAuthzCatalog()) {
|
||||
setAuthzCatalog(mergedAuthzCatalog);
|
||||
}
|
||||
|
||||
// Middleware is already an ordered array of functions.
|
||||
const getMiddleware = async (): Promise<Middleware[]> => manifest.middleware;
|
||||
@@ -463,7 +471,6 @@ export function createProductionHandlers(
|
||||
security: opts.security,
|
||||
observability: opts.observability,
|
||||
tenancy: opts.tenancy,
|
||||
authz: authzCatalog,
|
||||
navigation: opts.navigation,
|
||||
maxBodyBytes: opts.maxBodyBytes,
|
||||
realtimeBus: realtimeBusFromConfig(opts.realtime),
|
||||
|
||||
@@ -63,7 +63,6 @@ import {
|
||||
requestStoreContainer,
|
||||
} from "@wrnexus/ssr/store-context";
|
||||
import type { StoreDefinition } from "@wrnexus/store";
|
||||
import type { AuthzCatalog } from "@wrnexus/authz";
|
||||
import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
|
||||
import { CacheCoordinator } from "@wrnexus/cache";
|
||||
import { generateServiceWorker } from "@wrnexus/pwa";
|
||||
@@ -186,17 +185,6 @@ export interface RuntimeDeps {
|
||||
health?: HealthRegistry;
|
||||
/** Built-in tenant identity resolution. */
|
||||
tenancy?: TenancyConfig;
|
||||
/**
|
||||
* Process-wide authorization catalog, merged from `app/authz/*.ts` at boot
|
||||
* (dev: `loadAppAuthzCatalog`; prod: `mergeCatalogs` over the build's static
|
||||
* imports). Also reachable via `@wrnexus/authz`'s `getAuthzCatalog()`
|
||||
* singleton, which is what the app's own `authzMiddleware` registration
|
||||
* actually reads — this field exists so the request pipeline can see the
|
||||
* catalog without importing that singleton directly. The framework never
|
||||
* installs `authzMiddleware` itself; the app always registers it with its
|
||||
* own store.
|
||||
*/
|
||||
authz?: AuthzCatalog;
|
||||
/** Max request body size in bytes (413 above this). Default 10 MB. */
|
||||
maxBodyBytes?: number;
|
||||
/** HMR hub for browser live-update sockets (dev only). */
|
||||
|
||||
Reference in New Issue
Block a user