Files
ng-eventually/docs/briefs/2026-07-20-caps-emulation-alignment.md
T
Sylvain Duchesne 1791c31f42 docs: vision du polyfill (shape-fidelity), modèle ReadCap/NURI réel, brief caps
vision.md — la charte : le polyfill n'est PAS une couche de sécurité (wallet
partagé + pas de crypto = insécurité ACCEPTÉE) ; seul objectif = exposer la
BONNE FORME des primitives futures pour que les consommateurs n'aient rien à
réécrire. Invariant tenu par une simulation crypto légère : un `did` nu (sans
ReadCap) ne permet PAS de lire ; un NURI avec ReadCap est suffisant et requis.

readcap-and-nuri-model.md — le vrai modèle, VÉRIFIÉ par lecture de
`nextgraph-rs` : ReadCap = ObjectRef {id BLAKE3, clé ChaCha20} = possession de
clé, PAS une ACL ; grant = sceller la clé à l'inbox du destinataire ;
révocation = re-key grossier et non-rétroactif ; grammaire NURI cap-less vs
cap-porteur (le segment `:k:` est le discriminant) ; table des divergences avec
l'émulation `caps.ts` (aujourd'hui une ACL — l'inversion exacte).

briefs/2026-07-20-caps-emulation-alignment.md — le chantier d'alignement :
spike P0 keyless-resolve (verdicts vérifiés : existence sans clé OUI,
détection de suppression sans clé NON, confidentialité OUI), puis P1 cap-less
vs cap-porteur, P2 possession, P3 re-key, P4 migration d'API. Inclut la revue
adverse (WriteCap = membership et non possession ; sans crypto la privacy de
lecture n'est pas applicable ; migration = re-architecture consommateur).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-27 11:30:33 +02:00

10 KiB

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<Nuri, Set<PrincipalId>>, 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/grantReadseal(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<doc, Set<holder>> = 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.