Merge main into ng-eventually: shared-wallet shim + multi-browser e2e

Brings 266e335 (staging shared wallet: file-assisted import + multi-browser
e2e) into the ng-eventually branch. Conflicts resolved so both lines of work
coexist and route through the lib where they overlap:

- harness-ng.tsx: combine ReadCap FilterProbe (ours) with main's SmokeProbe/
  FanoutProbe; useShape + ng imported from @ng-eventually/client.
- ngSession.ts (auto): our single-injection-point configure() + main's hidden
  logoutNg, which uses the lib's ng.
- useShapeWithDefaults.ts (auto): lib useShape + main's { graphs } multistore
  scope.
- cucumber.json: single "tags": "not @wip" (both branches added it).
- brief_2026-06-15_shared-wallet-shim: keep main's implemented status; record
  that the read filter now lives in the lib (decision_2026-06-17) while the
  rest of the shim (storeRegistry/accounts/isolation) is still in-app, slated
  to move into the lib.

Build OK; harness-ng bundles. TODO (next): verify all of main's NextGraph
surface routes through @ng-eventually/client (storeRegistry uses ng directly).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-06-30 13:05:21 +02:00
39 changed files with 2226 additions and 378 deletions
+2 -1
View File
@@ -2,7 +2,7 @@
type: _overview
summary: BDD Cucumber/Gherkin en français sur 3 couches (@ui, @data, @e2e) — setup, contrat de couches (quoi tester où), harness broker réel, et le piège des vestiges source-grep
triggers:
keywords: [cucumber, gherkin, bdd, feature, scenario, scénario, step, steps, "@ui", "@data", "@e2e", playwright, broker, harness, wallet, world, hooks, renderHelper]
keywords: [cucumber, gherkin, bdd, feature, scenario, scénario, step, steps, "@ui", "@data", "@e2e", playwright, broker, harness, wallet, world, hooks, renderHelper, multibrowser, multi-navigateur, "@multibrowser", "@private-wallet", "@shared-wallet", storageState, "@wip"]
paths: ["src/modules/*/features/**", "src/modules/*/steps/**", "src/shared/steps/**", "src/shared/support/**", "src/shared/test-harness/**", "cucumber.json"]
---
@@ -30,6 +30,7 @@ Tests BDD **Cucumber/Gherkin en français** (`Etant donné`, `Quand`, `Alors`) s
- [[knowledge_ui-layer]] — couche `@ui` : render helper, fixtures, bons/anti patterns
- [[knowledge_data-layer-broker]] — couche `@data` : harness broker, cycle de vie wallet, bridge
- [[knowledge_e2e-layer]] — couche `@e2e` : app réelle dans l'iframe
- [[knowledge_multibrowser-harness]] — plusieurs navigateurs isolés × modèle de wallet (private/shared), injection storageState
- [[decision_2026-03-12_headless-wallet-creation]] — pourquoi le wallet de test est créé en UI headless
- [[caveat_source-grep-vestiges]] — vestiges de l'ère « analyse de source » dans `world.ts`
- [[cookbook_add-scenario]] — ajouter un scénario/step (couches, piège de sérialisation `evaluate`, `@wip`)
@@ -0,0 +1,71 @@
---
type: knowledge
summary: Harness multi-navigateur sur DEUX axes orthogonaux — nombre de navigateurs (machinerie, contextes frais isolés via un freshBrowser non-persistant) ET modèle de wallet (own/@private-wallet vs shared/@shared-wallet) ; shared provisionné par injection storageState (test) ; e2e @humain qui valide le mécanisme produit RÉEL via la vraie app staging (fichier .ngw téléchargé depuis l'écran → import nextgraph.eu « Import a Wallet File » → Entrer → connecté) ; convention @wip exclue via cucumber.json
last_checked: 2026-06-16
---
# Harness multi-navigateur (private-wallet vs shared-wallet)
Capacité du harness `@data`/`@e2e` à piloter **plusieurs navigateurs isolés** dans un même scénario, sous **deux axes orthogonaux**. Sert à tester le stopgap wallet partagé (cf. concept `nextgraph-platform``brief_2026-06-15_shared-wallet-shim`) **et** le modèle cible (chacun son wallet).
## Les deux axes (orthogonaux)
| Axe | Ce qu'il décide | Exprimé par |
|---|---|---|
| **Nombre de navigateurs** (machinerie) | 1..N contextes nommés isolés | `openBrowser(name, …)` + steps `… dans le navigateur "X"` |
| **Modèle de wallet** | identité NG distincte vs partagée | **phrasing du step + tag** (voir ci-dessous) |
Ne **pas** confondre `@multibrowser` (plusieurs navigateurs) avec `@shared-wallet` (même wallet) : on fait du multibrowser **en private** (utile dès que NextGraph livrera la lecture cross-wallet — le modèle cible) **et en shared** (stopgap), et on compare les deux setups avec les **mêmes** steps de comportement.
## Modèle de wallet : phrasing + tags
- `Étant donné un navigateur "A" avec son propre wallet` → modèle **own**, tag `@private-wallet`.
- `Étant donné un navigateur "A" avec le wallet partagé` → modèle **shared**, tag `@shared-wallet`.
- Tag umbrella `@multibrowser` (feature entière).
## Architecture (où vit quoi)
- **`src/shared/support/browserPool.ts`** — état partagé + fabrique. Hors du contexte Chromium **persistant** porteur du wallet partagé (legacy mono-navigateur `@data`/`@e2e`, **inchangé**, cf. [[knowledge_data-layer-broker]]), le harness lance un navigateur **non-persistant** `freshBrowser` (`chromium.launch`) qui mint des contextes frais et isolés à la demande (`spawnContext(wallet)`). Module importé par `hooks.ts` (cycle de vie) et `world.ts` (usage par scénario) — pas de cycle d'import.
- **`world.ts`** — API : `openBrowser(name, wallet)`, `browser(name)`, `loadAppInBrowser(name, 'app'|'harness')`, `closeBrowsers()` ; registre `browsers: Map<name, NamedBrowser>`. Navigateurs nommés fermés en `After`, `freshBrowser` en `AfterAll`.
- **`hooks.ts`** — un scénario taggé `@multibrowser` **ne reçoit pas** la page unique legacy ; les steps ouvrent les navigateurs. Exige le mode broker réel (`freshBrowser` indispo en fallback mock).
## Provisioning du wallet
- **own** : `newContext()` vide → identité NG distincte / pas de wallet.
- **shared** : `newContext({ storageState })`, où `storageState` est **capturé une fois** au `BeforeAll` depuis le profil persistant (warm-up via `setupBrokerPage` puis `browserContext.storageState()`), exposé par `pool.sharedWalletState`. **Vérifié empiriquement (2026-06-16)** : les origines `nextgraph.eu` + `nextgraph.net` round-trippent dans les contextes frais, et deux navigateurs **shared** atteignent tous deux l'app **connectée** à NextGraph (`window.__testData.ready`) **sans login manuel**.
> Ce provisioning est **de test** — distinct du mécanisme **produit** (import assisté par FICHIER). Le scénario shared-wallet par storageState **court-circuite l'import** ; pour valider le mécanisme RÉEL, voir l'e2e `@humain` ci-dessous.
## Parcours humain — e2e du mécanisme produit (vert)
Scénario `@humain` : valide le flux RÉEL de distribution du wallet **de bout en bout, via la vraie app**, pas l'injection de test (cf. concept `nextgraph-platform``decision_2026-06-17_assisted-wallet-import`). Un navigateur vierge ouvre l'app staging → l'`AccessGateScreen` propose le **fichier** + le **mot de passe** → on télécharge le fichier **depuis l'écran**, on vérifie que le mot de passe affiché **égale** celui du wallet → import sur `nextgraph.eu` « Import a Wallet File » → retour → clic « Entrer » → app connectée (`ConnexionScreen`).
- **Wallet e2e** : un fichier `.ngw` (`festipod-e2e-tests`, mot de passe = identifiant) placé **à la racine du worktree** ; `findE2eWalletFile()` le localise (`*.ngw`). Gitignoré → chaque environnement doit l'ajouter (sinon erreur claire).
- `pool.ensureStagingApp()` (`hooks.ts`) — build **isolé** `bun run build.ts --outdir=dist-staging` (barrière d'accès **ON par défaut** ; mot de passe gravé + **fichier copié** en `/shared-wallet.ngw`, cf. `build.ts`), servi statiquement. Mémoïsé, lazy (seul `@humain` le paie).
- **Bypass de la barrière pour `@e2e`** : le harness fait `browserContext.addInitScript` sur le **contexte persistant** pour poser `globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ = true` (s'applique à l'iframe app avant ses scripts) → `@e2e` voit l'app directement, pas la barrière. Les contextes frais (`@humain`) n'y touchent pas → barrière ON (cf. `decision_2026-06-17`). L'ancien `LoginScreen` `/login` a été retiré.
- `pool.importWalletViaFile(page, filePath, password)``nextgraph.eu/#/wallet/login``setInputFiles('input[type=file]')` (attendre que la SPA rende, sinon `EncryptionError`) → champ password → unlock.
- `pool.completeBrokerLogin(page, appUrl, walletPassword?)` — moitié « login broker » extraite de `setupBrokerPage`. **Attente robuste** : après le redirect (multi-hop), attend l'iframe app OU le lien « Click here to login with your wallet », puis déverrouille avec le mot de passe. La session broker n'étant **pas** persistée entre lancements, ce login wallet est requis à chaque run (warm-up + `@e2e` + `@humain`).
> **C'est l'e2e qui garantit que ça marche pour un humain réel** : Festipod fournit le BON fichier + mot de passe, et ce fichier importé donne un wallet fonctionnel sur un device vierge. Le scénario `@shared-wallet` (storageState) reste un raccourci de provisioning de test, il ne valide pas l'import.
## Isolation (garantie à 3 niveaux, prouvée par les scénarios)
1. `freshBrowser` est un **process séparé** du profil persistant porteur du wallet → un navigateur **own** démarre **sans wallet**.
2. Chaque `newContext()` est une **partition de stockage hermétique** (garantie Playwright).
3. Isolation prouvée non seulement sur l'origine **locale** (`127.0.0.1`) mais aussi sur l'**origine broker** `nextgraph.net` **où vit réellement le wallet** (sonde localStorage écrite dans A absente de B).
## Fichiers
- Feature : `src/modules/workshop/features/multibrowser-harness.feature`.
- Steps : `src/modules/workshop/steps/data/multibrowser.steps.ts`.
- Route `/blank` ajoutée au serveur harness (`hooks.ts`) : page minimale **sans stack NG**, pour les checks d'isolation localStorage.
## Convention `@wip` (désormais appliquée)
`cucumber.json` (profile `default`) porte `"tags": "not @wip"`. Le `cookbook_add-scenario` prescrivait `@wip` pour le non-implémenté mais ce n'était **exclu nulle part** ; maintenant `not @wip` s'**AND** avec les filtres CLI (ex. `--tags @data``(not @wip) and @data`, vérifié).
## Liens
- [[knowledge_data-layer-broker]] — la couche `@data` mono-navigateur (profil persistant) que cette capability étend.
- [[cookbook_add-scenario]] — convention `@wip`, pièges de steps.
- Concept `nextgraph-platform``brief_2026-06-15_shared-wallet-shim` — le stopgap wallet partagé que ce harness sert à tester.
@@ -2,8 +2,8 @@
type: _overview
summary: NextGraph comme système EXTERNE (stores, permissions, inbox, modèle d'intégration iframe, limites SDK) + les 4 briefs prospectifs qui dérivent la structure de données cible et le chemin multi-user de Festipod
triggers:
keywords: [nextgraph-rs, store, group store, dialog store, protected_store, public_store, inbox, capability, nuri, fork, ngd, broker, verifier, multi-store, multi-user, sharedWalletShim, storeRegistry, permission, social_query, OpenRepo]
paths: ["src/shared/utils/ngGraph.ts", "src/shared/hooks/useShapeWithDefaults.ts", "scripts/build-ng-packages.sh"]
keywords: [nextgraph-rs, store, group store, dialog store, protected_store, public_store, inbox, capability, nuri, fork, ngd, broker, verifier, multi-store, multi-user, sharedWalletShim, storeRegistry, permission, social_query, OpenRepo, wallet, auto-import, textcode, wallet import, AccessGateScreen]
paths: ["src/shared/utils/ngGraph.ts", "src/shared/hooks/useShapeWithDefaults.ts", "scripts/build-ng-packages.sh", "src/modules/auth/sharedWallet.ts", "src/modules/auth/screens/AccessGateScreen.tsx"]
---
# NextGraph platform
@@ -23,9 +23,14 @@ Le repo `nextgraph-rs` est cloné en `/home/sylvain/projects/nextgraph/nextgraph
- [[knowledge_stores-permissions]] — 5 types de stores, document/repo, capabilities/Nuri, inbox, exposition SDK JS
- [[knowledge_integration-model]] — paquets JS, modèle iframe, où tourne le verifier, broker `ngd`, déploiement, reciblage build-time
- [[knowledge_broker-import-constraint]] — le broker hébergé n'autorise pas l'auto-import d'un wallet par une web-app tierce (vérifié 2026-06-17)
## Briefs (chantiers prospectifs)
- [[brief_2026-05-17_multi-store-refactor]] — passer du mono-store actuel à une structure par entité
- [[brief_2026-06-15_shared-wallet-shim]] — stopgap staging : wallet partagé unique + `storeRegistry`
- [[brief_2026-05-21_fork-nextgraph-inbox]] — forker `nextgraph-rs` pour exposer l'inbox au SDK JS
## Décisions
- [[decision_2026-06-17_assisted-wallet-import]] — distribution du wallet partagé par import assisté (l'auto-import zéro-touche étant impossible)
@@ -1,154 +1,185 @@
---
type: brief
summary: Stopgap staging multi-user — un wallet partagé unique (Piste A) + couche storeRegistry, comptes/login Festipod simulés (username seul, cf. decision login-flow), 1 document PAR ENTITÉ (événement/PdR) + index global de découverte, participations groupées en protected, filtre d'isolation applicatif ; structure préfigurant l'infra cible (stores per-user, Group store pour l'index), sharedWalletShim jetable. Prototype implémenté puis réverti — à (ré)implémenter.
summary: Stopgap staging multi-user — deux visions cadrées. Lointaine (cible) — multi-wallet, 3 stores natifs par utilisateur + Dialog, 1 document par entité (événement/PdR), partage par capabilities, inbox native du PdR. Adaptée (stopgap) — UN wallet partagé, 1 document par entité dans son private_store, périmètre = métadonnée logique + index, filtre d'isolation applicatif, login simulé. Obstacles NextGraph — pas de lecture cross-wallet (OpenRepo TODO, ReadCap ignoré), capabilities/inbox non exposées au SDK, login non programmable. Shim livré in-app (storeRegistry/comptes/isolation) ; le filtre de lecture est désormais dans la lib ng-eventually (cf. decision_2026-06-17). sharedWalletShim + filtre = jetables à la migration.
last_updated: 2026-06-16
---
# Stopgap multi-user : wallet partagé unique (`sharedWalletShim`)
**Status:** Conçu — décisions prises ([[decision_2026-06-15_shared-wallet-login-flow]], [[decision_2026-06-16_discovery-model]], granularité par entité). Faits clés vérifiés sur broker via un **prototype**. ⚠️ **Le prototype a été réverti — aucun code dans le tree** ; à (ré)implémenter.
**Last updated:** 2026-06-16
**Status:** En cours — compte/login + isolation livrés et vérifiés ; **granularité 1 document par entité implémentée** ; primitives + **fan-out multi-documents validés sur broker** (`@data`, 3 scénarios). Reste : reactivity in-app de la création (best-effort) + seeding multi-doc. **Le filtre de lecture a migré dans la lib `ng-eventually` (ReadCap par document, cf. [[decision_2026-06-17_eventually-library]])** ; le reste du shim (storeRegistry/comptes/isolation) est encore in-app, destiné à rejoindre la lib.
## Context
## Objectif & posture
NextGraph ne permet **aucun partage de données entre wallets** aujourd'hui. Vérifié dans `nextgraph-rs` (2026-06-15) :
Mettre Festipod en **staging** avec des **utilisateurs amicaux**, **sans enjeu de sécurité**, branché sur le vrai NextGraph, et **structuré au plus près de l'infra cible** pour qu'une migration soit un simple changement de résolveur, pas une réécriture.
- une session de verifier ne contient que ses **3 stores** dans `self.repos` ;
- un NURI étranger lève `RepoNotFound` (`engine/verifier/src/request_processor.rs`, `resolve_target`) ;
- `OpenRepo` est un **TODO non implémenté** côté broker (`engine/verifier/src/verifier.rs:1423`) ;
- le champ `access`/`ReadCap` du NURI **n'est jamais inspecté** → les capabilities sont ignorées.
Trois choses doivent rester nettes pour ne pas dériver, et structurent ce brief :
Donc lire le store d'un autre utilisateur — **même son `public_store`** — est impossible via le SDK. Cela élimine toute la famille « chacun garde son wallet, les autres lisent son public » (piste C ci-dessous).
1. la **vision lointaine** — ce qu'on aura quand NextGraph offrira lecture cross-wallet, capabilities et inbox ;
2. les **obstacles NextGraph** — ce qui, aujourd'hui, empêche cette vision ;
3. la **vision adaptée** (stopgap) — au plus proche de la lointaine, compte tenu des obstacles.
**Objectif :** mettre Festipod en **staging** avec des **utilisateurs amicaux**, **sans enjeu de sécurité**, tout en branchant l'app sur le vrai NextGraph et en **préfigurant l'infra cible** (structure dérivée dans [[brief_2026-05-18_authorization-matrix]]).
> **Invariant directeur** : tout ce que fait la vision adaptée doit avoir une **correspondance 1:1** explicite avec la vision lointaine (table en fin de section adaptée). Si un choix du stopgap n'a pas d'image claire dans la cible, c'est un signal de dérive.
**Décision retenue :** Piste A (wallet partagé unique) + couche `storeRegistry`, broker **`nextgraph.net`**.
---
> **Évolution majeure (2026-06-17)** : tout ce polyfill est désormais **encapsulé dans une librairie générique externe** (`ng-eventually-js`, hors repo), pas dans l'app — voir [[decision_2026-06-17_eventually-library]]. L'app Festipod ne dépendra que de `@ng-eventually/client` (wrapper SDK-identique). Ce brief décrit donc la **conception du polyfill** ; son lieu d'implémentation est la lib, et les mécanismes ci-dessous (storeRegistry, caps émulées, inbox, index) y sont réalisés. Le `storeRegistry` et le filtre d'isolation décrits plus bas sont la **version « dans l'app »** désormais remplacée par la lib.
> **Direction (2026-06-17 → en cours)** : ce polyfill a vocation à être **encapsulé dans une librairie générique externe** (`ng-eventually-js`, hors repo) plutôt que dispersé dans l'app — voir [[decision_2026-06-17_eventually-library]]. **Déjà fait** : le routage du SDK (`useShape`/`init`/`ng`) et le **filtre de lecture ReadCap** (par document) vivent dans `@ng-eventually/client`. **Encore in-app** (livré par le stopgap décrit ci-dessous) : `storeRegistry`, couche comptes, filtre d'isolation — à migrer dans la lib ensuite. L'app ne dépend déjà plus directement du SDK que par un point d'injection unique (`ngSession.configure`).
## What We Know
## 1. Vision lointaine (cible finale)
### Les trois familles de contournement (et pourquoi A)
Dérivée de [[brief_2026-05-18_authorization-matrix]] et [[knowledge_stores-permissions]]. Périmètre **validé** (hors communautés / listes curées / suivi, encore hors périmètre).
### Identité & login
- **1 utilisateur = 1 wallet NextGraph.** Le wallet **est** l'identité ; pas de compte applicatif séparé.
- **Login = ouvrir son propre wallet** (redirect broker). C'est un vrai login par-utilisateur.
### Stores & granularité documents
Modèle natif : `1 document = 1 repo = 1 frontière de permission = 1 inbox`. Un **store** est un document-conteneur qui regroupe et permissionne d'autres documents. Chaque utilisateur a **3 stores natifs** ; les entités sont des **documents individuels** dedans (pas un gros graphe par store).
| Entité | Store (propriétaire) | Granularité | Notes |
|---|---|---|---|
| Événement | `public_store` du déclarant | **1 document / événement** | adressable par NURI (utile pour la déduplication) |
| Point de rencontre (PdR) | `public_store` de l'hôte | **1 document / PdR** | **possède son inbox native** (reçoit les inscriptions) |
| Profil réseau | `protected_store` | 1 document | nom, avatar, bio, ville, intérêts |
| Participation / Inscription | `protected_store` de l'inscrit | 1 document / inscription | + dépôt d'un lien dans l'**inbox du PdR** |
| Profil privé (settings, email) | `private_store` | 1 document | soi seul |
| Connexion A↔B | **Dialog store** A↔B | doc connexion (+ messagerie) | deux écrivains |
| Index des connexions | `protected_store` | 1 document | liste les NURIs des Dialog stores |
**Aucun Group store** sur le périmètre validé (les 3 stores + Dialog + inboxes suffisent).
### Partage & visibilité
- Par **capabilities** : on transmet un Nuri portant un read/write cap. **Public** = lisible par tous sans cap. **Protected** = cap obtenue via la connexion. **Privé** = soi.
- Ajout de permission asynchrone ; retrait synchrone (SyncSignature).
### Inbox & notifications
- L'**inbox native du document PdR** reçoit les dépôts d'inscription (lien DID cap). `from` optionnel ⇒ **identifié si connexion de l'hôte, anonyme sinon**, gratuitement.
### Découverte
- **Pas d'annuaire central.** On découvre via les `public_store` et le graphe de connexions (et plus tard `social_query`).
---
## 2. Obstacles côté NextGraph (ce qui empêche la vision lointaine aujourd'hui)
Vérifiés dans `nextgraph-rs` (2026-06-15).
| Élément de la cible | Obstacle actuel | Preuve |
|---|---|---|
| Lire le store d'un **autre** utilisateur | `OpenRepo` **non implémenté** ; un NURI étranger lève `RepoNotFound` ; une session ne contient que ses 3 stores dans `self.repos` | `engine/verifier/src/verifier.rs:1423`, `request_processor.rs` `resolve_target` |
| Partager une **capability** (Nuri + droits) | non exposé au SDK ; le champ `access`/`ReadCap` du NURI **n'est jamais inspecté** | [[knowledge_stores-permissions]] |
| **Inbox** d'un document (notif d'inscription) | pas exposée au SDK JS (nécessite un fork moteur) | [[brief_2026-05-21_fork-nextgraph-inbox]] |
| **Login per-utilisateur** fluide | login **non programmable** (redirect web vers le broker) | [[decision_2026-06-15_shared-wallet-login-flow]] |
**Conséquence centrale** : tant que la lecture cross-wallet n'existe pas, **aucune donnée ne franchit la frontière entre deux wallets**. Bob ne peut pas lire le `public_store` d'Alice. Toute approche « chacun son wallet » est donc bloquée à la racine.
---
## 3. Vision adaptée (stopgap) — au plus proche de la cible
### Principe : UN wallet partagé
Tous les utilisateurs amicaux ouvrent **le même** wallet. NextGraph ne voit qu'une identité → **tout est techniquement lisible** (on contourne l'absence de lecture cross-wallet en supprimant la frontière). Le « multi-utilisateur » devient une **fiction applicative**.
### Granularité documents — **identique à la cible** : 1 document par entité
Pour rester fidèle, on reproduit les **deux niveaux** de la cible (conteneur → documents) :
- chaque **événement** et chaque **PdR** = **son propre document** (`doc_create`), tous physiquement dans le `private_store` de l'unique wallet partagé ;
- le **périmètre** (public/protected/private) est une **métadonnée logique** portée par le document, **pas** un store physique ;
- un **document-index par (utilisateur × périmètre)** liste les NURIs des entités de ce périmètre — il **joue le rôle du futur store-conteneur** (`docPublic` ≈ futur `public_store`, etc.).
Garder la granularité « 1 doc par entité » est ce qui rend la migration 1:1 **et** ce qui rendra l'**inbox du PdR** possible plus tard sans refonte (l'inbox est un attribut de document).
| Entité | Périmètre logique | Document stopgap | Indexé dans |
|---|---|---|---|
| Événement | public | 1 doc / événement | `docPublic` du déclarant |
| PdR | public | 1 doc / PdR | `docPublic` de l'hôte |
| Profil réseau | protected | doc profil réseau | `docProtected` |
| Participation | protected | 1 doc / participation (ou groupé) | `docProtected` de l'inscrit |
| Profil privé | private | doc settings | `docPrivate` |
| Connexion A↔B | dialog | doc connexion | index de connexions |
### `sharedWalletShim` (échafaudage, sans équivalent cible)
Index des **comptes** simulés → leurs documents-index : `username → profileId → { docPublic, docProtected, docPrivate }`. Ancré dans le `private_store` du wallet partagé (`session.private_store_id`, toujours connu → ancre de bootstrap). Rend possibles le **login cross-device** et le **picker d'utilisateurs**. **N'a aucun équivalent cible** (la cible n'a pas d'annuaire central) → **jetable**.
### Partage simulé : filtre d'isolation applicatif
Un seul wallet ⇒ tout lisible. Pour **se comporter** comme la cible, la couche données filtre les lectures par `currentAccountId` + connexions : `private` → propriétaire ; `protected` → propriétaire + connexions ; `public` → tous. Remplace les **capabilities** (pas appliqué par la crypto, mais honoré par l'app) → **jetable**.
### Identité & login simulés
- **Couche réelle (technique, invisible)** : le redirect broker du wallet partagé, présenté comme **barrière d'accès à l'environnement** (pas un login). Cf. [[decision_2026-06-15_shared-wallet-login-flow]].
- **Couche applicative (le login perçu)** : écran « Connexion » = **username seul** (déclaratif, sans mot de passe) → `localStorage`. « Déconnexion » = efface le username, sans toucher NG. Vrai logout planqué.
### Inbox / notification d'inscription
**Hors périmètre du stopgap** (nécessite le fork). Mais la granularité « 1 doc/PdR » est le **pré-requis** qui la rendra branchable plus tard sans refonte.
### Correspondance stopgap → cible (l'invariant 1:1)
| Stopgap | Vision lointaine | Migration |
|---|---|---|
| doc événement (dans le wallet partagé) | doc événement dans le `public_store` du déclarant | déplacer le doc + appliquer cap publique |
| doc PdR | doc PdR dans le `public_store` de l'hôte **+ inbox** | déplacer + brancher l'inbox |
| `docPublic`/`docProtected`/`docPrivate` (index) | `public_store`/`protected_store`/`private_store` | l'index devient le store natif |
| filtre d'isolation applicatif | capabilities (read caps via connexion) | retirer le filtre, poser les caps |
| login applicatif (username) | login = ouvrir son wallet | retirer la couche compte |
| `sharedWalletShim` | — (rien) | supprimer |
| wallet partagé unique | un wallet par utilisateur | éclater par propriétaire |
**Migration = swap du résolveur `storeRegistry`** (de « doc dans le wallet partagé » vers « doc dans le vrai store du propriétaire ») + déplacement des documents + pose des capabilities + suppression de l'échafaudage. **Les écrans ne changent pas.**
---
## Familles de contournement (pourquoi A)
| Famille | Idée | Verdict |
|---|---|---|
| **A — wallet partagé** | un seul wallet pour tous, multi-user simulé côté app | **retenue** : livrable vite, zéro travail moteur, local-first préservé |
| B — NG comme backend | un backend Bun détient un wallet, clients en HTTP | écartée : abandonne le local-first, plus lourd |
| C — lecture cross-wallet | chacun son wallet, on lit le public des autres | **infaisable** (cf. Context) sans fork moteur |
| D — fork moteur | patcher `OpenRepo` + capabilities | hors stopgap : chemin cible réel, lourd (cf. [[brief_2026-05-21_fork-nextgraph-inbox]]) |
| **A — wallet partagé** | un seul wallet, multi-user simulé côté app | **retenue** : livrable vite, zéro travail moteur, local-first préservé |
| B — NG comme backend | un backend Bun détient un wallet, clients HTTP | écartée : abandonne le local-first, plus lourd |
| C — lecture cross-wallet | chacun son wallet, lecture du public des autres | **infaisable** (obstacle §2) sans fork moteur |
| D — fork moteur (`OpenRepo` + capabilities) | rendre la cible réelle | hors stopgap : c'est le chemin cible, lourd (cf. [[brief_2026-05-21_fork-nextgraph-inbox]]) |
### Architecture en trois couches
---
```
┌─ Couche COMPTE (simulée — UX cible, jetable à la migration) ────────┐
│ signup / login Festipod · currentAccountId en localStorage │
├─ Couche STORES VIRTUELS (fidèle — survit à la migration) ───────────┤
│ storeRegistry : 1 document PAR ENTITÉ (événement/PdR) via │
│ doc_create ; index global de découverte ; protected groupé/compte │
├─ Couche NEXTGRAPH (réelle mais invisible) ──────────────────────────┤
│ UN wallet partagé, mêmes credentials pour tous │
└─────────────────────────────────────────────────────────────────────┘
```
## État d'implémentation (2026-06-16)
1. **NextGraph (réelle mais invisible)** — un wallet partagé, mêmes credentials. Le login NextGraph **n'est pas programmable** (redirect web vers `nextgraph.net/redir`, cf. `NextGraphContext`, concept `data-layer`) ; il est donc présenté comme une **barrière technique d'accès** avant l'app, pas comme un login (flux arrêté dans [[decision_2026-06-15_shared-wallet-login-flow]]). Session **persistante** côté iframe broker → ouverture **une fois par device** dans une même session navigateur.
2. **Stores virtuels (fidèle, survit à la migration)****1 document par entité** (événement/PdR) via `doc_create`, référencé par un **index global de découverte** (cf. [[decision_2026-06-16_discovery-model]]) ; les données *protected* (profil, participations) restent **groupées** par compte. Vérifié : `doc_create` retourne un NURI `did:ng:o:…`, le repo est **inséré immédiatement** dans `self.repos` (`verifier.rs:2900`), et `orm_start_graph`/`sparql_update` l'acceptent **sans pin explicite** (`sdk/rust/src/tests/sparql_regressions.rs:136-200`). À la migration : **swap du résolveur** `storeRegistry` vers les vrais stores, sans réécrire les écrans.
3. **Compte/login simulés (UX, jetable)**signup/login Festipod, **username seul** (pas de mot de passe), `currentAccountId` en `localStorage`.
Deux drapeaux de build, **OFF par défaut** (le mono-store validé reste le défaut ; dev/`@ui`/`@e2e` inchangés) :
- **`FESTIPOD_STAGING=1`** — flux login option 2 (découplé de `NODE_ENV` pour ne pas bloquer `@e2e`).
- **`FESTIPOD_MULTISTORE=1`** — couche multi-document (storeRegistry).
### `sharedWalletShim`
| Pièce | Fichier | État |
|---|---|---|
| Couche compte (faux login, localStorage) | `src/shared/context/AccountContext.tsx` | ✅ livré, vérifié (build + `@ui`) |
| Gate technique + écran « Connexion » + orchestrateur | `src/modules/auth/screens/{AccessGateScreen,ConnexionScreen}.tsx`, `src/app/AuthGate.tsx` | ✅ livré |
| Vrai logout planqué | `ngSession.ts:logoutNg`, `SettingsScreen.tsx` | ✅ livré |
| Filtre d'isolation (mode connecté) | `src/shared/utils/isolation.ts` + `FestipodDataContext` | ✅ livré, pur, vérifié |
| storeRegistry + sharedWalletShim | `src/shared/utils/storeRegistry.ts` | ✅ **1 doc/entité** (`createEntityDoc`/`listEntityDocs` + index par périmètre) ; primitives **validées broker** |
| Câblage multi-document (reads fan-out `{graphs}` + write per-entité `createEntityDoc`) | `FestipodDataContext` (useNgData) derrière `MULTISTORE` | ✅ lecture fan-out validée ; ⚠️ reactivity de la création in-app = **best-effort** (le nouveau doc est ajouté au fan-out, l'`@id` peut être en attente jusqu'au re-subscribe) |
| Validation broker (`@data`) | `src/modules/workshop/{features/multistore-stopgap.feature, steps/data/multistore.steps.ts}` | ✅ **3 scénarios verts** (ORM-sur-doc-créé, shim r/w, **fan-out par entité**) |
Nom **volontairement explicite** du mapping temporaire (hack) : comptes simulés → NURIs des stores virtuels. Ancré dans le **`private_store` du wallet partagé** (`session.private_store_id`, toujours présent → ancre de bootstrap). **Sans équivalent cible** : la cible n'a pas d'**annuaire de comptes** (l'identité = le wallet). Rend possibles le **login cross-device** et le **picker d'utilisateurs**. **À supprimer à la migration.**
**Granularité = 1 document par entité (fait)** : public (événements/PdR) → un `doc_create` **par entité**, NURI ajouté à l'**index** du périmètre (le futur store-conteneur) ; protected (profil, participations) → **groupé** dans l'index protected. Lecture publique = `listEntityDocs('public')``useShape({graphs:[…]})`. Mono-store (défaut) inchangé.
> **Découverte des événements** (à ne pas confondre avec le shim) : modèle tranché dans [[decision_2026-06-16_discovery-model]] — un **index global unique** (pas une découverte par-compte), **alimenté via son inbox** : le créateur **dépose** une référence dans l'inbox de l'index ; l'index (document **possédé**, lecture publique) est matérialisé depuis son inbox. Architecture en 3 étapes (découverte → synchronisation → requête locale). **Pas de Group store** (index = doc possédé + inbox native) ; inbox + watcher réutilisés (même mécanisme que l'inscription au PdR).
Contenu par compte : `username → profileId → { docPublic, docProtected, docPrivate }`. Chaîne de bootstrap d'un device : session → `private_store_id` → lire le `sharedWalletShim` → comptes + NURIs par périmètre.
### Placement des entités
Dérivée de [[brief_2026-05-18_authorization-matrix]] (périmètres) + [[decision_2026-06-16_discovery-model]] (granularité par entité + index global) :
| Entité | Périmètre | Doc stopgap (wallet partagé) | Cible |
|---|---|---|---|
| Événement (créé par U) | public | **1 doc / événement**, référencé dans l'index global | doc dans le `public_store` de U |
| PdR (hôte U) | public | **1 doc / PdR**, référencé dans l'index global | doc dans le `public_store` de U (+ inbox native) |
| **Index global des événements** | — (possédé + inbox) | **1 doc** alimenté via son **inbox** (dépôt + matérialisation) | doc **possédé** (lecture publique) **+ inbox** dans le `public_store` du propriétaire |
| Profil réseau de U | protected | groupé dans `U/protected` | `protected_store` de U |
| Participation de U | protected | groupé dans `U/protected` | `protected_store` de U |
| Index des connexions de U | protected | groupé dans `U/protected` | `protected_store` de U |
| Profil privé de U (settings, email) | private | `U/private` | `private_store` de U |
| Connexion A↔B | dialog | `dialog/A∙B` | Dialog store A↔B |
### Filtre d'isolation (retenu)
Un seul wallet ⇒ tout lisible par tous. Pour que le staging se **comporte** comme la cible, la couche données filtre les lectures par `currentAccountId` + connexions : `private` → propriétaire seul ; `protected` → propriétaire + connexions ; `public` → tous. Isolation **pas appliquée par la crypto** mais **honorée** par l'app (démo réaliste, bugs de conception attrapés tôt). **Supprimé à la migration** (la crypto prend le relais).
### Ce qui survit vs ce qui est jetable
- **Survit** : mapping entité→périmètre, abstraction `storeRegistry`, séparation par documents, docs dialog, forme UX signup/login.
- **Jetable** : le wallet partagé unique, le `sharedWalletShim`, le filtre d'isolation. (Pas de mots de passe applicatifs — **username seul**.)
### Code impacté
- `src/shared/utils/ngGraph.ts``ensureGraphNuri()` remplacé par `storeRegistry`.
- `src/shared/hooks/useShapeWithDefaults.ts``storeNuri` résolu par (entité, compte).
- `src/shared/context/FestipodDataContext.tsx` — câbler `joinEvent`/`leaveEvent` (no-op aujourd'hui) ; appliquer le filtre d'isolation.
- `CURRENT_USER_ID` constant → `currentAccountId` sélectionnable (persisté `localStorage`).
- `src/shared/utils/ngBootstrap.ts` — seed réparti **par documents**.
## Plan d'implémentation & faits vérifiés
> ⚠️ **Un prototype a été développé puis réverti** : le code décrit ici **n'est pas dans le tree** (working tree propre). Cette section sert de **plan de (ré)implémentation** et consigne les **faits vérifiés sur broker** durant le prototype — ils restent vrais indépendamment du code.
**Mécanisme prévu — deux drapeaux de build, OFF par défaut** (mono-store reste le défaut ; dev/`@ui`/`@e2e` inchangés) :
- `FESTIPOD_STAGING=1` — flux login option 2 (gate technique → écran « Connexion »). **À découpler de `NODE_ENV`** (sinon un `@e2e` en build prod est bloqué par le gate).
- `FESTIPOD_MULTISTORE=1` — couche multi-document (storeRegistry).
**Pièces à (ré)créer** :
| Pièce | Fichier prévu |
|---|---|
| Couche compte (faux login, localStorage) | `src/shared/context/AccountContext.tsx` |
| Gate technique + écran « Connexion » + orchestrateur | `src/modules/auth/screens/{AccessGateScreen,ConnexionScreen}.tsx`, `src/app/AuthGate.tsx` |
| Vrai logout planqué | `ngSession.ts:logoutNg`, `SettingsScreen` |
| Filtre d'isolation (mode connecté) | `src/shared/utils/isolation.ts` |
| storeRegistry (1 doc/entité + index global) + sharedWalletShim | `src/shared/utils/storeRegistry.ts` |
| Câblage multi-document (lecture via index global + fan-out ; écriture per-entité) | `FestipodDataContext` derrière `MULTISTORE` |
| Scénarios `@data` de validation | `src/modules/workshop/…` |
> **Piège vérifié sur le prototype** : un `process.env.FESTIPOD_*` lu au **top-level** plante dans le navigateur (« process is not defined ») car le bundler n'inline que `NODE_ENV` ; lire en `typeof process !== 'undefined' && process.env.X`.
**Faits vérifiés sur broker** (prototype, `@data` contre `nextgraph.net`, 2026-06-16) — restent vrais :
1.`doc_create("Graph","data:graph","store",undefined)` → NURI utilisable comme `@graph` ORM (write+read via `useShape({graphs:[nuri]})`). On n'est **pas** limité au `private_store` comme scope (cf. [[rule_private-store-scope]]).
2.**Shim r/w** : 3 docs créés + `sparql_update`, rechargés via `sparql_query`.
3.**Fan-out par entité** : 2 comptes × 1 doc-événement → `useShape({graphs:[docA,docB]})` lit les deux ; un index liste les deux.
**Restes à traiter** (à la réimplémentation) :
- **Découverte réactive via l'index global** (un doc partagé réactif) — cf. [[decision_2026-06-16_discovery-model]] ; remplace le fan-out par-compte du prototype.
- **Réactivité de la création in-app** (best-effort : le nouveau doc rejoint le fan-out ; `@id` éventuellement en attente → envisager de frapper l'`@id` soi-même).
- **Synchronisation** (étape 2 du modèle 3-étapes) et **seeding** multi-document.
**Validation broker (2026-06-16, `@data` contre `nextgraph.net`, 3 scénarios verts)** :
1.`doc_create("Graph","data:graph","store",undefined)` → NURI utilisable comme `@graph` ORM (write+read d'une `Participation` via `useShape({graphs:[nuri]})`). On n'est **pas** limité au `private_store` comme scope (cf. [[rule_private-store-scope]]).
2. ✅ Shim r/w : 3 docs créés + `sparql_update`, rechargés via `sparql_query` (`readBindings` tolérant, OK en pratique).
3.**Fan-out par entité** : 2 comptes × 1 doc-événement (`createEntityDoc` + indexé), un `useShape({graphs:[docA,docB]})` lit **les deux** événements, l'index public liste les deux docs.
4.**Reste** : reactivity de la création in-app (best-effort, à itérer sur broker) + seeding multi-document (auto-seed neutralisé en `MULTISTORE`).
## Open Questions
- ~~**Login NextGraph invisible**~~ → **tranché** ([[decision_2026-06-15_shared-wallet-login-flow]]).
- ~~**Modèle de découverte**~~ → **tranché** : index global à écriture ouverte ([[decision_2026-06-16_discovery-model]]).
- ~~**Granularité documentaire**~~ → **tranché** : 1 doc par entité (événement/PdR) ; protected groupé.
- **Synchronisation** : prochain sujet — cible (réplication des docs souscrits via le broker) vs simulation dans le wallet partagé.
- **Picker d'utilisateurs** : UX de l'écran « Connexion » (saisie libre vs liste des comptes du `sharedWalletShim`).
## Possible Approaches
Posture retenue : **A + `storeRegistry` maintenant**, structuré pour la migration. Introduire dès à présent l'indirection `storeRegistry` (esquissée dans [[brief_2026-05-17_multi-store-refactor]]) — chaque entité *déclare* le store où elle *devrait* vivre, le résolveur renvoyant aujourd'hui vers le document du périmètre dans le wallet partagé. Le jour du vrai multi-user (fork moteur D ou solution upstream), on **bascule le résolveur** sans réécrire les écrans.
- **Modèle d'écriture de l'événement** (propriétaire / wiki / immuable) — *ouvert dans la matrice*. Propriétaire/immuable → événement = doc dans le `public_store` du déclarant (granularité par entité exacte). **Wiki** → exigerait un Group store (hors périmètre) et **changerait la cible**.
- **Création des documents** : `doc_create` à la création de l'entité (retenu par la granularité par entité) ; création paresseuse des index de périmètre.
- **Picker d'utilisateurs** : saisie libre vs liste des comptes du `sharedWalletShim`.
## Out of Scope
- Le **vrai multi-user** (lecture cross-wallet) : suspendu à un **fork moteur** (`OpenRepo` + capabilities) voir [[brief_2026-05-21_fork-nextgraph-inbox]].
- L'**auto-hébergement** du broker/ng-app (le staging tourne sur `nextgraph.net`).
- Toute **sécurité réelle** (credential partagé, mots de passe, chiffrement par utilisateur).
- Le **vrai multi-user** (lecture cross-wallet) fork moteur (`OpenRepo` + capabilities), voir [[brief_2026-05-21_fork-nextgraph-inbox]].
- L'**inbox du PdR** (notif d'inscription) — même fork.
- L'**auto-hébergement** du broker/ng-app (staging sur `nextgraph.net`).
- Toute **sécurité réelle** (credential partagé, pas de mot de passe, pas de chiffrement par utilisateur).
## Starting Points
- [[brief_2026-05-18_authorization-matrix]] — les périmètres repris exactement
- [[brief_2026-05-17_multi-store-refactor]] — l'indirection `storeRegistry` y est esquissée
- [[brief_2026-05-21_fork-nextgraph-inbox]] — le chemin cible réel (hors stopgap)
- Concept `data-layer` — état actuel mono-store ; [[knowledge_stores-permissions]] — limites SDK, inbox
- `src/shared/utils/ngGraph.ts`, `src/shared/hooks/useShapeWithDefaults.ts`, `src/shared/context/FestipodDataContext.tsx`, `src/shared/utils/ngBootstrap.ts`
- Source `nextgraph-rs` : `sdk/rust/src/tests/sparql_regressions.rs:136-200` (preuve multi-document), `engine/verifier/src/verifier.rs` (TODO `OpenRepo`)
- [[brief_2026-05-18_authorization-matrix]] — périmètres et partition de la vision lointaine
- [[brief_2026-05-17_multi-store-refactor]] — l'indirection `storeRegistry`
- [[brief_2026-05-21_fork-nextgraph-inbox]] — le chemin cible réel (cross-wallet + inbox)
- [[decision_2026-06-15_shared-wallet-login-flow]] — flux login/logout
- [[knowledge_stores-permissions]] — stores, capabilities, inbox, limites SDK
- `src/shared/utils/storeRegistry.ts`, `src/shared/context/FestipodDataContext.tsx`, `src/app/AuthGate.tsx`, `src/shared/utils/isolation.ts`
- Source `nextgraph-rs` : `sdk/rust/src/tests/sparql_regressions.rs:136-200` (multi-document), `engine/verifier/src/verifier.rs:1423` (TODO `OpenRepo`)
@@ -0,0 +1,53 @@
---
type: decision
summary: Distribution du wallet partagé par IMPORT ASSISTÉ PAR FICHIER (.ngw) — l'auto-import zéro-touche étant impossible (broker hébergé), Festipod sert le FICHIER du wallet (téléchargement) + le mot de passe et guide un import unique sur nextgraph.eu « Import a Wallet File » depuis l'AccessGateScreen. Le TextCode a d'abord été retenu puis CORRIGÉ (transfert temporaire 5 min, inutilisable à embarquer). Barrière d'accès ON par défaut (ACCESS_GATE_DISABLED=1 pour bypass tests/dev), ancien LoginScreen retiré. Alternatives écartées (auto-import app, lien magique, self-host) ; provisioning de test (storageState) distinct
last_updated: 2026-06-29
---
# Décision 2026-06-17 — Distribution du wallet partagé par import assisté
Comment un utilisateur récupère le wallet partagé sur un nouveau navigateur, dans le stopgap [[brief_2026-06-15_shared-wallet-shim]]. Frozen.
## Contrainte de départ
L'auto-import zéro-touche par l'app est **impossible** avec le broker hébergé (fait vérifié : [[knowledge_broker-import-constraint]]). Le wallet doit préexister dans le navigateur, importé sur `nextgraph.eu` (cross-origin, non pilotable par Festipod). La question n'est donc pas « comment auto-importer » mais « comment **minimiser la friction de récupération** » — le problème initial étant que l'utilisateur devait d'abord *se procurer* le wallet.
## Décision : import assisté par FICHIER
Festipod **sert le FICHIER `.ngw`** du wallet partagé (téléchargement) et **affiche le mot de passe** dans l'`AccessGateScreen` (la barrière d'accès, cf. [[decision_2026-06-15_shared-wallet-login-flow]]), avec un guide en 3 étapes :
1. Télécharger le fichier du wallet partagé (bouton de téléchargement).
2. Ouvrir `https://nextgraph.eu/#/wallet/login` (nouvel onglet) → « Import a Wallet File » → choisir le fichier → saisir le mot de passe affiché.
3. Revenir et cliquer « Entrer » (redirect broker → demande de déverrouiller le wallet → mot de passe → app).
Festipod **fournit** ainsi le wallet (fin de la friction de récupération) ; l'**import lui-même reste un geste manuel unique par device**, incompressible avec le broker hébergé. Posture **zéro-sécurité, credential partagé** assumée (cf. brief) → embarquer le fichier + le mot de passe est cohérent.
> **Correction 2026-06-25 (le TextCode était une fausse piste)** : la 1ʳᵉ version embarquait le **TextCode**. Or le TextCode est un **transfert temporaire** (5 min, deux devices en ligne, usage unique — cf. [[knowledge_broker-import-constraint]]), donc **inutilisable embarqué** (un testeur arrivant plus tard aurait un code mort). Le test e2e passait quand même car il génère+importe le code dans la foulée. La primitive correcte est le **FICHIER statique**.
## Pourquoi (alternatives écartées)
- **(a) Auto-import embarqué par l'app** — *impossible* : le broker ne laisse aucune fenêtre d'exécution avant son gate wallet ([[knowledge_broker-import-constraint]]).
- **(b) TextCode embarqué** — *cassé* : transfert temporaire 5 min, non réutilisable (cf. correction ci-dessus).
- **(c) Lien magique pré-rempli** vers le broker — pas de route d'import par URL côté broker hébergé.
- **(d) Self-host / fork du ng-app** — seule voie vers le **vrai zéro-touche**, mais lourde ; track séparé ([[brief_2026-05-21_fork-nextgraph-inbox]]). Non retenu pour le stopgap.
## Conséquences côté code (Festipod)
- `src/modules/auth/sharedWallet.ts``SHARED_WALLET_PASSWORD` lu depuis un **global gravé au build** `globalThis.__FESTIPOD_SHARED_WALLET_PASSWORD__` (`define` dans `build.ts`, depuis `FESTIPOD_SHARED_WALLET_PASSWORD`) ; `SHARED_WALLET_FILE_URL = /shared-wallet.ngw` ; `hasSharedWallet()` (mot de passe non vide) pilote l'affichage. Vide par défaut → la barrière retombe sur le flux simple.
- `build.ts` — copie le fichier (`FESTIPOD_SHARED_WALLET_FILE`) dans le bundle en `/shared-wallet.ngw` + grave le mot de passe.
- `src/modules/auth/screens/AccessGateScreen.tsx` — section assistée (téléchargement du fichier + mot de passe + guide) affichée si `hasSharedWallet()`.
- `src/app/AuthGate.tsx` — la barrière est **ON PAR DÉFAUT** (« Festipod ne fonctionne jamais sans NextGraph »). **Révision 2026-06-29** : drapeau **inversé** — la barrière n'est désactivée que si `globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ === true` (gravé par `build.ts` depuis `ACCESS_GATE_DISABLED=1`, ou injecté par le harness via `context.addInitScript` pour `@e2e`). Absent → barrière ON. (Remplace l'ancien `FESTIPOD_STAGING`/`__FESTIPOD_REQUIRE_NG__`, qui était OFF par défaut.)
- **Ancien `LoginScreen` retiré** (`/login`, bouton « Se connecter avec NextGraph » + login démo email/mdp) : obsolète puisque l'`AccessGateScreen` précède le routeur. Évite le dead-end « connecter sans wallet ». Route `/login` supprimée.
- **Atterrissage post-login** : après le choix du pseudo, `ConnexionScreen` navigue vers `/home` (la redirection vers l'accueil que faisait l'ancien `LoginScreen` avait disparu avec lui → on retombait sur l'onboarding `WelcomeScreen` à `/`). Filet pour les retours : `WelcomeScreen` redirige vers `/home` si déjà connecté. L'e2e `@humain` va désormais jusqu'à l'accueil pour couvrir ça.
- L'admin exporte le fichier une fois (nextgraph.eu : menu wallet → Download/Export Wallet File) et l'injecte au build (`FESTIPOD_SHARED_WALLET_FILE=<chemin.ngw> FESTIPOD_SHARED_WALLET_PASSWORD=<mdp> bun run build` ; barrière ON par défaut, pas de drapeau à poser).
## À distinguer du provisioning de test
Le harness multi-navigateur provisionne le wallet partagé par **injection storageState** (niveau navigateur, sans la contrainte broker) — concept `bdd-testing``knowledge_multibrowser-harness`. Cela **prouve** « wallet partagé → app connectée » mais **court-circuite l'import**. Le mécanisme RÉEL est validé **de bout en bout par la vraie app** (e2e `@humain`) : un navigateur vierge ouvre l'app staging, **télécharge le fichier proposé par l'`AccessGateScreen`** (et vérifie que le mot de passe affiché est celui du wallet), l'importe via le vrai flux `nextgraph.eu` « Import a Wallet File », revient, clique « Entrer » et atteint l'app connectée. Les deux ne se confondent pas.
## See Also
- [[knowledge_broker-import-constraint]] — le fait technique qui force cette décision
- [[brief_2026-06-15_shared-wallet-shim]] — le stopgap
- [[decision_2026-06-15_shared-wallet-login-flow]] — le flux d'accès dont l'AccessGateScreen est la barrière
- [[knowledge_stores-permissions]] — wallet, capabilities, limites SDK
@@ -0,0 +1,69 @@
---
type: knowledge
summary: Le broker NextGraph hébergé n'autorise pas l'auto-import d'un wallet par une web-app tierce — init() top-level redirige, toute méthode ng.* exige d'être déjà loggé dans l'iframe, et le broker renvoie un device sans wallet vers nextgraph.eu (cross-origin, non pilotable). Un wallet doit préexister dans le navigateur. Des 4 méthodes d'import nextgraph.eu, seul le FICHIER .ngw est statique/réutilisable ; le TextCode/QR sont des transferts temporaires (5 min, deux devices en ligne) inutilisables à embarquer.
last_checked: 2026-06-25
---
# Contrainte : pas d'auto-import de wallet par une web-app tierce
Fait technique **vérifié empiriquement (2026-06-17)** : avec le broker NextGraph **hébergé** (`nextgraph.net`), une web-app tierce (Festipod) **ne peut pas** provisionner/importer un wallet par programme. Le wallet doit **préexister** dans le navigateur avant que le redirect d'authentification puisse réussir.
## Pourquoi (mécanisme du proxy `@ng-org/web`)
Lecture de `ngweb.js` (dist de `@ng-org/web`) :
- **`init()` top-level REDIRIGE** : si `window.self === window.top`, il fait `window.location.href = https://nextgraph.net/redir/#/?o=<url>`. Le code de l'app ne tourne plus.
- **Toute méthode `ng.*` est relayée** par `parent.postMessage` vers `nextgraph.net`, et le handler **lève `"you must call init() first"` tant que la session n'est pas établie** (garde interne `d !== false`). Cela inclut `wallet_import_from_code`, `add_in_memory_wallet`, `session_in_memory_start`.
- L'app tierce ne s'exécute **dans l'iframe qu'APRÈS** que le broker a déjà ouvert un wallet et établi la session. **Il n'existe aucune fenêtre** où notre code tourne *avant* le gate wallet du broker → **rien à quoi accrocher un auto-import**.
> Vérifier : `node_modules/@ng-org/web/dist/ngweb.js` — fonction `init` (redirect / postMessage selon top-vs-iframe) et le handler `apply` du Proxy `ng`. Cohérent avec [[knowledge_integration-model]] (le verifier tourne dans l'iframe du ng-app, le proxy ne fait que relayer).
## Ce que montre le broker (probe Playwright, navigateur frais sans wallet)
Sur `https://nextgraph.net/redir/#/?o=...`, le broker affiche **littéralement** :
> « We could not find a wallet in your browser. For now, creating a new wallet while a Web App is authenticating, is not implemented. Please create or import your wallet in a new tab. »
…et renvoie vers `https://nextgraph.eu/` (app wallet **standalone**). L'import standalone (`/#/wallet/login`) propose **4 méthodes**, mais elles ne sont **PAS équivalentes** :
| Méthode | Nature | Embarquable / réutilisable ? |
|---|---|---|
| **Import a Wallet File** (`.ngw`) | **fichier statique** (export portable, hors-ligne) | ✅ **oui** — statique, sans expiration, sans device source |
| Import with TextCode | **transfert temporaire** device↔device via leurs serveurs : **5 min**, **les deux appareils en ligne**, usage unique | ❌ non |
| Import with QR-Code | transfert live (même famille que TextCode) | ❌ non |
| Import via Username | récupération liée à un compte | à étudier |
> **Piège vérifié (2026-06-25)** : le **TextCode n'est PAS un export statique** — l'écran nextgraph.eu le dit (« temporarily stored on our servers for up to 5 minutes », « both devices need to be online »). L'embarquer dans l'app est **inutilisable** : il expire / est à usage unique. Un test automatisé qui génère ET importe le code dans la foulée **passe** (live), masquant le problème — d'où une fausse piste initiale. **La primitive correcte pour un wallet partagé embarqué = le FICHIER `.ngw`.**
## Logique du redirect nextgraph.net (broker discovery) + ORDRE critique
`init()` redirige vers `nextgraph.net/redir`. Là, nextgraph.net regarde dans le **localStorage** s'il connaît un **broker** (le domaine du broker, posé lors d'un import/login wallet antérieur) :
- **trouvé** → redirige vers le broker (`nextgraph.eu/auth/#/wallet/login` → « Click here to login with your wallet <nom> » → mot de passe) → app. **Cas qui marche.**
- **absent** → message « We could not find a wallet in your browser… Please create or import your wallet in a new tab by clicking here ». Le lien ouvre `nextgraph.eu` (page **Welcome**) → **Login****Import a Wallet File** → on rejoint l'import.
> **Ordre critique** : il faut **importer le wallet AVANT** d'atteindre nextgraph.net. Le guide de l'`AccessGateScreen` impose cet ordre (télécharger + importer, *puis* « Entrer »).
**Deux routes vers l'import** : **A** = lien direct `nextgraph.eu/#/wallet/login` (celui de Festipod + des tests — saute la page Welcome/Login) ; **B** = fallback nextgraph.net « no wallet → clicking here » → Welcome → **Login** → Import (si on atteint nextgraph.net sans wallet).
**Pourquoi l'e2e `@humain` ne bute pas sur le « no wallet »** : il importe le fichier (route A) **avant** le « Entrer », donc nextgraph.net trouve déjà le broker. Le test **ne couvre pas** la route B (message « no wallet » + page Welcome/Login de nextgraph.eu) — ce sont des comportements nextgraph, pas Festipod, mais un humain cliquant « Entrer » en premier y tombe.
> **Atténuation côté Festipod (2026-06-29)** : la barrière `AccessGateScreen` (qui **fournit** le fichier wallet + le mot de passe + le guide) est désormais l'écran d'entrée **par défaut** (cf. [[decision_2026-06-17_assisted-wallet-import]]) — l'ancien `LoginScreen` « Se connecter avec NextGraph » (qui menait directement au redirect sans fournir le wallet) a été retiré. Un utilisateur ne peut donc plus atteindre nextgraph.net **sans** que Festipod lui ait d'abord proposé le wallet. La route B reste possible s'il clique « Entrer » avant d'importer, mais il a le wallet sous les yeux pour le faire.
> **Contrainte UX irréductible + dé-piégeage** : « Entrer » fait une **redirection pleine-page**. Si on clique AVANT d'importer → message « no wallet » → l'import se fait dans un **autre onglet** sans retour auto vers Festipod (le broker hébergé ne sait pas revenir). Il faut donc **importer d'abord, PUIS Entrer** (guide de l'`AccessGateScreen`, ordre du `@humain`). Piège corrigé (2026-06-29) : au retour (back) après un « Entrer » prématuré, la page standalone était restaurée du bfcache avec l'état figé sur `connecting` → bouton « Accès en cours » bloqué ; `NextGraphContext` écoute `pageshow.persisted` (sans session) et réinitialise sur `disconnected` pour permettre de réessayer.
## Pistes d'élimination du va-et-vient — testées, ÉCARTÉES (2026-06-30)
Deux idées pour éviter le 2ᵉ onglet ; les deux **infaisables** sans fork :
1. **Embarquer `nextgraph.eu` en iframe** dans Festipod pour guider l'import sur le même écran. nextgraph.eu **n'a pas** d'en-tête anti-framing (embed possible, import OK dans l'iframe), **MAIS** le wallet importé atterrit dans le stockage **partitionné** `(top: festipod, frame: nextgraph.eu)` — invisible du login top-level → « no wallet ». **Confirmé en vrai navigateur (Chrome).** ⚠️ **Le Chromium de Playwright N'applique PAS ce partitioning → faux positif** : un probe Playwright montrait le wallet « transmis », alors que le vrai Chrome bloque. Ne jamais valider une question d'**isolation de stockage** via Playwright ; tester en vrai navigateur.
2. **Déclencher l'écriture cross-origin sur nextgraph.net** : le mécanisme existe (pont `/auth` iframe+postMessage qui écrit `ng_bootstrap` sur nextgraph.net pendant le login nextgraph.eu) mais ce sont **les pages de NextGraph** qui l'orchestrent ; un tiers ne peut pas écrire le localStorage d'une autre origine (same-origin policy), et ça ne couvrirait que la découverte du broker, pas le wallet.
→ Seule élimination réelle = self-host/fork du ng-app ([[brief_2026-05-21_fork-nextgraph-inbox]]). Le flow stopgap reste l'import **en onglet séparé** (top-level `nextgraph.eu`, première-partie → pas de partitioning → fonctionne).
## Conséquences
- Le wallet doit être importé **sur `nextgraph.eu` (cross-origin)** — Festipod ne peut **pas** piloter ce flux ni pré-remplir l'import (pas de route d'import par URL côté broker hébergé).
- Le **vrai zéro-touche** exigerait de **self-host/forker le ng-app** (territoire de [[brief_2026-05-21_fork-nextgraph-inbox]]).
- Distribution produit retenue, vu cette contrainte : **import assisté par FICHIER** — Festipod fournit le `.ngw` (téléchargement) + le mot de passe et guide l'import — voir [[decision_2026-06-17_assisted-wallet-import]].
- À ne pas confondre avec le **provisioning de TEST** (injection storageState dans le harness), qui n'a pas cette contrainte car il agit au niveau navigateur — concept `bdd-testing``knowledge_multibrowser-harness`.