Files
WRNexusJS/packages/authz
ClintchizandClaude Opus 5 1cc0b97a72 fix(authz): replace vacuous concurrency test, validate effect in memory store
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>
2026-08-04 20:52:18 +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).