docs: ce qu'un NURI transporte, et les quatre besoins d'un lien

Une discussion de conception a calé plusieurs fois sur une séparation supposée
entre « un NURI » et « un lien ». Elle n'existe pas, et l'avoir crue est ce qui a
fait livrer un `linkTo` faux sur trois points à la fois.

`NuriV0` porte exactement ce qu'un lien porte — `target`, `overlay`,
`access: Vec<NgAccessV0>`, `locator` — et `NgLinkV0` en est la forme structurée.
La vraie question n'est donc pas « NURI ou lien » mais ce qu'on met dans
`access` et `locator`. Nuance qui compte pour tout ce qui s'imprime : le TYPE a
tous les emplacements, la GRAMMAIRE de chaîne ne les expose pas tous.

`access` est une LISTE de formes d'accès (`ReadCap | Token | ExtRequest | Key |
Inbox | Topic`), donc une liste VIDE est un état légitime et courant : la
référence NOMME sa cible et n'accorde rien.

D'où les quatre besoins, chacun avec ce que la référence doit porter : nommer
(access vide), donner à lire (+ ReadCap, irréversible), faire trouver un
document public (overlay/locator, aucune clé), joindre depuis ailleurs
(locator, sans quoi un inconnu n'ouvre rien).

Trois confusions nommées parce que chacune a été faite ici : « sans clé » ne
veut pas dire « public » ; nommer n'est pas lire et c'est l'acte PAR DÉFAUT
(ce que notre test central prouve déjà) ; rien n'est vérifié à l'accès, donc
une référence ne peut pas être neutre en droits — ce qu'on y met EST
l'autorisation, et un lien diffusé ne se révoque pas.

Et la divergence la plus lourde, qui n'était consignée nulle part : publier
signifie ICI distribuer une clé, alors qu'en amont un document public est
lisible parce que son STORE l'est et que les brokers le servent. L'émulation
est trop stricte, pas inversée — elle sous-accorde — mais un consommateur ne
doit pas en conclure que publier est un acte de distribution de clé, ni
attendre quoi que ce soit de per-lecteur sur un document public.
This commit is contained in:
Sylvain Duchesne
2026-08-06 12:52:35 +02:00
parent c8d02619b1
commit 7672915bb9
3 changed files with 140 additions and 0 deletions
+56
View File
@@ -269,6 +269,62 @@ What remains true, and is a separate matter — the *delivery* path is unimpleme
*Consequence for this library*: **both durable registers are now emulated** (2026-07-30) — `AddRepo` as a `shim:readCap` record on a distinct subject of the store document (`storeBranch`), `AddLink` as `shim:link` on another (`userBranch`) — and the in-memory `CapRegistry` is what it always was, level 3: the cache. Caps are READ back from those records, never recomputed. What stays an invention is representing branches as RDF subjects at all: upstream both branches carry `BranchCrdt::None` and hold service commits, not triples. What is faithful is that the key sits beside the document, and that the listing (`contains`, the Main branch) is separate from the keys.
## 4sexies. What a NURI TRANSPORTS — and the four things one may want from a link
**VERIFIED 2026-08-06** by reading `nextgraph-rs`, after a design discussion kept stalling on an assumed split between "a NURI" and "a link". There is no such split, and getting that wrong is what made this library ship a `linkTo` that was wrong three ways at once.
### A NURI and a Link are one thing in two shapes
`NuriV0` (`engine/net/src/app_protocol.rs:181-194`) carries exactly what a link carries:
```rust
NuriV0 { identity, target, entire_store, objects, signature,
branch, overlay, access: Vec<NgAccessV0>, topic, locator: Option<Locator> }
```
`NgLinkV0 = Repo | PublicRepo | Branch | Object` (`engine/net/src/types.rs:5206-5211`) is the structured form of the same information; the NURI is its URI form. So "should we hand out a NURI or a link?" is not a question — they are the same thing, and the real question is **what one puts in the `access` and `locator` slots**.
*Caveat, and it matters for anything printed on paper:* the **type** has all the slots, the **string grammar does not expose them all**. A self-contained share URL exists for objects, commits and files (`…:v:{overlay}(:[cj]:{id}:k:{key})+:l:{locator}`); the repo case is not exercised. See [`document-links.md`](./document-links.md).
### `access` is a LIST of access forms, not a key flag
```rust
NgAccessV0 = ReadCap(ReadCap) | Token(Digest) | ExtRequest(Vec<u8>)
| Key(BlockKey) | Inbox(PrivKey) | Topic(PrivKey)
```
(`app_protocol.rs:54-62`.) An **empty** `access` is therefore a legitimate, meaningful state: the reference NAMES its target and grants nothing. That is not a degenerate link — see below, it is the common case.
### The four things one may want, and what each requires
| What you want | What the reference must carry |
|---|---|
| **Name** a document — cite it, reference it from another document, let someone ask you for it | `target`; `access` EMPTY |
| **Let someone read** a non-public document | `target` + `ReadCap`. Irreversible: whoever receives the message reads, and nothing checks anything later |
| **Let someone find** a public document | `target` + `overlay`/`locator`; `access` EMPTY — the broker serves it, no key travels |
| **Let someone JOIN from elsewhere** | `locator`. Without it a stranger with no broker in common opens nothing, key or no key |
### Three confusions worth naming, because each one was made here
**"No key" does not mean "public".** A key-less reference to a PROTECTED document withholds access deliberately — the recipient can name it, not read it. A `PublicRepoLinkV0` (`types.rs:5105-5127`) carries no `read_cap` for an unrelated reason: the content is served publicly, so there is nothing to hand over. Same shape, opposite situations.
**Naming is not reading, and it is the DEFAULT act.** This is what `test/cross-user-access.test.ts` proves: Alice publishes a public document that REFERENCES her protected one; Bob follows the public link, finds the reference, and can name the protected document while reading nothing of it. Publication is not recursive. Withholding the key is the ordinary, reversible gesture; including it is the exceptional, irreversible one.
**Nothing is checked at access time.** Reading IS possession — the engine verifies a permission on WRITE only (`verify_permission``PermissionDenied` in `Commit::verify`), never on read. So a reference cannot be "neutral about rights, resolved later": what you put in it IS the grant. There is no revoking a link you have circulated, only rotating the key (and `RepoLinkV0`'s own comment says shared caps go stale on refresh — `PermaShare` / PermaCap, `engine/repo/src/types.rs:1761`, is the reserved answer to that, unimplemented).
### Publishing here means distributing a key; upstream it does not
The sharpest divergence this discussion surfaced, and it is not in `linkTo` — it is one level down.
This library mints a cap on publication (`emulated-verifier/caps.ts` `publishRepoLink`) and its possession filter gates public documents like any other. So *publish* means, here, **distribute a key**. Upstream, a public document is readable because its STORE is public and brokers serve it accordingly (`expose_outer`); the link carries no key at all, and any holder fetches the current one from the outer overlay.
The emulation is therefore **over-strict, not inverted** — it under-grants. A cap-less reference to a public document resolves upstream and does not here. That matters in two ways: "circulate the link" stays the right gesture at migration (so the consumer act survives), but a consumer must not conclude that publishing is an act of key distribution, nor expect anything per-reader on a public document — upstream there is no grant, no revoke, no audience to build a UI on.
### Consequence for this library
`linkTo` (added 2026-08-05) fails all three: it always embeds the key — including where none is needed — never carries a locator, and offers no way to withhold. It must be rebuilt from the table above rather than patched. The recipient verb is missing entirely, which is why Bob's side of the scenario cannot be tested by the multi-actor rule.
## 5. What the polyfill emulates (caps.ts) — and where it still diverges
**Realigned 2026-07-28 (batch P1a).** `packages/client/src/emulated-verifier/caps.ts` used to model `readers: Map<Nuri, Set<PrincipalId>>` + `grantRead(doc, grantee)` — a per-document **ACL of principals**, the exact INVERSION of the real model. It now records, **per identity**, the caps that identity holds (`Map<Nuri, ReadCap>`) — whose only question is `capFor(nuri)` — and `nuri.ts` carries the cap-less / cap-bearing distinction on the `r:` segment. The durable registers are emulated in `shared-wallet/account-registry.ts` (`readCap` on the Store branch, `link` on the User branch); this in-memory record is their cache.