npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@velajs/authz

v1.30.0

Published

Framework-agnostic permission/role engine for Vela: defineRole, fail-closed can(), composition + masking helpers

Readme

@velajs/authz

Framework-agnostic permission/role engine for Vela: defineRole, fail-closed can(), composition + masking helpers. Zero runtime dependencies, edge-runtime safe (no node:*, no Buffer, no process).

Why

Authorization is one decision — "is this identity allowed to do this?" — that has to be answered identically across HTTP, WebSocket, live queries, and background jobs. This package is that single answer. It resolves an Identity to a set of granted permissions and decides can() fail-closed: absent a session, the anonymous zero-privilege identity is used and nothing is granted.

The core is a plain function library — no decorators, no container, no framework coupling. Optional Vela integration lives behind a subpath.

Install

pnpm add @velajs/authz

Quick start

import { createAuthz, defineRole } from '@velajs/authz';

const authz = createAuthz({
  roles: [
    defineRole('editor', ['posts:read', 'posts:write']),
    defineRole('admin', ['*']),
  ],
});

await authz.can({ roles: ['editor'] }, 'posts:write'); // true
await authz.can({ roles: ['editor'] }, 'posts:delete'); // false
await authz.can({ roles: ['admin'] }, 'anything:at:all'); // true (wildcard)

The model

  • Identity — who is asking. All fields optional, so {} and anonymous are valid zero-privilege identities.
  • defineRole(name, permissions) / definePermission(name) — declare the role→permission table. defineRole copies the permissions array, so the returned RoleDef never aliases the caller's input.
  • createAuthz(options) — builds an Authz over a role table (or a custom resolver). Returns { can, resolver }. If you pass a permissions allow-list, createAuthz throws when a role grants an undeclared (non-wildcard) permission — a build-time guard against typos.
  • can(identity, permission, resolver) — the standalone fail-closed check; authz.can(identity, permission) is the same check bound to the built resolver.
interface Identity {
  issuer?: string;
  subject?: string;
  principalType?: 'user' | 'service';
  /** @deprecated compatibility alias for subject */
  userId?: string;
  roles?: string[];
  claims?: Record<string, unknown>;
}

interface PermissionResolver {
  grants(identity: Identity): Set<string> | Promise<Set<string>>;
}

Authenticated adapters should populate { issuer, subject, principalType }. Treat the (issuer, subject) pair as the durable principal key: OIDC subjects are issuer-local and can collide across identity providers. userId remains as a compatibility alias while applications migrate.

anonymous is the zero-privilege identity ({ roles: [] }, frozen) — the fail-closed default when no session is present.

Wildcards

A granted permission string matches the requested permission when:

  • it is * — grants everything;
  • it equals the requested permission exactly (e.g. posts:write);
  • it is resource:* — grants any action under that resource (e.g. posts:* grants posts:delete).

Wildcards live on the granted side (what a role holds), not the requested side.

Fail-closed rules

Authorization defaults to deny. Every ambiguous or broken path denies rather than leaks:

  • No session → use anonymous → grants nothing.
  • Unknown role / missing permission → denied.
  • A resolver that throws → denied (there is no allow-on-error path).
  • anyOf() with no policies → denied (nothing grants access).
  • A policy that throws inside anyOf/allOf → that branch is denied; it can never allow.
  • mask whose transform throws → redacts to null, never leaks the raw value.

(allOf() with no policies is vacuously true — an empty AND — but an empty OR denies.)

Composition + masking

Policy is a plain predicate over a context and a resource:

type Policy<C = { identity: Identity }, R = unknown> =
  (ctx: C, resource: R) => boolean | Promise<boolean>;

Combine capability checks with resource-level rules (ownership, tenancy, state):

  • anyOf(...policies) — OR, read semantics. Any policy granting → allowed. Empty → denied.
  • allOf(...policies) — AND, write semantics. All must allow.
  • hasPerm(authz, permission) — bridge a capability check into a Policy that reads only ctx.identity.
  • mask(fn) — wrap a field transform so a throw redacts to null instead of leaking.
import { anyOf, allOf, hasPerm, mask } from '@velajs/authz';

const isOwner = (ctx: { identity: { userId?: string } }, post: { authorId: string }) =>
  ctx.identity.userId === post.authorId;

// A reader may see a post if they own it OR hold posts:read.
const canRead = anyOf(isOwner, hasPerm(authz, 'posts:read'));

// A writer must own it AND hold posts:write.
const canWrite = allOf(isOwner, hasPerm(authz, 'posts:write'));

await canRead({ identity: { userId: 'u1', roles: [] } }, { authorId: 'u1' }); // true

// Redact a sensitive field, fail-closed to null on any error.
const lastFour = mask((_ctx: unknown, r: { ssn: string }) => r.ssn.slice(-4));
lastFour({}, { ssn: '123456789' }); // '6789'

Vela integration

Optional. @velajs/authz/vela wires the engine into a Vela app. @velajs/vela is an optional peer dependency — the core engine has no framework coupling and runs anywhere (edge, Node, Workers, Deno, Bun).

The framework entrypoint owns the shared PermissionGuard / RequirePermission (all permissions), RolesGuard / Roles (any local role), and CurrentIdentity decorator. Authentication providers publish verified state into core; these guards never infer identity from request headers, Hono variables, compatibility symbols, or Better Auth user metadata.

getContextIdentity(context) reads core's unexpired HTTP identity, or the normalized WebSocket connection principal/tenant/expiry. Socket role or claim fields are not authority; a permission resolver can look up grants using the connection principal and tenant. HTTP role/claim snapshots cannot be mutated after publication.

HTTP-backed custom dispatchers must explicitly bind each execution context using core's bindTrustedRequestContext(context, originalRequest). The guards then read live request authority, including tenant admission and later invalidation. Bind before dispatch and authenticate/admit once at the outer HTTP boundary before running concurrent fields.

The permission guard resolves exactly one AUTHZ engine visible from the route module, then rechecks identity after asynchronous decisions to reject expiry or replacement during resolution. can() also rejects expired identities before and after invoking its resolver. Missing identity, missing/ambiguous engine, and resolver exceptions deny access.

import { AuthzModule, PermissionGuard, RequirePermission } from '@velajs/authz/vela';

@UseGuards(CloudflareAccessGuard, PermissionGuard) // or AuthGuard, PermissionGuard
@Controller('/posts')
class PostsController {
  @Post()
  @RequirePermission(['posts:write'])
  create() { /* ... */ }
}

Startup wiring audit

Opt into the mounted HTTP route audit after all route/global-guard adapters:

import { authorizationAudit } from '@velajs/authz/vela';

const app = await VelaFactory.create(AppModule, {
  adapters: [authorizationAudit()],
});

The audit checks effective class/method RequirePermission and Roles declarations, verifiable built-in guard wiring, and exactly one module-visible AUTHZ engine for permission checks. It follows provider aliases using read-only DI snapshots, respects empty method overrides, and uses mounted controller identity plus declaring module. It never invokes a factory, constructs a request provider, or audits an unused imported class. Unresolved factories and custom guard implementations cannot be proven to enforce the built-in declarations and produce an explicit unverified-guard diagnostic.

Use authorizationAudit({ mode: 'warn', onDiagnostic }) for adoption without changing startup behavior; the default opt-in mode throws on findings. inspectAuthorizationWiring({ container, routeManager }) returns frozen diagnostics for tooling. This checks configuration, not application policy semantics. Authentication, Cedar, middleware-only authorization, non-HTTP dispatchers and later runtime rewiring need their own review; runtime guards continue checking authority and engine visibility.

License

MIT © Kauan Guesser