U+0085 (NEL), U+2028 (LINE SEPARATOR), and U+2029 (PARAGRAPH SEPARATOR) are treated as line terminators by some log shippers and by JS's own lexical grammar (and are not escaped by JSON.stringify by default), so they could still be used to forge audit log entries even after the initial C0/DEL fix. logSafe now strips all five categories.
@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).