@zanzibar-ts/core
v0.1.1
Published
TypeScript-native, Zanzibar-style ReBAC authorization engine — in-process ~µs checks, a typed model builder, conditions (CEL/ABAC), and a D1 store for Cloudflare Workers
Maintainers
Readme
@zanzibar-ts/core
A TypeScript-native, Zanzibar-style relationship-based authorization (ReBAC) engine.
You describe a permission model, store relationships (tuples) — who relates how to what — and ask check / list questions. It runs in-process: a check is a ~microsecond function call, not a network round-trip, and the engine bundles to ~28KB minified (~8KB gzip) for Cloudflare Workers and the edge (measured).
- In-process / edge. No server, no network hop — works on Node, Bun, and Cloudflare Workers (with a bundled D1 store for durable, multi-tenant persistence).
- TypeScript-native. The model is code, authored with a typed builder; queries are type-checked against it — a typo'd relation is a compile error.
- Trustworthy by construction. Every release is proven by a five-way differential against OpenFGA, SpiceDB, a Datalog oracle, and an independent reference evaluator — plus property tests, fuzzing, and mutation testing. The provenance (AI-authored, verification-first) and the honest limits are disclosed in TRUST.md.
Quickstart
Define a model, create an in-memory engine, write a relationship, and check it. Anne is a member of
the eng group, and the eng group is a viewer of the readme, so anne can view the readme:
import { deepStrictEqual as assertEquals } from 'node:assert/strict';
import { defineModel } from '@zanzibar-ts/core/model';
import { createEngine } from '@zanzibar-ts/core';
// 1. Define the model — types and how permissions are derived. Checked by `tsc`.
const authz = defineModel((t) => ({
user: t.type(),
group: t.type({
// a group's members are users, or members of nested groups
member: t.assign('user', 'group#member'),
}),
document: t.type({
// a viewer is anyone assigned directly, OR a member of an assigned group
viewer: t.assign('user', 'group#member'),
}),
}));
// 2. Create an engine (in-memory by default).
const engine = createEngine(authz);
// 3. Write relationships.
await engine.write([
{ object: 'group:eng', relation: 'member', subject: 'user:anne' },
{ object: 'document:readme', relation: 'viewer', subject: 'group:eng#member' },
]);
// 4. Check. A decision is a Result VALUE — it never throws for "deny".
const decision = await engine.check({
object: 'document:readme',
relation: 'viewer',
subject: 'user:anne',
});
assertEquals(decision, { ok: true, value: true }); // ✅ anne views the readme via group:eng#member(The assertEquals alias only makes the example double as a CI-run test — in your app, branch on
the returned Result instead.)
Beyond check: listObjects ("what can anne access?"), listUsers ("who can access this?"),
expand (the membership tree), explain (why a decision happened), conditions (CEL-gated
grants — ABAC), consistency tokens (read-your-writes on replicated stores), and versioned model
storage.
Docs
- Full README & tutorial — the model toolkit, conditions, type safety, explain.
- How-to recipes — GitHub-style roles, time-bound access, multi-tenant isolation, OpenFGA DSL migration.
- Concepts — ReBAC & tuples, the consistency model, caveats/ABAC, limits & gotchas.
- Deploy to Cloudflare Workers + D1.
- TRUST.md — how correctness is proven, and what remains unproven.
Every ```ts block in the docs (including this one) is executed in CI.
Migrating from OpenFGA? @zanzibar-ts/dsl lowers
existing OpenFGA DSL text to a validated model.
License
MIT
