feat(auth): staging wallet partagé — import assisté par fichier + e2e multi-navigateur

Stopgap staging multi-user sur wallet partagé (cf. brief_2026-06-15_shared-wallet-shim).

Distribution / import du wallet :
- AccessGateScreen : barrière d'accès ON PAR DÉFAUT (désactivable via
  globalThis.__FESTIPOD_ACCESS_GATE_DISABLED__ pour tests/dev). Fournit le FICHIER
  .ngw + le mot de passe + un guide en 3 étapes (import assisté sur nextgraph.eu —
  le broker hébergé n'autorise pas l'import inline pendant l'auth web-app).
- sharedWallet.ts + build.ts : fichier copié en /shared-wallet.ngw, mot de passe gravé.
- Ancien LoginScreen (/login) retiré ; atterrissage post-login -> /home.
- NextGraphContext : dé-piégeage de l'état "connecting" au retour (pageshow/bfcache).

Couche multistore stopgap : storeRegistry, isolation, AccountContext, FestipodDataContext.

Tests e2e multi-navigateur :
- browserPool + world.openBrowser : contextes frais isolés, 2 axes orthogonaux
  (nb de navigateurs × modèle de wallet own/shared).
- @humain : parcours humain complet (télécharge -> importe le fichier sur
  nextgraph.eu -> Entrer -> pseudo -> accueil).
- Bypass de la barrière pour @e2e via context.addInitScript.
- Convention @wip exclue via cucumber.json.

Docs (concepts) : nextgraph-platform (knowledge_broker-import-constraint,
decision_2026-06-17_assisted-wallet-import), bdd-testing (knowledge_multibrowser-harness).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-06-30 12:04:02 +02:00
parent 222658a75d
commit 266e33556d
40 changed files with 2224 additions and 360 deletions
+118 -1
View File
@@ -12,6 +12,7 @@ import { createRoot } from 'react-dom/client';
import { NextGraphProvider, useNextGraph } from '../context/NextGraphContext';
import { FestipodDataProvider, useFestipodData } from '../context/FestipodDataContext';
import { useShape } from '@ng-org/orm/react';
import { ng } from '@ng-org/web';
import type { DeepSignalSet } from '@ng-org/alien-deepsignals';
import {
FpEventShapeType,
@@ -67,6 +68,11 @@ function ConnectedHarness() {
const participations = useShape(FpParticipationShapeType, privateNuri) as DeepSignalSet<FpParticipation>;
const [bridgeReady, setBridgeReady] = useState(false);
// Stopgap multi-store validation: a doc created on demand via doc_create,
// mounted into a real useShape({graphs}) by <SmokeProbe>.
const [smokeDoc, setSmokeDoc] = useState<string | null>(null);
// Per-entity fan-out validation: several entity docs read together.
const [fanoutGraphs, setFanoutGraphs] = useState<string[]>([]);
useEffect(() => {
// Small delay for useShape to populate
@@ -146,6 +152,54 @@ function ConnectedHarness() {
loadTestData() {
return bootstrapWallet(events as any, users as any, participations as any);
},
// --- Stopgap multi-store validation (see brief_2026-06-15_shared-wallet-shim) ---
/**
* Create a fresh graph document via doc_create and mount it into a real
* useShape({graphs}) subscription (<SmokeProbe>). Returns the NURI.
* Validates: doc_create returns a usable graph NURI.
*/
async createSmokeDoc() {
const nuri = await ng.doc_create(session.session_id, 'Graph', 'data:graph', 'store', undefined);
setSmokeDoc(nuri);
return nuri;
},
/**
* Round-trip the sharedWalletShim through the wallet: create an account
* (3 docs + SPARQL INSERT), drop the cache, reload from the wallet via
* SPARQL SELECT. Validates: doc_create ×3 + shim sparql_update/query.
*/
async validateShim(username: string) {
const reg = await import('../utils/storeRegistry');
reg.resetRegistryCache();
const created = await reg.ensureAccount(username);
reg.resetRegistryCache();
const reloaded = (await reg.allAccounts()).find(
a => a.username === username,
) ?? null;
return { created, reloaded };
},
/**
* Per-entity granularity + fan-out: 2 accounts, one event document each
* (via createEntityDoc → indexed), then mount a multi-graph useShape over
* both (<FanoutProbe>). Returns the two doc NURIs and the index listing.
* Validates: 1-doc-per-entity, index append/read, fan-out across N docs.
*/
async setupFanout() {
const reg = await import('../utils/storeRegistry');
reg.resetRegistryCache();
await reg.ensureAccount('@fan-a');
await reg.ensureAccount('@fan-b');
const docA = await reg.createEntityDoc('@fan-a', 'public');
const docB = await reg.createEntityDoc('@fan-b', 'public');
reg.resetRegistryCache();
const listed = await reg.listEntityDocs('public');
setFanoutGraphs([docA, docB]);
return { docA, docB, listed };
},
};
console.log('[HarnessNG] Ready — events:', events.size, 'users:', users.size,
@@ -157,7 +211,70 @@ function ConnectedHarness() {
return () => clearTimeout(timer);
}, [events, users, participations, ngCtx, appData]);
return <div id="harness-status">{bridgeReady ? 'READY' : 'LOADING_SHAPES'}</div>;
return (
<>
<div id="harness-status">{bridgeReady ? 'READY' : 'LOADING_SHAPES'}</div>
{smokeDoc && <SmokeProbe docNuri={smokeDoc} />}
{fanoutGraphs.length > 0 && <FanoutProbe graphs={fanoutGraphs} />}
</>
);
}
// ============================================================================
// FanoutProbe — real useShape({graphs}) over SEVERAL entity documents.
// Exposes window.__fanout for the per-entity fan-out @data scenario.
// ============================================================================
function FanoutProbe({ graphs }: { graphs: string[] }) {
const set = useShape(FpEventShapeType, { graphs } as any) as DeepSignalSet<FpEvent>;
useEffect(() => {
(window as any).__fanout = {
ready: true,
graphs,
addEventTo(docNuri: string, title: string) {
set.add({
'@graph': docNuri,
'@type': 'http://festipod.org/Event',
'@id': '',
title,
participantCount: 1,
} as FpEvent);
},
count() { return set.size; },
titles() { return [...set].map(e => e.title); },
};
}, [set, graphs]);
return null;
}
// ============================================================================
// SmokeProbe — real useShape({graphs}) on a doc_create'd document.
// Exposes window.__smoke for the multi-store @data validation scenario.
// ============================================================================
function SmokeProbe({ docNuri }: { docNuri: string }) {
const set = useShape(FpParticipationShapeType, { graphs: [docNuri] } as any) as DeepSignalSet<FpParticipation>;
useEffect(() => {
(window as any).__smoke = {
ready: true,
docNuri,
add() {
set.add({
'@graph': docNuri,
'@type': 'http://festipod.org/Participation',
'@id': '',
event: 'urn:smoke:event',
user: 'urn:smoke:user',
isConfirmed: true,
} as FpParticipation);
},
count() { return set.size; },
items() {
return [...set].map(p => ({ '@id': p['@id'], event: p.event, user: p.user }));
},
};
}, [set, docNuri]);
return null;
}
// ============================================================================