/** * public-store — a document in a PUBLIC store gives up its ReadCap to whoever asks. * * ── The upstream mechanism this emulates — a DECLARED model, so a BET ────── * **Labelled VERIFIED until 2026-08-10, wrongly.** What supports it is a doc COMMENT * on a type nothing constructs — a statement of intent, not of behaviour — and this * repo's own rules say both halves of that: a comment describing the current state is * not the intent, and an absent implementation is not evidence either. So this is a * bet, and `docs/document-links.md` § 5 and `docs/readcap-and-nuri-model.md` § 4sexies * already called it one. This header now says the same word. * * What IS read in source: `PublicRepoLinkV0` (`engine/net/src/types.rs:5098-5124`) * carries `repo`, `public_store` and `peers` — and **no `read_cap`**. Its own doc * comment says why: * * > *"The latest ReadCap of the branch (or main branch) will be **downloaded from * > the outerOverlay**, **if the peer brokers listed below allow it**. […] This link * > is durable, because the public site are **served differently by brokers**."* * * So for a repo in a public store, the key is not something a sender hands over: it is * something the **network gives to whoever asks — and whom the peer brokers allow**. * That condition is part of the mechanism, not decoration: the broker decides, by * pinning the outer overlay (`expose_outer`, `engine/broker/src/server_storage/core/overlay.rs:103-133`). * Nothing about the reader; everything about where the document sits and how brokers * serve it. * * And what is NOT wired, which is precisely why this is a bet: both `PinRepo` * constructors hard-code `expose_outer: false` * (`engine/net/src/actors/client/pin_repo.rs:51,79`), so no client ever asks for the * exposure; and `ExtTopicSyncReq` — the anonymous branch-sync such a link needs — is * declared and falls into `unimplemented!()` (`engine/net/src/types.rs:4523,4533`). * The emulation follows the model the engine DECLARES, in a place the engine does not * yet serve. That is this library's intended posture, named here as the bet it is. * * ── What that means for the model, and why nothing is special-cased ─────── * Possession stays the ONE criterion. A public document is readable not because the * guard makes an exception for it, but because its cap is **obtainable**: you ask, you * receive, you hold it, and from there the ordinary path applies. `reach.ts` is * untouched, and "whoever has the reference AND the key reads" still describes * everything — a public store hands the key to whoever has the reference, where the * brokers serving that store allow it (see the condition above). * * The consequence an application must be able to rely on: **a bare reference to a * document in a public store is enough**, and that is why nothing in this library * needs to put a key into a link (see `readcap-and-nuri-model.md` § 0 — a call that * returns a key where a reference was asked for is the failure mode to watch for). * * Non-recursive, like everything else here: a public document may REFERENCE a * protected one, and following that reference gets you a name, not a key. Only the * document actually sitting in the public store exposes its cap. * * ── The two halves, and which door each uses ────────────────────────────── * - {@link exposeReadCap} — the OWNER's side, at creation: the cap is written on the * document's Header branch, the compartment meant for what any reader may see. It * goes through the guarded surface, because the owner holds the document. * - {@link fetchReadCap} — the NETWORK's side: read through the **physical** door * (`shared-wallet/physical.ts`), unguarded, because that is precisely the point — * the broker serving an outer overlay does not ask who is asking. Using the guarded * read here would be circular: you would need the cap to obtain the cap. * * ── Where the emulation is honest about its shape ───────────────────────── * Upstream nothing is *written* anywhere to make a repo public: the store is public, * and the broker exposes its outer overlay. Here there is one broker serving every * virtual user identically, so "which documents are in a public store" has to be * recorded somewhere the machinery can read — and the document itself is the one place * that needs no index and no enumeration. At migration this whole module goes: the * scope stops being a fact we record and becomes the store the document lives in. * * The gap that leaves: a reader learns a document is public by ASKING that document, * so a document it has never heard of stays invisible. Upstream the broker would serve * it just the same. That limits discovery, not access — an application that holds the * reference reads, which is the property this module exists to provide. */ import { registerUpdate } from "./register-write"; import { physicalQuery, ensurePhysicalRepoOpen } from "../shared-wallet/physical"; import { getCaps } from "../shared-wallet/bootstrap"; import { escapeLiteral } from "../surface/sparql"; import { hasReadCap, targetOf } from "../model/nuri"; import { accessLogPrefix } from "../shared-wallet/access-log"; import { P, HEADER_BRANCH_SUBJECT, readBindings, bindingValue, session, } from "../shared-wallet/account-registry"; import type { Nuri, ReadCap } from "../model/types"; /** * Targets whose outer-overlay fetch has been attempted in this session, and WHAT it * returned — the cap, or `null` for "not in a public store". * * ── The memo caches the answer, never the filing ────────────────────────── * It cached a boolean until 2026-08-07, and that was a bug an adversarial review found: * the first holder to ask triggered the download, the cap was filed for THEM, and every * later holder in the same session hit the memo, got `true`, and held nothing. Their next * read was refused. Upstream a broker serving a pinned outer overlay answers EVERY asker * (`expose_outer`, `engine/broker/src/server_storage/core/overlay.rs:103-133`), so * "served once, to whoever asked first" is a relation the model does not have. * * The round-trip is what is worth saving, not the filing. So the memo holds the value and * the caller files it for whoever is connected, every time. * * A scope never changes here (a document is created in a store and stays there), so a * cached `null` cannot go stale for a document that existed when it was taken. It CAN for * one created afterwards in the same page — {@link resetPublicStoreFetches} is the way * out, and a session or wallet reset calls it. */ const attempted = new Map>(); /** Forget every outer-overlay fetch (tests / a switched session or wallet). */ export function resetPublicStoreFetches(): void { attempted.clear(); } /** * Expose `cap` on `doc`'s Header branch — the emulated `expose_outer`. Called when a * document is created in a PUBLIC store, and only then: this is what makes the cap * obtainable by anyone, which for a public store is the intended property and for any * other scope would be a disclosure. * * Replacement, not addition, like every Header-branch register: one document has one * current cap, and two would leave a fetcher picking between them. */ export async function exposeReadCap(doc: Nuri, cap: ReadCap): Promise { const s = await session(); try { // Two separate updates: `DELETE WHERE { … }` is the form verified against the real // broker (`docs/decisions/sparql-delete-for-orm-objects.md`); a `;`-joined update // is not exercised anywhere in this library. await registerUpdate( s.sessionId, `DELETE WHERE { <${HEADER_BRANCH_SUBJECT}> <${P.exposedReadCap}> ?c }`, doc, "exposeReadCap:clear", ); await registerUpdate( s.sessionId, `INSERT DATA { <${HEADER_BRANCH_SUBJECT}> <${P.exposedReadCap}> "${escapeLiteral(cap)}" }`, doc, "exposeReadCap", ); } catch (error) { console.error(accessLogPrefix() + " exposeReadCap failed:", error); } } /** * Ask the (emulated) network for `doc`'s ReadCap, and file it if it answers — the * emulated *"downloaded from the outerOverlay"*. Returns whether a cap was obtained. * * Nothing is asked when the cap is already held: a document you can read needs no * fetching, and skipping it keeps the ordinary path free of physical reads. * * Never throws — a document that is not in a public store simply answers nothing, which * is not an error but the normal case. */ export async function fetchReadCap(docLike: Nuri): Promise { const doc = targetOf(docLike); const caps = getCaps(); // Inert until the emulation is in force, like the guard it serves: before the first // cap exists everything reads anyway, so there is nothing to obtain and asking would // be a physical round-trip bought for nothing. if (!caps.isEnforcing()) return false; if (caps.capFor(doc) !== undefined) return true; let pending = attempted.get(doc); if (pending === undefined) { pending = downloadReadCap(doc); attempted.set(doc, pending); } const cap = await pending; if (cap === null) return false; // Filed for whoever is connected NOW, on every call — the memo spares the round-trip, // not the filing. See the note on {@link attempted}. caps.learnFromPublicStore(cap); caps.markInPublicStore(doc); return true; } /** The fetch itself, through the machinery's door. Returns the cap, or `null` when the * document is not in a public store — which is the normal case, not an error. */ async function downloadReadCap(doc: Nuri): Promise { const s = await session(); try { // The repo has to be in the session before an anchored read resolves it — the // cold-start heal, through the PHYSICAL door: this is the emulated broker serving // an outer overlay, and it does not ask who is asking (see `open-repo.ts`). await ensurePhysicalRepoOpen(doc); const res = await physicalQuery( s.sessionId, `SELECT ?c WHERE { <${HEADER_BRANCH_SUBJECT}> <${P.exposedReadCap}> ?c }`, undefined, doc, "fetchReadCap", ); for (const row of readBindings(res)) { const cap = bindingValue(row, "c"); // `targetOf` guards the one confusion that would matter: a cap exposed on // document A must not file a cap for document B. A document only ever speaks // for itself. if (cap && hasReadCap(cap) && targetOf(cap) === doc) return cap; } } catch (error) { // Not in a public store, not synced, or no such document — all of them mean the // same thing to the caller: no cap was obtained. console.error(accessLogPrefix() + " fetchReadCap failed:", error); } return null; } /** * Ask for a SET of documents' caps, in parallel — what a batch read does before it * decides which documents it may touch. Each fetch is independent and tolerant. */ export async function fetchReadCaps(docs: Nuri[]): Promise { const unique = [...new Set(docs.filter(Boolean))]; if (unique.length === 0) return; await Promise.all(unique.map((d) => fetchReadCap(d))); }