# Brief — aligner l'émulation des caps sur le vrai modèle NextGraph **Brief (incubation) — 2026-07-20.** Voir la référence `docs/readcap-and-nuri-model.md`. ## Problème `caps.ts` émule les droits de lecture comme une **ACL** (`Map>`, `grantRead(doc, grantee)`) — **l'inversion** du vrai modèle NextGraph (possession de clé). Conséquences : pas de notion de **référence cap-less**, grant/révocation **instantanés et totaux** (au lieu de scellage durable + re-key), et une API (`declareConnections`) que les consommateurs doivent **re-déclarer à chaque session**. Cet écart empêche de bâtir correctement des modèles qui reposent sur la vraie sémantique — notamment la **présence anonyme** (nommer/compter sans lire). ## Objectif : shape-fidelity, PAS sécurité Le polyfill **n'égale PAS** la sécurité de NextGraph fini, et n'essaie pas. Le wallet partagé + l'absence de crypto rendent l'émulation **volontairement non-sécurisée** (tout est en clair, tout marqueur est forgeable) — véhicule de dev/staging, pas un but. **Seul objectif** : exposer la **BONNE FORME** des primitives futures pour que les consommateurs (Festipod) soient codés contre le **modèle mental correct** et **n'aient pas à être réécrits** quand NextGraph sera fini. Corollaire : **« pas de crypto » n'est pas un problème** ; ce qui compte est d'être **dans la même logique, avec RIGUEUR**. Une critique « un attaquant lit le clair / forge un marqueur » est **exacte mais hors-scope**. Ce qui est **inacceptable** = exposer la **mauvaise forme** (ex. une ACL là où le réel est possession de clé) → le consommateur code contre un modèle qui n'existera pas. **L'inversion ACL des ReadCaps EST ce manque de rigueur** — le défaut central à corriger. ## Mécanisme d'enforcement : simulation crypto LÉGÈRE (anti-ACL, anti-raccourci) Pour que la forme soit **réellement** possession-de-clé (et pas une ACL déguisée), la donnée d'un doc est **stockée chiffrée** (chiffrement symétrique par-doc, même léger) et le **ReadCap = la clé**. Invariant (cf. `docs/vision.md`) : > un **`did` nu (sans ReadCap)** ne permet **PAS** de lire ; un **NURI avec > ReadCap** est **suffisant et requis**. Ça **empêche les raccourcis** que l'adversaire a relevés (#4/#6 : lire le clair, `sparqlQuery`/`inbox.read` qui bypass le filtre) et **interdit** de retomber sur une ACL — c'est le cœur de « même logique, avec rigueur ». **Cible — PAS l'état actuel** : toutes les surfaces de lecture (`read-model`, `read-filter`, `sparqlQuery`, `inbox.read`) devront passer par le déchiffrement-avec-clé. **Aujourd'hui c'est FAUX** : `inbox.read` (`inbox.ts:198`) et `sparqlQuery` brut (`docs.ts:82`) **bypass** le filtre, et le seul garde (`caps.canRead`, `caps.ts:95`) est une **ACL set-membership** — l'inversion même que la vision interdit. **Fermer ces voies = le cœur de P1.** 1. **Deux formes de référence distinctes** : cap-less (nomme/localise sans lire — aligné sur le NURI sans `:k:`) vs cap-porteur (id + clé/token). Aujourd'hui absent. 2. **Grant = livrer un cap-token à un destinataire** (émuler le scellage : le destinataire *reçoit* le token dans son inbox ; c'est la **possession** du token qui autorise la lecture — pas une ligne d'ACL vérifiée par principal). 3. **Enforcement par possession** : les lecteurs (`read-filter`, `use-shape`) ne voient que ce dont ils **détiennent le token**, pas « ce dont ils sont dans le set de readers ». 4. **Résoudre un cap-less** = nommer / prouver l'existence / compter, **sans** exposer le contenu (support de la présence anonyme). 5. **Révocation = re-key** émulé : invalider l'ancien token, re-livrer un nouveau aux autorisés restants ; **non-rétroactif**. ## Questions ouvertes - **TRANCHÉ (directive PO, 2026-07-21)** : on **simule la crypto** (donnée chiffrée par-doc, cap = clé). La « sémantique seule » (registre de tokens) est **écartée** — elle redevient une ACL et laisse lire le clair. Reste à trancher le **niveau** de simulation (chiffrement réel léger vs projection read-model masquée), **avant P1**. - Représentation NURI cap-less vs cap-porteur dans l'émulation (calquer `:k:`). - Faut-il permettre le **fetch keyless** (résoudre un cap-less en existence/compte sans le contenu) — dépend de ce que le vrai broker autorise (INFÉRÉ, non tracé). - Migration d'API : `declareConnections`/`grantRead` → `seal(cap, recipient)` + `inbox → caps reçus`. Casse les consommateurs (`declareConnections` app disparaît). ## P0 — Spike « keyless-resolve » (le verrou, AVANT tout P1) **Question porteuse** : un détenteur d'une **référence cap-less** (`did:ng:o:{id}:v:{overlay}`, sans `:k:`) peut-il, **sans jamais lire le contenu** : - **Q1 — Existence / fetch** : prouver/récupérer la présence des blocs (chiffrés) auprès du broker ? Ou le broker exige-t-il un ReadCap/membership pour servir les blocs ? - **Q2 — Suppression** : distinguer « existe » de « supprimé » ? *(Le point FRAGILE : NextGraph est append-only CRDT — une désinscription = un **commit tombstone** qu'il faudrait **lire** pour connaître → potentiellement **la clé est requise**. Or le **décrément au leave** en dépend.)* - **Q3 — Confidentialité** : la clé (`:k:`) reste-t-elle **requise** pour déchiffrer (le keyless ne donne jamais le contenu) ? **Pourquoi c'est le verrou** : tout le **compteur anonyme** (compter/valider des réfs cap-less sans lire) ET le **décrément au leave** en dépendent. **Si NON** → « compteur anonyme par réf cap-less » est **inconstruisible en cible** → Festipod ne doit **pas** coder cette forme (réécriture garantie). **Si OUI** → P1 expose `resolveCapLess(nuri) → {exists|deleted}` (jamais de contenu), et l'émulation la simule fidèlement. **Méthode** (bon marché, décisif) : 1. **Tracer** dans `nextgraph-rs` le chemin d'**autorisation de fetch** du broker/verifier : qui sert les blocs (`BlocksGet`/`TopicSync`/`OverlaySync`) ? un cap/membership est-il vérifié, ou `id+overlay` suffit ? l'overlay *outer* est-il public ? une suppression est-elle observable sans clé ? 2. *(Optionnel)* **test e2e décisif** (façon `e2e/reactivity-doc-subscribe.ts`) : B détient la réf cap-less, tente fetch/existence **sans** la clé, vérifie qu'il **n'accède pas** au contenu. Preuve empirique > source. 3. *(Ou)* confirmer avec le dev NextGraph — le plus rapide. **Livrable** : YES/NO/PARTIAL par Q1/Q2/Q3 + le primitif exact (file:line) + la forme d'API à exposer (si OUI), ou le constat que le compteur change (si NON). **Décision gatée** : OUI → P1 ; NON → le brief inscriptions revoit le compteur (pas anonyme, ou autre primitif). ## Esquisse de phases - **P1** — distinction cap-less / cap-porteur dans les NURI + le read-model (résoudre un cap-less = nommer/compter, pas lire). - **P2** — remplacer l'ACL par un modèle de **possession de token** (grant = livrer à un destinataire ; enforcement = possession). - **P3** — révocation par re-key (invalidation + re-livraison, non-rétroactive). - **P4** — adapter l'API consommateur + `migration-guide.md`. ## Revue adverse (2026-07-20) — à intégrer Un adversaire a réfuté le brief (6 constats). **À lire au filtre de l'Objectif ci-dessus** (forme, pas sécurité). Les critiques purement **sécurité** — contenu en clair lisible (#4), marqueurs forgeables — sont **ACCEPTÉES / hors-scope** : le polyfill ne cherche pas à les empêcher. Restent les vrais défauts de **FORME / rigueur** (à corriger), et une question de **modèle futur** (#5) : 1. **WriteCap oublié, et « possession » y est FAUX.** L'écriture est **membership/permissions** (`AddMember`) — une **liste d'autorisation**, pas de la possession de clé (réf §1) ; `ng-proxy.ts:28-48` garde chaque `sparql_update`. → garder une **piste WriteCap = membership** ; la **possession ne concerne QUE la lecture**. 2. **P2 « possession sans crypto » = l'ACL renommée.** Sans crypto, « qui détient quel token » = `Map>` = le `readers` actuel : **aucun delta observable**. Les vrais deltas sont **durabilité + cap-less + re-partage par le détenteur** — c'est ÇA le contenu de P2, pas « inverser l'ACL ». 3. **Révocation non-rétroactive INÉMULABLE** sans versioning : `read-model.ts:112-118` ne lit que l'état courant → « invalider l'ancien token » = retrait total = l'inverse du réel (l'ex-détenteur déchiffre les versions **antérieures**). → n'émuler que « plus de nouvelles lectures après re-key » + **documenter la non-rétroactivité comme non-émulable**. 4. **cap-less « sans exposer le contenu » ILLUSOIRE dans l'émulation** : contenu en **clair** dans le wallet partagé ; `sparqlQuery`/`inbox.read` **bypass** le filtre ; `read-filter.ts:30-35` est tout-ou-rien. → l'anonymat cap-less exige soit du **vrai crypto**, soit une **projection read-model masquée** (compter sans lire). « Remplacement pas refonte » est **surévalué**. 5. **Keyless-fetch = INFÉRÉ et load-bearing** : ajouter un **spike P0** qui le vérifie **avant** P1 (sinon le modèle — polyfill ET Festipod — est inconstruisible). 6. **Migration ≠ swap d'API.** `declareConnections` se re-joue chaque session parce que la map est éphémère ; des scellages durables déplacent le grant à l'**acceptation de connexion** + persistent « déjà scellé » — pas d'analogue de `protectedDocsOf` + la boucle de re-dérivation. **Re-architecture consommateur.** 7. *(Plausible)* livrer un cap par inbox async **ne re-déclenche pas** `watchShape` (souscrit aux docs de données, pas aux caps) → vues illisibles **périmées** jusqu'à un autre changement. → prévoir un signal de mutation de caps. **Conséquence** : ajouter en tête **P0 (spike keyless-fetch)** et une **piste WriteCap distincte** ; requalifier P2 (le vrai contenu = durabilité + cap-less + re-partage, pas « inverser l'ACL ») ; acter que **sans crypto, la privacy de lecture n'est pas applicable** (choisir : vrai crypto vs projection masquée). Liens : `readcap-and-nuri-model.md`, `packages/client/src/caps.ts`. Côté consommateur, le brief Festipod « réaligner les inscriptions » dépend de ce chantier.