import { routes as templatebuilder, workerPath } from "./packages/templatebuilder/index"; import { routes as extractor } from "./packages/extractor/index"; import { routes as summarizer } from "./packages/summarizer/index"; import { join } from "node:path"; import { rm } from "node:fs/promises"; import { dataPath } from "./deployPaths"; import { addSecurityHeaders, getCsrfCookieName, isAuthenticated, isSafeStem, login, loginPage, logout, requireAuth, securityHeaders, withAuth, } from "./security"; const SUMMARIZER_OUTPUTS_DIR = process.env.BMP_SUMMARIZER_OUTPUTS_DIR ?? dataPath(join(import.meta.dir, "packages/summarizer/outputs"), "summarizer", "outputs"); const EXTRACTOR_OUTPUTS_DIR = process.env.BMP_EXTRACTOR_OUTPUTS_DIR ?? dataPath(join(import.meta.dir, "packages/extractor/outputs"), "extractor", "outputs"); const ROOT_PAGE_PATH = join(import.meta.dir, "index.html"); const BMP_LOGO_PATH = join(import.meta.dir, "assets/bmp-logo.png"); const PORT = Number(process.env.PORT ?? 3000); function contentTypeFor(path: string): string { if (path.endsWith(".json")) return "application/json; charset=utf-8"; if (path.endsWith(".xlsx")) return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; if (path.endsWith(".pdf")) return "application/pdf"; if (path.endsWith(".html")) return "text/html; charset=utf-8"; return "application/octet-stream"; } async function serveSummaryDownload(req: Request, stem: string, file: string): Promise { const blocked = await requireAuth(req); if (blocked) return blocked; if ( !isSafeStem(stem) || file !== `${stem}.json` && file !== `${stem}.xlsx` && file !== `${stem}.pdf` && file !== `${stem}.report.html` && file !== `${stem}.questions.pdf` && file !== `${stem}.questions.html` ) { return new Response("Not found", { status: 404 }); } const path = join(SUMMARIZER_OUTPUTS_DIR, stem, file); const output = Bun.file(path); if (!(await output.exists())) { return new Response("Not found", { status: 404 }); } return new Response(output, { headers: { ...securityHeaders, "Content-Type": contentTypeFor(file), "Cache-Control": "private, no-store", "Content-Disposition": `attachment; filename="${file.replaceAll('"', "")}"`, }, }); } Bun.serve({ port: PORT, routes: { "/login": { GET: loginPage, POST: login, }, "/healthz": { GET: () => Response.json({ ok: true }), }, "/logout": { POST: withAuth(async () => logout(), { csrf: true }), }, "/api/auth/session": { GET: async (req: Request) => addSecurityHeaders(Response.json({ authenticated: await isAuthenticated(req), csrfCookieName: getCsrfCookieName(), })), }, "/": { GET: withAuth(async () => new Response(Bun.file(ROOT_PAGE_PATH), { headers: { "Content-Type": "text/html; charset=utf-8" }, })), }, "/assets/bmp-logo.png": { GET: withAuth(async () => new Response(Bun.file(BMP_LOGO_PATH), { headers: { "Content-Type": "image/png", "Cache-Control": "private, max-age=86400" }, })), }, "/api/results/:stem": { DELETE: withAuth(async (req: Request) => { const stem = (req as Request & { params: Record }).params.stem ?? ""; if (!isSafeStem(stem)) { return Response.json({ error: "Invalid result id" }, { status: 400 }); } const targets = [ join(EXTRACTOR_OUTPUTS_DIR, stem), join(SUMMARIZER_OUTPUTS_DIR, stem), ]; await Promise.all(targets.map((target) => rm(target, { recursive: true, force: true }))); return Response.json({ ok: true, stem, deleted: targets.length }); }, { csrf: true, limit: { key: "result-delete", max: 20, windowMs: 60_000 }, }), }, "/api/pipeline/result/:stem": { GET: withAuth(async (req: Request) => { const stem = (req as Request & { params: Record }).params.stem ?? ""; if (!isSafeStem(stem)) { return Response.json({ error: "Invalid result id" }, { status: 400 }); } const jsonPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.json`); const xlsxPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.xlsx`); const pdfPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.pdf`); const reportHtmlPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.report.html`); const questionsPdfPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.questions.pdf`); const questionsHtmlPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.questions.html`); const jsonFile = Bun.file(jsonPath); if (!(await jsonFile.exists())) { return Response.json({ error: "Summary result not found" }, { status: 404 }); } const downloads = [ { label: "JSON herunterladen", url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.json`)}`, }, ]; if (await Bun.file(xlsxPath).exists()) { downloads.push({ label: "XLSX herunterladen", url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.xlsx`)}`, }); } if (await Bun.file(pdfPath).exists()) { downloads.push({ label: "PDF herunterladen", url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.pdf`)}`, }); } if (await Bun.file(reportHtmlPath).exists()) { downloads.push({ label: "Report-HTML herunterladen", url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.report.html`)}`, }); } if (await Bun.file(questionsPdfPath).exists()) { downloads.push({ label: "Fragen-PDF herunterladen", url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.questions.pdf`)}`, }); } if (await Bun.file(questionsHtmlPath).exists()) { downloads.push({ label: "Fragen-HTML herunterladen", url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.questions.html`)}`, }); } return Response.json({ ok: true, stem, summary: await jsonFile.json(), downloads, }); }), }, "/downloads/summarizer/:stem/:file": { GET: async (req: Request) => { const params = (req as Request & { params: Record }).params; const stem = params.stem ?? ""; const file = params.file ?? ""; return serveSummaryDownload(req, stem, file); }, }, ...templatebuilder, ...extractor, ...summarizer, }, async fetch(req) { const { pathname } = new URL(req.url); if (pathname === "/pdf.worker.mjs") { const blocked = await requireAuth(req); if (blocked) return blocked; return addSecurityHeaders(new Response(Bun.file(workerPath), { headers: { "Content-Type": "application/javascript", "Cache-Control": "private, max-age=3600" }, })); } if (pathname === "/login" && req.method === "POST") { return login(req); } if (pathname === "/logout" && req.method === "POST") { const blocked = await requireAuth(req, { csrf: true }); if (blocked) return blocked; return logout(); } if (pathname === "/login" && req.method === "GET") { return loginPage(req); } if (pathname === "/healthz" && req.method === "GET") { return Response.json({ ok: true }); } if (pathname === "/") { const blocked = await requireAuth(req); if (blocked) return blocked; return addSecurityHeaders(new Response(Bun.file(ROOT_PAGE_PATH), { headers: { "Content-Type": "text/html; charset=utf-8" }, })); } if (pathname === "/assets/bmp-logo.png") { const blocked = await requireAuth(req); if (blocked) return blocked; return addSecurityHeaders(new Response(Bun.file(BMP_LOGO_PATH), { headers: { "Content-Type": "image/png", "Cache-Control": "private, max-age=86400" }, })); } if (pathname.startsWith("/_bun/")) { const blocked = await requireAuth(req); if (blocked) return blocked; return new Response("Not found", { status: 404, headers: securityHeaders, }); } if (pathname.startsWith("/downloads/summarizer/")) { const [stem = "", file = ""] = pathname .slice("/downloads/summarizer/".length) .split("/") .map((part) => decodeURIComponent(part)); return serveSummaryDownload(req, stem, file); } return new Response("Not found", { status: 404, headers: securityHeaders }); }, development: { hmr: true, console: true }, }); console.log("bmp-rewrite"); console.log(` pipeline → http://localhost:${PORT}/`); console.log(` templatebuilder → http://localhost:${PORT}/builder`); console.log(` extractor → http://localhost:${PORT}/extractor`); console.log(` summarizer → http://localhost:${PORT}/summarizer`);