docs: NextGraph multi-user data model — stores, auth matrix, inbox fork plan

Capture the multi-user design exploration as project knowledge + briefs:
- knowledge: NextGraph store types/permissions (+ inbox at protocol, SDK
  exposure, local repo path); integration model (iframe, where the verifier
  runs, generic JS plumbing, ngd stateful, build-time broker target)
- briefs: multi-store refactor; authorization matrix + query inventory +
  derived store partitions; temporary fork to expose the inbox (3 layers:
  SDK fork, Coolify self-hosting, Festipod integration; libs via build:ng)
- fix stale @ng-org versions (alpha.11 -> alpha.13) and a broken
  decision-record link in data-layer.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-05-21 17:38:51 +02:00
parent ffda889f34
commit 445a448031
7 changed files with 468 additions and 40 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ See [decision record](../decisions/2026-03-17-1800-sparql-delete-for-orm-objects
- `src/shared/utils/ngGraph.ts``ensureGraphNuri()` returns `@graph` for entity creation
- `src/shared/utils/ngBootstrap.ts` — Seeds test data using `ensureGraphNuri()` for `@graph`
See [decision record](.project/decisions/2026-03-17-1600-private-store-nuri-scope.md) for why.
See [decision record](../decisions/2026-03-17-1600-private-store-nuri-scope.md) for why.
## Context Providers
@@ -0,0 +1,75 @@
# Modèle d'intégration et de déploiement NextGraph
Comment une app web tierce s'intègre à NextGraph, et où tourne le moteur (verifier).
## Overview
NextGraph s'utilise depuis une app web via un **proxy iframe** (`@ng-org/web`) : l'app tierce ne contient pas le moteur, elle délègue à un ng-app hébergé (par défaut `nextgraph.net`) qui exécute le moteur dans une iframe. Comprendre ce découpage est nécessaire pour savoir ce qu'on peut modifier sans auto-héberger. Vérifié dans `nextgraph-rs` le 2026-05-21 (voir [chemin du repo local](./nextgraph-stores-permissions.md#code-source-local)).
## Les paquets JS
- **`@ng-org/web`** — paquet **publié**. Proxy postMessage léger (aucun wasm embarqué). C'est **le** chemin d'intégration d'une app web tierce. `@ng-org/orm` et tous les exemples officiels (expense-tracker…) en dépendent. **Festipod l'utilise.**
- **`@ng-org/api-web`** — paquet **privé** (`"private": true`, non publié). Moteur navigateur complet : charge `@ng-org/lib-wasm` dans un Web Worker (`?worker&inline`), utilise `sessionStorage`/`Worker`. Consommé uniquement par `app/nextgraph` (le frontend ng-app) et `engine/broker/auth`. C'est le moteur **interne** de l'app NextGraph, **pas** une cible d'intégration tierce.
- **`@ng-org/lib-wasm`** — le moteur compilé en wasm (contient le verifier via la dépendance `nextgraph` / `local_broker`). Source : `sdk/js/lib-wasm/`.
- **`nextgraph`** (npm) — l'API **NodeJS** (build `pkg-node` de lib-wasm).
- **`@ng-org/orm`** — l'ORM réactif (`useShape`…), bâti sur `@ng-org/web`.
## Où tourne le verifier
Dans le modèle web standard (iframe), le verifier tourne **dans l'iframe** : `app/nextgraph` charge `api-web``lib-wasm` dans un Web Worker, côté navigateur. Le broker (`ngd`) ne fait que **le transport et le stockage**.
**Conséquence** : modifier la logique du verifier (ex. `request_processor`, `inbox_processor`) = reconstruire le **ng-app**, pas le broker.
## Le modèle iframe (intégration tierce)
- `@ng-org/web` redirige vers le ng-app hébergé, qui recharge l'app tierce dans une iframe après authentification, puis relaie les appels par `postMessage`.
- **Reciblable au build** via variables d'env (fichier `sdk/js/web/src/index.ts`) :
| Variable | Cible |
|---|---|
| `NG_REDIR_SERVER` | défaut `nextgraph.net` |
| `NG_DEV3` | `127.0.0.1:3033` |
| `NG_DEV` | `localhost:14402` (redir) / `14404` (origin) |
| `NG_DEV_LOCAL_BROKER` | `localhost:1421` |
Une app tierce peut donc pointer `@ng-org/web` vers un ng-app **auto-hébergé** sans changer son code, juste en rebuildant avec ces variables.
## Build pipeline lib-wasm
Scripts cargo dans `sdk/js/lib-wasm/Cargo.toml` (`[package.metadata.scripts]`) :
- `web` / `webdev``wasm-pack build --target web`
- `node` / `nodedev``wasm-pack build -t nodejs`
- `app` / `appdev``wasm-pack build --target bundler`
Post-traités par `prepare-web.js` / `prepare-node.js`.
## Plomberie proxy ↔ iframe ↔ worker (générique)
Le chemin d'appel d'une méthode du moteur est **entièrement générique** — aucune allowlist :
- `@ng-org/web` (proxy) : un `Proxy` JS qui relaie *n'importe quel* nom de méthode à l'iframe par `postMessage` (`apply``postMessage({method, args})`). Seules les méthodes *streamées* ont une entrée dans une table interne (positions d'arguments) ; les autres passent en simple requête/réponse.
- `app/nextgraph``api-web/wasm-worker.js` : dispatch générique `Reflect.apply(ng[method], null, args)` (la table `mapping` est commentée/inutilisée).
**Conséquence** : une nouvelle fonction wasm en **requête/réponse simple** est *atteignable* de bout en bout via ce forwarding générique sans modifier le JS. Mais c'est un mécanisme de relais, **pas un substitut à une API typée** : l'appeler ainsi est un appel string non typé/non documenté (hack de test). Pour une intégration propre, on ajoute la méthode à la surface d'API du paquet (`@ng-org/web`) et à ses `.d.ts`, et éventuellement à `@ng-org/orm` (qui, lui, n'est **pas** un forwarder générique).
Cas **streamé** : une méthode en flux exige une entrée dans la table de streaming **des deux côtés**`E` dans `@ng-org/web` (`ngweb.js`) **et** `streamed_api` dans `api-web/main.ts`. (Méthodes streamées actuelles : `doc_subscribe`, `orm_start_graph`, `orm_start_discrete`, `file_get`, `app_request_stream`.)
## Ciblage du broker : build-time uniquement
La cible (broker/ng-app) est figée **au build** de `@ng-org/web` via `import.meta.env` (`sdk/js/web/src/index.ts`) — **pas d'override runtime**, et `init()` ne prend pas d'URL de broker. Pour pointer une app vers un ng-app auto-hébergé, il faut donc **rebuilder `@ng-org/web`** avec `NG_REDIR_SERVER`/`NG_DEV*` (paquet en TypeScript pur, sans wasm → build trivial).
## Le broker (ngd)
- Supporte déjà nativement l'inbox (`inbox_post`, `inbox_register`, `inbox_pop_for_user` dans `engine/net/src/server_broker.rs`). Un `ngd` standard router­ait l'inbox — aucun patch broker nécessaire.
- C'est un démon **WebSocket** (`async-tungstenite`), **stateful** : stockage RocksDB sous `--base-path`, identité de pair (PeerId) persistée. Le volume est critique (clés + données chiffrées des users).
- CLI (`bin/ngd/src/cli.rs`) : `--local PORT`, et surtout `--domain DOMAIN:PORT,LOCAL_PORT` = mode « derrière reverse-proxy TLS-terminé qui envoie X-Forwarded-For » (adapté à Traefik/Coolify).
- **Ne sert pas de fichiers statiques** : pas de `ServeDir`/HTTP statique dans le crate. Le **ng-app frontend est un déploiement statique séparé** (`pnpm webfilebuild`). En prod, un reverse-proxy sert le statique du ng-app et proxy le WebSocket vers ngd sur un même domaine.
- Premier démarrage **interactif** : ngd émet un lien d'invitation pour créer le wallet admin (cf. DEV.md « first run »). Wrinkle pour un déploiement conteneurisé headless.
- Les Dockerfiles officiels (`bin/ngd/docker/Dockerfile.{alpine,fedora,ubuntu}`) sont **incomplets/cassés** (chemins obsolètes, échec de link llvm/clang documenté en commentaire) — pas de build conteneur turnkey.
## See Also
- [Stores NextGraph et droits d'accès](./nextgraph-stores-permissions.md) — stores, permissions, inbox au protocole, chemin du repo local
- [Data Layer](./data-layer.md) — usage actuel côté Festipod (auto-init iframe conditionnel)
- [Brief : forker NextGraph pour l'inbox](../briefs/fork-nextgraph-inbox.md) — consommateur de cette fiche
@@ -0,0 +1,108 @@
# Stores NextGraph et droits d'accès
Fiche de référence des 5 types de stores NextGraph et de leurs droits de lecture/écriture.
## Overview
Décrit les primitives de stockage et de permission de NextGraph (système externe, pas le code de Festipod). Sert de socle aux briefs [multi-store-refactor](../briefs/multi-store-refactor.md) et [authorization-matrix](../briefs/authorization-matrix.md), qui dérivent la structure de données cible de Festipod à partir de ces primitives.
Source : doc NextGraph officielle — [Documents & Stores](https://docs.nextgraph.org/en/documents/) et [Getting started](https://docs.nextgraph.org/en/getting-started/), vérifiée le 2026-05-21.
## Code source local
Le repo `nextgraph-rs` est cloné localement à **`../../nextgraph/nextgraph-rs`** (relatif à la racine du projet, soit `/home/sylvain/projects/nextgraph/nextgraph-rs`). À consulter pour vérifier ce qui est réellement exposé au protocole/SDK plutôt que de se fier à la doc. Points d'entrée utiles :
- `sdk/js/lib-wasm/src/lib.rs` — l'API wasm effectivement exposée au JS (`@ng-org/web` n'est qu'un proxy postMessage vers ces fonctions).
- `engine/net/src/app_protocol.rs` — l'enum `AppRequestCommandV0` (commandes de l'app protocol) et `NuriV0` (formats de NURI).
- `engine/verifier/src/request_processor.rs` — le dispatch effectif des commandes `app_request` (la vérité sur ce qui est *traité*, pas seulement déclaré).
- `engine/net/src/types.rs` — types inbox (`InboxPost`, `InboxMsg`, `InboxMsgContent`).
- `engine/verifier/src/inbox_processor.rs` — traitement des messages d'inbox.
## Les 5 types de stores
| Store | Lecture | Écriture | Création |
|---|---|---|---|
| **Private** | Titulaire seul | Titulaire seul | Par défaut |
| **Protected** | Titulaire + utilisateurs disposant d'un lien + permission (capability) | Titulaire + collaborateurs permissionnés | Par défaut |
| **Public** | Tout le monde, sans capability | Titulaire seul | Par défaut |
| **Group** | Membres du groupe | Membres du groupe (collaboratif) | À la demande |
| **Dialog** | Les deux utilisateurs uniquement | Les deux utilisateurs uniquement | À la demande |
### Citations doc (verbatim)
- **Private** — *« this is a place where you put only private and personal information that only you have access to »*, *« It is not possible to share the documents of your private store with anybody else »*.
- **Protected** — *« a space where you can share data, documents, and media with other users, but they will need a special link and permission in order to access them »* ; fait office de *« protected social profile »*.
- **Public** — *« equivalent to your website, blog, or public profile on social networks … that you want everybody to have access to, without the need for special permissions »*.
- **Group** — *« each Group is a separate Store … you can configure the store so that all the documents included in this store, inherit the permissions of the store »*.
- **Dialog** — *« hold all the data you exchange with another user (and only with that other user) … You cannot add more users to this store »*.
### Stores par défaut vs à la demande
Tout wallet utilisateur dispose d'office des **3 stores** private / protected / public. Ils sont exposés dans la session du SDK sous `private_store_id`, `protected_store_id`, `public_store_id`. Les **Group** et **Dialog** stores se créent à la demande.
## Concepts transverses
### Document vs Repo
- *« A Repo is basically the equivalent of an E2EE group for one and only one Document. »*
- **1 document = 1 repo.** Le repo détient les commits (changements) **et** les permissions du document.
- Identifiant du repo : `did:ng:o:<RepoID>` (RepoID de 44 caractères).
- Un **store** est lui-même un document spécial qui regroupe et permissionne d'autres documents.
### Granularité des permissions
- **Écriture** : gérée au niveau du **Document (repo)**, pas de la branche ni du bloc — *« Write permissions are managed at the level of the Document, not at the level of the branch or block »*.
- **Lecture** : peut être plus fine, **par bloc ou par branche***« Read permissions can be by block or branch »*.
- **Héritage** : un store (notamment Group) peut être configuré pour que tous les documents qu'il contient héritent des permissions du store.
### Capability / Nuri
- Le partage se fait en transmettant un **Nuri** qui embarque la capability cryptographique (lecture et/ou écriture). Pas d'ACL centralisée : la possession du Nuri = le droit.
- *« adding permissions can be done offline »* — l'ajout de permission est asynchrone.
- *« removing permissions is a synchronous operation that requires a SyncSignature »* — le retrait est synchrone et nécessite une SyncSignature.
### Inbox
- **Chaque document a une inbox native.** Un non-éditeur (sans capability d'écriture) peut y **déposer un lien (DID cap)** sans être invité comme éditeur.
- Le propriétaire **modère** : accepter / rejeter / retirer.
- Citation : *« each document has an inbox, which is used in this case to drop the link »*.
- C'est le mécanisme retenu par Festipod pour la notification d'inscription à un point de rencontre (voir [authorization-matrix](../briefs/authorization-matrix.md)).
#### Modèle inbox au protocole (vérifié dans `nextgraph-rs`, 2026-05-21)
- NURI d'inbox : `did:ng:d:<inbox_id>`.
- Contenu : enum `InboxMsgContent` avec les variantes `ContactDetails`, `DialogRequest`, **`Link`**, `Patch`, `ServiceRequest`, `ExtRequest`, `RemoteQuery`, `SocialQuery` (`Comment`, `Transaction`, `BackLink` encore en TODO).
- Le message est **scellé** (`crypto_box::seal`) vers la pubkey de l'inbox destinataire → seul le titulaire de l'inbox déchiffre.
- Le champ `from` est **optionnel** → l'expéditeur peut être **anonyme** (pas de signature, pas de `from_inbox`). C'est exactement le « identifié si connu, anonyme sinon » voulu par Festipod, **natif au protocole**.
#### Exposition côté SDK JS : l'inbox n'est PAS utilisable directement
Investigation dans `lib-wasm` + `request_processor.rs` :
- `app_request(request)` est exposé au JS, et l'enum `AppRequestCommandV0::InboxPost` + le constructeur `AppRequest::inbox_post()` existent.
- **MAIS** le `request_processor` du verifier (qui traite les `app_request`) **n'a aucun bras `InboxPost`**. Commandes réellement traitées : `OrmStart`, `OrmStartDiscrete`, `Fetch`, `FileGet`, `OrmUpdate`, `OrmDiscreteUpdate`, `SocialQueryStart`, `QrCodeProfile`, `QrCodeProfileImport`, `Header`, `Create`, `FilePut`. Envoyer un `InboxPost` via `app_request` ne déclenche donc rien.
- En plus, construire un `InboxPost` exige le scellement crypto côté Rust ; **aucun helper wasm** n'expose cette construction.
- Le dépôt en inbox n'est déclenché qu'**en interne** par deux features, elles exposées au JS :
- `QrCodeProfileImport``post_to_inbox(InboxPost::new_contact_details(...))` (échange de contact) ;
- `social_query_start(...)` → propagation de requête sociale via les inbox des **contacts**.
**Conséquence** : pas de moyen propre, aujourd'hui, de faire un « drop a Link » arbitraire dans l'inbox d'un PdR depuis le SDK JS. Il faudrait, dans `nextgraph-rs`, soit exposer un helper `inbox_post_link(...)` dans `lib-wasm` **et** ajouter le bras `InboxPost` au `request_processor`, soit détourner `social_query`.
**Piste connexe — `social_query_start`** : EST exposé au JS. C'est une requête fédérée sur le graphe social (propagée via inbox jusqu'à `degree` sauts), pertinente pour « qui dans mon réseau participe à X » et pour la découverte. Limite : ne touche que les **contacts**, donc ne couvre pas la notification anonyme vers un hôte non-connecté.
## Limites du SDK JS
Le SDK `@ng-org/web` (vérifié en `0.1.2-alpha.13`, soit `upstream/main` au 2026-05-21 — la version installée dans Festipod) **n'expose pas** les primitives suivantes, pourtant présentes au niveau protocole :
- création de Group / Dialog store ;
- partage de capability (transmission de Nuri avec droits) ;
- manipulation de permissions (ajout / retrait) ;
- dépôt et lecture d'inbox.
Méthodes JS effectivement disponibles : `doc_create`, `doc_subscribe`, `sparql_query`, `sparql_update`, `orm_start_graph`, `orm_start_discrete`, `graph_orm_update`, `discrete_orm_update`, `file_get`, `app_request_stream`. La doc annonce qu'*« An API will be provided for permission manipulation »* (sans date). Détail dans [multi-store-refactor §Contrainte SDK](../briefs/multi-store-refactor.md).
## See Also
- [Brief : refactor multi-store](../briefs/multi-store-refactor.md) — consommateur de cette fiche
- [Brief : matrice d'autorisations](../briefs/authorization-matrix.md) — dérive la structure de stores Festipod
- [Knowledge : data layer](./data-layer.md) — état actuel mono-store de l'app