diff --git a/.env.example b/.env.example index d2f14e8..8c6debf 100644 --- a/.env.example +++ b/.env.example @@ -18,10 +18,19 @@ FESTIPOD_SHARED_WALLET_PASSWORD= # Chemin vers le fichier portefeuille partagé (.ngw), absolu ou relatif à la -# racine. Servi en téléchargement à /shared-wallet.ngw — par le build de l'app, -# et par le serveur du harness pendant les tests. +# racine. Servi en téléchargement à /shared-wallet.ngw. C'est la forme du DEV +# LOCAL et du harness de TESTS : le fichier vit sur le disque de la machine. FESTIPOD_SHARED_WALLET_FILE=/chemin/absolu/vers/festipod-wallet.ngw +# Contenu du portefeuille partagé (.ngw), encodé en base64 — deuxième source +# pour le même fichier. C'est la forme des DÉPLOIEMENTS (conteneur) : *.ngw est +# gitignoré, donc `COPY . .` n'en embarque aucun et rien n'en monte un ; le +# fichier n'étant pas un secret (l'app le sert à quiconque ouvre l'app), il +# voyage comme une variable de config. `FESTIPOD_SHARED_WALLET_FILE` est +# prioritaire quand les deux sont renseignées — voir le commentaire dans +# src/index.ts. Générer la valeur avec, p.ex., `base64 -w0 festipod-wallet.ngw`. +FESTIPOD_SHARED_WALLET_FILE_BASE64= + # ── 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. diff --git a/.project/concepts/tech-stack/_debt.md b/.project/concepts/tech-stack/_debt.md new file mode 100644 index 0000000..a6ab8df --- /dev/null +++ b/.project/concepts/tech-stack/_debt.md @@ -0,0 +1,7 @@ +# Doc-debt — tech-stack + +> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file. +> One block = one "big change": `why` + `files` + `verify` (leaves to review). + +## Raw markers (consolidate into blocks, then delete) +- TOUCHED src/index.ts @2026-08-17 (session 0b064e8b-1717-421f-a20e-a4318ad217b1) diff --git a/Dockerfile b/Dockerfile index 4f21190..19d7a21 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM oven/bun:1-alpine AS base WORKDIR /app # Install dependencies with pnpm. -# - git: the @ng-eventually/client polyfill is a git+https (public Gitea) dependency → no auth. +# - git: @ng-eventually/polyfill is a git+https (public Gitea) dependency → no auth. # - nodejs + npm: pnpm is a Node CLI; we pin the exact pnpm version via `npm i -g` # (Alpine's nodejs package does not bundle corepack). # The `bun` npm peer (pulled by bun-plugin-tailwind) is approved to build in package.json diff --git a/src/index.ts b/src/index.ts index 90d8f1b..190f4d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,24 @@ import index from "./index.html"; const port = process.env.PORT ? parseInt(process.env.PORT) : 3000; +// Strict base64 check (not a mere `Buffer.from` attempt, which silently drops invalid +// characters instead of failing): reject anything that is not a well-formed base64 body +// before decoding, so a typo'd env var is reported instead of served as 810 garbage bytes. +const BASE64_SHAPE = /^[A-Za-z0-9+/]+={0,2}$/; + +function decodeBase64WalletOrThrow(raw: string) { + const trimmed = raw.trim(); + if (trimmed.length === 0 || trimmed.length % 4 !== 0 || !BASE64_SHAPE.test(trimmed)) { + throw new Error("not valid base64 (bad characters, or length not a multiple of 4)"); + } + // Web `atob` (not Node's `Buffer`, whose `ArrayBufferLike` generic doesn't line up + // with `Response`'s `BodyInit`) — decodes to a binary string, rebuilt into bytes below. + const binary = atob(trimmed); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return bytes; +} + const server = serve({ port, routes: { @@ -54,12 +72,44 @@ const server = serve({ }), // The shared wallet file — the `fileUrl` the app hands the SDK, when configured. + // + // Two sources, read fresh on every request (never captured at module evaluation): + // - FESTIPOD_SHARED_WALLET_FILE: a filesystem path. What local dev and the test + // harness set today — a file sitting at the working-copy root. + // - FESTIPOD_SHARED_WALLET_FILE_BASE64: the file's bytes, base64-encoded. What a + // container sets instead, since *.ngw is gitignored and nothing mounts one there. + // + // Precedence: FILE wins whenever it is set, even if the path turns out unreadable — + // it is NOT "whichever resolves". This keeps dev/test behaviour byte-for-byte + // unchanged (they set only FILE, never BASE64) and makes the rule predictable: a + // deployment picks exactly one variable to set, and setting both is a leftover, not + // an intentional fallback chain. "/shared-wallet.ngw": async () => { - const p = process.env.FESTIPOD_SHARED_WALLET_FILE; - if (p) { - const file = Bun.file(p); + const path = process.env.FESTIPOD_SHARED_WALLET_FILE; + if (path) { + const file = Bun.file(path); if (await file.exists()) return new Response(file); + return new Response("No shared wallet file configured.", { status: 404 }); } + + const encoded = process.env.FESTIPOD_SHARED_WALLET_FILE_BASE64; + if (encoded) { + // Malformed must fail loudly: a 404 here would look identical to "not + // configured", which is exactly the confusion this project is removing. + try { + const bytes = decodeBase64WalletOrThrow(encoded); + return new Response(bytes, { + headers: { "Content-Type": "application/octet-stream" }, + }); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return new Response( + `FESTIPOD_SHARED_WALLET_FILE_BASE64 is set but ${message}.`, + { status: 500 } + ); + } + } + return new Response("No shared wallet file configured.", { status: 404 }); },