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
7.3 KiB
Modèle ReadCap & NURI de NextGraph — et l'émulation caps du polyfill
Établi 2026-07-20, VÉRIFIÉ par lecture directe du cœur Rust nextgraph-rs
(sauf points marqués INFÉRÉ). Les file:line sont datés — les numéros de ligne
sont volatils, se repérer par symbole/regex.
But : donner la vérité-terrain du modèle de droits d'accès NextGraph, pour
aligner l'émulation caps.ts du polyfill (aujourd'hui une ACL — l'inverse du
modèle réel). C'est la base de l'item « aligner ReadCap/WriteCap avec NextGraph ».
1. Un ReadCap = possession d'une clé, PAS une ACL par-identité
Un ReadCap est fondamentalement une clé cryptographique que l'on détient, pas une entrée d'ACL liée à un wallet. « Qui détient la clé peut lire. »
- Structure :
ReadCap = ObjectRef = BlockRef { id: BlockId, key: SymKey }(engine/repo/src/types.rs:461, 463-471, 557, 565).id: BlockId= digest BLAKE3 (adresse de l'objet chiffré).key: SymKey = ChaCha20Key([u8;32])= la clé de déchiffrement. Détenir le couple → le broker sert les blocs chiffrés parid, on déchiffre localement aveckey.
- Granularité : par commit/objet l'
ObjectRefest le cap ; pour une branche → commit de définition ; pour un repo → RootBranch ; pour un store → cap du repo racine (types.rs:559-565).ReadCapSecret= la moitié clé (:567-570). - Il n'y a PAS de read-ACL. L'appartenance/permissions d'un repo
(
RootBranch,AddMember,AddPermission) gouvernent l'écriture/admin, pas la lecture. La lecture n'est gardée que par la possession de la clé.
2. Accorder la lecture = sceller la clé au destinataire
« Grant » = livrer le cap scellé (crypto_box seal, chiffrement à clé
publique anonyme) à la pubkey d'inbox du destinataire — seul lui l'ouvre avec
sa clé privée.
- Message d'inbox scellé :
InboxMsgBody.msg=crypto_box::seal(... to_inbox ...), ouvert avec la clé secrète d'inbox (engine/net/src/types.rs:4272, 4299, 4319). - Le payload peut porter un cap :
ContactDetails.read_cap: Option<ReadCap>(« if user wants to share the content of profile ») (net/types.rs:4232-4233) → grant dirigé (scellé à un destinataire). - Variante non-dirigée :
RepoLinkV0.read_cap= un lien partageable que quiconque le reçoit peut ouvrir (net/types.rs:5061-5078).
Le « ciblage wallet » vit donc dans l'enveloppe de scellage, pas dans le cap :
le cap reste {id, clé}, possession-based.
3. Révocation = re-key (grossier, non-rétroactif)
On ne « reprend » pas une clé livrée. Révoquer = re-chiffrer avec une nouvelle clé et ne la re-sceller qu'aux autorisés restants.
- « Capabilities are not durable: they can be refreshed by members and previously
shared Caps become obsolete/revoked… if [a member] doesn't subscribe, they lose
access after the refresh » (
net/types.rs:5055-5058). - Mécanisme :
RootCapRefresh/BranchCapRefresh(repo/src/commit.rs:616,630; permstypes.rs:1748-1749). - Conséquences : grossier (échelle repo/branche), non-rétroactif (ce qui a été lu avant reste connu de l'ex-détenteur ; il ne déchiffre que les versions antérieures au refresh).
- Livraison durable d'un cap =
PermaCap— encore TODO (repo/types.rs:578).
4. Grammaire NURI : cap-less vs cap-porteur (le segment :k:)
Le discriminant est le segment :k:{clé} : présent = cap-porteur ; absent =
cap-less (nomme/localise sans donner le droit de lire). C'est de première
classe dans le type : NuriV0.target (des ids) et access/objects (le cap)
sont des champs séparés — un NURI d'id parse avec access: vec![]
(engine/net/src/app_protocol.rs:53-62, 99-118, 181-195, 659-677).
Cap-less (id + overlay éventuel, pas de clé) — formatters app_protocol.rs,
regexes net/types.rs :
did:ng:o:{repo_id}(:315,RE_REPO_Otypes.rs:52)did:ng:o:{repo_id}:v:{overlay_id}(:263,RE_REPOtypes.rs:55)did:ng:o:{repo_id}:v:{overlay_id}:b:{branch_id}(RE_BRANCHtypes.rs:58)did:ng:o:{repo_id}:c:{commit_id}(:355)did:ng:b:{branch}/h:{topic}/v:{overlay}/d:{inbox}(:327,323,319,359)
Cap-porteur (embarque la clé) :
did:ng:j:{id}:k:{clé}— read cap d'objet/fichier (repo/types.rs:511,RE_FILE_READ_CAPtypes.rs:49)did:ng:o:{repo}:c:{commit}:k:{clé}(RE_COMMITtypes.rs:73)- liste
RE_OBJECTS…:[cj]:{id}:k:{clé}…:l:{locator}(types.rs:64)
L'overlay est dérivé publiquement de l'id du repo (OverlayId::outer(store_id),
:259) → id+overlay ne fuitent aucun secret.
INFÉRÉ : un détenteur sans clé peut vraisemblablement récupérer les blocs chiffrés (avec l'overlay, toujours cap-less) → vérifier l'existence d'un doc sans lire son contenu. Le chemin d'autorisation de fetch broker pour un détenteur sans clé n'a pas été tracé — à confirmer avant de s'en servir.
5. Ce que le polyfill émule (caps.ts) — et où ça diverge
packages/client/src/caps.ts modélise readers: Map<Nuri, Set<PrincipalId>> +
grantRead(doc, grantee) (:29-30, 41-42) — une ACL de principals par
document, soit l'INVERSION exacte du modèle réel (clé). Divergences :
| Réel NextGraph | Émulation caps.ts | |
|---|---|---|
| Nature | possession de clé | ACL (set de principals) |
| Grant | sceller la clé (crypto_box) à l'inbox | ajouter un principal au set |
| Durabilité | durable (clé livrée une fois) | éphémère (Map vide à chaque session → re-déclarée) |
| Révocation | re-key grossier, non-rétroactif | retrait du set : instantané et total |
| Granularité | repo / branche / commit / objet | un cap par doc-NURI |
| Réf. sans droit | NURI cap-less (sans :k:) |
pas de notion (l'ACL dit qui peut) |
Face app : declareConnections (côté consommateur) qui re-déclare « mes
connexions lisent mes entités protected » à chaque session est un artefact
de cette ACL éphémère — sans objet dans le modèle réel (les scellages y sont
durables ; on scelle par-doc au partage, pas par-session).
6. Implications pour les consommateurs (ex. Festipod)
- « scope protected = mon réseau peut lire » n'est pas une ACL vérifiée par le broker : c'est « j'ai scellé ma read key à chacune de mes connexions ». Le modèle mental « scope = ACL » est faux au niveau NextGraph.
- Références anonymes possibles : mettre un NURI cap-less dans une collection tierce laisse le tiers nommer/compter sans lire l'identité ; sceller le cap-porteur séparément aux seuls autorisés. (Base d'un modèle de présence « participation auto-possédée + Set curé cap-less + cap scellé aux connexions ».)
- Alignement à faire : quand les vraies opérations de cap seront disponibles,
remplacer l'ACL émulée par du scellage de clé durable par-doc, et
declareConnections-comme-ACL-ré-déclarée disparaît.
Réserves / lacunes
file:linedatés (2026-07) — re-vérifier par symbole ; le core bouge.- INFÉRÉ : fetch broker keyless (existence sans clé) — non tracé au runtime.
- Non tracé : exécution complète de
RootCapRefreshcôté verifier (verifier/src/commits/mod.rs:616), stockage wallet deprivate_store_read_cap(repo/types.rs:945,976).