Skip to Content
AuthorizationAPI Reference

API reference

Not on npm yet

npm install @evanion/acl does not resolve. The package is private: true, so a release run versions and tags it without publishing: npm cannot configure a trusted publisher for a package that does not exist on the registry, and the first version has to go up by hand.

@evanion/acl exports the evaluator, the typed builder, the document types and the construction errors, one heading each below. Node 20 or newer, ESM only. The React provider and hooks are @evanion/react-acl.

hydratePolicy

hydratePolicy builds an evaluator over a matrix document.

function hydratePolicy< Sub = Subject, R = AnyObjects, Keys extends string = string, >(matrix: Matrix, options?: AccessOptions): Access<Sub, R, Keys>;
Signature — a shape, not a call

hydratePolicy copies the document, freezes and validates the copy, and throws on an unknown key. All three type parameters default to the open forms JSON carries.

parseMatrix

parseMatrix is hydratePolicy with closed: true, for a document another service published.

function parseMatrix< Sub = Subject, R = AnyObjects, Keys extends string = string, >(matrix: Matrix, options?: AccessOptions): Access<Sub, R, Keys>;
Signature — a shape, not a call

An unknown object kind or action answers unknown-action and nothing throws. Keys is the union the consumer expects, unchecked against the document. See Adopting a foreign matrix.

policy

policy returns the typed builder, one .for() per object kind. See Typed authoring.

function policy< Sub, Objects, Vocabs extends Partial<Record<keyof Objects, string>> = Record<never, never>, >( options?: PolicyOptions, ): Policy<Sub, Objects, Vocabs, Record<never, never>, never>;
Signature — a shape, not a call
Type parameterWhat it is
Subthe subject type every subject.* path is checked against
Objectseach object kind mapped to its row type
Vocabseach kind mapped to the actions it answers for
Actionthe vocabulary a kind Vocabs omits gets, carrying no wildcard member; docs/specs/2026-09-17-acl-wildcard-action.md records why
VocabularyOfwhat one kind’s block may declare from; string there opens the kind to any action

Policy

interface Policy< Sub, Objects, Vocabs extends Partial<Record<keyof Objects, string>>, R, Keys extends string = string, > { for<K extends keyof Objects & string, Act extends string = string>( key: K, build: ( p: Actions<Sub, Objects[K], never, VocabularyOf<Vocabs, K>>, ) => Actions<Sub, Objects[K], Act, VocabularyOf<Vocabs, K>> | void, ): Policy< Sub, Objects, Vocabs, R & Record<K, Objects[K]>, Keys | PermissionKeys<K, Act> >; readonly matrix: Readonly<Matrix>; build(options?: AccessOptions): Access<Sub, R, Keys>; }
Signature — a shape, not a call
Member or typeWhat it is
fortakes no type arguments, reading the object type out of Objects, the vocabulary out of Vocabs and Act off the chain its block returns
matrixflattens the blocks without building one
buildvalidates every block once and returns the Access
PermissionKeysthe keys one block contributes; an Act the compiler reads as string contributes string, widening the document’s whole key union
KeysOflifts those keys out of an Access type; a producer exports KeysOf<typeof access> for a consumer compiled against its source
ActionOfthe action half of the keys one object kind holds, checked against every query’s action parameter; open, empty and unmatched key unions answer string

PolicyOptions

type PolicyOptions = Pick<Matrix, 'version' | 'schema'>;
Signature — a shape, not a call

Both are document fields, so they land in the flattened JSON.

AccessOptions

interface AccessOptions { version?: string | number; closed?: boolean; fetchedAt?: Instant; maxStale?: number; }
Signature — a shape, not a call
OptionWhat it is
versionoverrides the document’s own version
closedfails closed on an unknown key
fetchedAtwhen this holder last validated; a holder that supplies it obliges the document to state maxStale
maxStalea local ceiling, min-ed with the document’s

Access

Access is the evaluator over one frozen document.

