software-review-panel.ts 2.9 KB

123456789101112131415161718192021222324252627282930313233
  1. import { randomBytes } from "node:crypto"
  2. import { softwareReviewHTML } from "./software-review-ui"
  3. import { softwareDashboardHTML } from "./software-dashboard-ui"
  4. import { requestJSON, type Connection } from "./agentpaas"
  5. // Credentials never enter the page. This explicit operator UI is bound to one
  6. // review, with Host/Origin/CSRF checks and no general-purpose forwarding route.
  7. export function startSoftwareReviewPanel(config: Connection, reviewID: string, port = 0, mode: "review" | "dashboard" = "review") {
  8. if (!/^[a-f0-9]{32}$/.test(reviewID)) throw new Error("无效审阅编号")
  9. const token = randomBytes(32).toString("hex")
  10. const server = Bun.serve({
  11. hostname: "127.0.0.1", port,
  12. async fetch(req, server) {
  13. const origin = `http://127.0.0.1:${server.port}`
  14. const route = new URL(req.url).pathname
  15. const headers = { "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "no-referrer" }
  16. if (req.headers.get("host") !== `127.0.0.1:${server.port}`) return new Response("Forbidden", { status: 403 })
  17. if (req.method === "GET" && route === "/") return new Response(mode === "dashboard" ? softwareDashboardHTML(token) : softwareReviewHTML(token), { headers: { ...headers, "Content-Type": "text/html; charset=utf-8", "Content-Security-Policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'" } })
  18. if (req.headers.get("X-Review-Token") !== token || (req.method !== "GET" && req.headers.get("origin") !== origin)) return Response.json({ error: "Forbidden" }, { status: 403 })
  19. if (mode === "dashboard" && req.method !== "GET") return new Response("Read only", { status: 405, headers })
  20. const suffix = route === "/api/review" && req.method === "GET" ? "" : route === "/api/evidence" && req.method === "GET" ? "/evidence" : route === "/api/evidence-text" && req.method === "GET" ? "/evidence-text" + new URL(req.url).search : route === "/api/selection" && req.method === "POST" ? "/selection" : route === "/api/execute" && req.method === "POST" ? "/execute" : route === "/api/reopen" && req.method === "POST" ? "/reopen" : undefined
  21. if (suffix === undefined) return new Response("Not found", { status: 404 })
  22. try {
  23. const body = req.method === "POST" ? await req.text() : undefined
  24. if (body && body.length > 64000) return new Response("Too large", { status: 413 })
  25. return Response.json(await requestJSON(config, `/software-reviews/${reviewID}${suffix}`, new AbortController().signal, { method: req.method, headers: { "Content-Type": "application/json" }, body }), { headers })
  26. } catch (error) {
  27. return Response.json({ error: error instanceof Error ? error.message : "读取失败" }, { status: 502, headers })
  28. }
  29. },
  30. })
  31. return { url: `http://127.0.0.1:${server.port}/`, server }
  32. }