npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dleangen/cage-iam

v0.1.3

Published

Generic Zanzibar-style Identity and Access Management for the CAGE ecosystem — the Actor Model / Model Layer implementation.

Readme

cage-iam

cage-iam is a generic, headless Identity and Access Management (IAM) design for the CAGE ecosystem: a Zanzibar-style authorization engine over a small set of generic building blocks — Actor, Asset, Group, and a relation graph (reader → contributor → editor → admin → owner) — plus event-sourced per-tenant persistence, a delegation model, and GDPR erasure via crypto-shredding.

It has no domain knowledge of any specific business entity. It provides identity and access primitives that any calling system can use to register actors, register assets, and manage the access relationships between them.

Status

Proof of concept — usable, not enterprise-grade. (Revised from this repo's original "design hypothesis" framing per #37's thinking session: every open design question raised along the way is now closed or explicitly, deliberately parked — see that issue for the full resolution.) "Usable" means the Model Layer, the resolution engine, Delegation, and GDPR erasure (including Persona discovery, below) are real, working, and tested — not a sketch. "Not enterprise-grade" means the shipped defaults (SchemaResolutionEngine, and the EventStore/PiiKeyStore examples in docs/examples/) are deliberately lightweight rather than durable; scaling to production load means swapping in a real durable EventStore/PiiKeyStore and, for the resolution engine, SpiceDB or OpenFGA (both free, Apache-2.0, self-hosted) via the existing ResolutionEngine seam — see "Package: the resolution engine implementation" below for why that swap is meant to be cheap, not a rewrite.

The design in docs/design.md is a draft (v0.5.0), spun out of cage-core#29 into its own repo per cage-coordinator#69, since the design's scope is a standalone library rather than a patch to cage-core's existing responsibilities.

