import { serve } from "bun"; 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: { // Serve Cucumber HTML report "/reports/cucumber": async () => { const file = Bun.file("reports/cucumber-report.html"); if (await file.exists()) { return new Response(file, { headers: { "Content-Type": "text/html" }, }); } return new Response("No test report found. Run 'bun run test:cucumber' first.", { status: 404, }); }, "/api/hello": { async GET(req) { return Response.json({ message: "Hello, world!", method: "GET", }); }, async PUT(req) { return Response.json({ message: "Hello, world!", method: "PUT", }); }, }, "/api/hello/:name": async req => { const name = req.params.name; return Response.json({ message: `Hello, ${name}!`, }); }, // Shared-wallet config, exposed at RUNTIME for the dev server + `bun run start` // (both serve from src/, so they miss build.ts's compile-time `define`). The app // entry (frontend.tsx) fetches this before it loads the app tree, so // `sharedWallet.ts` sees the password. Empty env → '' → no shared wallet. "/festipod-config.json": () => Response.json({ sharedWalletPassword: process.env.FESTIPOD_SHARED_WALLET_PASSWORD ?? "", // Auto-seed gate (OFF by default): only set when the env var is present, so // the front seeds an empty wallet only on explicit opt-in (see autoSeed.ts). autoSeed: process.env.FESTIPOD_AUTO_SEED ?? "", }), // 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 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 }); }, // Serve index.html for all unmatched routes (must be last) "/*": index, }, development: process.env.NODE_ENV !== "production" && { // Enable browser hot reloading in development hmr: true, // Echo console logs from the browser to the server console: true, }, }); console.log(`🚀 Server running at ${server.url}`);