interface Access<Sub = Subject, R = AnyObjects, Keys extends string = string> { readonly matrix: Readonly<Matrix>; readonly version: string | number | undefined; readonly schema: MatrixSchema | undefined; can(subject, key, action, object?, now?): Decision; canMany(subject, key, action, objects, now?): Decision[]; canFields(subject, key, action, object, axis, proposed?, now?): FieldDecision; capabilities(subject, now?): Record<Keys, Decision>; authorize(subject, options?: { now?: Instant }): Authorized<R, Keys>; object(key): BoundKind<Sub, R[K], ActionOf<Keys, K>>; readsObject(key, action): boolean; }
Signature — a shape, not a call
MemberWhat it answers
canone decision for one action on one row
canManyone decision per row, settling one clock for the whole list
canFieldsthe field-level decision on the read or write axis
capabilitiesevery permission key, decided against one subject
authorizean Authorized handle carrying the subject
objecta BoundKind handle carrying the object kind
readsObjectwhether the permission needs the row before it can decide
matrix, version, schemathe frozen document, and the two envelope fields off it

Every object parameter takes a partial: a projection is the unevaluable case. An absent subject.* path is a definite miss.

Authorized

Authorized is what authorize(subject) returns.

interface Authorized<R = AnyObjects, Keys extends string = string> { can<K extends keyof R & string>( key: K, action: ActionOf<Keys, K>, object?: Partial<R[K]>, ): Decision; canMany<K extends keyof R & string>( key: K, action: ActionOf<Keys, K>, objects: readonly Partial<R[K]>[], ): Decision[]; canFields<K extends keyof R & string>( key: K, action: ActionOf<Keys, K>, object: Partial<R[K]>, axis: 'read' | 'write', proposed?: Partial<R[K]>, ): FieldDecision; capabilities(): Record<Keys, Decision>; }
Signature — a shape, not a call

Authorized omits matrix, version, schema and readsObject, which are not facts about one subject.

BoundKind

BoundKind is what object(key) returns.

interface BoundKind<Sub, Obj, Act extends string = string> { can( subject: Sub, action: Act, object?: Partial<Obj>, now?: Instant, ): Decision; canMany( subject: Sub, action: Act, objects: readonly Partial<Obj>[], now?: Instant, ): Decision[]; canFields( subject: Sub, action: Act, object: Partial<Obj>, axis: 'read' | 'write', proposed?: Partial<Obj>, now?: Instant, ): FieldDecision; readsObject(action: Act): boolean; }
Signature — a shape, not a call

authorize binds the other axis; the two compose.

KeysOf

type KeysOf<A> = A extends Access<never, never, infer Keys> ? Keys : never;
Signature — a shape, not a call

ActionOf

type ActionOf<Keys extends string, K extends string> = [Keys] extends [never] ? string : string extends Keys ? string : [Extract<Keys, `${K}.${string}`>] extends [never] ? string : Keys extends `${K}.${infer Act}` ? Act : never;
Signature — a shape, not a call

CRUD_ACTIONS

const CRUD_ACTIONS: readonly ['create', 'read', 'update', 'delete'];
Signature — a shape, not a call

CRUD_ACTIONS declares the four default verbs, and Action reads off it. Pass it to allowEach.

pickAllowedFields

pickAllowedFields returns the subset of a proposed write the decision marked allowed, and throws ActionNotAllowedError on a refused action. See the write path.

function pickAllowedFields<T extends Record<string, unknown>>( decision: FieldDecision, proposed: T, ): Partial<T>;
Signature — a shape, not a call

serialize

function serialize<Sub, R, Keys>( access: Access<Sub, R, Keys>, mode: 'full', ): Matrix; function serialize<Sub, R, Keys>( access: Access<Sub, R, Keys>, mode: 'reduced', options?: SerializeOptions<Keys>, ): Matrix;
Signature — a shape, not a call

