docs: passer vision, readcap-and-nuri-model et l'incident en anglais
Le reste du dossier docs/ était déjà en anglais ; ces trois fichiers avaient été rédigés en français par erreur. Traduction fidèle, sans changement de fond : mêmes sections, mêmes tableaux, mêmes blocs de code. Le retour à la ligne dur à 78 colonnes est levé (une ligne par paragraphe, convention du projet). Marqueurs épistémiques préservés et rendus aussi visibles : VERIFIED / INFERRED / CORRECTED / DIRECTION / GAP. Les citations verbatim de commentaires amont restent intactes. Deux incohérences de FOND signalées par la traduction et corrigées ici — elles étaient invisibles tant qu'on lisait chaque section isolément : - readcap-and-nuri-model, section « Caveats / gaps » : elle listait encore le fetch keyless comme hypothèse INFÉRÉE à confirmer, alors que le bloc CORRIGÉ du §4bis la déclare fausse et non constructible. Contradiction interne née de ma correction partielle. Conservée barrée plutôt que supprimée : l'hypothèse est intuitive et se reformera sinon. - incident write-loss : l'intro affirmait en fait établi que « l'écriture n'atteint jamais durablement le broker », alors que la réserve épistémique plus bas dit explicitement que l'alternative (perte d'écriture vs réhydratation à froid) n'est pas tranchée. L'intro ne rapporte plus que le symptôme observé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
@@ -1,57 +1,57 @@
|
|||||||
# Perte d'écriture lors d'une mort de socket (`SerializationError`)
|
# Write loss on socket death (`SerializationError`)
|
||||||
|
|
||||||
**Post-mortem — 2026-07-14 · Statut : OUVERT (non traité).**
|
**Post-mortem — 2026-07-14 · Status: OPEN (not addressed).**
|
||||||
|
|
||||||
Une entité écrite juste avant une période d'inactivité peut être **perdue silencieusement** : l'écriture n'atteint jamais durablement le broker, et l'entité est absente à la reconnexion. Le **compte / l'identité survit** (pas de fork). Observé en conditions réelles (Festipod, Firefox) lors d'une pause après login/création.
|
An entity written just before a period of inactivity can be **silently lost**: it is absent on reconnection. *(Whether the write never durably reached the broker, or reached it and is not read back on a cold reconnection, is **not settled** — see Epistemic caveat below. The wording here deliberately states only the observed symptom.)* The **account / identity survives** (no fork). Observed in real conditions (Festipod, Firefox) during a pause after login/creation.
|
||||||
|
|
||||||
## Symptôme
|
## Symptom
|
||||||
|
|
||||||
1. L'utilisateur se connecte, l'app crée une entité (un événement Festipod).
|
1. The user logs in, the app creates an entity (a Festipod event).
|
||||||
2. Une période d'inactivité suit (idle, onglet en arrière-plan…).
|
2. A period of inactivity follows (idle, tab in the background…).
|
||||||
3. Le socket broker meurt spontanément avec `SOCKET IS CLOSED Some(Left(SerializationError))`.
|
3. The broker socket dies spontaneously with `SOCKET IS CLOSED Some(Left(SerializationError))`.
|
||||||
4. À la reconnexion, l'entité créée a disparu ; l'app relit son propre scope **vide**.
|
4. On reconnection, the created entity has disappeared; the app reads back its own scope **empty**.
|
||||||
|
|
||||||
## Preuves (VÉRIFIÉ — logs Firefox en direct, verbatim)
|
## Evidence (VERIFIED — live Firefox logs, verbatim)
|
||||||
|
|
||||||
```
|
```
|
||||||
… REPLAY TOPIC NOT FOUND <topic> IN OVERLAY <overlay>
|
… REPLAY TOPIC NOT FOUND <topic> IN OVERLAY <overlay>
|
||||||
… NEED REPLAY true
|
… NEED REPLAY true
|
||||||
… SENDING EVENTS FROM OUTBOX RETURNED: Err(TopicNotFound)
|
… SENDING EVENTS FROM OUTBOX RETURNED: Err(TopicNotFound)
|
||||||
[user1][polyfill] resolveAccount(user1) → 1 record ← le compte SURVIT (pas de fork)
|
[user1][polyfill] resolveAccount(user1) → 1 record ← the account SURVIVES (no fork)
|
||||||
[user1][polyfill] readScopeIndex(…) → 0 entities ← mais le scope est VIDE
|
[user1][polyfill] readScopeIndex(…) → 0 entities ← but the scope is EMPTY
|
||||||
… set reçu: 0 objets Event (public)
|
… set reçu: 0 objets Event (public)
|
||||||
… SOCKET IS CLOSED Some(Left(SerializationError)) [51, 3, 223, …]
|
… SOCKET IS CLOSED Some(Left(SerializationError)) [51, 3, 223, …]
|
||||||
```
|
```
|
||||||
|
|
||||||
Lecture (**mécanisme plausible, non tranché**) : l'écriture a été poussée dans l'**outbox** local, mais le socket est mort avant qu'elle ne soit **flushée durablement** dans le topic broker ; à la reconnexion, le replay de l'outbox échoue (`Err(TopicNotFound)`) parce que le topic n'a **jamais été créé côté broker** → l'événement est abandonné. Le compte, lui, avait déjà été résolu durablement (`resolveAccount → 1 record`) : il n'est ni perdu ni forké.
|
Interpretation (**plausible mechanism, not settled**): the write was pushed into the local **outbox**, but the socket died before it was **durably flushed** into the broker topic; on reconnection, the outbox replay fails (`Err(TopicNotFound)`) because the topic was **never created on the broker side** → the event is abandoned. The account, for its part, had already been durably resolved (`resolveAccount → 1 record`): it is neither lost nor forked.
|
||||||
|
|
||||||
> **Réserve épistémique.** Les preuves établissent le *symptôme* (perte + `Err(TopicNotFound)` + `readScopeIndex → 0`). Le *mécanisme* exact n'est pas tranché entre **(i) perte à l'écriture** (l'écriture n'atteint jamais durablement le broker) et **(ii) échec de réhydratation à froid** (l'écriture *est* sur le broker mais une session fraîche ne rouvre pas le scope propre). Le `Err(TopicNotFound)` sur le replay outbox penche pour **(i) dans ce cas Firefox**. Voir la repro @data ci-dessous, qui expose un symptôme voisin mais **ne tranche pas** (i) vs (ii).
|
> **Epistemic caveat.** The evidence establishes the *symptom* (loss + `Err(TopicNotFound)` + `readScopeIndex → 0`). The exact *mechanism* is not settled between **(i) loss at write time** (the write never durably reaches the broker) and **(ii) cold-rehydration failure** (the write *is* on the broker but a fresh session does not reopen its own scope). The `Err(TopicNotFound)` on the outbox replay leans toward **(i) in this Firefox case**. See the @data repro below, which exhibits a neighboring symptom but **does not settle** (i) vs (ii).
|
||||||
|
|
||||||
## Chaîne causale (TRACÉ — lecture du core NextGraph, à re-vérifier)
|
## Causal chain (TRACED — reading of the NextGraph core, to be re-verified)
|
||||||
|
|
||||||
- La `SerializationError` ferme le socket. Le core émet la déconnexion : `broker.rs` → `LocalBrokerMessage::Disconnected` → `disconnections_sender.send(...)` (≈ `broker.rs:1051`, à re-vérifier — numéro volatil, se repérer par le symbole).
|
- The `SerializationError` closes the socket. The core emits the disconnection: `broker.rs` → `LocalBrokerMessage::Disconnected` → `disconnections_sender.send(...)` (≈ `broker.rs:1051`, to be re-verified — volatile number, navigate by symbol).
|
||||||
- Cette déconnexion est **poussée** aux abonnés via `disconnections_subscribe(cb)` (flux PUSH).
|
- This disconnection is **pushed** to subscribers via `disconnections_subscribe(cb)` (PUSH stream).
|
||||||
- **La reconnexion NextGraph est un `// TODO` non implémenté** (≈ `broker.rs:1051-1076`) : rien ne rétablit le socket ni ne re-flushe l'outbox.
|
- **NextGraph reconnection is an unimplemented `// TODO`** (≈ `broker.rs:1051-1076`): nothing re-establishes the socket nor re-flushes the outbox.
|
||||||
- `user_connect` renvoie un **instantané** `{ server_id, server_ip, error, since }` au moment de l'appel — pas un flux, inutilisable pour détecter une chute ultérieure.
|
- `user_connect` returns a **snapshot** `{ server_id, server_ip, error, since }` at call time — not a stream, unusable for detecting a later drop.
|
||||||
- **Aucune API de confirmation de durabilité d'écriture** : un appelant ne peut pas `await` la garantie qu'une écriture a atteint le broker.
|
- **No write-durability confirmation API**: a caller cannot `await` the guarantee that a write has reached the broker.
|
||||||
|
|
||||||
## Ce que le SDK expose mais ne consomme pas
|
## What the SDK exposes but does not consume
|
||||||
|
|
||||||
`disconnections_subscribe` **se déclenche** sur cette panne — mais ni le polyfill (`@ng-eventually/client`) ni l'app consommateur ne s'y abonnent. Le signal existe, personne ne l'écoute ; côté app, aucun mécanisme ne re-tente ni n'avertit l'utilisateur.
|
`disconnections_subscribe` **does fire** on this failure — but neither the polyfill (`@ng-eventually/client`) nor the consumer app subscribes to it. The signal exists, nobody listens to it; on the app side, no mechanism retries or warns the user.
|
||||||
|
|
||||||
## Portée & non-reproduit
|
## Scope & not reproduced
|
||||||
|
|
||||||
- **Observé Firefox uniquement** à ce jour. Un test manuel sur un autre navigateur n'a pas déclenché la `SerializationError` ni ses conséquences.
|
- **Observed on Firefox only** to date. A manual test on another browser did not trigger the `SerializationError` nor its consequences.
|
||||||
- **Reproduction @data (Chromium, broker réel) — 2026-07-14, décisive.** Le test de reconnexion @data existant (`reconnexion-meme-identite`) était **faux-vert** : il relisait les repos de A depuis l'**IndexedDB local** du profil persistant, jamais depuis le broker. Un lecteur **réellement à froid** (contexte non-persistant `freshBrowser`, **même** wallet/compte A, aucun état local — seedé du wallet capturé avant l'événement) lit **0** événement de A (`BARRIER timed-out (8000ms)`, `CONNECTION ESTABLISHED`). Signature **différente** du cas Firefox (pas de mort de socket ; l'`OUTBOX empty` est celui du lecteur, trivialement vide) et **ne tranche pas** (i) vs (ii) — un barrier vide est compatible avec les deux. Établi en revanche : **@data n'a jamais vérifié la durabilité broker des lectures propres de A**, et la réhydratation à froid depuis le broker échoue. Repro : `src/modules/event/features/reconnexion-froide-sans-local.feature` (Festipod).
|
- **@data reproduction (Chromium, real broker) — 2026-07-14, decisive.** The existing @data reconnection test (`reconnexion-meme-identite`) was a **false green**: it read A's repos back from the persistent profile's **local IndexedDB**, never from the broker. A **genuinely cold** reader (non-persistent `freshBrowser` context, the **same** wallet/account A, no local state — seeded from the wallet captured before the event) reads **0** events from A (`BARRIER timed-out (8000ms)`, `CONNECTION ESTABLISHED`). A **different** signature from the Firefox case (no socket death; the `OUTBOX empty` is the reader's, trivially empty) and it **does not settle** (i) vs (ii) — an empty barrier is compatible with both. Established on the other hand: **@data has never verified the broker durability of A's own reads**, and cold rehydration from the broker fails. Repro: `src/modules/event/features/reconnexion-froide-sans-local.feature` (Festipod).
|
||||||
- **Pour trancher (i) vs (ii)** : vérifier indépendamment que l'écriture de A atteint le broker — p.ex. un lecteur *chaud* / une seconde identité lit le doc public de l'événement (le scénario d'isolation deux-identités). S'il le voit → l'écriture est durable → le 0 du lecteur à froid est un **(ii)** (réhydratation). Sinon → **(i)**.
|
- **To settle (i) vs (ii)**: independently verify that A's write reaches the broker — e.g. a *warm* reader / a second identity reads the event's public doc (the two-identity isolation scenario). If it sees it → the write is durable → the cold reader's 0 is a **(ii)** (rehydration). Otherwise → **(i)**.
|
||||||
|
|
||||||
## Pistes de correction (non arbitré)
|
## Fix leads (not arbitrated)
|
||||||
|
|
||||||
1. **Core** — corriger la `SerializationError` **et** implémenter le TODO de reconnexion (rétablir le socket + re-flusher l'outbox).
|
1. **Core** — fix the `SerializationError` **and** implement the reconnection TODO (re-establish the socket + re-flush the outbox).
|
||||||
2. **SDK / polyfill** — consommer `disconnections_subscribe` → reconnexion + re-flush outbox comme mitigation, indépendamment du core.
|
2. **SDK / polyfill** — consume `disconnections_subscribe` → reconnection + outbox re-flush as a mitigation, independently of the core.
|
||||||
3. **API de durabilité** — exposer une confirmation qu'une écriture a atteint le broker, pour que l'appelant puisse l'`await`.
|
3. **Durability API** — expose a confirmation that a write has reached the broker, so that the caller can `await` it.
|
||||||
|
|
||||||
## Liens
|
## Links
|
||||||
|
|
||||||
- `docs/nextgraph-current-state.md` — état courant du core (déconnexion / reconnexion à cross-référencer ici).
|
- `docs/nextgraph-current-state.md` — current state of the core (disconnection / reconnection to be cross-referenced here).
|
||||||
- Impact produit + caveat côté consommateur : concept Festipod `data-layer` → `caveat_write-durability-across-disconnect`.
|
- Product impact + consumer-side caveat: Festipod concept `data-layer` → `caveat_write-durability-across-disconnect`.
|
||||||
|
|||||||
+84
-205
@@ -1,268 +1,147 @@
|
|||||||
# Modèle ReadCap & NURI de NextGraph — et l'émulation caps du polyfill
|
# NextGraph's ReadCap & NURI model — and the polyfill's caps emulation
|
||||||
|
|
||||||
**Établi 2026-07-20**, VÉRIFIÉ par lecture directe du cœur Rust `nextgraph-rs`
|
**Established 2026-07-20**, VERIFIED by direct reading of the `nextgraph-rs` Rust core (except for points marked INFERRED). The `file:line` references are dated — line numbers are volatile, navigate by symbol/regex.
|
||||||
(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
|
Purpose: to give the ground truth of NextGraph's access-rights model, in order to align the polyfill's `caps.ts` emulation (today an ACL — the inverse of the real model). This is the basis for the item "align ReadCap/WriteCap with NextGraph".
|
||||||
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é
|
## 1. A ReadCap = possession of a key, NOT a per-identity ACL
|
||||||
|
|
||||||
Un **ReadCap est fondamentalement une clé cryptographique que l'on détient**, pas
|
A **ReadCap is fundamentally a cryptographic key that one holds**, not an ACL entry tied to a wallet. "Whoever holds the key can read."
|
||||||
une entrée d'ACL liée à un wallet. « Qui détient la clé peut lire. »
|
|
||||||
|
|
||||||
- Structure : `ReadCap = ObjectRef = BlockRef { id: BlockId, key: SymKey }`
|
- Structure: `ReadCap = ObjectRef = BlockRef { id: BlockId, key: SymKey }` (`engine/repo/src/types.rs:461, 463-471, 557, 565`).
|
||||||
(`engine/repo/src/types.rs:461, 463-471, 557, 565`).
|
- `id: BlockId` = **BLAKE3** digest (address of the encrypted object).
|
||||||
- `id: BlockId` = digest **BLAKE3** (adresse de l'objet chiffré).
|
- `key: SymKey = ChaCha20Key([u8;32])` = the **decryption key**.
|
||||||
- `key: SymKey = ChaCha20Key([u8;32])` = la **clé de déchiffrement**.
|
Holding the pair → the broker serves the encrypted blocks by `id`, and one decrypts **locally** with `key`.
|
||||||
Détenir le couple → le broker sert les blocs chiffrés par `id`, on déchiffre
|
- Granularity: per commit/object the `ObjectRef` **is** the cap; for a branch → its defining commit; for a repo → RootBranch; for a store → the root repo's cap (`types.rs:559-565`). `ReadCapSecret` = the key half (`:567-570`).
|
||||||
**localement** avec `key`.
|
- **There is NO read-ACL.** A repo's membership/permissions (`RootBranch`, `AddMember`, `AddPermission`) govern **writing/admin**, not reading. Reading is guarded only by key possession.
|
||||||
- Granularité : par commit/objet l'`ObjectRef` **est** 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
|
## 2. Granting read access = sealing the key to the recipient
|
||||||
|
|
||||||
« Grant » = livrer le cap **scellé** (`crypto_box seal`, chiffrement à clé
|
"Grant" = delivering the cap **sealed** (`crypto_box seal`, anonymous public-key encryption) to the recipient's **inbox pubkey** — only they can open it with their private key.
|
||||||
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 ...)`,
|
- Sealed inbox message: `InboxMsgBody.msg` = `crypto_box::seal(... to_inbox ...)`, opened with the inbox secret key (`engine/net/src/types.rs:4272, 4299, 4319`).
|
||||||
ouvert avec la clé secrète d'inbox (`engine/net/src/types.rs:4272, 4299, 4319`).
|
- The payload can carry a cap: `ContactDetails.read_cap: Option<ReadCap>` ("if user wants to share the content of profile") (`net/types.rs:4232-4233`) → **directed grant** (sealed to one recipient).
|
||||||
- Le payload peut porter un cap : `ContactDetails.read_cap: Option<ReadCap>`
|
- **Undirected** variant: `RepoLinkV0.read_cap` = a shareable link that **whoever receives it** can open (`net/types.rs:5061-5078`).
|
||||||
(« 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 :
|
So "wallet targeting" lives in the **sealing envelope**, not in the cap: the cap remains `{id, key}`, possession-based.
|
||||||
le cap reste `{id, clé}`, possession-based.
|
|
||||||
|
|
||||||
> **État courant (2026-07-27) — le chemin est un MANQUE, pas un désaccord.** Le
|
> **Current state (2026-07-27) — the path is a GAP, not a disagreement.** The `ContactDetails.read_cap` field exists, but the construction of the message is `unimplemented!()` (its only caller passes "without read_cap") and the receiver **discards** the cap it would receive. The *shape* is therefore the right one; the implementation is not there. The polyfill emulates it in the meantime — filed in the bug-inbox.
|
||||||
> champ `ContactDetails.read_cap` existe, mais la construction du message est
|
|
||||||
> `unimplemented!()` (son unique appelant passe « sans read_cap ») et le récepteur
|
|
||||||
> **jette** le cap qu'il recevrait. La *forme* est donc la bonne ; l'implémentation
|
|
||||||
> n'est pas là. Le polyfill l'émule en attendant — fiche dans le bug-inbox.
|
|
||||||
|
|
||||||
## 3. Révocation = re-key (grossier, non-rétroactif)
|
## 3. Revocation = re-key (coarse, non-retroactive)
|
||||||
|
|
||||||
On ne « reprend » pas une clé livrée. Révoquer = **re-chiffrer** avec une nouvelle
|
A delivered key is not "taken back". To revoke = **re-encrypt** with a new key and re-seal it only to the remaining authorized holders.
|
||||||
clé et ne la re-sceller qu'aux autorisés restants.
|
|
||||||
|
|
||||||
- « Capabilities are not durable: they can be refreshed by members and previously
|
- "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`).
|
||||||
shared Caps become obsolete/revoked… if [a member] doesn't subscribe, they lose
|
- Mechanism: `RootCapRefresh` / `BranchCapRefresh` (`repo/src/commit.rs:616,630`; perms `types.rs:1748-1749`).
|
||||||
access after the refresh » (`net/types.rs:5055-5058`).
|
- Consequences: **coarse** (repo/branch scale), **non-retroactive** (what was read before remains known to the former holder; they only decrypt the versions **prior to** the refresh).
|
||||||
- Mécanisme : `RootCapRefresh` / `BranchCapRefresh` (`repo/src/commit.rs:616,630`;
|
- **Durable** delivery of a cap = `PermaCap` — still **TODO** (`repo/types.rs:578`).
|
||||||
perms `types.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`).
|
|
||||||
|
|
||||||
### DIRECTION — la rotation ne fait PAS perdre l'accès (confirmé PO, 2026-07-27)
|
### DIRECTION — rotation does NOT cause access to be lost (confirmed by the PO, 2026-07-27)
|
||||||
|
|
||||||
**Ne pas lire le commentaire ci-dessus comme l'intention.** « *if they don't
|
**Do not read the comment above as the intent.** "*if they don't subscribe, they lose access after the refresh*" describes **the current state**, not the target. What NextGraph is aiming for:
|
||||||
subscribe, they lose access after the refresh* » décrit **l'état courant**, pas la
|
|
||||||
cible. Ce que NextGraph vise :
|
|
||||||
|
|
||||||
> Quand une clé tourne, la nouvelle est **envoyée dans l'inbox** des utilisateurs
|
> When a key is rotated, the new one is **sent to the inbox** of the users who retain the access right. That inbox is **processed automatically** as soon as one of the user's clients connects.
|
||||||
> qui conservent le droit d'accès. Cette inbox est **traitée automatiquement** dès
|
|
||||||
> qu'un client de l'utilisateur se connecte.
|
|
||||||
|
|
||||||
Donc l'accès n'est **pas perdu**, il est **différé** jusqu'à la prochaine connexion
|
So access is **not lost**, it is **deferred** until the next connection — consistent with local-first. Shape consequences: **no subscription obligation** to expose to the consumer; a re-delivery takes **the same channel** as the initial delivery, so the sharing mechanism covers both with no special case. **Revocation** remains "stop re-delivering", non-retroactive.
|
||||||
— cohérent avec le local-first. Conséquences de forme : **aucune obligation
|
|
||||||
d'abonnement** à exposer au consommateur ; une re-livraison emprunte **le même
|
|
||||||
canal** que la livraison initiale, donc le mécanisme de partage couvre les deux
|
|
||||||
sans cas particulier. La **révocation** reste « cesser de re-livrer », non
|
|
||||||
rétroactive.
|
|
||||||
|
|
||||||
## 4. Grammaire NURI : cap-less vs cap-porteur (le segment `:k:`)
|
## 4. NURI grammar: cap-less vs cap-bearing (the `:k:` segment)
|
||||||
|
|
||||||
**Lever la confusion d'abord** : `did:ng:` n'est **pas** un marqueur de « sans
|
**Clearing up the confusion first**: `did:ng:` is **not** a "cap-less" marker, it is the **URI scheme prefix** — present everywhere (inbox `did:ng:d:…`, branch `did:ng:b:…`, overlay `did:ng:v:…`, document `did:ng:o:…`). A NURI **is** a `did:ng:…`. So there is no "the did" on one side and "the NURI" on the other: it is **a single object**, with or without the key inside it — a single type upstream, `NuriV0 { target, access }`, where a cap-less NURI simply has an empty `access`.
|
||||||
cap », c'est le **préfixe de schéma d'URI** — présent partout (inbox `did:ng:d:…`,
|
|
||||||
branche `did:ng:b:…`, overlay `did:ng:v:…`, document `did:ng:o:…`). Un NURI **est**
|
|
||||||
un `did:ng:…`. Il n'y a donc pas « le did » d'un côté et « le NURI » de l'autre :
|
|
||||||
c'est **un seul objet**, avec ou sans la clé dedans — un seul type en amont,
|
|
||||||
`NuriV0 { target, access }`, où un NURI cap-less a simplement `access` vide.
|
|
||||||
|
|
||||||
Le discriminant est le segment **`:k:{clé}`** : présent = cap-porteur ; **absent =
|
The discriminant is the **`:k:{key}`** segment: present = cap-bearing; **absent = cap-less** (names/locates **without** granting the right to read). This is **first-class** in the type: `NuriV0.target` (ids) and `access`/`objects` (the cap) are **separate fields** — an id-only NURI parses with `access: vec![]` (`engine/net/src/app_protocol.rs:53-62, 99-118, 181-195, 659-677`).
|
||||||
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`,
|
**Cap-less** (id + optional overlay, no key) — formatters in `app_protocol.rs`, regexes in `net/types.rs`:
|
||||||
regexes `net/types.rs` :
|
|
||||||
- `did:ng:o:{repo_id}` (`:315`, `RE_REPO_O` types.rs:52)
|
- `did:ng:o:{repo_id}` (`:315`, `RE_REPO_O` types.rs:52)
|
||||||
- `did:ng:o:{repo_id}:v:{overlay_id}` (`:263`, `RE_REPO` types.rs:55)
|
- `did:ng:o:{repo_id}:v:{overlay_id}` (`:263`, `RE_REPO` types.rs:55)
|
||||||
- `did:ng:o:{repo_id}:v:{overlay_id}:b:{branch_id}` (`RE_BRANCH` types.rs:58)
|
- `did:ng:o:{repo_id}:v:{overlay_id}:b:{branch_id}` (`RE_BRANCH` types.rs:58)
|
||||||
- `did:ng:o:{repo_id}:c:{commit_id}` (`:355`)
|
- `did:ng:o:{repo_id}:c:{commit_id}` (`:355`)
|
||||||
- `did:ng:b:{branch}` / `h:{topic}` / `v:{overlay}` / `d:{inbox}` (`:327,323,319,359`)
|
- `did:ng:b:{branch}` / `h:{topic}` / `v:{overlay}` / `d:{inbox}` (`:327,323,319,359`)
|
||||||
|
|
||||||
**Cap-porteur** (embarque la clé) :
|
**Cap-bearing** (embeds the key):
|
||||||
- `did:ng:j:{id}:k:{clé}` — read cap d'objet/fichier (`repo/types.rs:511`,
|
- `did:ng:j:{id}:k:{key}` — object/file read cap (`repo/types.rs:511`, `RE_FILE_READ_CAP` types.rs:49)
|
||||||
`RE_FILE_READ_CAP` types.rs:49)
|
- `did:ng:o:{repo}:c:{commit}:k:{key}` (`RE_COMMIT` types.rs:73)
|
||||||
- `did:ng:o:{repo}:c:{commit}:k:{clé}` (`RE_COMMIT` types.rs:73)
|
- list `RE_OBJECTS` `…:[cj]:{id}:k:{key}…:l:{locator}` (types.rs:64)
|
||||||
- liste `RE_OBJECTS` `…:[cj]:{id}:k:{clé}…:l:{locator}` (types.rs:64)
|
|
||||||
|
|
||||||
Le segment `:v:` est l'**overlay**, qui a sa propre section ci-dessous — c'est le
|
The `:v:` segment is the **overlay**, which has its own section below — it is the point with the heaviest consequences for anonymous-presence models.
|
||||||
point le plus lourd de conséquences pour les modèles de présence anonyme.
|
|
||||||
|
|
||||||
## 4bis. L'overlay est l'espace réseau d'un STORE — jamais d'un document
|
## 4bis. The overlay is the network space of a STORE — never of a document
|
||||||
|
|
||||||
**L'overlay est l'unité d'adressage réseau d'un store.** Chez le broker, les blocs
|
**The overlay is a store's unit of network addressing.** At the broker, blocks are filed under a `(overlay, block_id)` key, and peers synchronize *within* an overlay. Two forms per store:
|
||||||
sont rangés sous une clé `(overlay, block_id)`, et les pairs se synchronisent
|
|
||||||
*dans* un overlay. Deux formes par store :
|
|
||||||
|
|
||||||
| | Dérivation | Qui peut le calculer |
|
| | Derivation | Who can compute it |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **outer** | `OverlayId::outer(store_id)` = BLAKE3 **public** | tout le monde (le store_id suffit) |
|
| **outer** | `OverlayId::outer(store_id)` = **public** BLAKE3 | everyone (the store_id is enough) |
|
||||||
| **inner** | `OverlayId::inner(store_id, readcap_secret)` = BLAKE3 **keyed** | seulement qui détient la clé de lecture du store |
|
| **inner** | `OverlayId::inner(store_id, readcap_secret)` = **keyed** BLAKE3 | only whoever holds the store's read key |
|
||||||
|
|
||||||
Cohérent avec le reste du modèle : pas de rôle ni de liste, seulement « détiens-tu
|
Consistent with the rest of the model: no role and no list, only "do you hold the key that lets you derive this identifier". `outer` = the store's public name, `inner` = its private name.
|
||||||
la clé qui permet de dériver cet identifiant ». `outer` = le nom public du store,
|
|
||||||
`inner` = son nom privé.
|
|
||||||
|
|
||||||
**Le `:v:` d'un NURI de DOCUMENT porte l'overlay de son STORE** (VÉRIFIÉ, chaîne
|
**The `:v:` of a DOCUMENT NURI carries the overlay of its STORE** (VERIFIED, chain read end to end): `NuriV0::repo_graph_name(repo_id, overlay_id)` formats `o:{repo_id}:v:{overlay_id}`; in `doc_create` the value injected is `store.outer_overlay()` — the **containing** store, never the `repo_id`. A `Repo` carries **no** overlay field (only `store: Arc<Store>`); it is `Store` that carries `overlay_id`. **Mechanical counter-proof**: in `Store`, `get`/`put`/`del`/`has` all pass `&self.overlay_id` to the block storage — every document of a store shares the same block namespace, so a per-document overlay is structurally impossible.
|
||||||
lue de bout en bout) : `NuriV0::repo_graph_name(repo_id, overlay_id)` formate
|
|
||||||
`o:{repo_id}:v:{overlay_id}` ; dans `doc_create` la valeur injectée est
|
|
||||||
`store.outer_overlay()` — le store **contenant**, jamais le `repo_id`. Un `Repo` ne
|
|
||||||
porte **aucun** champ overlay (seulement `store: Arc<Store>`) ; c'est `Store` qui
|
|
||||||
porte `overlay_id`. **Contre-preuve mécanique** : dans `Store`, `get`/`put`/`del`/`has`
|
|
||||||
passent tous `&self.overlay_id` au block storage — tous les documents d'un store
|
|
||||||
partagent le namespace de blocs, donc un overlay par-document est structurellement
|
|
||||||
impossible.
|
|
||||||
|
|
||||||
### La conséquence à connaître : le `:v:` est un pseudonyme stable
|
### The consequence to know about: the `:v:` is a stable pseudonym
|
||||||
|
|
||||||
**Tous les documents d'une même personne dans son store protected portent le MÊME
|
**All of one person's documents in their protected store carry the SAME `:v:`** = `outer(protected_store_id)`. So a cap-less reference — precisely the one used to "name without granting read" — **exposes store membership**, that is to say a **stable and permanent pseudonymous identifier of the person**. The store_id itself does not leak (BLAKE3 is not invertible), so it does not say *who*; but it is a **constant handle**, the same everywhere and forever, correlatable by anyone who collects cap-less references.
|
||||||
`:v:`** = `outer(protected_store_id)`. Donc une référence cap-less — précisément
|
|
||||||
celle qu'on utilise pour « nommer sans donner à lire » — **expose l'appartenance
|
|
||||||
au store**, c'est-à-dire un **identifiant pseudonyme stable et permanent de la
|
|
||||||
personne**. Le store_id lui-même ne fuit pas (BLAKE3 non inversible), donc ça ne
|
|
||||||
dit pas *qui* ; mais c'est un **handle constant**, le même partout et pour
|
|
||||||
toujours, corrélable par quiconque collecte des références cap-less.
|
|
||||||
|
|
||||||
**Le couplage qui en résulte, et qui contraint tout modèle de présence anonyme** :
|
**The coupling that results, and that constrains any anonymous-presence model**: that same `:v:` is *simultaneously* (a) what makes it possible to **deduplicate** references without reading them — two references with the same `:v:` come from the same person — and (b) what makes it possible to **track** that person from one context to another. **It is the same bit of information.** You cannot get the dedup without conceding the tracking, nor remove the tracking without losing the dedup — short of changing how the stores are carved up, which moves the cursor but does not remove the trade-off.
|
||||||
ce même `:v:` est *simultanément* (a) ce qui permet de **dédupliquer** des
|
|
||||||
références sans les lire — deux références de même `:v:` viennent de la même
|
|
||||||
personne — et (b) ce qui permet de **tracer** cette personne d'un contexte à
|
|
||||||
l'autre. **C'est le même bit d'information.** On ne peut pas obtenir la dédup sans
|
|
||||||
concéder le traçage, ni supprimer le traçage sans perdre la dédup — sauf à changer
|
|
||||||
le découpage en stores, ce qui déplace le curseur mais ne supprime pas l'arbitrage.
|
|
||||||
|
|
||||||
*Nuances.* Le `:v:` du NURI est l'overlay **outer**, alors que le trafic
|
*Nuances.* The NURI's `:v:` is the **outer** overlay, whereas client↔broker traffic and local storage use the **inner** one — a different value, but derived from the store as well, so the property holds in both cases. A `Dialog` store returns an `Inner`, still store-scoped.
|
||||||
client↔broker et le stockage local utilisent l'**inner** — valeur différente, mais
|
|
||||||
tirée du store elle aussi, donc la propriété tient dans les deux cas. Un store
|
|
||||||
`Dialog` renvoie un `Inner`, toujours store-scopé.
|
|
||||||
|
|
||||||
**CORRIGÉ le 2026-07-27 — cette hypothèse était FAUSSE.** On avait inféré, puis
|
**CORRECTED on 2026-07-27 — this hypothesis was FALSE.** We had inferred, then believed we had verified, that a holder **without a key** could fetch the encrypted blocks and therefore prove a document's **existence**. An adversarial review showed that the reasoning stopped at *access control* without looking at **addressing**:
|
||||||
cru vérifier, qu'un détenteur **sans clé** pouvait récupérer les blocs chiffrés et
|
|
||||||
donc prouver l'**existence** d'un document. Une revue adverse a montré que le
|
|
||||||
raisonnement s'arrêtait au *contrôle d'accès* sans regarder l'**adressage** :
|
|
||||||
|
|
||||||
- Il n'existe **aucune commande d'existence au niveau SDK**.
|
- There is **no existence command at the SDK level**.
|
||||||
- La seule sonde (`BlocksExist`) est **interne au crate**, exige des `BlockId`
|
- The only probe (`BlocksExist`) is **internal to the crate**, requires `BlockId`s **and** an already **loaded** repo, and addresses the **inner** overlay — which is derived from the **read secret**.
|
||||||
**et** un repo déjà **chargé**, et adresse l'overlay **inner** — lequel est
|
- A cap-less reference carries a RepoId and the **outer** overlay: no `BlockId` to probe. And the outer is never registered anyway (`expose_outer` hard-coded to `false`, with no SDK parameter).
|
||||||
dérivé du **secret de lecture**.
|
- The only primitive accessible to a non-member (`ExtObjectGet`) requires the ObjectIds **and their keys**.
|
||||||
- Une référence cap-less porte un RepoId et l'overlay **outer** : aucun `BlockId`
|
|
||||||
à sonder. Et l'outer n'est de toute façon jamais enregistré (`expose_outer`
|
|
||||||
codé en dur à `false`, sans paramètre SDK).
|
|
||||||
- Le seul primitif accessible à un non-membre (`ExtObjectGet`) exige les ObjectIds
|
|
||||||
**et leurs clés**.
|
|
||||||
|
|
||||||
> **L'adressage lui-même présuppose le cap.** Prouver l'existence d'un document
|
> **Addressing itself presupposes the cap.** Proving a document's existence without holding its key is not constructible today, and nothing indicates that it is planned.
|
||||||
> sans détenir sa clé n'est pas constructible aujourd'hui, et rien n'indique que
|
|
||||||
> ce soit prévu.
|
|
||||||
|
|
||||||
Leçon transposable : vérifier qu'une garde d'accès **laisse passer** ne prouve pas
|
Transferable lesson: verifying that an access guard **lets you through** does not prove that an operation is reachable — you still have to be able to **name** what you are asking for.
|
||||||
qu'une opération est atteignable — encore faut-il pouvoir **nommer** ce qu'on
|
|
||||||
demande.
|
|
||||||
|
|
||||||
## 4ter. Le store public : lisible par l'URL, et NON récursif
|
## 4ter. The public store: readable by URL, and NOT recursive
|
||||||
|
|
||||||
Principe cible (confirmé PO, 2026-07-27) :
|
Target principle (confirmed by the PO, 2026-07-27):
|
||||||
|
|
||||||
> **Un élément du store public est public : qui a l'URL lit le contenu.**
|
> **An element of the public store is public: whoever has the URL reads the content.**
|
||||||
> Mais **pas récursivement** — un contenu public peut *référencer* du contenu
|
> But **not recursively** — public content can *reference* private content, and the reference does **not** give access to the referenced.
|
||||||
> privé, et la référence ne donne **pas** accès au référencé.
|
|
||||||
|
|
||||||
C'est un **second mécanisme**, à côté de la possession de clé (§1) — pas une
|
This is a **second mechanism**, alongside key possession (§1) — not a breach of it. And it is the **non-recursiveness** that carries the value: it allows a public object that **points** to private identity, without divulging it. That is exactly the pattern an anonymous-presence model needs.
|
||||||
entorse. Et c'est la **non-récursivité** qui porte la valeur : elle autorise un
|
|
||||||
objet public qui **pointe** vers de l'identité privée, sans la divulguer. C'est
|
|
||||||
exactement le motif dont un modèle de présence anonyme a besoin.
|
|
||||||
|
|
||||||
*Détail d'implémentation, à ne PAS faire porter par la forme* : NextGraph s'oriente
|
*Implementation detail, NOT to be carried by the shape*: NextGraph is moving toward **not encrypting** the content of the public store (the data remaining **signed**). A surface must not depend on it. And if the public store does not behave the way this principle describes, it is **the polyfill** that adapts, not the consumer.
|
||||||
vers un **non-chiffrement** du contenu du store public (les données restant
|
|
||||||
**signées**). Une surface ne doit pas en dépendre. Et si le store public ne se
|
|
||||||
comporte pas comme ce principe le décrit, c'est **le polyfill** qui s'adapte, pas
|
|
||||||
le consommateur.
|
|
||||||
|
|
||||||
## 4quater. Le trousseau : d'où le propriétaire tire les caps de SES documents
|
## 4quater. The keyring: where the owner gets the caps for THEIR OWN documents
|
||||||
|
|
||||||
À chaque création de document, un `AddRepo { read_cap }` est commité sur une
|
On every document creation, an `AddRepo { read_cap }` is committed to a **store branch** — the store being itself a repo, endowed with **typed** branches (the word "branch" has nothing to do with git: it is a compartment with a defined role). That branch lists **the store's documents, each with its read key**.
|
||||||
**branche du store** — le store étant lui-même un repo, doté de branches **typées**
|
|
||||||
(le mot « branche » n'a rien de git : c'est un compartiment à rôle défini). Cette
|
|
||||||
branche liste **les documents du store, chacun avec sa clé de lecture**.
|
|
||||||
|
|
||||||
Elle **est** donc le **trousseau du propriétaire** : le mécanisme par lequel il
|
So it **is** the **owner's keyring**: the mechanism by which they find the caps of their own documents. Upstream of that, the keyring is the **wallet**.
|
||||||
retrouve les caps de ses propres documents. En amont, le trousseau, c'est le
|
|
||||||
**wallet**.
|
|
||||||
|
|
||||||
**Ce n'est PAS le mécanisme de partage.** Confusion facile et coûteuse : en déduire
|
**This is NOT the sharing mechanism.** An easy and costly confusion: concluding "we share at the store level" is wrong — delivering a store cap would give access to **all** of its content, present and future. **The unit of sharing is the document** (§2). The keyring is a private index, not an act of sharing.
|
||||||
« on partage au niveau du store » est faux — livrer un cap de store donnerait accès
|
|
||||||
à **tout** son contenu, présent et futur. **L'unité de partage est le document**
|
|
||||||
(§2). Le trousseau est un index privé, pas un geste de partage.
|
|
||||||
|
|
||||||
*(VÉRIFIÉ pour le mécanisme `AddRepo { read_cap }` ; le **nom exact** des branches
|
*(VERIFIED for the `AddRepo { read_cap }` mechanism; the **exact name** of the branches and the enumeration of their types have not been re-traced — to be confirmed if this point becomes load-bearing.)*
|
||||||
et l'énumération de leurs types n'ont pas été re-tracés — à confirmer si ce point
|
|
||||||
devient porteur.)*
|
|
||||||
|
|
||||||
## 5. Ce que le polyfill émule (caps.ts) — et où ça diverge
|
## 5. What the polyfill emulates (caps.ts) — and where it diverges
|
||||||
|
|
||||||
`packages/client/src/caps.ts` modélise `readers: Map<Nuri, Set<PrincipalId>>` +
|
`packages/client/src/caps.ts` models `readers: Map<Nuri, Set<PrincipalId>>` + `grantRead(doc, grantee)` (`:29-30, 41-42`) — **a per-document ACL of principals, that is the exact INVERSION of the real model** (key). Divergences:
|
||||||
`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 |
|
| | Real NextGraph | caps.ts emulation |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Nature | possession de **clé** | **ACL** (set de principals) |
|
| Nature | possession of a **key** | **ACL** (set of principals) |
|
||||||
| Grant | sceller la clé (crypto_box) à l'inbox | ajouter un principal au set |
|
| Grant | seal the key (crypto_box) to the inbox | add a principal to the set |
|
||||||
| Durabilité | **durable** (clé livrée une fois) | **éphémère** (Map vide à chaque session → re-déclarée) |
|
| Durability | **durable** (key delivered once) | **ephemeral** (Map empty every session → re-declared) |
|
||||||
| Révocation | **re-key** grossier, non-rétroactif | retrait du set : **instantané et total** |
|
| Revocation | coarse **re-key**, non-retroactive | removal from the set: **instantaneous and total** |
|
||||||
| Granularité | repo / branche / commit / objet | **un cap par doc-NURI** |
|
| Granularity | repo / branch / commit / object | **one cap per doc-NURI** |
|
||||||
| Réf. sans droit | **NURI cap-less** (sans `:k:`) | pas de notion (l'ACL dit qui peut) |
|
| Ref. without rights | **cap-less NURI** (no `:k:`) | no such notion (the ACL says who may) |
|
||||||
|
|
||||||
**Face app** : `declareConnections` (côté consommateur) qui re-déclare « mes
|
**App-facing**: `declareConnections` (on the consumer side), which re-declares "my connections read my protected entities" **every session**, is an **artifact of this ephemeral ACL** — moot in the real model (there the seals are durable; one seals per-doc at share time, not per-session).
|
||||||
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)
|
## 6. Implications for consumers (e.g. Festipod)
|
||||||
|
|
||||||
- « **scope protected = mon réseau peut lire** » n'est **pas** une ACL vérifiée
|
- "**protected scope = my network can read**" is **not** an ACL checked by the broker: it is "I have **sealed my read key** to each of my connections". The "scope = ACL" mental model is wrong at the NextGraph level.
|
||||||
par le broker : c'est « j'ai **scellé ma read key** à chacune de mes
|
- **Anonymous references are possible**: putting a **cap-less NURI** in a third party's collection lets that third party **name/count** without **reading the identity**; the cap-bearing one is sealed separately to the authorized parties only. (Basis for a presence model of the form "self-owned participation + curated cap-less Set + cap sealed to the connections".)
|
||||||
connexions ». Le modèle mental « scope = ACL » est faux au niveau NextGraph.
|
- **Alignment to do**: when the real cap operations become available, replace the emulated ACL with durable per-doc key sealing, and `declareConnections`-as-a-re-declared-ACL disappears.
|
||||||
- **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
|
## Caveats / gaps
|
||||||
|
|
||||||
- `file:line` datés (2026-07) — re-vérifier par symbole ; le core bouge.
|
- `file:line` references are dated (2026-07) — re-verify by symbol; the core moves.
|
||||||
- INFÉRÉ : fetch broker keyless (existence sans clé) — non tracé au runtime.
|
- ~~INFERRED: keyless broker fetch (existence without a key)~~ — **RESOLVED and REFUTED, 2026-07-27**: not constructible. See the CORRECTED block in §4bis. Kept struck through because the hypothesis is intuitive and will otherwise be re-formed.
|
||||||
- Non tracé : exécution complète de `RootCapRefresh` côté verifier
|
- Not traced: the full execution of `RootCapRefresh` on the verifier side (`verifier/src/commits/mod.rs:616`), wallet storage of `private_store_read_cap` (`repo/types.rs:945,976`).
|
||||||
(`verifier/src/commits/mod.rs:616`), stockage wallet de `private_store_read_cap`
|
|
||||||
(`repo/types.rs:945,976`).
|
|
||||||
|
|||||||
+19
-48
@@ -1,61 +1,32 @@
|
|||||||
# Vision & principes du polyfill `@ng-eventually/client`
|
# Vision & principles of the `@ng-eventually/client` polyfill
|
||||||
|
|
||||||
## Raison d'être
|
## Purpose
|
||||||
|
|
||||||
Un **stand-in fidèle en FORME** des primitives futures de NextGraph. Objectif
|
A **stand-in faithful in SHAPE** to NextGraph's future primitives. **Single** objective: that consumers (Festipod) be **coded against the CORRECT mental model** — the one of finished NextGraph — and have **NOTHING to rewrite** when NextGraph provides the real primitives.
|
||||||
**unique** : que les consommateurs (Festipod) soient **codés contre le modèle
|
|
||||||
mental CORRECT** — celui de NextGraph fini — et **n'aient RIEN à réécrire** quand
|
|
||||||
NextGraph fournira les vraies primitives.
|
|
||||||
|
|
||||||
## Ce que le polyfill n'est PAS
|
## What the polyfill is NOT
|
||||||
|
|
||||||
Une couche de **sécurité**. Le **wallet partagé** (tout le monde partage les mêmes
|
A **security** layer. The **shared wallet** (everyone shares the same keys) plus the absence of real crypto make the emulation **infinitely less secure** than a wallet-per-user — it is a **dev/staging vehicle**, not a goal. **Insecurity is ACCEPTED.** An attacker who bypasses the emulation is not our problem.
|
||||||
clés) + l'absence de vraie crypto rendent l'émulation **infiniment moins
|
|
||||||
sécurisée** qu'un wallet-par-utilisateur — c'est un **véhicule de dev/staging**,
|
|
||||||
pas un but. **L'insécurité est ACCEPTÉE.** Un attaquant qui contourne l'émulation
|
|
||||||
n'est pas notre problème.
|
|
||||||
|
|
||||||
## Le seul critère : shape-fidelity, avec RIGUEUR
|
## The only criterion: shape-fidelity, with RIGOR
|
||||||
|
|
||||||
Les **surfaces exposées** doivent matcher **exactement la FORME** des primitives
|
The **exposed surfaces** must match the **exact SHAPE** of the future primitives, **even where enforcement is simulated**. The **failure mode to avoid**: exposing the **wrong shape** → the consumer codes against a model that will not exist → rewrite. The **ACL** inversion of ReadCaps was exactly that defect (an ACL where the real thing is **key possession**) — a lack of rigor.
|
||||||
futures, **même là où l'enforcement est simulé**. Le **mode d'échec à éviter** :
|
|
||||||
exposer la **mauvaise forme** → le consommateur code contre un modèle qui
|
|
||||||
n'existera pas → réécriture. L'inversion **ACL** des ReadCaps était exactement ce
|
|
||||||
défaut (une ACL là où le réel est **possession de clé**) — un manque de rigueur.
|
|
||||||
|
|
||||||
## Simuler la crypto pour EMPÊCHER les raccourcis
|
## Simulating crypto to PREVENT shortcuts
|
||||||
|
|
||||||
Sans un minimum de simulation crypto, des raccourcis préjudiciables sont pris (on
|
Without a minimum of crypto simulation, damaging shortcuts get taken (reading the plaintext, falling back on ACLs). The polyfill therefore **simulates** the final mechanism, enough to hold this **invariant**:
|
||||||
lit le clair, on retombe sur des ACLs). Le polyfill **simule** donc le mécanisme
|
|
||||||
final, assez pour tenir cet **invariant** :
|
|
||||||
|
|
||||||
> **Un `did` (id nu, SANS ReadCap) et un NURI (AVEC ReadCap) sont traités
|
> **A `did` (bare id, WITHOUT a ReadCap) and a NURI (WITH a ReadCap) are treated GENUINELY differently: the former does NOT allow reading the data; the latter is SUFFICIENT and REQUIRED.**
|
||||||
> VRAIMENT différemment : le premier ne permet PAS de lire la donnée ; le second
|
|
||||||
> est SUFFISANT et REQUIS.**
|
|
||||||
|
|
||||||
Concrètement : la donnée d'un document est **stockée chiffrée** (chiffrement
|
Concretely: a document's data is **stored encrypted** (per-doc symmetric encryption, however lightweight); the **ReadCap = the key**; without it, **decrypting/reading is impossible**. No ACL, no plaintext accessible "on the side". Obtaining read access = **holding the key**, exactly as in the target model.
|
||||||
symétrique par-doc, même léger) ; le **ReadCap = la clé** ; sans elle, **impossible
|
|
||||||
de déchiffrer/lire**. Pas d'ACL, pas de clair accessible « à côté ». Obtenir la
|
|
||||||
lecture = **détenir la clé**, exactement comme en cible.
|
|
||||||
|
|
||||||
## Conséquences de forme (à respecter partout)
|
## Shape consequences (to respect everywhere)
|
||||||
|
|
||||||
- **Tout est clés et URLs.** Il n'y a **pas** de notion d'appartenance, de rôle ni
|
- **Everything is keys and URLs.** There is **no** notion of membership, role, or authorization list in the model: only symmetric and asymmetric cryptography, URIs, and who holds which key. Any exposed shape that looks like an ACL, a `member`, a `role`, or a `permission` is a **wrong shape**, whatever scaffolding one may otherwise read in the current state of NextGraph.
|
||||||
de liste d'autorisation dans le modèle : uniquement de la cryptographie
|
- **Reading = possession of the read key** (ReadCap = `{id, key}`). A bare id (a `did` without a ReadCap) does not read.
|
||||||
symétrique et asymétrique, des URIs, et qui détient quelle clé. Toute forme
|
- **Writing = possession of the write key** — a key **distinct** from the read key, hence a distinct axis, but **possession too**.
|
||||||
exposée qui ressemble à une ACL, un `member`, un `role` ou une `permission` est
|
- **Sharing a cap = sealing it to a recipient** (**durable** delivery, at share time — NOT an ACL re-declared every session).
|
||||||
une **mauvaise forme**, quel que soit l'échafaudage qu'on peut lire par ailleurs
|
- **Revocation = re-key** (new key; former holders keep the old state). Non-retroactive.
|
||||||
dans l'état courant de NextGraph.
|
- **Cap-less reference** (naming/pointing without reading) **distinct** from the cap-bearing reference.
|
||||||
- **Lecture = possession de la clé de lecture** (ReadCap = `{id, clé}`). Un id nu
|
|
||||||
(un `did` sans ReadCap) ne lit pas.
|
|
||||||
- **Écriture = possession de la clé d'écriture** — une clé **distincte** de celle
|
|
||||||
de lecture, donc un axe distinct, mais **de la possession elle aussi**.
|
|
||||||
- **Partage d'un cap = le sceller à un destinataire** (livraison **durable**, au
|
|
||||||
moment du partage — PAS une ACL re-déclarée à chaque session).
|
|
||||||
- **Révocation = re-key** (nouvelle clé ; les anciens détenteurs gardent l'ancien
|
|
||||||
état). Non-rétroactif.
|
|
||||||
- **Référence cap-less** (nommer/pointer sans lire) **distincte** de la référence
|
|
||||||
cap-porteuse.
|
|
||||||
|
|
||||||
Voir `readcap-and-nuri-model.md` (le vrai modèle, vérifié dans `nextgraph-rs`) et
|
See `readcap-and-nuri-model.md` (the real model, verified in `nextgraph-rs`) and `briefs/2026-07-20-caps-emulation-alignment.md` (the alignment effort).
|
||||||
`briefs/2026-07-20-caps-emulation-alignment.md` (le chantier d'alignement).
|
|
||||||
|
|||||||
Reference in New Issue
Block a user