43aadbeb45
98 annotations posées à côté des déclarations, et un test qui les exige sur la surface publiée. Elles portent trois choses : le niveau qui répond, la référence amont, et la catégorie parmi les cinq. La cinquième est celle qui manquait : declared-not-wired, quand la cible DÉFINIT la forme et ne la câble pas. Neuf symboles en relèvent, dont readLinks — que j'avais classé « notre invention » en raisonnant depuis l'absence, alors que c'est le meilleur alignement disponible. Les références citent un SYMBOLE, jamais une ligne : trois citations du document avaient déjà pourri. Cinq corrections au passage, toutes vérifiées à la source — un chemin ORM qui n'existe pas, deux plages de lignes fausses, et surtout docs.* et subscribeDoc étiquetés PASSTHROUGH alors qu'ils sont alignés : nos noms, plus un argument jamais transmis. La sémantique survit à la migration, les sites d'appel non, et la nuance disparaissait sous une étiquette trop flatteuse. Le test échoue à l'annotation retirée, à la catégorie mal orthographiée, et à une invention qui prétendrait citer une référence — vérifié en cassant les trois. Il a aussi attrapé un défaut en lui-même : le gabarit de format placé dans index.ts se faisait analyser comme une annotation. La classification couvre l'interne qui prétend ressembler à la cible — tout emulated-verifier — et exclut ce qui ne le prétend pas. La faute d'origine portait sur une fonction non exportée ; n'être pas publié n'a protégé personne. Quatre symboles ont résisté et sont annotés avec leur catégorie dominante, la seconde nommée dans la note plutôt que lissée.
233 lines
13 KiB
TypeScript
233 lines
13 KiB
TypeScript
/**
|
|
* 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<Nuri, Promise<ReadCap | null>>();
|
|
|
|
/** Forget every outer-overlay fetch (tests / a switched session or wallet). */
|
|
// @provenance resetPublicStoreFetches kind=invention level=none ref=none — forgets the memoised fetches; upstream there is nothing to memoise, the broker answers
|
|
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.
|
|
*
|
|
* **Propagates a failed write.** This runs exactly once, at creation, and nothing exposes
|
|
* the cap afterwards — so a swallowed failure leaves a document that IS in a public store
|
|
* and serves nothing: every third party's {@link fetchReadCap} answers "no cap", the read
|
|
* guard refuses, and the document is unreadable by anyone but its creator, for good. Its
|
|
* caller `createEntityDoc` already refuses to hand back a reference whose bookkeeping did
|
|
* not land, for the same reason and in the same words.
|
|
*/
|
|
// @provenance exposeReadCap kind=divergent level=1 ref=engine/net/src/types.rs:PublicRepoLinkV0 — upstream NOTHING is written to make a repo public: the store is public and the broker exposes its outer overlay. Here the cap is written on the document's Header branch
|
|
export async function exposeReadCap(doc: Nuri, cap: ReadCap): Promise<void> {
|
|
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);
|
|
throw 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.
|
|
*/
|
|
// @provenance fetchReadCap kind=declared-not-wired level=1 ref=engine/net/src/types.rs:PublicRepoLinkV0 — the type DECLARES the cap is downloaded from the outer overlay if the brokers allow; it is constructed nowhere, both `PinRepo` constructors hard-code `expose_outer: false`, and `ExtTopicSyncReq` is `unimplemented!()`
|
|
export async function fetchReadCap(docLike: Nuri): Promise<boolean> {
|
|
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<ReadCap | null> {
|
|
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.
|
|
*/
|
|
// @provenance fetchReadCaps kind=declared-not-wired level=1 ref=engine/net/src/types.rs:PublicRepoLinkV0 — the batch form of the same unwired mechanism
|
|
export async function fetchReadCaps(docs: Nuri[]): Promise<void> {
|
|
const unique = [...new Set(docs.filter(Boolean))];
|
|
if (unique.length === 0) return;
|
|
await Promise.all(unique.map((d) => fetchReadCap(d)));
|
|
}
|