fix(authz): freeze catalog values after boot; correct compile-time-check claims
frozenMap only blocked the Map's own mutators, so
catalog.roles.get("editor").push("*") escalated a role to a full wildcard
past an error string claiming the catalog is frozen after boot; the same
applied to permission/attribute metadata objects and binding arrays.
mergeCatalogs now stores frozen copies of each, so the original declaring
module's objects are never mutated either.
Also corrects two docstrings (codegen.ts, the design doc) that claimed
`wrnexus authz generate`'s output makes a permission typo a type error —
can(), guardPermission(), and decideFor() all take a bare string and nothing
consumes the generated union automatically. Documents what it actually is:
a Permission/Role union to type your own helpers/constants against. Also
adds a README note on the subject.id contract (must be a non-empty string;
owner() compares with Object.is).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -204,6 +204,16 @@ import { getDb } from "@wrnexus/db";
|
||||
export default authzMiddleware({ catalog: getAuthzCatalog(), store: dbPermissionStore(getDb()) });
|
||||
```
|
||||
|
||||
> **`subject.id` must be a non-empty string.** The engine denies (and logs to
|
||||
> stderr) whenever `ctx.user.id` is present but not a non-empty string — this
|
||||
> includes the common case of an integer primary key. Coerce it before it
|
||||
> reaches `ctx.user`, e.g. `user.id = String(row.id)`, or every request for
|
||||
> that user denies with "Invalid subject" instead of resolving normally.
|
||||
> `owner()` (the built-in ownership policy) compares subject and resource ids
|
||||
> with `Object.is`, so both sides must be the same type too — `owner()` on a
|
||||
> numeric `resource.authorId` against a stringified `subject.id` never
|
||||
> matches even when they represent "the same" id.
|
||||
|
||||
There is no per-route `middleware` export — `app/middleware/*.ts` is the only
|
||||
place middleware is registered. To gate part of the app, branch on the
|
||||
request the same way any other conditional middleware does (compare
|
||||
@@ -279,3 +289,16 @@ wrnexus authz list # every registered permission, role, and policy
|
||||
wrnexus authz generate # app/authz/permissions.gen.ts type unions
|
||||
wrnexus authz init # scaffold the assignment-table migration
|
||||
```
|
||||
|
||||
`wrnexus authz generate`'s output is a plain `Permission | Role` string-literal
|
||||
union — `can()`, `guardPermission()`, and `decideFor()` all take a bare
|
||||
`string` and nothing reads this file automatically, so import it to type your
|
||||
own helpers/constants against the registered catalog, e.g.:
|
||||
|
||||
```ts
|
||||
import type { Permission } from "app/authz/permissions.gen.ts";
|
||||
|
||||
function guard(permission: Permission) {
|
||||
return guardPermission(permission);
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user