@wrnexus/authz
Role, permission, policy, and authorization guards.
Private registry access required
This package is not available from the public npm registry. After WorkRoot approves access and supplies private registry instructions, install the release-aligned package:
bun add @wrnexus/authz@0.2.59Request preview access. Never put registry tokens in source control.
Composable authorization for WRNexusJS — 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 WRNexusJS 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 WRNexusJS 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.
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)—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 WRNexusJS, this package targets the Bun runtime; Node is not supported.
- Works with [
@wrnexus/core](../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).
Complete TypeScript API
This declaration comes from the exact installed package and lists its exported functions, classes, interfaces, and types.
import { Context, Middleware } from '@wrnexus/core';
/**
* @wrnexus/authz — authorization: role-based (RBAC), policy-based (PBAC), and
* attribute-based (ABAC). Compose freely; all three reduce to a boolean check
* plus an `authorize()` guard middleware.
*
* const rbac = defineRbac({ admin: ["*"], editor: ["post:read", "post:write"] });
* rbac.can(user, "post:write");
*
* // PBAC/ABAC: a policy is a predicate over subject + resource + attributes
* const ownsPost: Policy<User, Post> = (u, post) => u.id === post.authorId;
* authorize((ctx) => ownsPost(ctx.user, resource)) // middleware
*/
interface Subject {
id?: string;
roles?: string[];
[attribute: string]: unknown;
}
interface Rbac {
/** True if any of the subject's roles grants `permission` (supports "*" and "ns:*"). */
can(subject: Subject | undefined, permission: string): boolean;
/** All permissions granted to a set of roles. */
permissionsFor(roles: string[]): Set<string>;
}
/** Build an RBAC checker from a role → permissions map. */
declare function defineRbac(roles: Record<string, string[]>): Rbac;
/** True if the subject has ALL of the given roles. */
declare function hasRole(subject: Subject | undefined, ...required: string[]): boolean;
/** A policy predicate: subject (+ optional resource/attributes) → allowed. */
type Policy<S = Subject, R = unknown> = (subject: S, resource?: R) => boolean | Promise<boolean>;
/** Combine policies: allow if ANY passes (OR). */
declare function any<S, R>(...policies: Policy<S, R>[]): Policy<S, R>;
/** Combine policies: allow only if ALL pass (AND). */
declare function all<S, R>(...policies: Policy<S, R>[]): Policy<S, R>;
/** ABAC helper: allow when an attribute matches (equality or predicate). */
declare function attr<S extends Subject>(name: string, match: unknown | ((value: unknown) => boolean)): Policy<S>;
/** Guard a route with a policy over `ctx` (reads `ctx.user` as the subject). */
declare function authorize(policy: (ctx: Context) => boolean | Promise<boolean>): Middleware;
/** Guard requiring one of the given roles. */
declare function requireRole(...roles: string[]): Middleware;
/** Guard requiring an RBAC permission. */
declare function requirePermission(rbac: Rbac, permission: string): Middleware;
export { type Policy, type Rbac, type Subject, all, any, attr, authorize, defineRbac, hasRole, requirePermission, requireRole };
Examples
Examples are taken from this package's installed documentation and must be evaluated with its requirements and stability notes.
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"); // trueGuarding 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,
);