Two halves, both implemented now:

  • The Model Layer — the modeled vocabulary described in the design doc (§1.4) — lives in src/ (see "Package: the Model Layer implementation" below).
  • The resolution engine — originally a separate proposal (docs/resolution-engine-spec.md, merged as #14) — is implemented in src/engine/ (see "Package: the resolution engine implementation" below and that directory's own README.md for the phase-by-phase detail). Kept in this repo rather than spun out to its own, per an explicit decision on #16 — that's a location choice, not a statement that the two halves are coupled; ResolutionEngine (src/resolution-engine.ts) is still the seam between them, and a real deployment can still bring an entirely different engine (SpiceDB, OpenFGA, anything that speaks the same three verbs) instead of this one.

Scope

  1. Identity — who exists. Registering and managing Actors (Personas, Organizations, Agents).
  2. Access — who can do what. Registering Assets as authorization boundaries, defining relationships (grants), checking permissions.
  3. Accounts — how you authenticate. Credentials, preferences, configuration. (Future scope, deferred until identity and access are solid.)

Package: the Model Layer implementation

npm install @dleangen/cage-iam
import { ActorModel, entityRef, SchemaResolutionEngine } from '@dleangen/cage-iam'
// Bring your own EventStore and PiiKeyStore — this package assumes both
// already exist; see pii-key-store.ts for why the PII key store must be
// a real, durable store and never the in-memory test double.
import { MyEventStore } from './my-event-store'
import { MyPiiKeyStore } from './my-pii-key-store'

// The ResolutionEngine can be this package's own in-repo implementation
// (src/engine/ — see "Package: the resolution engine implementation"
// below), loaded with the schema this package's own commands need — a
// short excerpt below; docs/schema-reference.md documents the real one
// in full (every namespace this package's commands actually write to).
const resolutionEngine = SchemaResolutionEngine.fromSource(`
  organization { relation owner }
  confinement   { relation owner relation reader }
  collection    { relation owner relation reader relation collection }
  group         { relation owner relation member permission access = owner | member }
`)
// ...or any other Zanzibar-compatible service that speaks the same
// four verbs (grant/revoke/check/listTuples) — a SpiceDB/OpenFGA
// client, etc.
// const resolutionEngine = new MyResolutionEngine()

const actorModel = new ActorModel(new MyEventStore(), resolutionEngine, new MyPiiKeyStore())

const org = await actorModel.registerOrganization('acme.com')
const person = await actorModel.registerPerson()
const jane = await actorModel.registerPersona(person.id, '[email protected]', 'Jane Doe')

const support = await actorModel.createGroup(org.id, 'Support Engineers')
await actorModel.addMember(entityRef('persona', jane.id), entityRef('group', support.id))

const vault = await actorModel.createCollection(org.id, 'Vault')
await actorModel.grantAccess(
  entityRef('group', support.id),
  'reader',
  entityRef('confinement', org.confinementId),
)

// true — the Group's own direct grant, resolved by the real schema above,
// not a stub. (This minimal excerpt has no traversal wiring Group
// membership into Confinement access, so checking *jane's* "reader" on
// the Confinement — as opposed to the Group's — would be `false` here;
// the full schema in docs/schema-reference.md is the same story until a
// deployment adds that wiring itself, per its own closing note.)
await actorModel.checkAccess(entityRef('group', support.id), 'reader', entityRef('confinement', org.confinementId))

src/testing/ ships InMemoryEventStore, InMemoryResolutionEngine, and InMemoryPiiKeyStore — used by this package's own test suite, and useful for prototyping, but not a substitute for a durable event store or durable key storage. InMemoryResolutionEngine (the testing one) does direct tuple matching only; it does not evaluate a schema or derive permissions — for a real schema-aware engine, even a non-durable one, see SchemaResolutionEngine below, not this stub. InMemoryPiiKeyStore especially must never reach production — losing its keys on a process restart would silently "erase" every Person that was never actually erased.

What's implemented

  • §1.2/§1.5 — Person, Persona, Organization (with its Extent, Confinement, and Root), Association, Group, Collection; the fixed five-level relation vocabulary plus member.
  • §2 — the automatic registration tuples for Organization, Association, Persona, Group, and Collection creation; the operational grant/revoke/membership tuple patterns, including Association's peer-based (non-hierarchical) membership with who-performed-it tracking.
  • §5/§6 — event-sourced persistence for the core aggregates and access operations.
  • §1.6 — Patterns as CRUD reference data, and applying one to an Organization.
  • §2 — cycle rejection for Group nesting ("Groups must always form a DAG"), checked at write time from the event log — see src/commands/group-nesting.ts for why that doesn't need the resolution engine's help. Collection nesting has no equivalent check because it needs none: a Collection's parent is fixed at creation time and this package has no reparent/move operation, so a cycle is structurally unreachable through the current API, not merely unlikely — see that file's doc comment.
  • §1.4 — Persona name (required) and optional description, alongside email.
  • §16 — Organization parent hierarchy (resolved in cage-iam#14: organization { relation parent; permission admin = owner | parent^ }). ActorModel.setOrganizationParent/removeOrganizationParent/getOrganizationParent, with the same event-log-based cycle rejection as Group nesting (src/commands/organization-parent.ts) and an enforced at-most-one-parent invariant (the design doc's "joint ownership needs a separate joint entity" language, Open Question #9, rules out silently allowing a second concurrent parent).
  • §10 GDPR crypto-shredding — complete: Persona email, name, and description are all stored encrypted (emailEnc/nameEnc/descriptionEnc, AES-256-GCM under a per-Person key) rather than in plaintext, closing the exact gap the design doc calls out by name. ActorModel.erasePerson implements all six steps of the erasure procedure. Steps 1/3 (sole-ownership precondition, cascading tuple revocation) are auto-discovered via resolutionEngine.listTuples (cage-iam#8's fix (2)) and cover every relation shape this package writes — access relations, Group/Extent membership, and, as of cage-iam#28's fix, Association membership too, via AssociationMemberRemoved's new reason: 'erasure' | 'governance' discriminant (performedByPersonaId correspondingly made optional — absent for 'erasure', required for 'governance'). Persona discovery (cage-iam#37 Q3, resolved) — personaIds is now optional: omit it and erasePerson discovers every Persona the Person has ever registered itself, via a reverse-index projection (queries/list-personas.ts) PersonaRepository.register maintains; pass an explicit list only to override. options.assetsToCheckOwnership/grantsToRevoke and an explicit personaIds are all pure supplements now, never requirements.
  • §1.2 Agent registration (resolved via a live Thinker session, cage-iam#10, CAGE-2026-0265) — ActorModel.registerAgent(personaId, name), atomic, mirroring registerPersona's shape. Zero automatic resolution-engine grant: unlike every other aggregate this package registers, a fresh Agent gets no owner tuple — registration is never a privilege-escalation event, so an Agent's capability set is always fully traceable to explicit Delegation grants, never partly implicit. See src/repositories/agent-repository.ts.
  • §9 Delegation (same session as Agent registration, cage-iam#10) — ActorModel.delegate(delegatorPersonaId, agentId, relation, object, expiresAt). Scope: strictly narrower than the delegator's own level, never equal (mechanical rule over the fixed vocabulary order; delegating owner itself is impossible — nothing is strictly above it). Expiry: mandatory, tracked at the Model Layer via a DelegationGranted event, not by the resolution engine — ActorModel.listExpiredDelegations discovers what's expired, revokeAccess is how a caller actually revokes it (no background sweeper). Depth: one hop only, Persona → Agent — enforced by delegate's own signature. Supersedes §9's original "composite subject syntax" proposal entirely; see src/commands/delegate.ts and design.md's Open Question #6 for the full reasoning. Its correctness depends on cage-iam#21's dispatch fix, which is why that fix landed first. Two threads flagged as real but explicitly non-blocking, same as design.md's own framing: delegated access isn't currently tracked back to the delegator's live state if later revoked, and Agent isn't yet covered by §10 erasure.

What's deferred

  • Authorization of who may call addMember/removeMember on an Extent — Root's "single hard-coded permission" is an application-level rule, not a resolution-engine tuple; not enforced by this package.
  • Dynamic schema support — open question in the design document itself (§10 note: partially informed by cage-iam#14's base+overlay schema composition proposal, not fully resolved).

A source-document ambiguity, flagged not silently resolved

The design document is internally inconsistent about tuple direction for Collection nesting: its own "Collection creation" example for nesting under a Confinement puts the child Collection as subject and the parent as object, while its next example (nesting under a parent Collection) and its separate "Collection hierarchy" section both put the parent as subject. This implementation follows the latter, majority form — grant(parent, collection, child) — uniformly for both cases. See the comment above COLLECTION_RELATION in src/repositories/collection-repository.ts. Flagging for the design author to confirm/correct upstream (cage-core#29).

Examples: durable adapters (cage-iam#37 Q1)

docs/examples/ has two illustrative, non-shipped adapters, each with a .smoke.ts script that actually exercises it against real infrastructure (not part of npm test — they need a real Postgres/KMS-compatible endpoint reachable; see each file's own header for how to run it):

  • postgres-event-store.ts — a durable EventStore backed by a single Postgres table, (stream_id, seq) as the primary key for real optimistic concurrency. Neither this nor TupleStore gets a maintained reference implementation from this package — "bring your own, we specify the interface" stays the permanent position for both; this is a worked example, not a first-party option.
  • kms-pii-key-store.ts — a durable PiiKeyStore using KMS envelope encryption (a real KMS Encrypt/Decrypt call is structurally required by the constructor — there is no "just use a local key" fallback to slip into). Treated differently from EventStore on purpose: a bad EventStore is merely unreliable, but a bad PiiKeyStore — like this package's own InMemoryPiiKeyStore test double — fails silently, which is a materially worse failure mode for something GDPR erasure depends on.

Both were verified against real (throwaway, Dockerized) infrastructure before being committed, not just written and trusted — the Postgres example caught a real bug this way (node-postgres returns a JS Date for TIMESTAMPTZ, not the ISO string EventStore expects).

Development

npm install
npm run build
npm test        # mocha + chai, then eslint via posttest

Package: the resolution engine implementation

src/engine/ implements docs/resolution-engine-spec.md (merged as #14) end to end: schema grammar and static validation, a tuple store with the bidirectional/by-relation indexing §1.1 requires, the check() resolution algorithm (dispatch, permission derivation, the inherit primitive, cycle-safe caching, a configurable depth bound), composed schemas (base+overlay), and batch grant/revoke/check. SchemaResolutionEngine is the class that ties all of it together into a real ResolutionEngine — see the usage example above, src/engine/README.md for the phase-by-phase detail, and src/engine/schema-resolution-engine.ts's own doc comment for what's still not durable (its default tuple store is in-memory, same tradeoff as everything else under testing/).

One thing flagged rather than silently resolved while building this, since fixed, plus one still open on the issue tracker:

  • #21, fixed — a permission and a relation sharing the same name on one type made the permission unreachable (the spec's original dispatch order checked "is this a declared relation" before "is this a declared permission"), which broke docs/schema-reference.md's intended owner-cascades-down chain for admin/editor/contributor/reader. A real tension between this package's Model Layer convention and the spec's original dispatch priority, not a typo — fixed by two coordinated pieces in src/engine/ (schema/parse.ts's term disambiguation, resolve.ts's dispatch order), neither requiring a schema or Model Layer change. owner now correctly cascades all the way down. See src/engine/README.md and docs/resolution-engine-spec.md's §2.3/§4.2 revision notes for the full reasoning.
  • #8, now fully closed — this package answers "who owns this specific object" from its own event log (fix (1)), and "what does this Persona own" from the resolution engine's reverse-by-subject index via listTuples (fix (2)). Together these are what let ActorModel.erasePerson's §10 steps 1/3 auto-discover rather than require caller-supplied candidates — see that command's own doc comment.

CAGE ecosystem

| Package | Description | |---------|-------------| | cage-core | Agent protocol and Agent Bundle contract | | cage-chart | Source Chart schema and compiler | | cage-cli | CLI for scaffolding CAGE-enabled projects | | cage-coordinator | Orchestration methodology and dispatch FSM | | cage-issues | Issue tracker | | cage-git | Git workflow hooks and safe branch landing | | cage-iam | Generic Zanzibar-style Identity and Access Management design (this repo) | | cage-documents | Per-repo document storage with namespace/index convention |

License

Proprietary — © 2026 David Leangen. All rights reserved.