import { join } from "jsr:@std/path"; import { serveDir } from "jsr:@std/http/file-server"; import { parse } from "jsr:@std/toml"; import type { OverlaySettings } from "../client/types.ts"; import type { OverlayConfigApiBody } from "../client/overlay-config.ts"; import { overlayTomlToDefaults, type OverlayToml } from "../client/overlay-config.ts"; const scriptDir = import.meta.dirname ?? "."; const root = join(scriptDir, "..", ".."); const port = Number(Deno.env.get("PORT") ?? "8080"); function readOverlayConfig(): { chatRequestDatabase: string | undefined; apiDefaults: Partial; } { const path = join(root, "overlay.toml"); let raw: string; try { raw = Deno.readTextFileSync(path); } catch { console.warn( "overlay.toml not found; using built-in defaults. Copy overlay.toml.example and set at least chat_request_database and beatleader_player_id.", ); return { chatRequestDatabase: undefined, apiDefaults: {} }; } let parsed: OverlayToml; try { parsed = parse(raw) as OverlayToml; } catch (e) { const msg = e instanceof Error ? e.message : String(e); console.error(`overlay.toml: parse error: ${msg}`); console.warn( "beatleader_player_id is not set; fix overlay.toml or set the BeatLeader id in the URL hash for friend scores.", ); return { chatRequestDatabase: undefined, apiDefaults: {} }; } const { chatRequestDatabase, defaults, beatleaderPlayerIdConfigured, warnings } = overlayTomlToDefaults( parsed, ); for (const w of warnings) console.warn(w); if (!beatleaderPlayerIdConfigured) { console.warn( "beatleader_player_id is not set; friend scores need a BeatLeader id in overlay.toml or in the URL hash.", ); } return { chatRequestDatabase, apiDefaults: defaults }; } const { chatRequestDatabase, apiDefaults } = readOverlayConfig(); function isSafeProxyPath(path: string): boolean { if (!path) return false; if (!path.startsWith("/")) return false; if (path.includes("://")) return false; if (path.includes("\\") || path.includes("\r") || path.includes("\n")) return false; return true; } async function proxyApiRequest(req: Request, upstreamBase: string): Promise { const url = new URL(req.url); const path = url.searchParams.get("path") ?? ""; if (!isSafeProxyPath(path)) { return new Response("Invalid path\n", { status: 400 }); } const upstream = new URL(`${upstreamBase}${path}`); for (const [key, value] of url.searchParams.entries()) { if (key === "path") continue; upstream.searchParams.append(key, value); } try { const upstreamRes = await fetch(upstream, { method: "GET" }); const headers = new Headers(); const contentType = upstreamRes.headers.get("content-type"); if (contentType) headers.set("content-type", contentType); headers.set("cache-control", "no-store"); return new Response(upstreamRes.body, { status: upstreamRes.status, statusText: upstreamRes.statusText, headers, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); return new Response(`Proxy request failed: ${message}\n`, { status: 502 }); } } function isChatRequestPath(pathname: string): boolean { const base = pathname.split("/").pop() ?? ""; return base === "ChatRequest.json"; } Deno.serve({ port, hostname: "127.0.0.1" }, async (req) => { const url = new URL(req.url); if (req.method === "GET" && url.pathname === "/api/overlay-config") { const body: OverlayConfigApiBody = { defaults: apiDefaults }; return Response.json(body, { headers: { "cache-control": "no-store" } }); } if (req.method === "GET" && url.pathname === "/api/beatleader") { return proxyApiRequest(req, "https://api.beatleader.com"); } if (req.method === "GET" && url.pathname === "/api/beatsaver") { return proxyApiRequest(req, "https://api.beatsaver.com"); } if (req.method === "GET" && chatRequestDatabase && isChatRequestPath(url.pathname)) { try { let text = await Deno.readTextFile(chatRequestDatabase); if (text.charCodeAt(0) === 0xfeff) text = text.slice(1); return new Response(text, { headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", }, }); } catch (e) { if (e instanceof Deno.errors.NotFound) { console.error(`chat_request_database not found: ${chatRequestDatabase}`); return new Response(null, { status: 404 }); } const msg = e instanceof Error ? e.message : String(e); console.error(`chat_request_database read error (${chatRequestDatabase}): ${msg}`); return new Response(`Failed to read chat_request_database: ${msg}\n`, { status: 500 }); } } return serveDir(req, { fsRoot: root, showDirListing: false }); }); console.log(`Overlay: http://127.0.0.1:${port}/index.html`); if (!chatRequestDatabase) { console.warn( "No chat_request_database in overlay.toml — place ChatRequest.json in the repo folder or set chat_request_database (see overlay.toml.example).", ); }