docs: sept affirmations sur NextGraph requalifiées à la source

Lot F de la revue adverse. Chaque affirmation relue dans `nextgraph-rs` par symbole avant
d'être réécrite ; aucune ne s'est révélée exacte.

- `AddLink` / `RemoveLink` / `RepoLinkV0` étaient présentés comme **implémentés**. Leurs
  arms de vérificateur sont des `Ok(())`, là où celui d'`AddRepo` fait un vrai travail, et
  rien ne les construit. Le tableau dit désormais « déclaré, stubbé », et la conclusion qui
  en déduisait « le registre existe, seule la livraison manque » est corrigée : **les deux
  bouts** sont déclarés-et-stubbés.
- La table `inboxes` était dite « reconstruite vide à chaque session » — elle est
  repeuplée au chargement, et la clé privée d'inbox est persistée par repo. L'argument de
  sécurité qui s'appuyait dessus repose maintenant sur le bon motif : la table est **par
  vérificateur**, pas éphémère.
- La citation de « `doc_create` laisse `inbox: None` » pointait un constructeur réservé aux
  tests ; re-ciblée sur le chemin de production.
- `ExtObjectGet` était dit « le seul » primitif accessible à un non-membre et exigeant les
  clés : il y en a trois, et sa structure n'a aucun champ de clé.
- L'en-tête de `public-store.ts` était marqué **VERIFIED** alors qu'il repose sur un
  commentaire de doc, et la condition qu'il citait (« si les brokers pairs l'autorisent »)
  disparaissait de la conclusion. Requalifié en **pari**, condition rétablie.
- Deux sur-restrictions corrigées (ce qu'écrit le traitement d'un `ContactDetails`, et le
  prétendu « miroir 1:1 » de `NuriV0`, qui a dix champs).

**Sur la grammaire du ReadCap, une correction de MA correction.** J'avais écrit que la
forme `{target}:r:{cap}` était notre invention. Faux : le segment `r:` et son encodage sont
ceux d'amont, et l'auteur de NextGraph l'a énoncé. Ce qui est établi est plus étroit —
aucun parseur amont n'accepte aujourd'hui un NURI de repo qui le porte, et le segment est
produit comme valeur de champ. J'avais conclu d'une implémentation absente à ce que la
cible ferait, ce que la doctrine du projet interdit nommément. Seule « P1b remplace la
valeur, pas la forme » est corrigée, requalifiée en **pari**.

