diff --git a/docs/readcap-and-nuri-model.md b/docs/readcap-and-nuri-model.md index 039e585..7c13d8d 100644 --- a/docs/readcap-and-nuri-model.md +++ b/docs/readcap-and-nuri-model.md @@ -16,6 +16,8 @@ Purpose: to give the ground truth of NextGraph's access-rights model, in order t > > - **A comment describing the CURRENT state is not the intent.** §3's DIRECTION block exists because `RepoLinkV0`'s comment was read as the target model. It is not. > - **A word you recognise probably does not mean what you think.** `branch` is not git's. `wallet` is only a keyring — what we call a virtual user is a **user** (a *site*). Check the type before using the word. +> - **NextGraph's naming ALWAYS wins over any rule of ours — including the one that follows.** Where upstream has a word for something, that is the word, full stop. `publisher` is a case in point: it appears 126 times in the engine (`as_publisher`, `publisher_advert`) for a **pub/sub role on a topic**, and it must be used, unchanged, whenever that is what is meant. +> - **What is banned is OUR loose "publish a document"**, which has no upstream referent and covers three different acts: *placing a document in a public store*, *making it findable*, and (in this emulation only) *handing out a key*. Every design discussion that used it drifted between them. NextGraph knows only the first — a document IS IN a public store, and brokers serve it accordingly. Say which act you mean. If you catch yourself writing "publish a document", you have not yet decided which one. This never licenses renaming something upstream calls `publisher`. > - **"I looked and it is not there" is not a finding.** §4quinquies once stated that no register existed for received caps, after checking one code path. `AddLink` had been sitting next to `AddRepo` in the same file the whole time. Absence needs at least as much evidence as presence — and an implementation *cache* (like local user storage) is never the model: it is what the model fills. --- diff --git a/packages/client/src/emulated-verifier/branch-registers.ts b/packages/client/src/emulated-verifier/branch-registers.ts index afc6015..c74ac6f 100644 --- a/packages/client/src/emulated-verifier/branch-registers.ts +++ b/packages/client/src/emulated-verifier/branch-registers.ts @@ -107,7 +107,7 @@ export function holdOwnCap(id: string, scope: Scope, doc: Nuri, cap: ReadCap): v // by the very session that created it. Mint once, store it, hold that one. caps.learn(cap); // Publication is a registry fact, not a stored one, so it is applied separately. - if (scope === "public") caps.publishRepoLink(doc); + if (scope === "public") caps.recordInPublicStore(doc); } /** diff --git a/packages/client/src/emulated-verifier/caps.ts b/packages/client/src/emulated-verifier/caps.ts index 2084078..71fd691 100644 --- a/packages/client/src/emulated-verifier/caps.ts +++ b/packages/client/src/emulated-verifier/caps.ts @@ -83,14 +83,14 @@ export class CapRegistry { /** holder → the caps they hold, indexed by the cap-less NURI. */ private heldByHolder = new Map>(); /** - * Documents published as a shareable repo link (`RepoLinkV0`) — the emulated - * public store. This is NOT a read grant: a published document is read by + * Documents in a PUBLIC store, as this emulation records it. This is NOT a read grant: + * such a document is read by * whoever HOLDS the link, exactly like §5 of the brief says ("whoever has the * URL reads the content"), and holding it means having received it. The set * exists so the library can refuse to surface a document its holder never - * published. *(This fed `discovery.submitToIndex`, removed 2026-07-30; the flag is kept because publishing is still what turns a document into a shareable link.)* + * in a public store. *(This fed `discovery.submitToIndex`, removed 2026-07-30.)* */ - private published = new Set(); + private inPublicStore = new Set(); /** doc NURI → principals holding its WRITE cap. Decorative until P1b. */ private writers = new Map>(); /** Fired whenever a holder gains a cap — a cap delivered asynchronously must @@ -181,23 +181,34 @@ export class CapRegistry { // --- publication (the public store) ------------------------------------- /** - * Publish `nuri` as a shareable repo link and return it — the upstream - * `RepoLinkV0 { read_cap }`, which whoever receives it can open. The consumer - * puts this link (not the bare NURI) in what it makes discoverable. + * Record that `nuri` sits in a PUBLIC store, and mint its cap. * - * NOT recursive: the published document may REFERENCE private documents, and the - * reference grants nothing on what it references — that non-recursiveness is - * what lets a public object point at a private identity without disclosing it. + * **Named for what it does here, not for what it means upstream** — and the gap is the + * point. It was called `publishRepoLink`, and "publish" is banned in this repo + * (`docs/readcap-and-nuri-model.md`, the traps block) precisely because it blurs three + * acts: placing a document in a public store, making it findable, and handing out a + * key. This method does the first and, as an emulation artefact, the third. + * + * Upstream a document in a public store is readable because the STORE is public and + * brokers serve it accordingly (`expose_outer`); a `PublicRepoLinkV0` carries no + * `read_cap` at all (`engine/net/src/types.rs:5105-5127`). Here there is no broker that + * serves differently, so possession stands in for it — the emulation is OVER-strict, + * not inverted: it under-grants, and "circulate the reference" remains the right + * gesture at migration. See `readcap-and-nuri-model.md` §4sexies. + * + * NOT recursive: a document in a public store may REFERENCE private ones, and the + * reference grants nothing on what it references. That non-recursiveness is what lets + * a public object point at private content without disclosing it. */ - publishRepoLink(nuri: Nuri): ReadCap { + recordInPublicStore(nuri: Nuri): ReadCap { const target = targetOf(nuri); - this.published.add(target); + this.inPublicStore.add(target); return this.mint(target); } - /** Was `nuri` published as a repo link? (An emitter-side guard, not a right.) */ - isPublished(nuri: Nuri): boolean { - return this.published.has(targetOf(nuri)); + /** Is `nuri` recorded as sitting in a public store? An emitter-side fact, not a right. */ + isInPublicStore(nuri: Nuri): boolean { + return this.inPublicStore.has(targetOf(nuri)); } /** @@ -211,7 +222,7 @@ export class CapRegistry { * arming their guard here would be enforcement this batch does not do. */ open(nuri: Nuri, scope: Scope): ReadCap { - return scope === "public" ? this.publishRepoLink(nuri) : this.mint(nuri); + return scope === "public" ? this.recordInPublicStore(nuri) : this.mint(nuri); } // --- enforcement gate --------------------------------------------------- @@ -280,7 +291,7 @@ export class CapRegistry { * NOT what an identity change does (that switches heldByHolder, see the header). */ clear(): void { this.heldByHolder.clear(); - this.published.clear(); + this.inPublicStore.clear(); this.writers.clear(); this.issued = false; this.notify(); diff --git a/packages/client/src/shared-wallet/account-registry.ts b/packages/client/src/shared-wallet/account-registry.ts index 6cffb53..cf68dd2 100644 --- a/packages/client/src/shared-wallet/account-registry.ts +++ b/packages/client/src/shared-wallet/account-registry.ts @@ -1038,7 +1038,7 @@ export async function listMyEntityDocs(id: string, scope: Scope): Promise { expect(bob.caps.capFor(doc)).toBe(cap); }); -test("publishRepoLink returns a cap-bearing link; reading it still means HOLDING it", () => { +test("recordInPublicStore returns a cap-bearing link; reading it still means HOLDING it", () => { const { caps, become } = registry("alice"); const doc = "did:ng:o:public-doc"; - const link = caps.publishRepoLink(doc); + const link = caps.recordInPublicStore(doc); expect(hasReadCap(link)).toBe(true); expect(targetOf(link)).toBe(doc); - expect(caps.isPublished(doc)).toBe(true); - expect(caps.isPublished("did:ng:o:other")).toBe(false); + expect(caps.isInPublicStore(doc)).toBe(true); + expect(caps.isInPublicStore("did:ng:o:other")).toBe(false); // Publication is not a world-wide read grant: whoever HAS the URL reads it. become("bob"); @@ -110,9 +110,9 @@ test("open(): a public document is published as a link, a private one is not", ( const prot = caps.open("did:ng:o:prot", "protected"); const priv = caps.open("did:ng:o:priv", "private"); - expect(caps.isPublished("did:ng:o:pub")).toBe(true); - expect(caps.isPublished("did:ng:o:prot")).toBe(false); - expect(caps.isPublished("did:ng:o:priv")).toBe(false); + expect(caps.isInPublicStore("did:ng:o:pub")).toBe(true); + expect(caps.isInPublicStore("did:ng:o:prot")).toBe(false); + expect(caps.isInPublicStore("did:ng:o:priv")).toBe(false); // All three are readable BY THEIR OWNER — a creator is never locked out. for (const [doc, cap] of [["did:ng:o:pub", pub], ["did:ng:o:prot", prot], ["did:ng:o:priv", priv]] as const) { expect(caps.capFor(doc)).toBe(cap); diff --git a/packages/client/test/isolation-active.test.ts b/packages/client/test/isolation-active.test.ts index 6186c2c..81c66c0 100644 --- a/packages/client/test/isolation-active.test.ts +++ b/packages/client/test/isolation-active.test.ts @@ -254,7 +254,7 @@ test("(b) a bare reference reads nothing; the repo link of a published document setCurrentUser("alice"); const pub = await createEntityDoc("alice", "public"); const items = [item(pub, "u1")]; - expect(getCaps().isPublished(pub)).toBe(true); + expect(getCaps().isInPublicStore(pub)).toBe(true); const link = linkTo(pub); // bob HAS the document's bare NURI (it is right there in `items`) and reads nothing. diff --git a/packages/client/test/read-filter.test.ts b/packages/client/test/read-filter.test.ts index 9508f59..eb2df10 100644 --- a/packages/client/test/read-filter.test.ts +++ b/packages/client/test/read-filter.test.ts @@ -18,7 +18,7 @@ function setup(initial: string | null = "alice") { const before = holder; holder = "alice"; caps.mint("did:ng:o:alice"); - const link = caps.publishRepoLink("did:ng:o:public"); + const link = caps.recordInPublicStore("did:ng:o:public"); holder = before; return { caps, link, become: (id: string | null) => (holder = id) }; } diff --git a/packages/client/test/vocabulary.test.ts b/packages/client/test/vocabulary.test.ts index 270acdd..93efadb 100644 --- a/packages/client/test/vocabulary.test.ts +++ b/packages/client/test/vocabulary.test.ts @@ -42,6 +42,11 @@ const TARGET_WORDS = new Set([ "fetch", "init", "watch", "sparql", "ng", "orm", "type", "types", // RDF / SPARQL terms the engine's own query paths use "subject", "base", "schema", "connected", "identity", "identities", + // `publisher` is upstream's word for a pub/sub role on a topic (`as_publisher`, + // `publisher_advert`, 126 occurrences in the engine). Our own "publish a document" is + // banned as ambiguous, but that ban never reaches upstream's term — see the traps + // block in `docs/readcap-and-nuri-model.md`. + "publisher", "topic", "advert", // the reactive model the ORM exposes (`OrmSubscription`, `DeepSignalSet`) "observable", "deep", "signal", "set", ]);