import express from "express"; import path from "node:path"; import fs from "node:fs/promises"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.join(__dirname, "..", ".."); const frontendDir = path.join(repoRoot, "frontend"); const transcriptsDir = path.join(repoRoot, "docs", "games"); const app = express(); const port = Number(process.env.PORT) || 3000; app.use(express.static(frontendDir)); // Local development convenience only -- writes into the repo working tree // so Claude can read a finished match's transcript straight off disk. Must // not be exposed publicly: this endpoint has no auth and happily creates // files under docs/games/ for anyone who can reach it. app.use(express.json({ limit: "4mb" })); app.post("/api/transcript", async (req, res) => { const { text, filename } = req.body ?? {}; if (typeof text !== "string" || typeof filename !== "string") { res.status(400).json({ error: "text and filename are required" }); return; } // path.basename strips any directory components, and the character // whitelist below removes everything else that could escape // transcriptsDir (including "..") -- together they're what confines // every write to docs/games/, regardless of what the client sends. const safeName = path.basename(filename).replace(/[^A-Za-z0-9._-]/g, "-"); if (!safeName.endsWith(".md") || safeName.startsWith(".")) { res.status(400).json({ error: "invalid filename" }); return; } try { await fs.mkdir(transcriptsDir, { recursive: true }); await fs.writeFile(path.join(transcriptsDir, safeName), text, "utf8"); res.json({ path: `docs/games/${safeName}` }); } catch (error) { console.error("Failed to write transcript:", error); res.status(500).json({ error: "could not write transcript" }); } }); app.listen(port, () => { console.log(`Server running at http://localhost:${port}`); });