ReadCap read filter: consume the lib's per-document model + validate against broker
Align Festipod's @data read-filter scenario and harness bridge with ng-eventually's grant→ReadCap refactor: the access unit is the document (an item's `@graph`), not the item. - harness-ng.tsx: governDocument(reader, user)/setUser via getCaps()/resetCaps() (replaces setupReadFilter/setGrantOf); FilterProbe exposes a lazy snapshot() reflecting the current user without remount. - read-filter.feature/steps: validate per-document ReadCap on the real DeepSignalSet — govern the wallet document, grant the cap to another user → current user sees 0; current user gets the cap → sees all (all-or-nothing in mono-store, the faithful behavior). 5/5 steps pass against the broker. - doctrine: knowledge_stores-permissions records the verified store/document/ repo/ReadCap model (containment by reference, no read-cap inheritance); decision_2026-06-17_eventually-library updates the access-rights + filter status to the ReadCap model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@ Tout le polyfill qui compense l'immaturité de NextGraph (pas de lecture cross-w
|
||||
|
||||
### Comment les mécanismes tranchés s'y logent
|
||||
- **Identité / login** : le client fixe l'utilisateur courant (username en polyfill ; wallet en cible — [[decision_2026-06-15_shared-wallet-login-flow]]).
|
||||
- **Droits d'accès** : **capabilities émulées** comme données (grants attachés aux documents), enforcées **génériquement** par le client. L'app **attache les grants** via des opérations de cap anticipées (créer public, accorder à une connexion…) — **comme en cible**. Aucune politique n'est injectée ; seuls les shapes et les *actes* d'attribution viennent du consommateur.
|
||||
- **Droits d'accès** : **ReadCap émulées** dans un registre **par DOCUMENT** (`CapRegistry` : qui détient la read/write-cap de chaque NURI ; docs publics lisibles sans cap), enforcées **génériquement** par le client. L'unité d'accès est le **document = le `@graph`** de l'item, **jamais l'item** — fidèle au modèle vérifié ([[knowledge_stores-permissions]] : un store est un repo conteneur ; détenir la cap du store ne donne PAS celles des repos qu'il référence ; pas d'héritage de lecture). En mono-store (tout dans un repo) le filtre est donc **tout-ou-rien** sur ce document → la granularité fine **exige 1 document par entité**. L'app **ouvre/accorde les caps** via des opérations anticipées (`open(doc, scope, owner)`, `grantRead`, `makePublic`) — **comme en cible**. Aucune politique n'est injectée ; seuls les shapes et les *actes* d'attribution viennent du consommateur.
|
||||
- **Inbox** : `inbox.post(...)` (signature anticipée) côté client ; **matérialisation** par un **curateur** (package séparé, **différé**). Mécanisme réutilisé pour l'inscription PdR **et** la soumission à l'index.
|
||||
- **Découverte** : index **alimenté via son inbox** ([[decision_2026-06-16_discovery-model]]). Le client **dépose** (inbox) + **lit** (abonnement) ; un **curateur** matérialise. Le **propriétaire cible** de l'index reste à décider (app singleton ?, incertain — [[knowledge_apps-and-services]]).
|
||||
- **Synchronisation** : `s'abonner à un document` (natif). En polyfill, wallet partagé ⇒ sync multi-device native entre sessions.
|
||||
@@ -35,7 +35,7 @@ Tout le polyfill qui compense l'immaturité de NextGraph (pas de lecture cross-w
|
||||
## Conséquences
|
||||
|
||||
- **Festipod ne dépend que de `@ng-eventually/client`** ; la complexité du polyfill est invisible côté app ; rien de Festipod dans la lib.
|
||||
- **Migration** : retirer l'alias de build + l'appel de bootstrap → le client redevient le vrai SDK ; **traduire les grants émulés en vraies caps** (étape de données). Le **mécanisme cible de l'index global** reste à décider (app singleton ?, [[knowledge_apps-and-services]]) — ce n'est **pas** un backend. Le code applicatif ne bouge pas.
|
||||
- **Migration** : retirer l'alias de build + l'appel de bootstrap → le client redevient le vrai SDK ; **traduire les ReadCap émulées (registre par document) en vraies caps NextGraph** (étape de données). Le **mécanisme cible de l'index global** reste à décider (app singleton ?, [[knowledge_apps-and-services]]) — ce n'est **pas** un backend. Le code applicatif ne bouge pas.
|
||||
- Le [[brief_2026-06-15_shared-wallet-shim]] décrit désormais **comment Festipod consomme `ng-eventually`** (les mécanismes y sont *réalisés par la lib*), plus une implémentation interne à l'app.
|
||||
|
||||
## Statut d'intégration (2026-06-25)
|
||||
@@ -51,10 +51,10 @@ Tout le polyfill qui compense l'immaturité de NextGraph (pas de lecture cross-w
|
||||
- **Point d'injection unique (option 1)** : dans l'app, **seul `ngSession`** importe le vrai SDK au runtime — uniquement pour `configure(...)`. Tout le reste de l'app (data, lifecycle, login, types) passe par la lib.
|
||||
- **Pourquoi pas « lib importe le SDK elle-même »** : la lib étant dans un **repo séparé** (arbre `node_modules` distinct), si elle importait `@ng-org` au runtime, le bundle aurait **deux copies** d'`@ng-org` → l'ORM (signaux mono-instance) casserait. L'injection garantit **un seul exemplaire** (celui de Festipod). *(Le « zéro accès direct » exigerait la lib en workspace dans le repo — écarté pour la garder externe ; cf. options 2/3 discutées.)*
|
||||
- **Exceptions assumées** (hors « app ») : `src/shared/test-harness/auth-setup.tsx` (bootstrap wallet de test) et `src/shared/test-harness/harness.tsx` (harness **mock**, `deepSignal`) gardent un import direct `@ng-org`. Les **bindings ORM générés** (`festipodShapes.*`) aussi (types générés).
|
||||
- **Filtre de lecture — IMPLÉMENTÉ & validé (2026-06-25)** : `read-filter.ts` — `makeReadFilteredView` (un **Proxy** sur le set réactif : itération/`size`/`forEach` filtrés par `canRead(grant, utilisateur)`, mutations forwardées) + `filterReadable` (pur). `useShape` l'applique **uniquement si un `grantOf` est configuré** (sinon passthrough → pas de régression). Grant = donnée portée par le document (résolveur `grantOf` injecté ; domaine-agnostique). Validé : **4 tests unitaires** (logique + Proxy + utilisateur dynamique) **et un scénario `@data`** qui l'exerce sur le **vrai `DeepSignalSet`** contre le broker (filtre actif → seules les participations de l'utilisateur ciblé). *Piège rencontré : ne pas cibler `td.currentUserId` (vide au build du bridge) — viser un vrai utilisateur des données.*
|
||||
- **Validé (global)** : build Festipod · `@ui` 4/4 · **`@data` 9/9 (47 steps) contre le broker** · lib (typecheck + **8 tests**).
|
||||
- **Filtre ReadCap — IMPLÉMENTÉ & validé (2026-06-29, refactor du modèle grant→ReadCap)** : `caps.ts` — `CapRegistry` (read/write-cap **par document NURI** + docs publics ; `open/grantRead/grantWrite/makePublic/canRead/canWrite/governsRead/hasReadPolicy`). `read-filter.ts` — `makeReadFilteredView` (un **Proxy** sur le set réactif : itération/`size`/`forEach` gardés par `caps.canRead(item['@graph'], utilisateur)` ; un item sans `@graph` ou dans un document non gouverné est conservé ; mutations forwardées) + `filterReadable` (pur). `useShape` l'applique **uniquement si `caps.hasReadPolicy()`** (sinon passthrough → pas de régression). **Plus de `grantOf` injecté** : le filtre lit l'`@graph` et consulte le registre — automatique et domaine-agnostique. Validé : **6 tests `caps` + 4 tests `read-filter`** (logique + Proxy + utilisateur dynamique + non-héritage entre documents) **et un scénario `@data`** sur le **vrai `DeepSignalSet`** contre le broker : on gouverne le document du wallet par une ReadCap accordée à un autre utilisateur → l'utilisateur courant voit **0** ; il obtient la cap → il voit **toutes** les participations (tout-ou-rien en mono-store, fidèle).
|
||||
- **Validé (global, 2026-06-29)** : `@data` ReadCap 5/5 steps contre le broker · lib (typecheck `rc=0` + **10 tests**). *(2 échecs e2e préexistants « J'y serai » = libellé obsolète depuis le portage redesign 5a29938, hors périmètre — l'app ne déclare aucune cap, `useShape` reste en passthrough.)*
|
||||
|
||||
Reste à implémenter dans la lib (stubs `TODO`, nécessitent la couche comptes/grants pour être *actifs* dans l'app) : **garde d'écriture**, **`inbox.post`** + matérialisation, **login wallet partagé**.
|
||||
Reste à implémenter dans la lib (stubs `TODO`, nécessitent la couche comptes/caps pour être *actifs* dans l'app) : **garde d'écriture** (`caps.canWrite` est prêt côté registre), **`inbox.post`** + matérialisation, **login wallet partagé**.
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -35,9 +35,13 @@ Tout wallet a d'office les **3 stores** private/protected/public (session : `pri
|
||||
|
||||
## Concepts transverses
|
||||
|
||||
**Document vs Repo.** *« A Repo is the equivalent of an E2EE group for one and only one Document. »* **1 document = 1 repo** (commits + permissions). Identifiant : `did:ng:o:<RepoID>`. Un **store** est lui-même un document spécial qui regroupe et permissionne d'autres documents.
|
||||
**Document vs Repo.** *« A Repo is the equivalent of an E2EE group for one and only one Document. »* **1 document = 1 repo** (commits + permissions). Identifiant : `did:ng:o:<RepoID>`. **Il n'existe pas de type `Document`** dans le code (`nextgraph-rs`, vérifié 2026-06-29) : « document » = **un repo quelconque**. Un **store est un repo spécial** (`is_store=true`, avec branches `Store`/`Overlay`/`User`) — donc *un store est un document, mais un document n'est pas forcément un store*.
|
||||
|
||||
**Granularité.** Écriture gérée au niveau **Document (repo)**, pas branche/bloc. Lecture plus fine possible (par bloc/branche). Héritage : un Group store peut faire hériter ses permissions à ses documents.
|
||||
**Containment (store → repos) par RÉFÉRENCE, pas par liste.** Un store **ne contient pas** un `Vec<RepoId>` : il référence ses repos via un **graphe RDF** dans sa branche Overlay/User. À l'inverse, chaque repo déclare son store parent via `RootBranchV0.store: StoreOverlay` (`engine/repo/src/types.rs`) → **un repo appartient à exactement un store**. C'est la « structure de graphe » : un store **peut contenir d'autres documents**.
|
||||
|
||||
**Granularité des caps.** `ReadCap = ObjectRef`. Granularité au niveau **repo ET branche** (chaque branche a son `read_cap`), jusqu'au **bloc** (clé `ObjectKey`/ChaCha20). Écriture gérée au niveau **Document (repo)**.
|
||||
|
||||
**Pas d'héritage de lecture automatique.** Détenir la ReadCap d'un **store** ne donne **pas** accès aux repos qu'il contient — **il faut la ReadCap de chaque repo**. L'héritage optionnel `inherit_perms_users_and_quorum_from_store: Option<ReadCap>` ne partage que les **users/quorum** (écriture/permissions), **pas** la possession de read-cap. (Repos d'un private_store : héritage implicite.) **Conséquence pour l'émulation** : l'unité d'accès en lecture est le **repo = le `@graph`** de chaque item — un filtre par document, pas par store ni par item (cf. [[decision_2026-06-17_eventually-library]]).
|
||||
|
||||
**Capability / Nuri.** Le partage transmet un **Nuri** embarquant la capability crypto (lecture et/ou écriture). Pas d'ACL centralisée : posséder le Nuri = le droit. *« adding permissions can be done offline »* ; *« removing permissions … requires a SyncSignature »* (synchrone).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user