The fix-round-1 test "a concurrent write is not lost to another method's failure" was vacuous: a single-process Promise.all cannot reliably land a bare write inside another method's open transaction, so it passed against both the fixed and the (previously) defective grant() implementation. The shared-connection rollback hazard it was meant to catch is real (confirmed separately by forcing the transaction open before the write), but this specific test could never reach that state and gave false assurance either way. Replaced it with "a rejected write leaves unrelated state intact", which asserts a grant() call with an invalid effect is refused without disturbing the subject's existing roles/grants, plus a NOTE documenting that the rollback hazard is now prevented structurally (no transactions) rather than by a dedicated concurrency test. memoryPermissionStore.grant() had no effect validation, so it failed the new test; added a guard mirroring the db adapter's CHECK constraint so both adapters agree on rejecting anything other than "allow"/"deny". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wrnexus/authz
Composable authorization for WrNexus — role-based (RBAC), policy-based (PBAC), and attribute-based (ABAC) access control that reduces to a boolean check plus an
authorize()guard.
Part of the WrNexus framework — an SSR-first, Bun-native full-stack web framework.
Overview
@wrnexus/authz is a small, server-side authorization toolkit. It gives you three
interchangeable models — RBAC (roles → permissions), PBAC (policy predicates), and
ABAC (attribute matchers) — that all collapse to a boolean | Promise<boolean> decision.
Wrap any decision in a Middleware guard (authorize, requireRole, requirePermission)
to protect WrNexus routes. Reach for it whenever a route or action needs to be gated on who
the user is, what roles they hold, or attributes of the user and the resource. It plugs into
@wrnexus/core by reading ctx.user as the authorization subject.
Installation
bun add @wrnexus/authz
Private package — the machine must be authenticated to the
wrnexusnpm org (a read token in~/.npmrc). Requires Bun (Node is not supported).
API
The package has a single entry point (@wrnexus/authz) exporting the following.
Types
| Symbol | Description |
|---|---|
Subject |
The authorized principal: { id?: string; roles?: string[]; [attribute: string]: unknown }. |
Rbac |
An RBAC checker: { can(subject, permission): boolean; permissionsFor(roles): Set<string> }. |
Policy<S = Subject, R = unknown> |
A predicate (subject: S, resource?: R) => boolean | Promise<boolean>. |
RBAC
defineRbac(roles: Record<string, string[]>): Rbac
Builds an RBAC checker from a role → permissions map. Supported permission forms:
"*"— grants every permission."ns:*"— namespace wildcard (e.g."post:*"grants"post:write")."role:<name>"— inherits all permissions of another role (resolved recursively, cycle-safe).
The returned Rbac provides:
can(subject, permission)—trueif any ofsubject.rolesgrantspermission(honouring*and namespace wildcards). Returnsfalsewhen the subject has no roles.permissionsFor(roles)— the resolvedSet<string>of all permissions granted to a set of roles.
hasRole(subject: Subject | undefined, ...required: string[]): boolean
true if the subject holds all of the given roles.
PBAC / ABAC combinators
any<S, R>(...policies: Policy<S, R>[]): Policy<S, R>— allow if any policy passes (OR); awaits async policies.all<S, R>(...policies: Policy<S, R>[]): Policy<S, R>— allow only if all policies pass (AND); awaits async policies.attr<S extends Subject>(name: string, match: unknown | ((value: unknown) => boolean)): Policy<S>— ABAC helper that allows whensubject[name]equalsmatch, or whenmatchis a function, whenmatch(value)is truthy.
Guards (middleware)
Each guard returns a @wrnexus/core Middleware. A denied request short-circuits with
Response.json({ ok: false, error: "Forbidden" }, { status: 403 }).
authorize(policy: (ctx: Context) => boolean | Promise<boolean>): Middleware— runspolicyagainst the requestContext; callsnext()when it resolves truthy, otherwise returns 403.requireRole(...roles: string[]): Middleware— allows whenctx.userholds any of the listed roles.requirePermission(rbac: Rbac, permission: string): Middleware— allows whenrbac.can(ctx.user, permission)istrue.
Usage
RBAC
import { defineRbac, hasRole } from "@wrnexus/authz";
const rbac = defineRbac({
admin: ["*"],
editor: ["post:read", "post:write"],
viewer: ["post:read"],
// role inheritance: lead gets everything an editor has, plus post:publish
lead: ["role:editor", "post:publish"],
});
const user = { id: "u1", roles: ["editor"] };
rbac.can(user, "post:write"); // true
rbac.can(user, "post:delete"); // false
rbac.permissionsFor(["lead"]); // Set { "post:read", "post:write", "post:publish" }
hasRole(user, "editor"); // true
Guarding routes
import { authorize, requireRole, requirePermission, defineRbac } from "@wrnexus/authz";
const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"] });
// Only admins or editors
app.get("/dashboard", requireRole("admin", "editor"), handler);
// Requires a specific permission
app.post("/posts", requirePermission(rbac, "post:write"), handler);
// Arbitrary policy over the request context
app.delete(
"/posts/:id",
authorize((ctx) => hasRole(ctx.user, "admin")),
handler,
);
PBAC / ABAC policies
import { any, all, attr, authorize, type Policy } from "@wrnexus/authz";
interface User {
id: string;
department?: string;
roles?: string[];
}
interface Post {
authorId: string;
}
// Ownership policy (subject + resource)
const ownsPost: Policy<User, Post> = (u, post) => u.id === post?.authorId;
// ABAC: attribute equality, or a predicate
const inEngineering = attr<User>("department", "engineering");
const isVerified = attr<User>("verified", (v) => v === true);
// Compose: allow if the user owns the post OR is in engineering AND verified
const canEdit = any(ownsPost, all(inEngineering, isVerified));
app.put(
"/posts/:id",
authorize((ctx) => canEdit(ctx.user as User, loadPost(ctx))),
handler,
);
Requirements / Notes
- Bun-only — like the rest of WrNexus, this package targets the Bun runtime; Node is not supported.
- Works with
@wrnexus/core— the guards returnMiddlewareand read the subject fromctx.useron the requestContext. Both types are imported from@wrnexus/core. - Policy combinators (
any,all) andauthorizeare async-aware, so policies may return aPromise<boolean>(e.g. for a database ownership check).