{"_id":"@dleangen/cage-iam","_rev":"4-1139fcc10797175366e8887f59a08bdc","name":"@dleangen/cage-iam","dist-tags":{"latest":"0.1.3"},"versions":{"0.1.0":{"name":"@dleangen/cage-iam","version":"0.1.0","keywords":["cage","iam","actor-model","zanzibar","authorization"],"license":"UNLICENSED","_id":"@dleangen/cage-iam@0.1.0","maintainers":[{"name":"dleangen","email":"npm@leangen.net"}],"homepage":"https://github.com/dleangen/cage-iam#readme","bugs":{"url":"https://github.com/dleangen/cage-iam/issues"},"dist":{"shasum":"0f40b00af0431f2528d959c648840d8fed75e3c6","tarball":"https://registry.npmjs.org/@dleangen/cage-iam/-/cage-iam-0.1.0.tgz","fileCount":108,"integrity":"sha512-SzbIhw0SQidW79QCpJcwVDfyEvpYSNSQDLVDjcaasBQlh5oR9RKB2K2PIkDJGxhPWzsADntdsNCOAg7DYs6qMA==","signatures":[{"sig":"MEYCIQCZafrf+YAZC3mhtjeMVfKmXRRpCwHcuyy6uT4R5yvLzgIhAP243rV+6NcP6H2yWP2++8TTS4EwND32V+YC4OTN44pF","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":476949},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18.0.0"},"gitHead":"5f996b8a8331528272bf4901ca8e859613b07b3f","scripts":{"lint":"eslint","test":"mocha --forbid-only \"test/**/*.test.ts\"","build":"shx rm -rf dist && tsc -b","posttest":"npm run lint"},"_npmUser":{"name":"dleangen","email":"npm@leangen.net"},"repository":{"url":"git+https://github.com/dleangen/cage-iam.git","type":"git"},"_npmVersion":"11.6.1","description":"Generic Zanzibar-style Identity and Access Management for the CAGE ecosystem — the Actor Model / Model Layer implementation.","directories":{},"_nodeVersion":"22.19.0","dependencies":{"uuid":"^9"},"_hasShrinkwrap":false,"devDependencies":{"pg":"^8.23.0","shx":"^0.3.3","chai":"^4","mocha":"^11","eslint":"^9","ts-node":"^10","@types/pg":"^8.21.0","typescript":"^5","@types/chai":"^4","@types/node":"^18","@types/uuid":"^9","@types/mocha":"^10","typescript-eslint":"^8","@aws-sdk/client-kms":"^3.1107.0","eslint-config-prettier":"^10"},"_npmOperationalInternal":{"tmp":"tmp/cage-iam_0.1.0_1786418325864_0.05920823485972537","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@dleangen/cage-iam","version":"0.1.1","keywords":["cage","iam","actor-model","zanzibar","authorization"],"license":"UNLICENSED","_id":"@dleangen/cage-iam@0.1.1","maintainers":[{"name":"dleangen","email":"npm@leangen.net"}],"homepage":"https://github.com/dleangen/cage-iam#readme","bugs":{"url":"https://github.com/dleangen/cage-iam/issues"},"dist":{"shasum":"328714facd170c497db699eaaa1364636f2b7bb0","tarball":"https://registry.npmjs.org/@dleangen/cage-iam/-/cage-iam-0.1.1.tgz","fileCount":108,"integrity":"sha512-DbOkYgV2eTyYaJJIcRY8yVFNpA7t1tZqC8y7w1PZf4GcLMEbEuoqZ9RUfz/JjmDZhJoZevx1GVPLcMyZn4OMLw==","signatures":[{"sig":"MEYCIQDzQQGj1q6V15Vlmn5z1lwMpegF2eUjw1x587fQVkqx+QIhANAMOLvs8yQrLOzpnIb4FcewFfsxHpEm1fPSboXhpMhV","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":476949},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18.0.0"},"gitHead":"911729201d7c781076f00388229d4ff8380a7a54","scripts":{"lint":"eslint","test":"mocha --forbid-only \"test/**/*.test.ts\"","build":"shx rm -rf dist && tsc -b","posttest":"npm run lint"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:2986968f-3760-4816-993b-db4936b0312c"}},"repository":{"url":"git+https://github.com/dleangen/cage-iam.git","type":"git"},"_npmVersion":"11.16.0","description":"Generic Zanzibar-style Identity and Access Management for the CAGE ecosystem — the Actor Model / Model Layer implementation.","directories":{},"_nodeVersion":"24.18.0","dependencies":{"uuid":"^9"},"_hasShrinkwrap":false,"devDependencies":{"pg":"^8.23.0","shx":"^0.3.3","chai":"^4","mocha":"^11","eslint":"^9","ts-node":"^10","@types/pg":"^8.21.0","typescript":"^5","@types/chai":"^4","@types/node":"^18","@types/uuid":"^9","@types/mocha":"^10","typescript-eslint":"^8","@aws-sdk/client-kms":"^3.1107.0","eslint-config-prettier":"^10"},"_npmOperationalInternal":{"tmp":"tmp/cage-iam_0.1.1_1786418601885_0.7721796306806652","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@dleangen/cage-iam","version":"0.1.2","keywords":["cage","iam","actor-model","zanzibar","authorization"],"license":"UNLICENSED","_id":"@dleangen/cage-iam@0.1.2","maintainers":[{"name":"dleangen","email":"npm@leangen.net"}],"homepage":"https://github.com/dleangen/cage-iam#readme","bugs":{"url":"https://github.com/dleangen/cage-iam/issues"},"dist":{"shasum":"2f2a7174a18fe80a519575fb407ebdb6e3c1e14a","tarball":"https://registry.npmjs.org/@dleangen/cage-iam/-/cage-iam-0.1.2.tgz","fileCount":108,"integrity":"sha512-9EDq+KQ9dWHj3dUbUCkrONFKO/PoIqgKfG8JPiaWDTeb6KwCZgaaVggy4y7lN8d2Wow7ssFVsjB3o8KapnQy2Q==","signatures":[{"sig":"MEQCIE5kYJXF8rANJ7rzhy+TS/x4B2UZBSE4m/QRVzHdUM4PAiBURyTxpS4uko8p0jAPfVToatPI20U32KXPW1kzfHZ1pg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":476925},"main":"dist/index.js","types":"dist/index.d.ts","engines":{"node":">=18.0.0"},"gitHead":"d5a5a17540df13c0c86a8b2bb2ebb3774dfd994c","scripts":{"lint":"eslint","test":"mocha --forbid-only \"test/**/*.test.ts\"","build":"shx rm -rf dist && tsc -b","posttest":"npm run lint"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:2986968f-3760-4816-993b-db4936b0312c"}},"repository":{"url":"git+https://github.com/dleangen/cage-iam.git","type":"git"},"_npmVersion":"11.16.0","description":"Generic Zanzibar-style Identity and Access Management for the CAGE ecosystem — the Actor Model / Model Layer implementation.","directories":{},"_nodeVersion":"24.18.0","dependencies":{"uuid":"^11"},"_hasShrinkwrap":false,"devDependencies":{"pg":"^8.23.0","shx":"^0.3.3","chai":"^4","mocha":"^11","eslint":"^9","ts-node":"^10","@types/pg":"^8.21.0","typescript":"^5","@types/chai":"^4","@types/node":"^18","@types/mocha":"^10","typescript-eslint":"^8","@aws-sdk/client-kms":"^3.1107.0","eslint-config-prettier":"^10"},"_npmOperationalInternal":{"tmp":"tmp/cage-iam_0.1.2_1786428646012_0.12642039685785122","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@dleangen/cage-iam","version":"0.1.3","description":"Generic Zanzibar-style Identity and Access Management for the CAGE ecosystem — the Actor Model / Model Layer implementation.","license":"UNLICENSED","main":"dist/index.js","types":"dist/index.d.ts","scripts":{"build":"shx rm -rf dist && tsc -b","lint":"eslint","posttest":"npm run lint","prepublishOnly":"npm run build","test":"mocha --forbid-only \"test/**/*.test.ts\""},"dependencies":{"uuid":"^11"},"devDependencies":{"@aws-sdk/client-kms":"^3.1107.0","@types/chai":"^4","@types/mocha":"^10","@types/node":"^18","@types/pg":"^8.21.0","chai":"^4","eslint":"^9","eslint-config-prettier":"^10","mocha":"^11","pg":"^8.23.0","shx":"^0.3.3","ts-node":"^10","typescript":"^5","typescript-eslint":"^8"},"engines":{"node":">=18.0.0"},"keywords":["cage","iam","actor-model","zanzibar","authorization"],"repository":{"type":"git","url":"git+https://github.com/dleangen/cage-iam.git"},"gitHead":"097b0179561a7e727f38c1fbab7db430004b8128","_id":"@dleangen/cage-iam@0.1.3","bugs":{"url":"https://github.com/dleangen/cage-iam/issues"},"homepage":"https://github.com/dleangen/cage-iam#readme","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-SaZey33t4LJMb1raOrjc0YVsQc8Iw261Z/0EV50bD2w98DfMoJtMxIuFAc9Y7fIEyeVZEpgH6Zt9YrqOdXRjrQ==","shasum":"e7e0baa67064f1832e5d55836ff28f1c99900fd9","tarball":"https://registry.npmjs.org/@dleangen/cage-iam/-/cage-iam-0.1.3.tgz","fileCount":108,"unpackedSize":478836,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBMcdKS8pQ+X4ZF/4qczocWZxo7ccp/KcJJn8CJZ69jcAiEAgTT9ukz5F6snPcofFD4xsFeDurhaxdoWIOjGyhQo8w8="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:2986968f-3760-4816-993b-db4936b0312c"}},"directories":{},"maintainers":[{"name":"dleangen","email":"npm@leangen.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/cage-iam_0.1.3_1786429475379_0.24206507348586537"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-11T03:18:45.651Z","modified":"2026-08-11T06:24:35.683Z","0.1.0":"2026-08-11T03:18:46.094Z","0.1.1":"2026-08-11T03:23:22.038Z","0.1.2":"2026-08-11T06:10:46.183Z","0.1.3":"2026-08-11T06:24:35.561Z"},"bugs":{"url":"https://github.com/dleangen/cage-iam/issues"},"license":"UNLICENSED","homepage":"https://github.com/dleangen/cage-iam#readme","keywords":["cage","iam","actor-model","zanzibar","authorization"],"repository":{"type":"git","url":"git+https://github.com/dleangen/cage-iam.git"},"description":"Generic Zanzibar-style Identity and Access Management for the CAGE ecosystem — the Actor Model / Model Layer implementation.","maintainers":[{"name":"dleangen","email":"npm@leangen.net"}],"readme":"# cage-iam\n\n`cage-iam` is a generic, headless Identity and Access Management (IAM) design for the [CAGE ecosystem](https://github.com/dleangen/cage-core): 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.\n\nIt 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.\n\n## Status\n\n**Proof of concept — usable, not enterprise-grade.** (Revised from this repo's original \"design hypothesis\" framing per [#37](https://github.com/dleangen/cage-iam/issues/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.\n\nThe design in [`docs/design.md`](docs/design.md) is a draft (v0.5.0), spun out of [cage-core#29](https://github.com/dleangen/cage-core/issues/29) into its own repo per [cage-coordinator#69](https://github.com/dleangen/cage-coordinator/issues/69), since the design's scope is a standalone library rather than a patch to cage-core's existing responsibilities.\n\nTwo halves, both implemented now:\n\n- The **Model Layer** — the modeled vocabulary described in the design doc (§1.4) — lives in `src/` (see \"Package: the Model Layer implementation\" below).\n- 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.\n\n## Scope\n\n1. **Identity** — who exists. Registering and managing Actors (Personas, Organizations, Agents).\n2. **Access** — who can do what. Registering Assets as authorization boundaries, defining relationships (grants), checking permissions.\n3. **Accounts** — how you authenticate. Credentials, preferences, configuration. *(Future scope, deferred until identity and access are solid.)*\n\n## Package: the Model Layer implementation\n\n```\nnpm install @dleangen/cage-iam\n```\n\n```ts\nimport { ActorModel, entityRef, SchemaResolutionEngine } from '@dleangen/cage-iam'\n// Bring your own EventStore and PiiKeyStore — this package assumes both\n// already exist; see pii-key-store.ts for why the PII key store must be\n// a real, durable store and never the in-memory test double.\nimport { MyEventStore } from './my-event-store'\nimport { MyPiiKeyStore } from './my-pii-key-store'\n\n// The ResolutionEngine can be this package's own in-repo implementation\n// (src/engine/ — see \"Package: the resolution engine implementation\"\n// below), loaded with the schema this package's own commands need — a\n// short excerpt below; docs/schema-reference.md documents the real one\n// in full (every namespace this package's commands actually write to).\nconst resolutionEngine = SchemaResolutionEngine.fromSource(`\n  organization { relation owner }\n  confinement   { relation owner relation reader }\n  collection    { relation owner relation reader relation collection }\n  group         { relation owner relation member permission access = owner | member }\n`)\n// ...or any other Zanzibar-compatible service that speaks the same\n// four verbs (grant/revoke/check/listTuples) — a SpiceDB/OpenFGA\n// client, etc.\n// const resolutionEngine = new MyResolutionEngine()\n\nconst actorModel = new ActorModel(new MyEventStore(), resolutionEngine, new MyPiiKeyStore())\n\nconst org = await actorModel.registerOrganization('acme.com')\nconst person = await actorModel.registerPerson()\nconst jane = await actorModel.registerPersona(person.id, 'jane@acme.com', 'Jane Doe')\n\nconst support = await actorModel.createGroup(org.id, 'Support Engineers')\nawait actorModel.addMember(entityRef('persona', jane.id), entityRef('group', support.id))\n\nconst vault = await actorModel.createCollection(org.id, 'Vault')\nawait actorModel.grantAccess(\n  entityRef('group', support.id),\n  'reader',\n  entityRef('confinement', org.confinementId),\n)\n\n// true — the Group's own direct grant, resolved by the real schema above,\n// not a stub. (This minimal excerpt has no traversal wiring Group\n// membership into Confinement access, so checking *jane's* \"reader\" on\n// the Confinement — as opposed to the Group's — would be `false` here;\n// the full schema in docs/schema-reference.md is the same story until a\n// deployment adds that wiring itself, per its own closing note.)\nawait actorModel.checkAccess(entityRef('group', support.id), 'reader', entityRef('confinement', org.confinementId))\n```\n\n`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.\n\n### What's implemented\n\n- §1.2/§1.5 — Person, Persona, Organization (with its Extent, Confinement, and Root), Association, Group, Collection; the fixed five-level relation vocabulary plus `member`.\n- §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.\n- §5/§6 — event-sourced persistence for the core aggregates and access operations.\n- §1.6 — Patterns as CRUD reference data, and applying one to an Organization.\n- §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.\n- §1.4 — Persona `name` (required) and optional `description`, alongside `email`.\n- §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).\n- §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.\n- §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`.\n- §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.\n\n### What's deferred\n\n- **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.\n- **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).\n\n### A source-document ambiguity, flagged not silently resolved\n\nThe 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).\n\n### Examples: durable adapters (cage-iam#37 Q1)\n\n`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):\n\n- **`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.\n- **`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.\n\nBoth 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).\n\n### Development\n\n```\nnpm install\nnpm run build\nnpm test        # mocha + chai, then eslint via posttest\n```\n\n## Package: the resolution engine implementation\n\n`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/`).\n\nOne thing flagged rather than silently resolved while building this, since fixed, plus one still open on the issue tracker:\n\n- **#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.\n- **#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.\n\n## CAGE ecosystem\n\n| Package | Description |\n|---------|-------------|\n| [cage-core](https://github.com/dleangen/cage-core) | Agent protocol and Agent Bundle contract |\n| [cage-chart](https://github.com/dleangen/cage-chart) | Source Chart schema and compiler |\n| [cage-cli](https://github.com/dleangen/cage-cli) | CLI for scaffolding CAGE-enabled projects |\n| [cage-coordinator](https://github.com/dleangen/cage-coordinator) | Orchestration methodology and dispatch FSM |\n| [cage-issues](https://github.com/dleangen/cage-issues) | Issue tracker |\n| [cage-git](https://github.com/dleangen/cage-git) | Git workflow hooks and safe branch landing |\n| **cage-iam** | Generic Zanzibar-style Identity and Access Management design (this repo) |\n| [cage-documents](https://github.com/dleangen/cage-documents) | Per-repo document storage with namespace/index convention |\n\n## License\n\nProprietary — © 2026 David Leangen. All rights reserved.\n","readmeFilename":"README.md"}