// fallow-ignore-file code-duplication complexity /** * File Server for Render Mode * * Lightweight HTTP server that serves the project directory inside Docker. * Key responsibility: inject the verified Hyperframe runtime + render mode extension * into index.html on-the-fly, so Puppeteer can load the composition with * all relative URLs (compositions, CSS, JS, assets) resolving correctly. */ import { Hono } from "hono"; import { serve } from "@hono/node-server"; import type { IncomingMessage } from "node:http"; import { existsSync, realpathSync, statSync, createReadStream } from "node:fs"; import { readFile } from "node:fs/promises"; import { Readable } from "node:stream"; import { join, extname, resolve, sep } from "node:path"; import { injectScriptsAtHeadStart, injectScriptsIntoHtml } from "@hyperframes/core/compiler"; import { fpsToNumber, type Fps } from "@hyperframes/core"; import { getVerifiedHyperframeRuntimeSource } from "./hyperframeRuntimeLoader.js"; import { getHfEarlyStub } from "../generated/hf-early-stub-inline.js"; import { defaultLogger, type ProducerLogger } from "../logger.js"; export { injectScriptsAtHeadStart }; type PathModuleLike = { resolve: (...segments: string[]) => string; sep: string; }; type IsPathInsideOptions = { resolveSymlinks?: boolean; /** * Path module used for resolution and separator comparison. Defaults to * `node:path` for the running platform. Tests inject `path.win32` / * `path.posix` to exercise cross-platform behavior on a single OS. */ pathModule?: PathModuleLike; }; /** * Returns true iff `child` is the same as, or nested inside, `parent` after * path normalization. Used to reject path-traversal attempts (e.g. * GET `/../etc/passwd`) before opening any file. * * `path.join(root, "..")` normalizes traversal segments and can escape `root` * entirely, so the join return value alone is not a safe guard. Callers must * resolve both sides and compare prefixes with the platform separator * appended to `parent` to avoid `/foo` matching `/foobar`. * * Exported for unit tests; not part of the public package surface. */ export function isPathInside( child: string, parent: string, options: IsPathInsideOptions = {}, ): boolean { const { resolveSymlinks = false, pathModule } = options; const resolveFn = pathModule?.resolve ?? resolve; const separator = pathModule?.sep ?? sep; const resolvedChild = resolveFn(child); const resolvedParent = resolveFn(parent); const normalizedChild = resolveSymlinks && existsSync(resolvedChild) ? realpathSync.native(resolvedChild) : resolvedChild; const normalizedParent = resolveSymlinks && existsSync(resolvedParent) ? realpathSync.native(resolvedParent) : resolvedParent; if (normalizedChild === normalizedParent) return true; const parentWithSep = normalizedParent.endsWith(separator) ? normalizedParent : normalizedParent + separator; return normalizedChild.startsWith(parentWithSep); } const MIME_TYPES: Record = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".mjs": "application/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".cube": "text/plain; charset=utf-8", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".webp": "image/webp", ".mp4": "video/mp4", ".webm": "video/webm", ".mp3": "audio/mpeg", ".wav": "audio/wav", ".ogg": "audio/ogg", ".aac": "audio/aac", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".otf": "font/otf", }; /** * Result of parsing a `Range:` request header against a known total size. * * - `kind: "satisfiable"`: `start <= end < size`. The response should be 206 * with `Content-Range: bytes start-end/size` and the sliced body. * - `kind: "unsatisfiable"`: the header was syntactically valid (`bytes=...`) * but the resolved range falls outside `[0, size)` (e.g. `start >= size`, * `end < start`, or a suffix request on a zero-byte file). Per RFC 7233 * the response should be 416 with `Content-Range: bytes (asterisk)/size`. * - `kind: "absent"`: there is no `Range:` header on the request, or it is * syntactically malformed, uses a non-`bytes` unit, or requests multiple * ranges. RFC 7233 allows ignoring such headers and serving the full body * with a 200, which is what callers should do. */ export type RangeRequest = | { kind: "satisfiable"; start: number; end: number } | { kind: "unsatisfiable" } | { kind: "absent" }; /** * Parse a single-range `Range:` request header per RFC 7233 §2.1. * * Supports the three forms of `bytes=...`: * - `bytes=START-END`: closed range, both bounds inclusive. * - `bytes=START-`: open-ended, serve from START to EOF. * - `bytes=-SUFFIX`: last SUFFIX bytes. * * Multi-range requests (`bytes=0-99,200-299`) are treated as `absent`. The * caller serves the full body with 200. The hyperframes producer's use case * (Chrome `