139 lines
5.4 KiB
TypeScript
139 lines
5.4 KiB
TypeScript
import { serve } from "bun";
|
|
import index from "./index.html";
|
|
|
|
const port = process.env.PORT ? parseInt(process.env.PORT) : 4000;
|
|
|
|
// 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": () => {
|
|
const backendUrl = process.env.VITE_BACKEND_URL?.trim() ?? "";
|
|
const backendClientId = process.env.VITE_BACKEND_CLIENT_ID?.trim() ?? "";
|
|
return 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 ?? "",
|
|
// ActivityPods backend coupling: the frontend needs the backend URL and the
|
|
// Solid-OIDC client_id (defaults to the app description document served by
|
|
// the backend). The Pod provider URL is optional; empty → user picks one.
|
|
backendUrl,
|
|
backendClientId,
|
|
podProviderUrl: process.env.VITE_POD_PROVIDER_URL?.trim() ?? "",
|
|
});
|
|
},
|
|
|
|
// 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}`);
|