import path from "node:path"; import { selectVariant } from "umtool-report-to-video/build-video"; import { decodeProjectSegment, deckPreviewFile, previewDirFor, rangeResponse, resolveReport, } from "@/lib/report/serve.mjs"; export const dynamic = "force-dynamic"; // The deck's preview composition, served to the iframe. // // /api/report/chrome/files///, where is the // project id base64url-encoded into one segment (POST /api/report/chrome/preview // hands out the src; nothing builds it by hand). The project and the variant // travel IN THE PATH because the composition names its assets by relative url, // and a relative url resolved against `index.html?project=…` drops the query. // // This is the one report route that takes a path from the client, so it is // confined twice: the project and the variant must be members of the server's // own scan and list, and the file must resolve -- symlinks followed -- inside // that cut's out//chrome/deck-preview/. deckPreviewFile is the rule, // and it is tested. // // The posts region's windows are served from the same prefix one segment // deeper: //posts-preview-/, where // must be an entry of this cut (previewDirFor), and the file is then confined // to THAT window's directory by the same deckPreviewFile. const TYPES: Record = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".svg": "image/svg+xml", ".woff2": "font/woff2", ".woff": "font/woff", ".ttf": "font/ttf", ".otf": "font/otf", }; export async function GET(request: Request, ctx: { params: Promise<{ path: string[] }> }) { const { path: segments } = await ctx.params; if (!Array.isArray(segments) || segments.length < 3) return new Response("not found", { status: 404 }); const [projectSeg, variant, ...rest] = segments; const projectId = decodeProjectSegment(projectSeg); if (!projectId) return new Response("not found", { status: 404 }); const r = await resolveReport(projectId, variant); if ("error" in r) return new Response(r.error, { status: r.status }); const ids = (selectVariant(r.manifest, r.variant).timeline ?? []).map((e: { id: string }) => e.id); const where = previewDirFor(r.project.dir, r.variant, rest, ids); if (!where) return new Response("not found", { status: 404 }); const file = await deckPreviewFile(where.dir, where.rest); if (!file) return new Response("not found", { status: 404 }); return rangeResponse(request, { abs: file.abs, size: file.size, headers: { "content-type": TYPES[path.extname(file.abs).toLowerCase()] ?? "application/octet-stream", "cache-control": "no-store", "x-content-type-options": "nosniff", }, }); }