b2cb774124
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
399 lines
28 KiB
Markdown
399 lines
28 KiB
Markdown
# 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** : 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.
|
|
|
|
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**.
|
|
|
|
## ~~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 :
|
|
|
|
<details>
|
|
<summary>Section retirée</summary>
|
|
|
|
**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`, `AddPermission` sur le
|
|
`RootBranch`). 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 `from`
|
|
dé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.**
|
|
|
|
</details>
|
|
|
|
*(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 (`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).
|
|
|
|
### 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'état `deleted`, 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 :
|
|
|
|
```ts
|
|
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.)*
|
|
|
|
```ts
|
|
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 »).
|
|
|
|
```ts
|
|
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`,
|
|
`sealCapTo` durable. 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 = membership~~ **RETIRÉ (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 dans
|
|
`nextgraph-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 ; `declareConnections` disparaî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) :
|
|
|
|
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.
|