Skip to Content
AuthorizationHow do I give my rules to another service?

Unreleased changes

These pages document main, which has changes 0.1.2 does not. npm install @evanion/acl gives you 0.1.2, documented here. Values on these pages are produced by running the current source on every build.

Publishing ACL Contracts

In a distributed system, one service often owns the authorization rules for a specific resource, but other services need to know those rules to provide a consistent user experience. For example, an orders service owns the rules for issuing refunds, but the storefront needs to know those rules to decide whether to show a “Request Refund” button.

If the storefront were to maintain its own copy of these rules, it would inevitably diverge from the source of truth. To prevent this, @evanion/acl allows a service to publish a “contract”—a reduced version of its policy matrix—that other services can adopt and evaluate locally.

Creating a Public Contract with serialize

A service defines which parts of its policy are intended for public consumption using the .visibility('public') mark. Any permission not marked as public is considered internal and is stripped away during serialization.

The serialize function produces the contract. A “reduced” serialization keeps only the public permissions and their associated rules, while a “full” serialization returns the entire document.

import { , , } from '@evanion/acl'; type = { : string; : string[]; : string }; type = { : string }; type = { : string }; const = < , { 'orders:order': ; 'orders:ledger': }, { 'orders:order': 'refund'; 'orders:ledger': 'reconcile' } >({ : 'orders@7' }) .('orders:order', () => .('refund', .('subject.roles', 'bookseller')) .('refund', .('subject.tier', 'probation')) .('public'), ) .('orders:ledger', () => .('reconcile', .('subject.roles', 'accountant')), ) .(); const = (, 'reduced'); ..(() => .); // -> ['orders:order.refund'] const = <, { 'orders:order': }>(); const = { : 'u1', : ['bookseller'], : 'permanent' }; .(, 'orders:order', 'refund').; // -> true

The resulting reduced document is the formal contract. It contains only the permissions the owner intended to share (e.g., orders:order.refund) and excludes internal-only permissions (e.g., orders:ledger.read). The consumer adopts this contract using parseMatrix, allowing them to make local decisions without making a network call to the owner for every check.

The serialization process for a public contract. Only permissions marked with public visibility are included in the reduced document. This contract is then adopted by consumers, who use it for local decisions, while the owner continues to enforce the rules on the server.

The Integrity of Published Permissions

When a permission is published, it is exported in its entirety. This includes all allow rules and all deny rules.

If a permission’s logic depends on a deny rule (e.g., “Allow refund unless the user is on probation”), that deny rule must be part of the published contract. If it were stripped, a consumer might incorrectly grant a permission that the owner would refuse.

import { , , } from '@evanion/acl'; import type { Matrix } from '@evanion/acl'; const : Matrix = { : 'orders@7', : 300_000, : { : { 'orders:order': { : { : 'string' } }, 'orders:ledger': { : { : 'string' } }, }, }, : [ { : 'orders:order.refund', : 'orders:order', : 'refund', : 'public', : [ { : [ { : 'subject.roles', : 'contains', : 'bookseller' }, ], }, ], : [ { : [{ : 'subject.tier', : 'eq', : 'probation' }] }, ], }, { : 'orders:ledger.reconcile', : 'orders:ledger', : 'reconcile', : 'internal', : [{ : [] }], }, ], }; const = (); const = (, 'reduced'); ..(() => .); // -> ['orders:order.refund'] .(.?. ?? {}); // -> ['orders:order'] 'visibility' in (.[0] ?? {}); // -> false // The consumer adopts it the way it adopts any foreign document, and reports // when it last checked the contract was current. const = (, { : .() }); const = { : 'u1', : ['bookseller'], : 'permanent' }; .(, 'orders:order', 'refund').; // -> true .(, 'orders:ledger', 'reconcile').; // -> 'unknown-action'

Because the contract includes the full set of rules for the published permission, the consumer’s local decision will exactly match the owner’s decision for that specific key.

Contract Terminality

A published contract is “terminal.” The visibility marks are stripped during serialization, meaning a consumer cannot re-serialize a contract to publish it again to a third party.

Managing Contract Freshness

Since consumers evaluate a local copy of the contract, there is a risk that the copy becomes stale. @evanion/acl manages this through a “freshness budget.”

The owner defines a maxStale value (in milliseconds) in the document, which sets the maximum allowed age of a copy. The consumer tracks when the document was last updated using fetchedAt.

BoundSourcePurpose
maxStale (Document)The OwnerSets the absolute ceiling for how long a copy may be considered fresh.
maxStale (Consumer)The ConsumerAllows the consumer to tighten the owner’s ceiling (e.g., for higher-security areas).
fetchedAtThe ConsumerThe timestamp of the last successful synchronization.

If the current time exceeds fetchedAt + min(ownerMaxStale, consumerMaxStale), any attempt to use the contract results in a stale-contract response. This forces the consumer to refetch the document to repair the budget.

Enabling Third-Party Vetoes

In some organizations, a separate team (such as Compliance or Fraud) must be able to override the permissions of a service. This is handled via Deny Overlays.

To allow another team to veto specific permissions, the owner must explicitly list those keys as vetoable during serialization. If a key is not published in the contract, a third party cannot write a valid overlay for it, as they would have no schema to validate their rules against.

serialize(access, 'reduced', { vetoable }) will throw an UnpublishedVetoableError if the vetoable list includes a key that would otherwise be dropped from the reduced contract.

Where to go next

  • Adopting Rules: Learn how to use parseMatrix and manage freshness budgets when consuming a contract.
  • Many services: Explore how to combine multiple published contracts into a single federated view.
  • What can throw?: See the list of errors that occur during contract serialization and adoption.
Last updated on