page wrnexusauthz { seo { title = "@wrnexus/authz" description = "Role, permission, policy, and authorization guards." } view {
W WRNexusJS
Browse documentation
Security · Preview

@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.59

Request 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

SymbolDescription
SubjectThe authorized principal: { id?: string; roles?: string[]; [attribute: string]: unknown }.
RbacAn 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 WRNexusJS, this package targets the Bun runtime; Node is not supported.
  • Works with [@wrnexus/core](../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).

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"); // 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,
);
} }