@vielzeug/ward
v2.2.0
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.
Installation
pnpm add @vielzeug/wardQuick Start
import { ANONYMOUS, WILDCARD, allow, createWard, deny, owns } from '@vielzeug/ward';
const ward = createWard<'read' | 'update', { authorId: string }>([
allow([ANONYMOUS, 'viewer'], 'posts', ['read']),
allow('editor', 'posts', ['update'], { when: owns('authorId') }),
deny('blocked', WILDCARD, [WILDCARD], { priority: 100 }),
]);
const principal = { id: 'u1', roles: ['editor'] };
const decision = ward.explain({
principal,
resource: 'posts',
action: 'update',
data: { authorId: 'u2' },
});
const batch = ward.checkAll(principal, [
{ resource: 'posts', action: 'read' },
{ resource: 'posts', action: 'update', data: { authorId: 'u1' } },
]);
const trace = ward.trace({
principal,
resource: 'posts',
action: 'update',
data: { authorId: 'u2' },
});
const bound = ward.forUser(principal);
bound.allowedActions({ resource: 'posts', knownActions: ['read', 'update', 'delete'] as const });
bound.explain({ resource: 'posts', action: 'update', data: { authorId: 'u2' } });API Notes
explain()andtrace()take object inputs:{ principal, resource, action, data? }.allowedActions()takes{ principal, resource, knownActions, data? }.rulesInScope()takes{ principal, resource, data? }.BoundWardmethods use object inputs withoutprincipal.trace()does not fire adecisionevent;explain()andcheckAll()do.- Use
explain()directly at request boundaries instead of middleware wrappers. - Subscribe to decision events with
tap()for logging and diagnostics:ward.tap((event) => console.debug(`ward:${event.type}`, event.decision));