full hands back access.matrix. reduced keeps each permission marked visibility: 'public' whole, strips the marking, and adds the schema entries for the kinds they name. See Many services.

SerializeMode

type SerializeMode = 'full' | 'reduced';
Signature — a shape, not a call

SerializeOptions

interface SerializeOptions<Keys extends string = string> { readonly vetoable?: readonly Keys[]; }
Signature — a shape, not a call

A listed key the reduction would drop raises UnpublishedVetoableError. The compiler refuses a key the document never held, where the builder authored the policy.

federatedPolicies

federatedPolicies composes one Access per origin and merges no document. See Many services.

function federatedPolicies( policies: Readonly<Record<string, Access<Subject, AnyObjects, never>>>, ): FederatedAccess;
Signature — a shape, not a call

FederatedAccess

interface FederatedAccess { can( subject: Subject, key: string, action: string, object?: Record<string, unknown>, now?: Instant, ): Decision; capabilities(subject: Subject, now?: Instant): Record<string, Decision>; get(origin: string): Access | undefined; }
Signature — a shape, not a call

can routes to the origin holding `${object}.${action}`; a key no member holds answers unknown-action. capabilities settles one instant for all.

applyDenyOverlay

applyDenyOverlay appends another team’s deny rules to the keys this document opens for veto. See Many services.

function applyDenyOverlay( matrix: Matrix, overlay: DenyOverlay, options: DenyOverlayOptions, ): Matrix;
Signature — a shape, not a call

DenyOverlay

type DenyOverlay = Readonly<Record<string, readonly Rule[]>>;
Signature — a shape, not a call

A Rule spells no allow and no field rule, so an overlay only subtracts.

DenyOverlayOptions

interface DenyOverlayOptions { readonly vetoable: readonly string[]; }
Signature — a shape, not a call

A key listed in vetoable obliges the matrix to declare that key’s object kind in schema.objects.

Matrix

Matrix is a JSON envelope over a flat list of permissions. See The matrix document.

