docs+fix: le modèle de sécurité local-first, et le retrait de linkTo qui le brisait

La règle d'accès est « qui a la référence ET la clé lit » — jamais « qui a la
référence lit ». Il n'y a pas de tiers dans cette phrase : le moteur ne vérifie
une permission qu'à l'ÉCRITURE, jamais à la lecture. Le contenu est chiffré, la
clé EST le droit. C'est ce que veut dire local-first ici : il n'y a personne à
qui demander, donc la possession est tout le mécanisme.

Ce que cette règle achète, et qui n'était consigné nulle part : une référence
n'accordant rien, **elle n'est pas récursive**. Un document largement diffusé
peut pointer vers un document restreint — la référence dit qu'il existe, la clé
dit qui le lit. D'où la confidentialité COMPOSABLE : un sommaire diffusé qui
renvoie à des chapitres restreints, un événement public qui renvoie à sa liste
de participants. L'auteur diffuse un document et décide encore, document
référencé par document référencé, qui reçoit la clé.

`linkTo` détruisait cette construction. Il rendait la CLÉ là où un appelant
demande une référence, transformant la règle en « qui a la référence lit » —
pour ce document et pour tout ce qu'il MENTIONNE. Ajouté et retiré le même jour.

Documenté en §0, avant tout le reste, avec la raison pour laquelle un agent
perd ce point avec constance : les réflexes client-serveur fournissent la moitié
manquante sans qu'on s'en aperçoive — quelque part un serveur vérifierait, un
lien serait un identifiant inoffensif, « Bob a-t-il le droit ? » aurait une
réponse. Aucun des trois n'est vrai ici, et du code écrit là-dessus n'échoue
pas : il accorde en silence. Rien ne passe au rouge.

Renommé au passage : `publishRepoLink`/`isPublished` → `recordInPublicStore`/
`isInPublicStore`. Ces méthodes n'ont pas de pendant amont et « publier » ne
désigne rien de précis ici.

171 tests unitaires, e2e 42/42 en 3,6 min.
This commit is contained in:
Sylvain Duchesne
2026-08-06 16:04:31 +02:00
parent 3c981ffadb
commit cb56f91b5e
6 changed files with 55 additions and 42 deletions
+11 -31
View File
@@ -40,36 +40,16 @@ export {
/** WHERE to deposit for a document — readable by any holder of it. `undefined` if none. */
} from "../shared-wallet/account-registry";
import { getCaps } from "../shared-wallet/bootstrap";
import { toNuri } from "../model/nuri";
import type { NuriLike, ReadCap } from "../model/types";
export { openDocumentInbox } from "../emulated-verifier/branch-registers";
/**
* The shareable link of a document — what you circulate so someone can open it.
*
* Distinct from {@link share}, and both are needed: a link is what TRAVELS (a message,
* a QR code, a page), whereas sharing hands the access to one named person through
* their inbox. Upstream the same split exists — a `RepoLinkV0 { read_cap }` is the
* thing you pass around, and `ContactDetails.read_cap` is the directed delivery.
*
* This is the one place an application legitimately holds a key, because a public
* document's link IS meant to be handled: you cannot circulate what you may not touch.
* A protected document's key never comes out this way — it goes through `share`.
*
* Typed `ReadCap`, since that is what it is — a reference with the key inside. A
* `ReadCap` is assignable wherever a `Nuri` is expected (a cap IS a NURI carrying the
* key, upstream's one `NuriV0`), so it hands straight to any call that takes a
* reference. Throws if you hold nothing: a link you cannot open is not a link.
*/
export function linkTo(doc: NuriLike): ReadCap {
const target = toNuri(doc, "linkTo");
const cap = getCaps().capFor(target);
if (!cap) {
throw new Error(
"[ng-eventually] linkTo: you hold no key for this document, so there is no link " +
`to hand out: ${JSON.stringify(target)}`,
);
}
return cap;
}
// No `linkTo` here, and its absence is deliberate (it existed 2026-08-06, one day).
//
// It returned a document's KEY where a caller would ask for its reference, which turns
// the access rule from "whoever has the reference AND the key reads" into "whoever has
// the reference reads" — see `docs/readcap-and-nuri-model.md` § 0. That is not a leak of
// hygiene, it is the rule changing: a document one circulates would grant everything it
// MENTIONS, and confidentiality could no longer be composed inside a shared document.
//
// An application names a document with the reference it already has (every call here
// returns bare ones), and grants access with `inbox.share(doc, toUser)`. What travels
// with a key in it is a deliberate act, not the result of asking for a link.