From 135a1aebb6b04f883f1f7c16f72ca38c697f5c1e Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 10 Jul 2026 10:20:23 +0200 Subject: [PATCH] subpath bugfixes --- src/index.ts | 68 +++++++++++++++++++++++++++++++++------------------ src/pwa/sw.ts | 2 +- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/index.ts b/src/index.ts index 000d9f8..78a5391 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,16 +58,19 @@ async function serveOrtFile(dir: string, name: string): Promise { async function handleApiRequest(request: Request): Promise { const { pathname } = new URL(request.url); + const apiIndex = pathname.indexOf("/api/"); + const apiPath = apiIndex !== -1 ? pathname.slice(apiIndex) : pathname; + // Runtime feature flag (D-015): with auth disabled, the endpoints don't // exist — the client's /me probe reads the 404 as "feature off". The // server engine (M5) is session-gated, so it disappears along with auth. if ( !AUTH_ENABLED && - (pathname.startsWith("/api/auth/") || pathname.startsWith("/api/transcribe")) + (apiPath.startsWith("/api/auth/") || apiPath.startsWith("/api/transcribe")) ) { return NOT_FOUND(); } - switch (`${request.method} ${pathname}`) { + switch (`${request.method} ${apiPath}`) { case "POST /api/auth/register": return auth.register(request); case "POST /api/auth/login": @@ -103,11 +106,20 @@ async function productionServeOptions(): Promise { async fetch(request) { const url = new URL(request.url); // API routes are dynamic (M4/M5) and network-only in the SW (D-007). - if (url.pathname.startsWith("/api/")) return handleApiRequest(request); + if (url.pathname.includes("/api/")) return handleApiRequest(request); - const relative = url.pathname === "/" ? "index.html" : decodeURIComponent(url.pathname.slice(1)); - const path = normalize(join(dist, relative)); - if (!path.startsWith(dist + sep)) return NOT_FOUND(); // traversal guard + let relative = url.pathname === "/" ? "index.html" : decodeURIComponent(url.pathname.slice(1)); + if (relative.endsWith("/")) relative += "index.html"; + + let path = normalize(join(dist, relative)); + + // Auto-strip proxy subpaths for static assets + while (relative.includes("/") && !(await Bun.file(path).exists())) { + relative = relative.substring(relative.indexOf("/") + 1); + path = normalize(join(dist, relative)); + } + + if (!path.startsWith(dist + sep) && path !== dist) return NOT_FOUND(); // traversal guard const response = await serveFile( path, @@ -166,27 +178,35 @@ async function developmentServeOptions(): Promise { const scriptHeaders = { "Content-Type": "text/javascript; charset=utf-8" }; return { - routes: { - "/inference.worker.js": new Response(workerScript, { headers: scriptHeaders }), - "/sw.js": new Response(swScript, { - // The browser's SW update check must always see fresh bytes. - headers: { ...scriptHeaders, "Cache-Control": "no-cache" }, - }), - "/manifest.webmanifest": () => - serveFile(join(import.meta.dir, "pwa/manifest.webmanifest")), - "/icons/:file": (request) => { - const name = request.params.file ?? ""; - return /^icon-\d+\.png$/.test(name) - ? serveFile(join(ICONS_DIR, name)) - : NOT_FOUND(); - }, - "/ort/:file": (request) => serveOrtFile(ORT_DIST, request.params.file ?? ""), + async fetch(request) { + const url = new URL(request.url); + const pathname = url.pathname; - // API routes go through the same dispatcher production uses. - "/api/*": handleApiRequest, + if (pathname.includes("/api/")) return handleApiRequest(request); + + if (pathname.endsWith("/inference.worker.js")) { + return new Response(workerScript, { headers: scriptHeaders }); + } + if (pathname.endsWith("/sw.js")) { + return new Response(swScript, { headers: { ...scriptHeaders, "Cache-Control": "no-cache" } }); + } + if (pathname.endsWith("/manifest.webmanifest")) { + return serveFile(join(import.meta.dir, "pwa/manifest.webmanifest")); + } + + const iconsMatch = pathname.match(/\/icons\/(icon-\d+\.png)$/); + if (iconsMatch) { + return serveFile(join(ICONS_DIR, iconsMatch[1])); + } + + const ortMatch = pathname.match(/\/ort\/(.*(?:wasm|mjs))$/); + if (ortMatch) { + return serveOrtFile(ORT_DIST, ortMatch[1]); + } // Serve the bundled React app for all other routes. - "/*": index, + // In development, this is a special Response object from Bun's HTML bundler. + return index; }, development: { diff --git a/src/pwa/sw.ts b/src/pwa/sw.ts index 79e5c70..db877e8 100644 --- a/src/pwa/sw.ts +++ b/src/pwa/sw.ts @@ -18,7 +18,6 @@ // In dev src/index.ts injects an empty list + "dev"; in production // scripts/build.ts injects the hashed dist asset list + a content hash. const PRECACHE: string[] = JSON.parse("__PRECACHE_MANIFEST__") as string[]; -const PRECACHE_URLS = PRECACHE.map(path => new URL(path, sw.location.href).pathname); const VERSION = "__CACHE_VERSION__"; const SHELL_CACHE = `whisper-shell-${VERSION}`; @@ -44,6 +43,7 @@ interface ServiceWorkerScope { addEventListener(type: "message", listener: (event: MessageEvent) => void): void; } const sw = self as unknown as ServiceWorkerScope; +const PRECACHE_URLS = PRECACHE.map(path => new URL(path, sw.location.href).pathname); sw.addEventListener("install", (event) => { event.waitUntil(