Fence displayed identity and native prompt generations, classify secret-free outcomes, recover atomic addition receipts without restoring old credentials, and expose truthful own-key verification metadata. Preserve required verifier and enrollment authority boundaries. Include real-store, production browser, native CTAP2 and visible responsive regressions. Validation: root check/fmt; API 115; passkey security 25; Web unit 712/component 605; auth/entry/activation/hosting browsers; product auth E2E 8 and two explicit browser suites. Server suite: 1262 lib+12 CLI, 3 ignored and 2 environment-specific exclusions. State-name checker retains 15 byte-identical baseline findings; no new exceptions. Physical keys/mobile and hosted CI remain unverified.
77 lines
2.6 KiB
TypeScript
77 lines
2.6 KiB
TypeScript
// 実Backendへの透過proxyと本番SPA配信をbrowser suite間で共有する。
|
|
// Cookie/Origin/署名検証/認可は置換しない。fixture入口は呼出側が明示的に渡す。
|
|
import { extname, join, normalize } from "@std/path";
|
|
|
|
export async function serveProductionShell(
|
|
backend: string,
|
|
origin: string,
|
|
build: string,
|
|
fixture?: (request: Request) => Promise<Response | undefined>,
|
|
) {
|
|
for (const address of [backend, origin]) {
|
|
const url = new URL(address);
|
|
if (
|
|
url.protocol !== "http:" ||
|
|
!["localhost", "127.0.0.1"].includes(url.hostname) || !url.port
|
|
) {
|
|
throw new Error("Browser E2E requires explicit owned loopback endpoints");
|
|
}
|
|
}
|
|
await Deno.stat(join(build, "index.html")); // 暗黙buildや別asset rootへのfallbackはしない。
|
|
const mime: Record<string, string> = {
|
|
".html": "text/html; charset=utf-8",
|
|
".js": "text/javascript",
|
|
".css": "text/css",
|
|
".json": "application/json",
|
|
".svg": "image/svg+xml",
|
|
".png": "image/png",
|
|
".woff2": "font/woff2",
|
|
".ico": "image/x-icon",
|
|
};
|
|
return Deno.serve({
|
|
hostname: "127.0.0.1",
|
|
port: Number(new URL(origin).port),
|
|
onListen() {},
|
|
}, async (request) => {
|
|
const url = new URL(request.url);
|
|
const special = await fixture?.(request);
|
|
if (special) return special;
|
|
if (url.pathname.startsWith("/api/")) {
|
|
const headers = new Headers(request.headers);
|
|
headers.delete("host");
|
|
headers.delete("connection");
|
|
return await fetch(new URL(url.pathname + url.search, backend), {
|
|
method: request.method,
|
|
headers,
|
|
body: ["GET", "HEAD"].includes(request.method)
|
|
? undefined
|
|
: await request.arrayBuffer(),
|
|
redirect: "manual",
|
|
});
|
|
}
|
|
const relative = normalize(
|
|
url.pathname.replace(/^\/+/, "") || "index.html",
|
|
);
|
|
if (relative.startsWith("..")) {
|
|
return new Response("not found", { status: 404 });
|
|
}
|
|
let file = join(build, relative);
|
|
try {
|
|
if ((await Deno.stat(file)).isDirectory) file = join(file, "index.html");
|
|
return new Response(await Deno.readFile(file), {
|
|
headers: {
|
|
"content-type": mime[extname(file)] ?? "application/octet-stream",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (!(error instanceof Deno.errors.NotFound)) throw error;
|
|
if (url.pathname.startsWith("/_app/")) {
|
|
return new Response("not found", { status: 404 });
|
|
}
|
|
return new Response(await Deno.readFile(join(build, "index.html")), {
|
|
headers: { "content-type": mime[".html"] },
|
|
});
|
|
}
|
|
});
|
|
}
|