Files
WRNexusJS/packages/authz
ClintchizandClaude Opus 5 cd82bec414 fix(authz): fix perf, doc, and fail-open gaps found in second review
Re-review of Task 6's fix round 1 (plan amendment d6a2d054) found three
items in that diff plus one adjacent pre-existing issue that C1 made
reachable:

- Important (perf): permissionsFor() rebuilt the deny Set on every
  entry in the granted set (O(grants x denies) allocations on a
  per-request path). Hoisted to build the Set once. Measured
  4000x4000: 665.92ms before, 3.90ms after.
- Important (contract accuracy): permissionsFor() only half-agrees
  with decide() — a narrow deny under a broad grant (e.g. role editor's
  "post:*" plus a deny on "post:delete") can't be represented in a flat
  Set, so the set still contains "post:*" while decide() correctly
  refuses "post:delete". Documented as NOT authoritative on the
  AuthzResolver interface, and pinned with a regression test asserting
  the divergence is deliberate.
- Minor: subject.id === "" was audited as subjectId: "" instead of
  omitted, so consoleAuditSink printed a blank subject= rather than
  subject=anonymous. Reused the same non-empty-string guard as the
  decide() path.
- Important (adjacent, advanced.ts): owner() compared subject[key] to
  resource[key] with Object.is without checking either side was
  present, so two absent ids (Object.is(undefined, undefined) ===
  true) satisfied ownership. Unreachable before this task, but C1 now
  runs bound policies for anonymous/empty subjects, putting this on a
  live path. Fixed to deny whenever either side is undefined or null.

Every fix's regression test was verified by reverting the fix and
confirming the test fails against the pre-fix code before restoring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:15:34 +05:30
..
2026-08-04 12:19:09 +05:30
2026-07-12 15:55:18 +05:30

@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 wrnexus npm 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)true if any of subject.roles grants permission (honouring * and namespace wildcards). Returns false when the subject has no roles.
  • permissionsFor(roles) — the resolved Set<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 when subject[name] equals match, or when match is a function, when match(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 — runs policy against the request Context; calls next() when it resolves truthy, otherwise returns 403.
  • requireRole(...roles: string[]): Middleware — allows when ctx.user holds any of the listed roles.
  • requirePermission(rbac: Rbac, permission: string): Middleware — allows when rbac.can(ctx.user, permission) is true.

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 return Middleware and read the subject from ctx.user on the request Context. Both types are imported from @wrnexus/core.
  • Policy combinators (any, all) and authorize are async-aware, so policies may return a Promise<boolean> (e.g. for a database ownership check).