@vielzeug/ward
v1.0.3
Published
Role-based access control — typed permissions, wildcard patterns, and composable predicates
Readme
@vielzeug/ward
Minimal authorization engine with deterministic precedence, wildcard support, and runtime predicates.
Package: @vielzeug/ward · Category: Auth
Key exports: createWard, allow, deny, predicate, owns, matchesPattern, patternCovers, guardRequest, guardRequestWith, WardPredicateError, WILDCARD, ANONYMOUS
When to use: Minimal authorization engine with deterministic precedence, wildcard support, and runtime predicates.
Related: @vielzeug/rune · @vielzeug/wayfinder · @vielzeug/conduit
@vielzeug/ward is part of Vielzeug and ships as a zero-dependency TypeScript package with ESM+CJS output.
Installation
pnpm add @vielzeug/ward
npm install @vielzeug/ward
yarn add @vielzeug/wardQuick Start
import { ANONYMOUS, WILDCARD, allow, createWard, deny, predicate } from '@vielzeug/ward';
const ward = createWard<'read' | 'update', { authorId: string }>([
// Multi-role rule: viewer and editor can both read
...allow(['viewer', 'editor'], 'posts', ['read']),
// Editor can update their own posts (ownership predicate)
...allow('editor', 'posts', ['update'], { when: predicate.owns('authorId') }),
// High-priority deny overrides any allow rule for blocked principals
...deny('blocked', WILDCARD, [WILDCARD], { priority: 100 }),
// Anonymous visitors can read posts
...allow(ANONYMOUS, 'posts', ['read']),
]);
const principal = { id: 'u1', roles: ['editor'] };
// Full decision object — narrow on .allowed for type-safe access
const decision = ward.explain(principal, 'posts', 'update', { authorId: 'u2' });
if (!decision.allowed) console.log(decision.reason); // 'no-matching-rule' | 'explicit-deny'
// Batch decisions across multiple resources/actions in one call
const results = ward.checkAll(principal, [
{ resource: 'posts', action: 'read' },
{ resource: 'posts', action: 'update', data: { authorId: 'u1' } },
]);
// Decision trace — candidates with index, score, priority, won (no logger fired)
const trace = ward.trace(principal, 'posts', 'update', { authorId: 'u2' });
trace.candidates.forEach((c) => console.log(`Rule[${c.index}]`, c.rule.effect, c.score, c.won));
// Principal-bound view — principal is snapshotted at bind time
const bound = ward.forUser(principal);
bound.allowedActions('posts', ['read', 'update', 'delete']);
bound.explain('posts', 'update', { authorId: 'u2' });
// Conflict detection — O(n²), lazy + cached
const conflicts = ward.detectConflicts();
if (conflicts.length > 0) console.warn('Policy conflicts:', conflicts);Documentation
License
MIT © Helmuth Saatkamp — part of the Vielzeug monorepo.
