131 lines
4.3 KiB
TypeScript
131 lines
4.3 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import { readFile, stat } from "node:fs/promises";
|
|
import { extname, join, relative, resolve, sep } from "node:path";
|
|
import { isSafeRequestPath } from "@wrnexus/core";
|
|
import type { Mode } from "@wrnexus/core";
|
|
|
|
const CONTENT_TYPES: Record<string, string> = {
|
|
".avif": "image/avif",
|
|
".css": "text/css; charset=utf-8",
|
|
".gif": "image/gif",
|
|
".html": "text/html; charset=utf-8",
|
|
".ico": "image/x-icon",
|
|
".jpeg": "image/jpeg",
|
|
".jpg": "image/jpeg",
|
|
".js": "text/javascript; charset=utf-8",
|
|
".json": "application/json; charset=utf-8",
|
|
".mjs": "text/javascript; charset=utf-8",
|
|
".pdf": "application/pdf",
|
|
".png": "image/png",
|
|
".svg": "image/svg+xml",
|
|
".txt": "text/plain; charset=utf-8",
|
|
".wasm": "application/wasm",
|
|
".webp": "image/webp",
|
|
".woff": "font/woff",
|
|
".woff2": "font/woff2",
|
|
".xml": "application/xml; charset=utf-8",
|
|
};
|
|
|
|
const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
|
|
const REVALIDATE_CACHE = "public, max-age=0, must-revalidate";
|
|
|
|
function cachePolicy(filePath: string, mode: Mode): string {
|
|
if (mode !== "production") return "no-cache";
|
|
const name = filePath.split(/[\\/]/).pop() ?? "";
|
|
if (/\.html?$/i.test(name)) return REVALIDATE_CACHE;
|
|
// Font URLs are content assets with expensive transfer costs and are almost
|
|
// always renamed when changed. Cache them like fingerprinted build assets.
|
|
if (/\.(?:woff2?|ttf|otf|eot)$/i.test(name)) return IMMUTABLE_CACHE;
|
|
return /(?:^|[.-])[a-f0-9]{8,}(?:[.-]|$)/i.test(name) ? IMMUTABLE_CACHE : "public, max-age=3600";
|
|
}
|
|
|
|
/** Cache the public-dir existence check so it isn't a sync stat on every request. */
|
|
const publicDirExistsCache = new Map<string, boolean>();
|
|
function publicDirExists(dir: string): boolean {
|
|
let exists = publicDirExistsCache.get(dir);
|
|
if (exists === undefined) {
|
|
exists = existsSync(dir);
|
|
publicDirExistsCache.set(dir, exists);
|
|
}
|
|
return exists;
|
|
}
|
|
|
|
const DEFAULT_FAVICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
<rect width="64" height="64" rx="14" fill="#0f172a"/>
|
|
<path d="M15 18h8l5 22 7-22h7l7 22 5-22h8L53 50h-8l-7-21-7 21h-8L15 18z" fill="#6c8cff"/>
|
|
</svg>`;
|
|
|
|
export async function servePublicAsset(
|
|
publicDir: string | undefined,
|
|
pathname: string,
|
|
mode: Mode,
|
|
): Promise<Response | null> {
|
|
if (pathname === "/" || pathname.startsWith("/__wrnexus/")) return null;
|
|
if (!isSafeRequestPath(pathname)) return null;
|
|
|
|
const fallback = pathname === "/favicon.ico" ? defaultFaviconResponse(mode) : null;
|
|
if (!publicDir || !publicDirExists(publicDir)) return fallback;
|
|
|
|
const rel = safePublicRelativePath(pathname);
|
|
if (!rel) return fallback;
|
|
|
|
const base = resolve(publicDir);
|
|
let filePath = resolve(base, rel);
|
|
if (!isInside(base, filePath)) return fallback;
|
|
|
|
try {
|
|
const info = await stat(filePath);
|
|
if (info.isDirectory()) {
|
|
filePath = resolve(filePath, "index.html");
|
|
if (!isInside(base, filePath)) return null;
|
|
}
|
|
|
|
const fileInfo = await stat(filePath);
|
|
if (!fileInfo.isFile()) return null;
|
|
|
|
const body = await readFile(filePath);
|
|
const contentType =
|
|
CONTENT_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
return new Response(body, {
|
|
headers: {
|
|
"content-type": contentType,
|
|
"cache-control": cachePolicy(filePath, mode),
|
|
"x-content-type-options": "nosniff",
|
|
},
|
|
});
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function defaultFaviconResponse(mode: Mode): Response {
|
|
return new Response(DEFAULT_FAVICON, {
|
|
headers: {
|
|
"content-type": "image/svg+xml; charset=utf-8",
|
|
"cache-control": mode === "production" ? "public, max-age=86400" : "no-cache",
|
|
"x-content-type-options": "nosniff",
|
|
},
|
|
});
|
|
}
|
|
|
|
function safePublicRelativePath(pathname: string): string | null {
|
|
let decoded: string;
|
|
try {
|
|
decoded = decodeURIComponent(pathname);
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
const segments = decoded.split("/").filter(Boolean);
|
|
if (segments.some((segment) => segment.startsWith(".") || segment.includes("\\"))) {
|
|
return null;
|
|
}
|
|
|
|
return segments.length ? join(...segments) : null;
|
|
}
|
|
|
|
function isInside(base: string, target: string): boolean {
|
|
const rel = relative(base, target);
|
|
return rel === "" || (!!rel && !rel.startsWith("..") && !rel.includes(`..${sep}`));
|
|
}
|