subpath bugfixes
This commit is contained in:
+44
-24
@@ -58,16 +58,19 @@ async function serveOrtFile(dir: string, name: string): Promise<Response> {
|
|||||||
|
|
||||||
async function handleApiRequest(request: Request): Promise<Response> {
|
async function handleApiRequest(request: Request): Promise<Response> {
|
||||||
const { pathname } = new URL(request.url);
|
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
|
// 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
|
// 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.
|
// server engine (M5) is session-gated, so it disappears along with auth.
|
||||||
if (
|
if (
|
||||||
!AUTH_ENABLED &&
|
!AUTH_ENABLED &&
|
||||||
(pathname.startsWith("/api/auth/") || pathname.startsWith("/api/transcribe"))
|
(apiPath.startsWith("/api/auth/") || apiPath.startsWith("/api/transcribe"))
|
||||||
) {
|
) {
|
||||||
return NOT_FOUND();
|
return NOT_FOUND();
|
||||||
}
|
}
|
||||||
switch (`${request.method} ${pathname}`) {
|
switch (`${request.method} ${apiPath}`) {
|
||||||
case "POST /api/auth/register":
|
case "POST /api/auth/register":
|
||||||
return auth.register(request);
|
return auth.register(request);
|
||||||
case "POST /api/auth/login":
|
case "POST /api/auth/login":
|
||||||
@@ -103,11 +106,20 @@ async function productionServeOptions(): Promise<ServeOptions> {
|
|||||||
async fetch(request) {
|
async fetch(request) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
// API routes are dynamic (M4/M5) and network-only in the SW (D-007).
|
// 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));
|
let relative = url.pathname === "/" ? "index.html" : decodeURIComponent(url.pathname.slice(1));
|
||||||
const path = normalize(join(dist, relative));
|
if (relative.endsWith("/")) relative += "index.html";
|
||||||
if (!path.startsWith(dist + sep)) return NOT_FOUND(); // traversal guard
|
|
||||||
|
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(
|
const response = await serveFile(
|
||||||
path,
|
path,
|
||||||
@@ -166,27 +178,35 @@ async function developmentServeOptions(): Promise<ServeOptions> {
|
|||||||
const scriptHeaders = { "Content-Type": "text/javascript; charset=utf-8" };
|
const scriptHeaders = { "Content-Type": "text/javascript; charset=utf-8" };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
routes: {
|
async fetch(request) {
|
||||||
"/inference.worker.js": new Response(workerScript, { headers: scriptHeaders }),
|
const url = new URL(request.url);
|
||||||
"/sw.js": new Response(swScript, {
|
const pathname = url.pathname;
|
||||||
// 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 ?? ""),
|
|
||||||
|
|
||||||
// API routes go through the same dispatcher production uses.
|
if (pathname.includes("/api/")) return handleApiRequest(request);
|
||||||
"/api/*": handleApiRequest,
|
|
||||||
|
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.
|
// 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: {
|
development: {
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,6 @@
|
|||||||
// In dev src/index.ts injects an empty list + "dev"; in production
|
// In dev src/index.ts injects an empty list + "dev"; in production
|
||||||
// scripts/build.ts injects the hashed dist asset list + a content hash.
|
// scripts/build.ts injects the hashed dist asset list + a content hash.
|
||||||
const PRECACHE: string[] = JSON.parse("__PRECACHE_MANIFEST__") as string[];
|
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 VERSION = "__CACHE_VERSION__";
|
||||||
|
|
||||||
const SHELL_CACHE = `whisper-shell-${VERSION}`;
|
const SHELL_CACHE = `whisper-shell-${VERSION}`;
|
||||||
@@ -44,6 +43,7 @@ interface ServiceWorkerScope {
|
|||||||
addEventListener(type: "message", listener: (event: MessageEvent<unknown>) => void): void;
|
addEventListener(type: "message", listener: (event: MessageEvent<unknown>) => void): void;
|
||||||
}
|
}
|
||||||
const sw = self as unknown as ServiceWorkerScope;
|
const sw = self as unknown as ServiceWorkerScope;
|
||||||
|
const PRECACHE_URLS = PRECACHE.map(path => new URL(path, sw.location.href).pathname);
|
||||||
|
|
||||||
sw.addEventListener("install", (event) => {
|
sw.addEventListener("install", (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
|
|||||||
Reference in New Issue
Block a user