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
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
didnu (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.
- 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. - 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).
- 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 ». - Résoudre un cap-less = nommer / prouver l'existence / compter, sans exposer le contenu (support de la présence anonyme).
- 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 (declareConnectionsapp 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) :
- Tracer dans
nextgraph-rsle chemin d'autorisation de fetch du broker/verifier : qui sert les blocs (BlocksGet/TopicSync/OverlaySync) ? un cap/membership est-il vérifié, ouid+overlaysuffit ? l'overlay outer est-il public ? une suppression est-elle observable sans clé ? - (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. - (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) :
- 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-48garde chaquesparql_update. → garder une piste WriteCap = membership ; la possession ne concerne QUE la lecture. - P2 « possession sans crypto » = l'ACL renommée. Sans crypto, « qui détient quel token » =
Map<doc, Set<holder>>= lereadersactuel : 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 ». - Révocation non-rétroactive INÉMULABLE sans versioning :
read-model.ts:112-118ne 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. - cap-less « sans exposer le contenu » ILLUSOIRE dans l'émulation : contenu en clair dans le wallet partagé ;
sparqlQuery/inbox.readbypass le filtre ;read-filter.ts:30-35est 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é. - 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).
- Migration ≠ swap d'API.
declareConnectionsse 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 deprotectedDocsOf+ la boucle de re-dérivation. Re-architecture consommateur. - (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.