**Et la surface ne publie plus de type que personne n'utilise.** `export * from
"./model/types"` publiait huit types en bloc ; c'est une liste nommée de six. `ReadCap`
sort — aucune signature publiée ne le prend ni ne le rend, seules deux fonctions privées
de `inbox.ts` s'en servent — et `InboxScope` aussi. Un type n'est publié que si une
signature publiée l'utilise.

197 tests, 0 échec ; les trois typechecks propres ; `lint` sans erreur.
This commit is contained in:
Sylvain Duchesne
2026-08-10 12:09:20 +02:00
parent 0d9e2bbe97
commit f378c71739
13 changed files with 183 additions and 67 deletions
@@ -432,12 +432,22 @@ export async function readLinks(): Promise<ReadCap[]> {
* construction, and at any time (see the User-branch note above).
*
* What is true is narrower: no code path CREATES one for a document — `new_store_default`
* attaches one only `if !private` (`verifier.rs:2994`), `doc_create` leaves `inbox: None`
* (`repo.rs:574`), and the only two `AddInboxCap` commits in the engine are for the
* attaches one only `if !private` (`verifier.rs:2994`), `doc_create` leaves `inbox: None`,
* and the only two `AddInboxCap` commits in the engine are for the
* public and protected STORE repos (`engine/verifier/src/site.rs:128,149`). So the
* capability exists and is simply unexposed above level 1: this function is aligned on
* the engine's model, it does not bet past it.
*
* *(The `inbox: None` claim is true; its citation was wrong until 2026-08-10. It pointed
* at `repo.rs:574`, inside `Repo::new_with_member` (`engine/repo/src/repo.rs:543`) —
* a constructor reached only from `Repo::new_with_perms`, itself gated
* `#[cfg(any(test, feature = "testing"))]` (`repo.rs:186-192`), and from `#[cfg(test)]`
* blocks (`branch.rs:387,490`; `commit.rs:1659,1849,1919`). The PRODUCTION path is
* `doc_create` → `Verifier::new_repo_default` (`engine/verifier/src/verifier.rs:3004`,
* called at `request_processor.rs:689`) → `Store::create_repo_default`
* (`engine/repo/src/store.rs:264`) → `create_repo_with_keys` (`store.rs:284`), which
* builds the `Repo` with `inbox: None` at `store.rs:691`.)*
*
* Lazy on purpose, for the same reason: creating an inbox document for every entity up
* front would double every `createEntityDoc` for inboxes most documents never receive
* anything in. Upstream the keypair is cheap; here an inbox is a document, so it is
@@ -472,9 +482,18 @@ export async function openDocumentInbox(docLike: NuriLike): Promise<Nuri> {
// false: that commit lands on the committer's OWN User branch, so anyone may write
// one naming anyone's repo. What protects upstream is that an inbox address is never
// PUBLISHED — it is TRANSMITTED (in a `ContactDetails` message, or a profile QR
// code), and `inboxes: PubKey → RepoId` is a per-verifier local table
// (`engine/verifier/src/verifier.rs:105`, rebuilt empty each session). A forged pair
// reaches nobody, because nobody was told about it.
// code), and `inboxes: PubKey → RepoId` is a table of the VERIFIER
// (`engine/verifier/src/verifier.rs:105`) — one per user. A forged pair reaches
// nobody because it only ever lands in the forger's OWN table; nobody else was told.
//
// The motive matters, and it was wrong here until 2026-08-10: this comment said the
// table is "rebuilt empty each session", which is not what the source does. It is
// initialized empty at construction (`:520`, `:2820`) and then REPOPULATED at every
// load — `Verifier::load` (`:534-566`) → `add_repo_without_saving` (`:2871`) →
// `add_repo_` (`:2887`), which re-inserts `repo.inbox.to_pub() → repo.id` for each
// repo it reloads — and the inbox private key itself is persisted per repo
// (`INBOX_CAP`, `engine/verifier/src/user_storage/repo.rs:61,171,207,362`). So the
// knowledge is durable; what it is not is SHARED. Per-verifier, not ephemeral.
//
// We publish instead of transmitting — the only way a third party can find the
// address at all here — which creates a vector upstream does not have: whoever can
@@ -1,26 +1,44 @@
/**
* public-store — a document in a PUBLIC store gives up its ReadCap to whoever asks.
*
* ── The upstream mechanism this emulates (VERIFIED) ───────────────────────
* `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 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**."*
* > 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 anyone who asks**. The broker decides, by pinning
* the outer overlay (`expose_outer`, `engine/broker/src/server_storage/core/overlay.rs:103-133`).
* That is the whole of the property — nothing about the reader, everything about where
* the document sits and how brokers serve it.
* 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 simply hands the key to whoever has the reference.
* 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
+17 -1
View File
@@ -46,7 +46,23 @@
// At migration the build alias is removed and these resolve to the real SDK. The
// per-symbol ruling, with its epistemic label, is in `docs/api-contract.md`.
export * from "./model/types";
// A type is published only when a PUBLISHED SIGNATURE uses it. `export *` published
// eight in one gesture (2026-08-10: it was a blanket re-export), of which two named
// nothing a consumer can reach — `ReadCap` (used only by two private helpers of
// `surface/inbox.ts`) and `InboxScope` (used only by the unpublished
// `account-registry.userInbox`). A published type with no published signature is a
// promise about the target that nothing here keeps: it invites a consumer to hold a
// value it has no call to obtain — and for `ReadCap`, the one value the model says a
// caller must never be handed on request. They stay DEFINED in `model/types.ts`, where
// the library uses them; they stop being surface. `docs/api-contract.md` § 10, § 14.
// Each one, and the signature that earns it its place:
// Nuri every reference the surface RETURNS
// NuriLike every reference the surface ACCEPTS
// Scope `storeRegistry.*`, `watchShape`
// PrincipalId `ensureIdentity`, `inbox.Deposit`/`PostOptions`, `EventuallyConfig`
// NgLike `EventuallyConfig.ng`
// UseShapeLike `EventuallyConfig.useShape`
export type { Nuri, NuriLike, Scope, PrincipalId, NgLike, UseShapeLike } from "./model/types";
export { useShape } from "./surface/use-shape";
export { watchShape } from "./surface/watch-shape";
export type { ShapeQuery, ShapeObservable } from "./surface/watch-shape";
+15 -9
View File
@@ -1,10 +1,13 @@
/**
* NURI primitives — the cap-less / cap-bearing distinction, kept as ONE object.
*
* Upstream a NURI is a single type, `NuriV0 { target, access }`: a cap-less NURI
* simply has an empty `access`. `did:ng:` is the URI SCHEME prefix (inboxes,
* branches and overlays all carry it) — it does NOT mean "without cap". The
* discriminant is the `:r:` segment:
* Upstream a NURI is a single type, `NuriV0` — TEN fields: `identity, target,
* entire_store, objects, signature, branch, overlay, access, topic, locator`
* (`engine/net/src/app_protocol.rs:181-194`) — and a cap-less NURI is simply one
* whose `access` is empty. This module transcribes **two** of those ten (`target`,
* and the cap half of `access`); the other eight have no counterpart here.
* `did:ng:` is the URI SCHEME prefix (inboxes, branches and overlays all carry it) —
* it does NOT mean "without cap". The discriminant is the `:r:` segment:
*
* did:ng:o:{doc}:v:{overlay} — names, does NOT read (a {@link Nuri})
* did:ng:o:{doc}:v:{overlay}:r:{cap} — names AND reads (a {@link ReadCap})
@@ -19,9 +22,11 @@
* 2026-07-30; it was the wrong letter *and* the wrong structure.
*
* These helpers are INTERNAL to the library. The parsed form {@link parseNuri}
* mirrors `NuriV0 { target, access }` 1:1 but never surfaces in the SDK-identical
* entry's signatures — the real SDK takes plain `String`s and enforces at runtime,
* through cryptography, so no branded type and no parsed struct leaks outward.
* mirrors that PAIR — `target` and the cap — and not the type: it was described as a
* "1:1 mirror of `NuriV0`" until 2026-08-10, which claimed eight fields it has never
* carried. It never surfaces in the SDK-identical entry's signatures either — the
* real SDK takes plain `String`s and enforces at runtime, through cryptography, so no
* branded type and no parsed struct leaks outward.
*
* ── The stand-in key (deliberately NOT a secret) ───────────────────────────
* This library is deliberately insecure (see docs/vision.md). The only question it
@@ -80,8 +85,9 @@ export function targetOf(nuri: Nuri): Nuri {
}
/**
* The parsed form — a 1:1 mirror of upstream `NuriV0 { target, access }`, where a
* cap-less NURI has no `readCap`. Library-internal (see the module header).
* The parsed form — upstream `NuriV0`'s `target` plus the cap half of its `access`,
* and none of the type's eight other fields; a cap-less NURI has no `readCap`.
* Library-internal (see the module header).
*/
export function parseNuri(nuri: Nuri): { target: Nuri; readCap?: ReadCap } {
return hasReadCap(nuri) ? { target: targetOf(nuri), readCap: nuri } : { target: nuri };
+7 -3
View File
@@ -292,9 +292,13 @@ function capOfPayload(payload: unknown): ReadCap | null {
* - the field exists, `ContactDetails.read_cap: Option<ReadCap>`
* (`engine/net/src/types.rs:4233`), but building a message that carries one is
* `read_cap: if with_readcap { unimplemented!() }` (`types.rs:3786`);
* - and the receiver ignores it: `InboxMsgContent::ContactDetails` writes only
* `ng:site`/`ng:protected` + `ng:*_inbox` into a fresh contact document
* (`engine/verifier/src/inbox_processor.rs:778-830`), never `details.read_cap`.
* - and the receiver ignores it: `InboxMsgContent::ContactDetails` creates a fresh
* contact document and writes `ng:site`/`ng:protected` + `ng:*_inbox`, a
* `vcard:Individual` type, a `vcard:fn` name and an optional `vcard:hasEmail`,
* then sets the header title (`engine/verifier/src/inbox_processor.rs:778-845`) —
* but never `details.read_cap`. *(The list was "only the two `ng:` predicates"
* until 2026-08-10, which understated what the arm writes; the load-bearing part
* is the omission, not the length of the list.)*
*
* Do NOT read `InboxMsgContent::Link` as the intended channel either: it is a **unit
* variant carrying nothing** (`engine/net/src/types.rs:4251`).