Files
deepseek-harness/packages/host/webserver/src/static.ts
2026-07-22 18:06:06 +08:00

61 lines
2.3 KiB
TypeScript

/**
* Static file serving for the web shell: the starter MIME table and the
* request handler with the semantics locked by the step1 acceptance list —
* traversal outside the dist root is 403, any miss falls back to index.html
* with HTTP 200 (SPA routing), unknown extensions ship as octet-stream.
*/
import type { ServerResponse } from 'node:http'
import { extname, join, normalize, resolve, sep } from 'node:path'
import { readFile } from 'node:fs/promises'
const MIME: Record<string, string> = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.svg': 'image/svg+xml',
'.json': 'application/json',
'.map': 'application/json',
}
/**
* Serve one GET/HEAD static request from the dist root.
* @param pathname - decoded URL pathname of the request.
* @param res - the node:http response to write.
* @param distRoot - absolute dist root directory (resolved by the caller).
* @param distIndex - absolute path of index.html inside distRoot.
* @param renderIndex - when set, produces the index.html body (boot-manifest
* injection) for `/` and every SPA fallback; undefined serves the file verbatim.
*/
export async function serveStatic(
pathname: string, res: ServerResponse, distRoot: string, distIndex: string,
renderIndex?: () => Promise<string>,
): Promise<void> {
const target = resolve(normalize(join(distRoot, pathname)))
// Traversal rejection: the target must be distRoot itself (`/`) or stay under
// it. `sep`, not '/': resolve() emits backslash paths on Windows, where a '/'
// suffix would reject every legitimate subpath as traversal.
if (target !== distRoot && !target.startsWith(distRoot + sep)) {
res.writeHead(403)
res.end()
return
}
const serveIndex = async (): Promise<void> => {
const body = renderIndex === undefined ? await readFile(distIndex) : await renderIndex()
res.writeHead(200, { 'content-type': MIME['.html'] })
res.end(body)
}
if (target === distRoot || target === distIndex) {
await serveIndex()
return
}
try {
const body = await readFile(target)
res.writeHead(200, { 'content-type': MIME[extname(target)] ?? 'application/octet-stream' })
res.end(body)
} catch {
// Miss (ENOENT/EISDIR) falls back to index.html with 200 (SPA routing).
await serveIndex()
}
}