test(authz): end-to-end integration coverage, worked example, and docs

Task 15 of the authz permissions plan: proves db store + cache + catalog +
middleware + audit compose correctly, wires a real (non-dangling) example
into auth-showcase, and documents the declaration/registration/precedence
surface in the package README.
This commit is contained in:
2026-08-05 01:22:13 +05:30
parent 57097c8204
commit fd5e2b7128
6 changed files with 348 additions and 0 deletions
@@ -0,0 +1,30 @@
import { defineAuthz } from "@wrnexus/authz";
/**
* `app/authz/<name>.ts` declarations are discovered automatically and merged
* into the process-wide catalog at boot (see `app/middleware/authz.ts`, which
* registers the middleware that resolves against it).
*/
export default defineAuthz({
permissions: {
"post:read": { title: "View posts", public: true },
"post:write": { title: "Create and edit posts" },
"post:delete": { title: "Delete posts", risk: "high" },
"admin:access": { title: "Reach the admin area", risk: "high" },
},
roles: {
viewer: ["post:read"],
editor: ["role:viewer", "post:write"],
admin: ["role:editor", "post:delete", "admin:access"],
},
policies: {
ownsPost: async (
subject: { id?: string },
resource?: { authorId?: string },
): Promise<{ allowed: boolean; reason?: string }> =>
resource?.authorId === subject?.id
? { allowed: true }
: { allowed: false, reason: "You are not the author" },
},
bindings: { "post:delete": ["ownsPost"] },
});
@@ -0,0 +1,24 @@
import { authzMiddleware, getAuthzCatalog, memoryPermissionStore } from "@wrnexus/authz";
/**
* Registers the per-request authorization resolver against the catalog merged
* from `app/authz/*.ts` (see `showcase.ts`). This is an eager, module-scope
* call — the same shape `authzMiddleware({...})` requires — so it must run
* after `getAuthzCatalog()` has been populated. Both the dev server and
* `wrnexus build`'s generated production entry guarantee that happens before
* any app middleware module evaluates.
*
* Middleware runs in alphabetical filename order, so `authz.ts` runs after
* `auth.ts`, which hydrates `ctx.user` from the session. Route handlers and
* pages can then call `can(ctx, "post:write")` or guard a route with
* `guardPermission("post:delete")`.
*
* A real deployment would swap `memoryPermissionStore()` for
* `dbPermissionStore(getDb())` from `@wrnexus/authz/db` so role and grant
* assignments survive a restart; the showcase keeps everything in memory so
* it stays dependency-free.
*/
export default authzMiddleware({
catalog: getAuthzCatalog(),
store: memoryPermissionStore(),
});