interface Matrix { readonly version?: string | number; readonly maxStale?: number; // how long a holder may keep deciding on it readonly schema?: MatrixSchema; readonly permissions: readonly Permission[]; }
Signature — a shape, not a call
TypeWhat it holds
Permissionone `${object}.${action}` key, its allow rules, its deny rules and its field rules
Rulean id and a list of AND-ed conditions
Conditionone comparison; The matrix document has the operator table
FieldRulesthe fields list, then one key per field carrying a write-axis config, so a field named fields cannot be configured
FieldConfigtargets allow-lists the proposed value and transitions is a state machine over the current one; see Fields
MatrixSchemaoptional for a producer, binding where present, per kind: a kind objects omits is unchecked
ObjectSchemaa kind’s field types, and the relations naming the kinds it points at, one hop; no condition compares a relation
FieldTypea flat string any language emits by reflection, where ? marks a field that may be absent
Instantan ISO 8601 string, epoch milliseconds, or a Date; the string and number forms survive JSON

Permission

interface Permission { key: string; // exactly `${object}.${action}` object: ObjectKey; action: string; // whatever the producer wrote rules?: readonly Rule[]; // allows, OR-ed denyRules?: readonly Rule[]; // denies, OR-ed; a matched deny wins fields?: FieldRules; visibility?: 'public' | 'internal'; // absent is internal }
Signature — a shape, not a call

Rule

interface Rule { id?: string; // what a decision reports as `rule`; defaults to `#0`, `#1`, … when?: readonly Condition[]; // AND-ed }
Signature — a shape, not a call

Condition

type Condition = | { field: 'now'; op: 'before' | 'after'; value: Instant } | { field: string; op: 'eq' | 'ne' | 'in' | 'not-in' | 'contains'; path?: string; // the `eq`/`ne` form only value?: unknown; };
Signature — a shape, not a call

FieldRules

interface FieldRules { fields?: readonly string[]; // ['*', '!status'] or ['body', 'title'] [field: string]: readonly string[] | FieldConfig | undefined; }
Signature — a shape, not a call

FieldConfig

type FieldConfig = | { targets: readonly unknown[] } | { transitions: Record<string, readonly unknown[]> };
Signature — a shape, not a call

MatrixSchema

interface MatrixSchema { readonly subject?: ObjectSchema; readonly objects?: Readonly<Record<ObjectKey, ObjectSchema>>; }
Signature — a shape, not a call

ObjectSchema

interface ObjectSchema { readonly fields?: Readonly<Record<string, FieldType>>; readonly relations?: Readonly<Record<string, ObjectKey>>; }
Signature — a shape, not a call

FieldType

type FieldType = | BaseFieldType | `${BaseFieldType}[]` | `${BaseFieldType}?` | `${BaseFieldType}[]?`;
Signature — a shape, not a call

BaseFieldType

type BaseFieldType = 'string' | 'number' | 'boolean' | 'instant';
Signature — a shape, not a call

Instant

type Instant = string | number | Date;
Signature — a shape, not a call

Decision

interface Decision { key: string; allowed: boolean; reason: Reason; rule?: string; missing?: readonly string[]; }
Signature — a shape, not a call
FieldWhat it carries
keythe permission key the query asked about
allowedthe gate
reasonoutput only; Decisions says which reason carries which
ruleoutput only; the id of the rule that decided
missingoutput only; the paths that did not read

Reason

type Reason = | 'allow' | 'no-rule-matched' | 'denied' | 'unknown-action' | 'unevaluable' | 'unusable-clock' | 'stale-contract';
Signature — a shape, not a call

FieldDecision

interface FieldDecision { allowed: boolean; // the action is allowed AND every decided field is action: Decision; // the action alone fields: Record<string, FieldState>; reasons: Record<string, FieldReason>; }
Signature — a shape, not a call
FieldWhat it carries
allowedthe action is allowed and every decided field is
actionthe action-level Decision alone
fieldsevery field name mapped to a FieldState; compare against 'allowed', since comparing against 'denied' treats an unevaluable field as writable
reasonsevery field name mapped to the FieldReason that settled it

FieldState

type FieldState = 'allowed' | 'denied' | 'unevaluable';
Signature — a shape, not a call

See Pitfalls.

FieldReason

type FieldReason = | 'allow' | 'not-listed' | 'targets-failed' | 'transition-failed' | 'missing-field' | 'proposed-required';
Signature — a shape, not a call

EvaluationContext

interface EvaluationContext { subject: Record<string, unknown>; object?: Record<string, unknown>; now?: Instant; }
Signature — a shape, not a call

The entry points assemble an EvaluationContext, and now settles to one epoch per call.

Subject

type Subject = Record<string, unknown>;
Signature — a shape, not a call

AnyObjects

type AnyObjects = Record<string, Record<string, unknown>>;
Signature — a shape, not a call

AnyObjects is the open key map Access defaults to.

ObjectKey

type ObjectKey = string;
Signature — a shape, not a call

Actions

Actions is the block parameter .for() hands you.

interface Actions< Sub, Obj, Act extends string = string, Vocab extends string = string, > extends Ops<Sub, Obj> { allow<A extends Vocab>( action: A, ...conditions: Cond[] ): Actions<Sub, Obj, Act | A, Vocab>; deny<A extends Vocab>( action: A, ...conditions: Cond[] ): Actions<Sub, Obj, Act | A, Vocab>; allowEach<A extends Vocab>( actions: readonly A[], ...conditions: Cond[] ): Actions<Sub, Obj, Act | A, Vocab>; denyEach<A extends Vocab>( actions: readonly A[], ...conditions: Cond[] ): Actions<Sub, Obj, Act | A, Vocab>; fields(rules: readonly string[] | FieldRules): Actions<Sub, Obj, Act, Vocab>; visibility(visibility: Visibility): Actions<Sub, Obj, Act, Vocab>; }
Signature — a shape, not a call
Member or typeWhat it does
allow, denyadd one rule to the action named
allowEach, denyEachwrite one rule across several actions, one ordinary permission each
fields, visibilitymark the action last declared, and raise AclConfigError after a batch
Vocabwhat this kind may declare
Actwhat this chain declared; only Act reaches the permission keys

Action

type Action = (typeof CRUD_ACTIONS)[number];
Signature — a shape, not a call

VocabularyOf

type VocabularyOf<Vocabs, K extends string> = K extends keyof Vocabs ? Vocabs[K] extends string ? Vocabs[K] : Action : Action;
Signature — a shape, not a call

Visibility

type Visibility = NonNullable<Permission['visibility']>;
Signature — a shape, not a call

Visibility is the marking p.visibility() writes and serialize(access, 'reduced') reads.

PermissionKeys

type PermissionKeys<K extends string, Act extends string> = string extends Act ? string : `${K}.${Act}`;
Signature — a shape, not a call

Ops

Ops holds the condition helpers, bound to the subject and object types.

interface Ops<Sub, Obj> { eq<A extends string, B>( field: Valid<A, Sub, Obj>, operand: Operand<B, Sub, Obj>, ): Cond; ne<A extends string, B>( field: Valid<A, Sub, Obj>, operand: Operand<B, Sub, Obj>, ): Cond; in<A extends string>( field: Valid<A, Sub, Obj>, values: readonly unknown[], ): Cond; notIn<A extends string>( field: Valid<A, Sub, Obj>, values: readonly unknown[], ): Cond; contains<A extends string>(field: Valid<A, Sub, Obj>, value: unknown): Cond; before(field: 'now', instant: Instant): Cond; after(field: 'now', instant: Instant): Cond; and(...conditions: Cond[]): Cond; or(...conditions: Cond[]): Cond; readonly always: Cond; }
Signature — a shape, not a call

not-in is not an identifier, so the helper is notIn.

TypeWhat it is
Condan opaque condition tree, which build() flattens to a rule per or branch
Operanda comparand shaped like a namespaced path is checked as one, and any other string is a literal
Pathsthe subject. and object. paths one bound type offers
Valida path those types carry; the failure branch is the parameter type, so the compiler names the bad path

Cond

interface Cond { readonly node: Node; }
Signature — a shape, not a call

Operand

type Operand<B, Sub, Obj> = B extends `subject.${string}` | `object.${string}` | 'now' ? Valid<B & string, Sub, Obj> : B;
Signature — a shape, not a call

Paths

type Paths<T, P extends string> = { [K in keyof T & string]: `${P}.${K}`; }[keyof T & string];
Signature — a shape, not a call

Valid

type Valid<S extends string, Sub, Obj> = S extends Paths<Sub, 'subject'> | Paths<Obj, 'object'> | 'now' ? S : `unknown path '${S}' on this resource`;
Signature — a shape, not a call

AclConfigError

class AclConfigError extends Error {}
Signature — a shape, not a call

AclConfigError is the base of every class below. Typed authoring tables the same set by authoring step.

ErrorRaised when
InvalidMatrixErrorthe envelope carries no permissions array, or a version of the wrong type
InvalidSchemaErrorthe schema’s own shape is wrong; where locates it inside the document
InvalidPermissionErrora permission’s object, action, rules or fields is malformed
InvalidRuleErrora rule is not an object, or its when is not an array
InvalidConditionErrora condition’s shape, namespace, path depth or operator pairing is unusable
KeyMismatchErrorkey is not exactly `${object}.${action}`
DuplicatePermissionErrortwo permissions share a key
UnknownFieldErrora condition names a field the schema does not declare
FieldTypeMismatchErrorcontains runs against a non-array, or a literal is of the wrong type
BangInAllowListErrora !name entry sits inside an allow-list
DenyWithoutBaselineErrora !name entry has no * baseline to subtract from
TargetsTransitionsConflictErrorone field configures both targets and transitions
UnknownObjectKeyErrora query on the open path names an unknown object kind; parseMatrix fails closed
UnknownPermissionErrora query on the open path names an unknown key; parseMatrix answers unknown-action
OriginCollisionErrortwo origins claim one key, which federatedPolicies raises while composing
UnvetoablePermissionErroran overlay names a key the target does not list as vetoable
MissingVetoSchemaErrora vetoable key’s object kind has no schema.objects entry
UnpublishedVetoableErrorserialize reduces away a vetoable key the contract would not ship
MissingFreshnessBudgetErrora holder reports fetchedAt against a document stating no maxStale
InvalidFreshnessErrorfetchedAt is not an instant, or maxStale is not a finite, non-negative number of milliseconds

InvalidMatrixError

class InvalidMatrixError extends AclConfigError {}
Signature — a shape, not a call

InvalidSchemaError

class InvalidSchemaError extends AclConfigError { readonly where: string; // schema.objects.question.fields.status }
Signature — a shape, not a call

InvalidPermissionError

class InvalidPermissionError extends AclConfigError { readonly key: string; readonly field: string; }
Signature — a shape, not a call

InvalidRuleError

class InvalidRuleError extends AclConfigError { readonly key: string; readonly field: string; // rules[2] }
Signature — a shape, not a call

InvalidConditionError

class InvalidConditionError extends AclConfigError { readonly key: string; readonly field: string; readonly where: string; }
Signature — a shape, not a call

KeyMismatchError

class KeyMismatchError extends AclConfigError { readonly key: string; readonly object: string; readonly action: string; }
Signature — a shape, not a call

DuplicatePermissionError

class DuplicatePermissionError extends AclConfigError { readonly key: string; }
Signature — a shape, not a call

UnknownFieldError

class UnknownFieldError extends AclConfigError { readonly key: string; readonly field: string; readonly where: string; }
Signature — a shape, not a call

FieldTypeMismatchError

class FieldTypeMismatchError extends AclConfigError { readonly key: string; readonly field: string; readonly where: string; }
Signature — a shape, not a call

BangInAllowListError

class BangInAllowListError extends AclConfigError {}
Signature — a shape, not a call

DenyWithoutBaselineError

class DenyWithoutBaselineError extends AclConfigError {}
Signature — a shape, not a call

TargetsTransitionsConflictError

class TargetsTransitionsConflictError extends AclConfigError {}
Signature — a shape, not a call

UnknownObjectKeyError

class UnknownObjectKeyError extends AclConfigError { readonly key: ObjectKey; }
Signature — a shape, not a call

UnknownPermissionError

class UnknownPermissionError extends AclConfigError { readonly key: string; }
Signature — a shape, not a call

OriginCollisionError

class OriginCollisionError extends AclConfigError { readonly key: string; readonly origins: readonly [string, string]; }
Signature — a shape, not a call

UnvetoablePermissionError

class UnvetoablePermissionError extends AclConfigError { readonly key: string; }
Signature — a shape, not a call

MissingVetoSchemaError

class MissingVetoSchemaError extends AclConfigError { readonly key: string; readonly object: ObjectKey; }
Signature — a shape, not a call

UnpublishedVetoableError

class UnpublishedVetoableError extends AclConfigError { readonly key: string; }
Signature — a shape, not a call

MissingFreshnessBudgetError

class MissingFreshnessBudgetError extends AclConfigError {}
Signature — a shape, not a call

InvalidFreshnessError

class InvalidFreshnessError extends AclConfigError { readonly field: string; }
Signature — a shape, not a call

ActionNotAllowedError

class ActionNotAllowedError extends Error { readonly key: string; readonly reason: Reason; }
Signature — a shape, not a call

ActionNotAllowedError is the only error the library raises outside construction. pickAllowedFields throws it when the action is refused.

What is not exported

@evanion/acl exports no decide, decideFields or evaluateCondition. Each answers a fragment and trusts its caller for the rest.

Last updated on