subpath bugfixes

This commit is contained in:
Lucas Winther
2026-07-10 10:20:23 +02:00
parent 076efc2101
commit 135a1aebb6
2 changed files with 45 additions and 25 deletions
+44 -24
View File
@@ -58,16 +58,19 @@ async function serveOrtFile(dir: string, name: string): Promise<Response> {
async function handleApiRequest(request: Request): Promise<Response> {
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<ServeOptions> {
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<ServeOptions> {
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: {
+1 -1
View File
@@ -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<unknown>) => 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(