/// import { ctRange, decryptRange } from "./crypto"; import { guessMime } from "./utils"; declare const self: ServiceWorkerGlobalScope; self.addEventListener("install", () => self.skipWaiting()); self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim())); interface Entry { key: CryptoKey; fileName: string; fileLen: number; } const entries = new Map(); // Parse Range header, resolving against total file size. // Returns null if no valid range, otherwise [start, end] inclusive. function parseRange(header: string | null, fileLen: number): [number, number] | null { if (!header || fileLen <= 0) return null; const m = header.match(/^bytes=(\d*)-(\d*)$/); if (!m) return null; let start: number, end: number; if (!m[1]) { start = Math.max(0, fileLen - parseInt(m[2]!)); end = fileLen - 1; } else { start = parseInt(m[1]); end = m[2] ? Math.min(parseInt(m[2]), fileLen - 1) : fileLen - 1; } return start <= end ? [start, end] : null; } self.addEventListener("message", (event: ExtendableMessageEvent) => { const { type, id, key, fileName, fileLen } = event.data; if (type === "register-key") { entries.set(id, { key, fileName, fileLen }); event.ports[0]?.postMessage("ok"); } else if (type === "claim") { event.waitUntil(self.clients.claim()); } }); self.addEventListener("fetch", (event: FetchEvent) => { const { pathname } = new URL(event.request.url); const match = pathname.match(/^\/([^/]+)\/decrypted$/); if (!match) return; const [, id] = match as [string, string]; event.respondWith( (async () => { const entry = entries.get(id); if (!entry) return new Response("Key not registered.", { status: 400 }); const { key, fileName, fileLen } = entry; const mime = guessMime(fileName); const range = parseRange(event.request.headers.get("Range"), fileLen); const plainStart = range?.[0] ?? 0; const plainEnd = range?.[1] ?? Math.max(fileLen - 1, 0); const responseLen = plainEnd - plainStart + 1; const headers: Record = { "Content-Type": mime, "Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`, "Content-Length": String(responseLen), "Accept-Ranges": "bytes", }; // Fetch ciphertext (range-narrowed for partial requests, full for non-range) const fetchHeaders: Record = {}; if (range) { const [ctStart, ctEnd] = ctRange(plainStart, plainEnd); fetchHeaders.Range = `bytes=${ctStart}-${ctEnd}`; } const ctResp = await fetch(`/api/file/${id}`, { headers: fetchHeaders }); if (!ctResp.ok && ctResp.status !== 206) { return new Response( ctResp.status === 404 ? "File not found or expired." : "Download failed.", { status: ctResp.status }, ); } const stream = decryptRange(ctResp.body!.getReader(), key, fileLen, plainStart, plainEnd); if (range) { headers["Content-Range"] = `bytes ${plainStart}-${plainEnd}/${fileLen}`; return new Response(stream, { status: 206, headers }); } return new Response(stream, { headers }); })().catch( (err: any) => new Response(err.message ?? "error", { status: err.status ?? 500 }), ), ); });