import { Router } from "@oak/oak"; import { fetchRichContent, fetchWithTimeout, isValidHttpUrl, } from "../services/rich-content-service.ts"; import { APIErrorCode } from "../model/interfaces.ts"; const previewRouter = new Router(); previewRouter.get("/api/preview", async (ctx) => { const url = ctx.request.url.searchParams.get("url") ?? ""; if (!isValidHttpUrl(url)) { ctx.response.status = 400; ctx.response.body = { success: false, error: { code: APIErrorCode.VALIDATION_ERROR, message: "Invalid URL" }, }; return; } const data = await fetchRichContent(url); ctx.response.body = { success: true, data: data ?? null }; }); /** * Proxy an external image through the server so HTTP thumbnail URLs don't * trigger mixed-content blocks when the frontend is served over HTTPS. */ previewRouter.get("/api/proxy-image", async (ctx) => { const url = ctx.request.url.searchParams.get("url") ?? ""; if (!isValidHttpUrl(url)) { ctx.response.status = 400; return; } try { const res = await fetchWithTimeout(url, 8000); const contentType = res.headers.get("content-type") ?? ""; if (!contentType.startsWith("image/")) { ctx.response.status = 400; return; } const MAX_SIZE = 5 * 1024 * 1024; // 5 MB const contentLength = Number(res.headers.get("content-length") ?? "0"); if (contentLength > MAX_SIZE) { ctx.response.status = 400; return; } const bytes = new Uint8Array(await res.arrayBuffer()); if (bytes.length > MAX_SIZE) { ctx.response.status = 400; return; } ctx.response.headers.set("Content-Type", contentType); ctx.response.headers.set("Cache-Control", "public, max-age=86400"); ctx.response.body = bytes; } catch { ctx.response.status = 502; } }); export default previewRouter;