/** * Capability emulation — generic, with no domain rules. It models NextGraph * ReadCaps (and write caps) as a data layer can. * * In NextGraph a ReadCap is possession of a document's (repo's) read key: the * broker only delivers documents the wallet holds a cap for. The access unit is * therefore the document = repo, identified here by its NURI — the `@graph` an * item lives in, rather than the item. (A store is just a container repo, and * holding a store's cap does not grant the repos it references — each document * carries its own cap — so this registry is purely per-document, with no * store-level inheritance.) * * Sharing here is DIRECTED: a grant issues one grantee the read cap of one * document (`grantRead(doc, granteeId)`). Whether two identities are "connected" * — and therefore whether such a grant should be issued — is an application * concept the consumer owns; this layer only records the resulting per-document * grants. At migration this whole layer disappears: the broker/verifier enforces * the real caps and `useShape` returns only authorized documents. */ import type { Nuri, PrincipalId, Scope } from "./types"; /** * Who holds the read/write cap of each document. The consumer populates it via * cap operations (make-public, directed grant…) exactly as it will in the * target; this layer enforces possession generically, with no policy of its own. */ export class CapRegistry { /** doc NURI → principals holding its READ cap. */ private readers = new Map>(); /** doc NURI → principals holding its WRITE cap. */ private writers = new Map>(); /** doc NURIs readable by everyone (public_store repos — no cap needed). */ private publicDocs = new Set(); /** doc NURI → its declared (scope, owner), as recorded at {@link open}. Lets * the consumer re-derive which documents are `protected` and who owns them * (see {@link protectedDocsOf}) so it can issue directed grants, without * re-supplying that per-document — it already declared it at open. */ private policy = new Map(); /** Grant `grantee` the READ cap of document `doc` — a directed grant. */ grantRead(doc: Nuri, grantee: PrincipalId): void { add(this.readers, doc, grantee); } /** Grant `principal` the WRITE cap of document `doc`. */ grantWrite(doc: Nuri, principal: PrincipalId): void { add(this.writers, doc, principal); } /** Mark `doc` public (readable without a cap — a public_store repo). */ makePublic(doc: Nuri): void { this.publicDocs.add(doc); } /** * Apply the caps a creator attaches to a fresh document, by scope. Public → * world-readable; protected/private → only the owner reads. The owner always * holds the write cap. Further sharing is a separate explicit grant. */ open(doc: Nuri, scope: Scope, owner: PrincipalId): void { if (scope === "public") this.makePublic(doc); else this.grantRead(doc, owner); this.grantWrite(doc, owner); this.policy.set(doc, { scope, owner }); } /** * The `protected` documents owned by `owner`, as recorded at {@link open}. The * consumer uses this to issue directed read grants: it decides who may read an * owner's protected documents (its own relationship concept) and calls * {@link grantRead} on each of these documents for each such reader. Public * documents are already world-readable and private documents stay owner-only, * so only the protected ones are surfaced here. * * This mirrors a native cap operation: in the target, sharing a protected repo * with another identity issues that identity the repo's ReadCap. Here the * consumer selects the documents via this accessor and grants the emulated read * cap on the same unit. */ protectedDocsOf(owner: PrincipalId): Nuri[] { const out: Nuri[] = []; for (const [doc, { scope, owner: o }] of this.policy) { if (scope === "protected" && o === owner) out.push(doc); } return out; } /** Is `doc` under any READ-cap policy? (Undeclared docs are not enforced.) */ governsRead(doc: Nuri): boolean { return this.publicDocs.has(doc) || this.readers.has(doc); } /** Does `principal` hold a READ cap for `doc` (or is `doc` public)? */ canRead(doc: Nuri, principal: PrincipalId | null): boolean { if (this.publicDocs.has(doc)) return true; if (principal === null) return false; return this.readers.get(doc)?.has(principal) ?? false; } /** Is `doc` under any WRITE-cap policy? */ governsWrite(doc: Nuri): boolean { return this.writers.has(doc); } /** Does `principal` hold a WRITE cap for `doc`? */ canWrite(doc: Nuri, principal: PrincipalId | null): boolean { if (principal === null) return false; return this.writers.get(doc)?.has(principal) ?? false; } /** No READ policy declared → the read filter stays inert (passthrough). */ hasReadPolicy(): boolean { return this.readers.size > 0 || this.publicDocs.size > 0; } /** No WRITE policy declared → the write guard stays inert (passthrough). */ hasWritePolicy(): boolean { return this.writers.size > 0; } clear(): void { this.readers.clear(); this.writers.clear(); this.publicDocs.clear(); this.policy.clear(); } } function add(m: Map>, doc: Nuri, principal: PrincipalId): void { let s = m.get(doc); if (!s) m.set(doc, (s = new Set())); s.add(principal); }