diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2f1799c --- /dev/null +++ b/.env.example @@ -0,0 +1,40 @@ +# Festipod — variables d'environnement (exemple) +# +# Copier en `.env` (chargé automatiquement par Bun) et renseigner les valeurs. +# En dev (`bun run dev`) ET en prod (`bun run start`), l'app sert depuis src/ et +# lit ces variables au RUNTIME (via l'endpoint /festipod-config.json de src/index.ts). +# Sans elles, l'app tombe en mode dégradé : la barrière d'accès n'affiche que le +# champ identifiant, sans l'assistance de chargement du portefeuille partagé. + +# ── Portefeuille partagé (stopgap staging) ───────────────────────────────── +# Mot de passe du portefeuille partagé. +# VIDE => hasSharedWallet() faux => la barrière n'affiche QUE le champ identifiant +# (pas les 3 étapes « télécharger + importer le portefeuille »). REQUIS en staging +# pour l'onboarding d'un appareil qui n'a pas encore de wallet. +FESTIPOD_SHARED_WALLET_PASSWORD= + +# Chemin ABSOLU vers le fichier portefeuille partagé (.ngw). Servi en +# téléchargement à /shared-wallet.ngw depuis la barrière d'accès. +FESTIPOD_SHARED_WALLET_FILE=/chemin/absolu/vers/festipod-wallet.ngw + +# ── Seed automatique (opt-in) ────────────────────────────────────────────── +# Non vide => l'app amorce des données de démo dans un wallet VIDE au 1er login. +# OFF par défaut : laisser vide en usage normal. +FESTIPOD_AUTO_SEED= + +# ── Serveur ──────────────────────────────────────────────────────────────── +# Port HTTP du serveur (défaut 3000). +PORT=3000 + +# NODE_ENV=production bascule `bun run start` (pas de HMR). En dev, laisser vide. +NODE_ENV= + +# ── Outillage dev (facultatif) ───────────────────────────────────────────── +# Override du chemin local du polyfill @ng-eventually/client pour `pnpm run +# link:polyfill` (lien local réactif). Défaut = ../nextgraph/ng-eventually-js/packages/client. +NG_EVENTUALLY_LOCAL= + +# ── Build only (build.ts / `bun run build`, PAS le runtime) ──────────────── +# ACCESS_GATE_DISABLED=1 => build SANS barrière d'accès (l'app démarre directement). +# Réservé à un build de démo/no-gate ; ne pas utiliser pour un déploiement réel. +ACCESS_GATE_DISABLED= diff --git a/src/modules/auth/screens/AccessGateScreen.tsx b/src/modules/auth/screens/AccessGateScreen.tsx index 63099bc..1c80f67 100644 --- a/src/modules/auth/screens/AccessGateScreen.tsx +++ b/src/modules/auth/screens/AccessGateScreen.tsx @@ -1,15 +1,22 @@ /** * AccessGateScreen — the *technical access barrier* of the stopgap. * + * SHARED WALLET IS THE SOLE SUPPORTED MODE. Festipod does not function without + * the shared wallet (the SDK polyfill runs on it). "No shared wallet configured" + * is therefore NOT an offered flow — it is a loud MISCONFIGURATION error + * (`!hasSharedWallet()` → a config-error block, no functional form). Configure it + * via FESTIPOD_SHARED_WALLET_PASSWORD. + * * STOPGAP (see decision_2026-06-15_shared-wallet-login-flow). This is the * REAL NextGraph login, shown before the app renders. Because it precedes the * app, the user reads it as "access to the test environment", not as an app * login. The user also types an IDENTIFIER here — the id that names their - * virtual space (a technical id, a pseudo in practice, not a Festipod username). + * virtual space (a technical id, a pseudo in practice, NOT a Festipod profile + * handle like `@mariedupont`). * Clicking "Entrer" records that identifier and triggers `connect()`, which * redirects to the broker to open the SHARED wallet. After return the identity * is already set (persisted before the redirect), so NG auto-connects straight - * into the app — there is no separate "pick a username" screen. + * into the app — there is no separate "choose a handle" screen. * * ASSISTED IMPORT (see decision_2026-06-17). The hosted broker can't import a * wallet inline during @@ -17,8 +24,8 @@ * dead-end. We therefore HAND the user the shared wallet FILE (download) + the * shared password and guide a one-time import on nextgraph.eu ("Import a Wallet * File"), BEFORE they click "Entrer". The wallet FILE is the correct static - * primitive — a TextCode is a transient 5-min transfer, unusable to embed. Shown - * only when a shared wallet is configured (FESTIPOD_SHARED_WALLET_PASSWORD). + * primitive — a TextCode is a transient 5-min transfer, unusable to embed. This + * assisted flow is the default whenever the shared wallet is open pending. */ import { useState, type ReactNode } from 'react'; @@ -58,7 +65,7 @@ export function AccessGateScreen({ status, error, initialIdentifier, onEnter }: const connecting = status === 'connecting'; const [copied, setCopied] = useState(false); // The identifier that names this virtual space (a technical id — a pseudo in - // practice, but not a Festipod username). Entered HERE, at wallet access, so a + // practice, but not a Festipod profile handle). Entered HERE, at wallet access, so a // single act both names the space and opens it. Normalized (lowercased) upstream. // PREFILLED from the stored identifier so a returning user (reload / broker // round-trip) sees the value they already chose and never re-types it. @@ -107,7 +114,14 @@ export function AccessGateScreen({ status, error, initialIdentifier, onEnter }: Festipod Espace de test - {hasSharedWallet() && status !== 'connected' ? ( + {!hasSharedWallet() ? ( + // SOLE-MODE guard: Festipod cannot run without the shared wallet, so a + // missing one is a misconfiguration, NOT a functional login form. + + Portefeuille partagé non configuré. Festipod ne fonctionne pas sans + (définir FESTIPOD_SHARED_WALLET_PASSWORD). + + ) : status !== 'connected' ? ( <> Première connexion sur cet appareil ?
Chargez le portefeuille partagé, une seule fois. diff --git a/src/modules/auth/steps/data/connexion.steps.ts b/src/modules/auth/steps/data/connexion.steps.ts index 913a3a0..e456445 100644 --- a/src/modules/auth/steps/data/connexion.steps.ts +++ b/src/modules/auth/steps/data/connexion.steps.ts @@ -5,8 +5,8 @@ import type { FestipodWorld } from '../../../../shared/support/world'; // --- Setup --- Given('le portefeuille est vide', async function (this: FestipodWorld) { - // Each @data scenario runs under a UNIQUE username (see hooks.ts - // freshScenarioUsername), so the shim hands it a FRESH, EMPTY virtual wallet: + // Each @data scenario runs under a UNIQUE identifier (see hooks.ts + // freshScenarioIdentifier), so the shim hands it a FRESH, EMPTY virtual wallet: // "le portefeuille est vide" is trivially true on entry. So this is a fast // INSTANT CHECK — assert the reactive read already shows nothing — NOT the old // `clearWallet` per-entity-doc fan-out (a full physical-wallet enumeration that diff --git a/src/modules/auth/steps/ui/barriere-acces.steps.ts b/src/modules/auth/steps/ui/barriere-acces.steps.ts index 3f37471..89151d1 100644 --- a/src/modules/auth/steps/ui/barriere-acces.steps.ts +++ b/src/modules/auth/steps/ui/barriere-acces.steps.ts @@ -8,14 +8,35 @@ * * Guards the reported regression: on return the barrier used to re-ask for a * bare, empty identifier despite one being stored. See AuthGate.tsx. + * + * SHARED WALLET IS THE SOLE SUPPORTED MODE (see AccessGateScreen header): the + * identifier field lives INSIDE the assisted-import flow, which renders only when + * a shared wallet is configured; otherwise the barrier shows a config-error with + * NO field. Production always configures one, but the @ui node harness does not + * inject the build global, so we set it HERE — before the screen module is first + * imported, so `sharedWallet.ts` captures it at module-eval — and lazy-import the + * screen. This file is the only @ui module that reaches sharedWallet.ts, so this + * ordering is deterministic. */ import { Given, When, Then } from '@cucumber/cucumber'; import { expect } from 'chai'; import React from 'react'; import { renderElement } from '../../../../shared/test-harness/renderHelper'; -import { AccessGateScreen } from '../../screens/AccessGateScreen'; import type { FestipodWorld } from '../../../../shared/support/world'; +globalThis.__FESTIPOD_SHARED_WALLET_PASSWORD__ = 'test-shared-wallet'; + +// Lazy so sharedWallet.ts evaluates AFTER the global above is set (a static +// import would hoist above it, capturing an empty password → config-error). +type Gate = typeof import('../../screens/AccessGateScreen')['AccessGateScreen']; +let gateComponent: Gate | null = null; +async function loadGate(): Promise { + if (!gateComponent) { + gateComponent = (await import('../../screens/AccessGateScreen')).AccessGateScreen; + } + return gateComponent; +} + // Local per-scenario state (kept off the World to avoid touching its type). interface GateState { doc: Document | null; @@ -34,6 +55,7 @@ function stateFor(world: object): GateState { async function renderGate(world: object, initialIdentifier?: string): Promise { const s = stateFor(world); s.entered = null; + const AccessGateScreen = await loadGate(); // 'connecting' would disable the button; 'disconnected' is the returning-user // state (session not yet restored) — the exact case that re-prompted before. s.doc = await renderElement( diff --git a/src/shared/context/AccountContext.tsx b/src/shared/context/AccountContext.tsx index c4960be..18040d4 100644 --- a/src/shared/context/AccountContext.tsx +++ b/src/shared/context/AccountContext.tsx @@ -49,9 +49,9 @@ import { setCurrentUser } from '@ng-eventually/client/polyfill'; // Festipod localStorage key for the current identifier (same-partition // prefill/convenience only — never the cross-frontier carrier; that's the URL -// param). Changed from the historical 'festipod.account.username' → any -// pre-existing stored "logins" under the old key are dropped (acceptable: this -// is a stopgap test env; the URL param carries identity anyway). +// param). Renamed to `.identifier` from a historical key that mislabeled this +// account id → any pre-existing stored logins under the old key are dropped +// (acceptable: this is a stopgap test env; the URL param carries identity anyway). const STORAGE_KEY = 'festipod.account.identifier'; /** Name of the URL param that carries the identifier across the broker frontier. */ diff --git a/src/shared/data/registration.ts b/src/shared/data/registration.ts index 7191412..a833dc1 100644 --- a/src/shared/data/registration.ts +++ b/src/shared/data/registration.ts @@ -343,17 +343,17 @@ export async function readRegistrationNotifications( * just-written participation, so a second join checking only the reactive set would * write a duplicate. Querying the broker sees the real state regardless of read lag. * - * Scoped to the CURRENT account (`username`) via `listMyEntityDocs` — a user's own + * Scoped to the CURRENT account (`identifier`) via `listMyEntityDocs` — a user's own * participations live in their own account, so there is NO need to fan out over all * accounts (which would open/sync other accounts' unsynced docs → the ~75s hang). */ export async function countUserParticipations( - username: string, + identifier: string, eventId: string, userId: string, ): Promise { const sid = (await sessionPromise).session_id; - const docs_ = await listMyEntityDocs(username, 'protected'); + const docs_ = await listMyEntityDocs(identifier, 'protected'); let total = 0; for (const g of docs_) { total += await countParticipations(sid, g, eventId, userId).catch(() => 0); diff --git a/src/shared/support/hooks.ts b/src/shared/support/hooks.ts index 2e81cb7..e7aa594 100644 --- a/src/shared/support/hooks.ts +++ b/src/shared/support/hooks.ts @@ -22,7 +22,7 @@ setDefaultTimeout(90000); // account (whose shim key uses a sentinel prefix `normalizeIdentifier` can't emit). const RUN_NONCE = Date.now().toString(36) + Math.random().toString(36).slice(2, 6); let scenarioSeq = 0; -function freshScenarioUsername(): string { +function freshScenarioIdentifier(): string { scenarioSeq += 1; return `test-${RUN_NONCE}-${scenarioSeq}`; } @@ -580,7 +580,7 @@ Before({ timeout: 60000 }, async function (this: FestipodWorld, scenario) { // the run self-heals instead of cascading failures across the rest. this.page = await newWalletPageResilient(); - // FRESH VIRTUAL WALLET per scenario (see freshScenarioUsername above). Set a + // FRESH VIRTUAL WALLET per scenario (see freshScenarioIdentifier above). Set a // UNIQUE app-level identifier into localStorage['festipod.account.identifier'] // on EVERY origin (the init script runs in each frame before its scripts do — // including the harness iframe on 127.0.0.1). At mount the harness's @@ -588,11 +588,11 @@ Before({ timeout: 60000 }, async function (this: FestipodWorld, scenario) { // login(DEFAULT_HARNESS_USER)` is skipped and the scenario runs on a fresh, // empty virtual wallet. Overwrites any value persisted in the Chromium profile // (init scripts run on each navigation), so no accumulated wallet leaks in. - const freshUser = freshScenarioUsername(); - (this as any).freshUser = freshUser; + const freshIdentifier = freshScenarioIdentifier(); + (this as any).freshIdentifier = freshIdentifier; await this.page.addInitScript((u: string) => { try { window.localStorage.setItem('festipod.account.identifier', u); } catch { /* opaque origin */ } - }, freshUser); + }, freshIdentifier); // Capture console for debugging AND collect into the World so smoke // scenarios can assert no runtime error was emitted during the connected @@ -622,8 +622,8 @@ Before({ timeout: 60000 }, async function (this: FestipodWorld, scenario) { ); // NO per-scenario registry/wallet reset needed anymore (was T03.j - // resetDataState). Each @data scenario now runs under a UNIQUE username - // (freshScenarioUsername, set into localStorage above), so the shim hands it + // resetDataState). Each @data scenario now runs under a UNIQUE identifier + // (freshScenarioIdentifier, set into localStorage above), so the shim hands it // a FRESH, EMPTY virtual wallet whose account registry starts empty by // construction — nothing to purge. This also drops the ≤10s reset cost that // shared the Before hook's budget with the (slow) broker login. diff --git a/src/shared/utils/ngSession.ts b/src/shared/utils/ngSession.ts index 9bd7f36..dd7ace4 100644 --- a/src/shared/utils/ngSession.ts +++ b/src/shared/utils/ngSession.ts @@ -82,7 +82,7 @@ export async function login() { * REAL NextGraph logout — stops the session of the SHARED wallet. * * STOPGAP: must stay HIDDEN (Settings/debug only). The everyday "Déconnexion" - * is the FAUX one (AccountContext.logout, clears the username only). Calling + * is the FAUX one (AccountContext.logout, clears the identifier only). Calling * this forces a new broker redirect on the next access — see * decision_2026-06-15_shared-wallet-login-flow. */