MODÈLE CIBLE (readcap-and-nuri-model) — trois ajouts, deux corrections :
- Store public : lisible par l'URL, et NON récursif — un contenu public peut
référencer du contenu privé sans y donner accès. C'est la non-récursivité qui
porte la valeur (objet public pointant vers de l'identité privée).
- Le trousseau : la branche de store, où chaque création commite AddRepo{read_cap}
— avec l'avertissement explicite que ce n'est PAS le mécanisme de partage.
Confondre l'index privé et le geste de partage mène à « on partage le store »,
ce qui livrerait tout son contenu présent et futur.
- Rotation de clé : re-livraison par inbox, traitée automatiquement à la
connexion. Écrit comme DIRECTION, en signalant que le commentaire amont dont ça
partait décrit l'état courant.
- Levée de la confusion did/NURI en tête de la section grammaire : `did🆖` est
un préfixe de schéma présent partout, pas un marqueur de « sans cap ». C'est un
seul objet, avec ou sans la clé dedans.
- Livraison de cap par inbox signalée comme MANQUE (forme bonne, chemin absent).
ÉTAT COURANT (nextgraph-current-state) — 218 lignes ajoutées, structure intacte :
livraison de cap par inbox non implémentée ; vérification de signature d'auteur
jamais appelée au runtime (members map vide, //TODO) ; aucune sonde d'existence
au niveau SDK ; expose_outer codé en dur à false, absent du SDK ; protocole Ext
sans aucun contrôle. Plus trois constats d'exploitation : heal cold-start,
fork de compte sur provision concurrente, et l'abort du flush outbox sur
TopicNotFound. La mort du socket est seulement référencée (déjà couverte).
CORRECTION D'UN FAIT QUE J'AVAIS ÉNONCÉ FAUX : le digest d'auteur n'est PAS clé
sous le secret de lecture — il est clé par l'overlay outer, public. C'est le
CONTENU du commit qui est chiffré. La conclusion « vérifier suppose de pouvoir
lire » tient, le mécanisme diffère.
Lot PW (WriteCap = membership) RETIRÉ de la liste des phases : il restait planifié
alors que le brief déclare plus haut qu'il n'y a pas de membership. Il était en
outre justifié par un besoin de dédup par signature que le consommateur n'a pas —
sa dédup s'appuie sur l'overlay.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
28 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 : toute surface rendant de la donnée devra passer
par le déchiffrement-avec-clé. Aujourd'hui c'est FAUX, et bien plus largement
que ce brief ne l'écrivait d'abord — cartographie du 2026-07-27, VÉRIFIÉE :
seuls 4 sites consultent les caps (use-shape, read-filter,
read-model.readUnion, discovery.submitToIndex). Tout le reste rend de la
donnée sans garde :
| Surface | État |
|---|---|
docs.sparqlQuery / sparqlUpdate |
contourne — appellent le ng injecté directement (contrainte assumée, pour éviter un DataCloneError de double-Proxy). La brèche la plus large : un id de session + un NURI suffisent à tout lire. |
inbox (read / readSynced / materialize / watch) |
contourne — aucun cap consulté ; les dépôts vont à qui les demande |
store-registry (zéro référence aux caps dans tout le fichier) |
contourne — la racine de confiance compte→NURI est en lecture universelle |
discovery.readIndex |
contourne en lecture (caps vérifiés à l'écriture seulement) |
subscribe, open-repo |
contournent — la poussée d'abonnement transporte l'état du doc sans contrôle |
watch-shape |
délègue volontairement à readUnion (ne re-filtre pas) |
Et le garde d'ÉCRITURE est déjà mort-né : ng-proxy garde sparql_update,
mais docs contourne le proxy par conception, et tous les écrivains
internes passent par docs. Le garde ne se déclenche donc que pour une app
appelant ng.sparql_update sur le ng exporté — ce que Festipod ne fait pas.
grantWrite / canWrite sont décoratifs. (Ce constat renforce §1 de la
revue adverse : l'écriture n'est pas un axe « à ajouter », c'est un axe qu'on
croyait couvert et qui ne l'est pas.)
Cet inventaire EST le périmètre de P1b. Le seul garde existant
(caps.canRead) est par ailleurs une ACL set-membership — l'inversion même
que la vision interdit.
- 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.
Périmètre élargi : le WriteCap (= membership) — RETIRÉ (2026-07-21)
Cette section était fausse et est conservée barrée comme garde-fou. Elle
importait une notion de membership lue dans l'état courant de nextgraph-rs
(AddMember, PermissionV0, member_pubkey) et la promouvait en forme cible.
Or (a) ces types sont un échafaudage inerte au runtime — verify_sig /
verify_perm ne sont appelés que dans des tests unitaires, les Repo sont
construits avec members: HashMap::new() ; et (b) le modèle cible n'a pas de
notion d'appartenance du tout : uniquement des clés et des URLs, symétrique
et asymétrique. Une forme en member/role/permission est donc exactement la
mauvaise forme que ce brief existe pour empêcher.
La leçon de méthode, qui vaut plus que la section retirée : lire l'état courant de NextGraph pour en déduire la forme cible est une erreur — l'état courant contient de l'inachevé qu'il ne faut pas figer dans le polyfill. Le source sert à vérifier un mécanisme existant, jamais à inférer une intention.
Contenu erroné conservé ci-dessous à titre de trace :
Section retirée
Pourquoi c'est ici et pas ailleurs. Le brief était d'abord centré ReadCap ; un constat adverse a montré qu'il ne compose pas avec son consommateur : le brief Festipod « inscriptions par Set » a besoin de dédupliquer les participations (un user = une participation par événement), et la seule base non-applicative disponible est la signature d'auteur du commit — donc un primitif d'écriture, pas de lecture. Un polyfill qui n'expose que la forme du ReadCap laisse le consommateur inventer sa propre dédup applicative → exactement la mauvaise forme.
La forme réelle (VÉRIFIÉE, cf. readcap-and-nuri-model.md §1) — et elle est
asymétrique de la lecture, ce qui est le point le plus facile à rater :
- Lecture = possession d'une clé. Pas d'ACL. Qui détient, lit.
- Écriture = membership + permissions (
AddMember,AddPermissionsur leRootBranch). C'est bel et bien une liste d'autorisation — pas de la possession. Émuler l'écriture « par possession de token » serait aussi faux que l'ACL de lecture actuelle, en miroir. - Les commits SONT signés par un
UserId(clé technique, distincte du profil) — donc un identifiant de dédup existe nativement, sans pseudonyme applicatif. - Mais vérifier une signature exige d'être membre du repo (accès au
member_pubkey). Un tiers non-membre voit un commit signé sans pouvoir l'attribuer. - Le dépôt d'inbox n'est PAS authentifié (sealed box anonyme) : un
fromdéclaré est du contenu, pas une preuve.
Ce que ça impose au polyfill. Exposer membership comme primitif distinct
de la possession de cap, avec au minimum : ajouter/retirer un membre d'un repo,
lire la members map quand on est membre, et vérifier l'auteur d'un commit
(→ un digest d'auteur, par-overlay donc par-store). C'est ce dernier point qui
débloque la dédup côté Festipod.
La conséquence de forme, à documenter explicitement (sinon le consommateur se trompe de modèle) : le digest d'auteur étant par-store, le choix du découpage en stores est le choix du niveau de corrélation. Un store stable par-user donne un identifiant traçable cross-événement ; un store par-événement donne un pseudonyme local à l'événement — dédup possible, corrélation impossible. Festipod a besoin du second. Le polyfill doit donc rendre ce découpage exprimable, pas le figer.
Reste ouvert : « le créateur d'un événement peut-il être membre du store qui contient les participations, sans pour autant en détenir la clé de lecture ? » — c'est-à-dire membership (écriture/vérification) et possession (lecture) réellement orthogonales. Si NextGraph les couple, la dédup vérifiée et l'anonymat s'excluent, et c'est le brief Festipod qui doit trancher ce qu'il sacrifie. À vérifier avant de façonner l'API.
(Question devenue sans objet : il n'y a pas de membership. La dédup ne passe pas par une vérification de signature — voir le brief Festipod « inscriptions ».)
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).
Verdict du spike (2026-07-21) — VÉRIFIÉ dans nextgraph-rs
| Réponse | Preuve | |
|---|---|---|
| Q1 — existence/fetch sans cap | NON (corrigé le 2026-07-27 — le verdict initial « OUI partiel » sur-lisait la preuve) | Le contrôle d'accès en lecture laisse effectivement passer (les lectures ne sont pas cap-gatées) — mais l'adressage présuppose le cap : aucune commande d'existence au niveau SDK ; la seule sonde est interne au crate, exige des BlockId et un repo chargé, et vise l'overlay inner dérivé du secret de lecture. Une référence cap-less n'a ni BlockId ni l'overlay requis. Voir readcap-and-nuri-model.md. |
| Q2 — détecter une suppression sans clé | NON | Broker append-only ; une suppression est un commit tombstone chiffré (RemoveRepo), no-op côté verifier. Sans clé on observe « une activité », jamais « une suppression ». |
| Q3 — confidentialité | OUI | Blocs stockés en ciphertext ; la clé est #[serde(skip)] (types.rs), dérivée du ReadCapSecret. Le keyless ne donne jamais le contenu. |
Ce que ça décide.
- P1 est débloqué :
resolveCapLess(nuri) → { exists }est la bonne forme — mais{ exists | deleted }ne l'est PAS. Ne pas exposer d'étatdeleted, ce serait inventer une capacité que la cible n'aura jamais (précisément le mode d'échec que ce brief combat). - Le retrait doit être un message, pas une observation. Côté consommateur : un nudge explicite. Le polyfill n'a rien à émuler pour ça — juste à ne pas prétendre le contraire.
- Tranché par la correction de Q1 : le compteur anonyme ne peut pas s'appuyer sur une validation d'existence — elle n'est pas constructible. Il repose donc sur du déclaratif, ce qui est acceptable (hors périmètre sécurité) tant que la forme exposée ne ment pas : ne pas exposer de primitif d'existence que la cible n'offrira pas.
P1a — la surface (spécification, révisée 2026-07-27 après double revue adverse)
Le découpage. P1a = la forme exposée aux consommateurs ; P1b = l'enforcement (chiffrement, fermeture de l'inventaire ci-dessus). Seul P1a bloque Festipod. Après P1a la forme est juste et l'isolation reste fausse : ne rien affirmer d'anonyme avant P1b.
Critère directeur, énoncé par le PO : être au plus près des concepts de NextGraph et de son SDK, pour réduire la complexité de développement et le nombre de notions à découvrir pour qui connaît déjà le SDK. Toute notion inventée ici est une dette de vocabulaire — le lecteur doit maintenir une table de traduction dans sa tête.
0. Ce que la première version de cette spec avait faux
Conservé délibérément : ces erreurs sont instructives et faciles à refaire.
| Erreur | Le réel |
|---|---|
Types DocRef / DocCap |
Deux noms pour un seul objet. En amont c'est un type, NuriV0 { target, access } : la clé est un champ, pas un autre type. Un NURI cap-less = access vide. did:ng: est le préfixe de schéma (inbox d:, branche b:, overlay v:…), pas un marqueur de « sans cap » — le discriminant est le segment :k:. |
DocCap comme nom |
S'appelle ReadCap en amont, et ce brief l'écrivait déjà ainsi. Un WriteCap étant prévu, un nom générique ne dirait même pas quel cap on manipule. |
| Types de marque TypeScript | Le SDK réel prend nuri: String. L'enforcement y est runtime, par la crypto, pas à la compilation. Une garantie de compilation est un concept que NextGraph n'a pas — et un consommateur qui typerait tout devrait dé-typer le jour où le vrai SDK arrive. L'inverse du but. |
recipient: PrincipalId |
Principal n'existe nulle part en amont. On adresse une inbox — et inbox.post(targetInbox: Nuri) le fait déjà ainsi dans ce paquet. |
sealCapTo promis durable |
Verbatim amont : « Those capabilities are not durable… if they don't subscribe, they will lose access after the refresh. For durable capabilities, see PermaCap » — et PermaCap est un TODO. |
resolveCapLess(ref) |
Inconstruisible : aucune commande d'existence au niveau SDK ; la seule sonde est interne au crate, exige des BlockId et un repo chargé, et adresse l'overlay inner (dérivé du secret de lecture). L'adressage présuppose le cap. |
parseNuri, refOf |
Doublons — assertNuri existe déjà à l'entrée SDK ; dégrader un cap se lit dans le champ target. |
1. Les types — un seul, la clé est un champ
Nuri reste ce qu'il est déjà dans ce paquet (une chaîne, ~90 usages) : la forme
cap-less, fidèle à la surface du SDK réel. On ajoute un seul nom, celui de
l'amont :
type Nuri = string // did:ng:o:{doc}:v:{overlay} — nomme, ne lit pas
type ReadCap = string // …:k:{clé} — nomme ET lit
La forme analysée { target, readCap? } — miroir 1:1 de NuriV0 { target, access } —
sert à l'intérieur du polyfill ; elle ne remonte pas dans les signatures de
l'entrée SDK. Un seul type nouveau, zéro churn, zéro cast aux frontières
ORM/SPARQL.
2. Le trousseau — d'où viennent les caps
Le premier jet posait « aucune fonction ne va d'une référence nue vers un cap », ce
qui rendait la spec inapplicable : doc_create renvoie un NURI cap-less, donc
le créateur ne pouvait jamais obtenir le cap de son propre document.
Le mécanisme réel était sous nos yeux : à chaque création, un AddRepo { read_cap }
est commité sur la branche du store. Cette branche est le trousseau du
propriétaire — c'est ainsi qu'il retrouve les caps de ses documents. (Ce n'est
PAS le mécanisme de partage : voir §3.)
capFor(nuri: Nuri): ReadCap | undefined // cherche dans le trousseau
L'invariant se reformule, et devient à la fois vrai et plus simple à dire :
On ne dérive pas un cap depuis une référence nue. On le cherche dans son trousseau — ou on l'a reçu.
Le trousseau, en amont, c'est le wallet : un concept que tout lecteur du SDK
connaît déjà. capFor absorbe canRead(doc) (capFor(n) !== undefined) et supprime
son verbe d'ACL.
3. Le partage — un DOCUMENT, à un ou plusieurs destinataires
L'unité de partage est le document, pas le store. Donner le cap d'un store livrerait tout son contenu, présent et futur — ce n'est pas le geste voulu, et c'est cohérent avec la doctrine consommateur (« le document est l'unité de partage et de droits »).
shareCap(cap: ReadCap, toInbox: Nuri): Promise<void>
On adresse une inbox, comme inbox.post le fait déjà. Les caps reçus
n'ont pas besoin d'une opération dédiée : ils arrivent comme dépôts d'inbox
d'un genre cap, consommés par l'inbox.watch existant. (Ce qui résout au
passage le point 7 de la revue adverse : la livraison d'un cap déclenche alors
naturellement une re-lecture, au lieu de laisser des vues périmées.)
Statut amont — c'est un MANQUE, pas un désaccord : le champ existe
(ContactDetails.read_cap, « if user wants to share the content of profile »)
mais le chemin est unimplemented!() et le récepteur jette le cap reçu. Le
polyfill l'émule donc en attendant → fiche dans le bug-inbox, avec ce qu'il faudra
en retirer le jour où l'amont l'implémente.
4. La rotation de clé — re-livraison automatique, pas perte d'accès
CORRIGÉ le 2026-07-27 (direction confirmée par le PO). Une version antérieure de cette section disait « qui n'est pas resté abonné perd l'accès » et demandait d'exposer une obligation d'abonnement. C'est faux comme cible. Le commentaire amont dont ça venait (« if they don't subscribe, they will lose access after the refresh ») décrit l'état courant, pas l'intention — l'erreur de méthode que ce brief met justement en garde de commettre.
La direction : quand une clé tourne, la nouvelle est envoyée dans l'inbox des utilisateurs qui conservent le droit d'accès, et cette inbox est traitée automatiquement dès qu'un client de l'utilisateur se connecte.
Conséquences pour la surface :
- Aucune obligation d'abonnement à exposer. Le consommateur n'a rien à implémenter pour « garder » un accès.
- L'accès n'est pas perdu, il est différé jusqu'à la prochaine connexion — cohérent avec le reste du modèle local-first.
- Une re-livraison emprunte le même canal que la livraison initiale : l'inbox. Donc le mécanisme de §3 couvre les deux, sans cas particulier.
- La révocation reste ce qu'elle est : on cesse de re-livrer à qui ne doit plus lire, et c'est non rétroactif.
5. Ce qui disparaît ou change de nom
| Aujourd'hui | Devient |
|---|---|
grantRead(doc, grantee) |
shareCap(cap, toInbox) |
canRead(doc, principal) |
absorbé par capFor(nuri) — le paramètre principal était l'inversion ACL |
protectedDocsOf(owner) |
supprimé — la boucle de re-dérivation disparaît |
makePublic(doc) |
publishRepoLink — le lien partageable a un nom en amont (RepoLinkV0) |
grantWrite / canWrite |
traités en P1b — aujourd'hui décoratifs (garde jamais déclenché) |
resetCaps() au changement d'identité |
basculer de trousseau, pas effacer |
PrincipalId dans la surface caps |
sort — on adresse des inboxes |
6. Le public — lisible par l'URL, et NON récursif
Principe cible (confirmé par le PO, 2026-07-27), à exposer tel quel :
Un élément du store public est public : qui a l'URL lit le contenu. Mais pas récursivement — un contenu public peut référencer du contenu privé, et la référence ne donne pas le contenu référencé.
C'est un second mécanisme à côté de la possession de clé du §2, pas une entorse à l'invariant. Et c'est la non-récursivité qui porte la valeur : elle permet un objet public qui pointe vers de l'identité privée — le cas exact du consommateur.
Détail d'implémentation à ignorer côté forme : NextGraph s'oriente vers un non-chiffrement du contenu du store public (les données restant signées). La surface ne doit pas en dépendre. Si le store public ne fonctionne pas comme ce principe le décrit, c'est le polyfill qui s'adapte — pas le consommateur.
7. Recette de P1a — vérifiable sans une ligne de crypto
watch-shape moissonne aujourd'hui toute chaîne did:ng: trouvée dans une
référence de découverte et replie ces documents dans l'ensemble lu — une
référence nue y donne donc lecture complète, la sémantique exactement inversée.
Après P1a : une référence nue moissonnée ne rend rien faute de cap au trousseau — ce que fait exactement le vrai NextGraph. Le test tient sans chiffrement, ce qui rend le découpage P1a/P1b honnête plutôt que cosmétique.
Esquisse de phases
- P1a — la surface : types
DocRef/DocCap,resolveCapLess,sealCapTodurable. Spécifié ci-dessus. Le seul lot qui bloque Festipod. - P1b — l'enforcement : chiffrement par-doc (cap = clé) et fermeture de l'inventaire des contournements. Sans lui la forme est juste mais l'isolation reste fausse — donc rien d'« anonyme » ne peut être affirmé.
- P2 — remplacer l'ACL par un modèle de possession de token (grant = livrer à un destinataire ; enforcement = possession). Requalifié par la revue adverse : le vrai contenu de P2 est durabilité + cap-less + re-partage par le détenteur, pas « inverser l'ACL » — sans crypto, inverser ne produit aucun delta observable.
- P3 — révocation par re-key (invalidation + re-livraison, non-rétroactive).
PW — WriteCap = membershipRETIRÉ (2026-07-27). Ce lot reposait sur une notion d'appartenance qui n'existe pas dans le modèle (tout est clés et URLs) ; voir la section barrée plus haut. Il était en outre justifié par un besoin de dédup par vérification de signature que le consommateur n'a finalement pas : sa dédup s'appuie sur l'overlay, store-scopé. Pour mémoire, deux faits vérifiés au passage, à ne pas re-découvrir : la vérification de signature d'auteur n'est pas appelée au runtime ; et le digest d'auteur n'est pas clé sous le secret de lecture — il est clé par l'overlay outer, public (c'est le contenu du commit qui est chiffré, d'où le fait que vérifier suppose quand même de pouvoir lire). Détail dansnextgraph-current-state.md.- P4 — adapter l'API consommateur +
migration-guide.md. La revue adverse requalifie ce lot : ce n'est pas un swap d'API mais une re-architecture consommateur (le grant se déplace vers l'acceptation de connexion et devient persistant ;declareConnectionsdisparaît).
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.