/** * Bug Report API — accepts user-submitted bug reports and emails them. */ import { Hono } from "hono"; import { getSmtpTransport } from "./notification-service"; export const bugReportRouter = new Hono(); const MAX_SCREENSHOT_BYTES = 3 * 1024 * 1024; // 3 MB function escapeHtml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } bugReportRouter.post("/", async (c) => { let body: any; try { body = await c.req.json(); } catch { return c.json({ error: "Invalid JSON" }, 400); } const { userAgent, url, errors, comments, screenshot } = body as { userAgent?: string; url?: string; errors?: string[]; comments?: string; screenshot?: string; // data URL }; if (!comments?.trim() && (!errors || errors.length === 0)) { return c.json({ error: "Please provide comments or error details" }, 400); } // Screenshot handling let attachments: any[] = []; let screenshotCid = ""; if (screenshot && typeof screenshot === "string" && screenshot.startsWith("data:image/")) { // Validate size (base64 is ~4/3 of raw, so check decoded) const base64Part = screenshot.split(",")[1]; if (!base64Part) return c.json({ error: "Invalid screenshot data URL" }, 400); const rawBytes = Buffer.from(base64Part, "base64"); if (rawBytes.length > MAX_SCREENSHOT_BYTES) { return c.json({ error: "Screenshot exceeds 3 MB limit" }, 413); } // Determine extension from mime const mimeMatch = screenshot.match(/^data:image\/(png|jpeg|jpg|gif|webp);/); const ext = mimeMatch ? mimeMatch[1].replace("jpeg", "jpg") : "png"; const filename = `bugreport-${Date.now()}.${ext}`; // Save to generated files const savePath = `/data/files/generated/${filename}`; try { await Bun.write(savePath, rawBytes); } catch { // Non-fatal — still send email without saved file } screenshotCid = `screenshot-${Date.now()}`; attachments = [{ filename, content: rawBytes, cid: screenshotCid, contentType: `image/${ext === "jpg" ? "jpeg" : ext}`, }]; } // Build HTML email const errorList = (errors && errors.length > 0) ? errors.map(e => `
  • ${escapeHtml(e)}
  • `).join("") : ""; const html = `

    Bug Report

    Page URL${escapeHtml(url || "unknown")}
    Device${escapeHtml(userAgent || "unknown")}
    ${errorList ? `
    Recent Errors:
      ${errorList}
    ` : ""} ${comments?.trim() ? `
    Comments:

    ${escapeHtml(comments)}

    ` : ""} ${screenshotCid ? `
    Screenshot:
    ` : ""}

    Sent from rSpace Bug Reporter

    `; try { const transport = await getSmtpTransport(); if (!transport) { console.error("[bug-report] No SMTP transport available"); return c.json({ error: "Email service unavailable" }, 503); } await transport.sendMail({ from: "rSpace Bugs ", to: "jeff@jeffemmett.com", subject: `[Bug] ${url || "rSpace"} — ${(comments || "").slice(0, 60) || "Error report"}`, html, attachments, }); console.log(`[bug-report] Report sent for ${url}`); return c.json({ ok: true }); } catch (err: any) { console.error("[bug-report] Failed to send:", err.message); return c.json({ error: "Failed to send report" }, 500); } });