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(),
});
+1
View File
@@ -12,6 +12,7 @@
},
"dependencies": {
"@wrnexus/auth": "workspace:*",
"@wrnexus/authz": "workspace:*",
"@wrnexus/captcha": "workspace:*",
"@wrnexus/core": "workspace:*",
"@wrnexus/validation": "workspace:*"
@@ -91,3 +91,23 @@ test("package auth schemas are shared by browser forms and API handlers", () =>
expect(forgotPassword).toContain("data-schema='{schema}'");
expect(forgotPassword).toContain("novalidate");
});
test("authz is wired with a real declaration and a registered middleware, not a dangling file", () => {
expect(existsSync(join(root, "app", "authz", "showcase.ts"))).toBe(true);
expect(existsSync(join(root, "app", "middleware", "authz.ts"))).toBe(true);
const declaration = read(root, "app", "authz", "showcase.ts");
expect(declaration).toContain("defineAuthz");
expect(declaration).toContain('"post:read": { title: "View posts", public: true }');
expect(declaration).toContain("bindings:");
const middleware = read(root, "app", "middleware", "authz.ts");
expect(middleware).toContain("authzMiddleware");
expect(middleware).toContain("getAuthzCatalog()");
expect(middleware).toContain("memoryPermissionStore()");
const manifest = JSON.parse(read(root, "package.json")) as {
dependencies?: Record<string, string>;
};
expect(manifest.dependencies?.["@wrnexus/authz"]).toBe("workspace:*");
});