Download server.mjs from Mike0021/chat-ui-agent-preview: direct link, hf CLI and curl.
- Browser
- Download file 33.6 kB
-
https://huggingface.co/spaces/Mike0021/chat-ui-agent-preview/resolve/main/server.mjs
- Command line
-
hf download hf://spaces/Mike0021/chat-ui-agent-preview/server.mjs
-
curl -L -o server.mjs https://huggingface.co/spaces/Mike0021/chat-ui-agent-preview/resolve/main/server.mjs
33.6 kB
| import http from "node:http"; | |
| import { Buffer } from "node:buffer"; | |
| import { Readable, Transform } from "node:stream"; | |
| import { pipeline } from "node:stream/promises"; | |
| import { pathToFileURL } from "node:url"; | |
| import WebSocket, { WebSocketServer } from "ws"; | |
| const CAPABILITY_PATTERN = /^[A-Za-z0-9_-]{43}$/; | |
| const SANDBOX_CONTROL_PORT = 49_983; | |
| const MIN_RESOLVER_SECRET_BYTES = 32; | |
| const MAX_RESOLVER_BODY_BYTES = 16 * 1024; | |
| const MAX_PREVIEW_REQUEST_BODY_BYTES = 10 * 1024 * 1024; | |
| const HTML_INJECTION_PROBE_BYTES = 64 * 1024; | |
| const MAX_WEBSOCKET_PAYLOAD_BYTES = 16 * 1024 * 1024; | |
| const PREVIEW_REVALIDATE_INTERVAL_MS = 10_000; | |
| const UPSTREAM_HANDSHAKE_TIMEOUT_MS = 10_000; | |
| // A healthy hf.jobs route can briefly return an edge 404 for one request. Retry | |
| // only bodyless safe methods so an application-level 404 remains observable | |
| // after a short, bounded propagation window. | |
| const UPSTREAM_ROUTE_RETRY_DELAYS_MS = [100, 250, 500, 1_000]; | |
| const BLOCKED_REQUEST_HEADERS = new Set([ | |
| "authorization", | |
| "cookie", | |
| "host", | |
| "connection", | |
| "content-length", | |
| "forwarded", | |
| "proxy-authenticate", | |
| "proxy-authorization", | |
| "referer", | |
| "te", | |
| "trailer", | |
| "transfer-encoding", | |
| "upgrade", | |
| ]); | |
| const BLOCKED_RESPONSE_HEADERS = new Set([ | |
| "access-control-allow-credentials", | |
| "access-control-allow-headers", | |
| "access-control-allow-methods", | |
| "access-control-allow-origin", | |
| "access-control-expose-headers", | |
| "access-control-max-age", | |
| "connection", | |
| "content-encoding", | |
| "content-length", | |
| "content-location", | |
| "content-security-policy", | |
| "content-security-policy-report-only", | |
| "cross-origin-embedder-policy", | |
| "cross-origin-opener-policy", | |
| "cross-origin-resource-policy", | |
| "keep-alive", | |
| "link", | |
| "permissions-policy", | |
| "proxy-authenticate", | |
| "proxy-authorization", | |
| "referrer-policy", | |
| "refresh", | |
| "service-worker-allowed", | |
| "strict-transport-security", | |
| "te", | |
| "trailer", | |
| "transfer-encoding", | |
| "upgrade", | |
| "www-authenticate", | |
| "x-content-type-options", | |
| "x-frame-options", | |
| ]); | |
| class PreviewRuntimeError extends Error { | |
| constructor(status, message) { | |
| super(message); | |
| this.name = "PreviewRuntimeError"; | |
| this.status = status; | |
| } | |
| } | |
| export function runtimeConfigFromEnv(env = process.env) { | |
| const chatBaseUrl = requireHttpsBaseUrl(env.CHAT_UI_ORIGIN, "CHAT_UI_ORIGIN"); | |
| const chatOrigin = new URL(chatBaseUrl).origin; | |
| const publicOrigin = requireHttpsOrigin(env.AGENT_PREVIEW_ORIGIN, "AGENT_PREVIEW_ORIGIN"); | |
| if (chatOrigin === publicOrigin) { | |
| throw new Error("AGENT_PREVIEW_ORIGIN must be distinct from CHAT_UI_ORIGIN"); | |
| } | |
| const resolverSecret = env.AGENT_PREVIEW_RESOLVER_SECRET ?? ""; | |
| if (Buffer.byteLength(resolverSecret, "utf8") < MIN_RESOLVER_SECRET_BYTES) { | |
| throw new Error("AGENT_PREVIEW_RESOLVER_SECRET must contain at least 32 bytes"); | |
| } | |
| const jobsToken = env.AGENT_HF_TOKEN || env.HF_TOKEN || ""; | |
| if ( | |
| !jobsToken || | |
| Buffer.byteLength(jobsToken, "utf8") > 4_096 || | |
| [...jobsToken].some((character) => { | |
| const code = character.charCodeAt(0); | |
| return code <= 31 || code === 127; | |
| }) | |
| ) { | |
| throw new Error("AGENT_HF_TOKEN must contain a valid server-side Hugging Face credential"); | |
| } | |
| const port = Number(env.PORT ?? 7860); | |
| if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) { | |
| throw new Error("PORT must be a valid TCP port"); | |
| } | |
| return { | |
| chatOrigin, | |
| publicOrigin, | |
| resolverSecret, | |
| jobsToken, | |
| resolverUrl: `${chatBaseUrl}/api/agent/preview/resolve`, | |
| port, | |
| }; | |
| } | |
| export function createPreviewRuntime(options) { | |
| const config = options.config; | |
| const fetchImpl = options.fetchImpl ?? fetch; | |
| const resolveCapability = | |
| options.resolveCapability ?? | |
| ((capability, signal) => resolvePreviewCapability(capability, signal, config, fetchImpl)); | |
| const validateResolution = options.validateResolution ?? validatePreviewResolution; | |
| const WebSocketImpl = options.WebSocketImpl ?? WebSocket; | |
| const server = http.createServer( | |
| { | |
| maxHeaderSize: 16 * 1024, | |
| requestTimeout: 60_000, | |
| headersTimeout: 10_000, | |
| keepAliveTimeout: 5_000, | |
| }, | |
| (request, response) => { | |
| void handleHttpRequest({ | |
| request, | |
| response, | |
| config, | |
| fetchImpl, | |
| resolveCapability, | |
| validateResolution, | |
| }).catch((cause) => sendError(response, cause)); | |
| } | |
| ); | |
| server.maxHeadersCount = 100; | |
| server.maxRequestsPerSocket = 1_000; | |
| server.on("upgrade", (request, socket, head) => { | |
| void handleWebSocketUpgrade({ | |
| request, | |
| socket, | |
| head, | |
| config, | |
| resolveCapability, | |
| validateResolution, | |
| WebSocketImpl, | |
| }).catch((cause) => rejectUpgrade(socket, cause)); | |
| }); | |
| server.on("clientError", (_cause, socket) => { | |
| if (socket.writable) socket.end("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"); | |
| }); | |
| return server; | |
| } | |
| async function handleHttpRequest({ | |
| request, | |
| response, | |
| config, | |
| fetchImpl, | |
| resolveCapability, | |
| validateResolution, | |
| }) { | |
| const requestUrl = new URL(request.url ?? "/", config.publicOrigin); | |
| if (isServiceWorkerRequest(request)) { | |
| throw new PreviewRuntimeError(403, "Service workers are disabled for previews"); | |
| } | |
| if (requestUrl.pathname === "/" || requestUrl.pathname === "/healthcheck") { | |
| return sendLanding(response); | |
| } | |
| const recovered = recoverRootRelativeRequest(request, requestUrl, config.publicOrigin); | |
| if (recovered) { | |
| response.writeHead(307, { | |
| location: recovered, | |
| "cache-control": "no-store", | |
| "referrer-policy": "same-origin", | |
| }); | |
| response.end(); | |
| return; | |
| } | |
| const scoped = parseScopedRequest(requestUrl); | |
| if (!scoped) throw new PreviewRuntimeError(404, "Preview capability required"); | |
| if (scoped.canonicalLocation) { | |
| response.writeHead(308, { location: scoped.canonicalLocation, "cache-control": "no-store" }); | |
| response.end(); | |
| return; | |
| } | |
| const abort = new AbortController(); | |
| const onAborted = () => abort.abort(new Error("Preview client disconnected")); | |
| let clearResolutionGuard = () => {}; | |
| request.once("aborted", onAborted); | |
| try { | |
| const resolution = validateResolution(await resolveCapability(scoped.capability, abort.signal)); | |
| clearResolutionGuard = guardHttpResolution({ | |
| resolution, | |
| refresh: async () => | |
| validateResolution(await resolveCapability(scoped.capability, abort.signal)), | |
| abort, | |
| }); | |
| if (isCorsPreflight(request)) { | |
| return sendCorsPreflight(response, request); | |
| } | |
| const target = sandboxProxyUrl(resolution, scoped.upstreamPath, "https:"); | |
| const headers = sanitizedUpstreamHeaders(request.headers, resolution.port); | |
| headers.set("authorization", `Bearer ${config.jobsToken}`); | |
| headers.set("x-sandbox-token", resolution.sandboxToken); | |
| headers.set("accept-encoding", "identity"); | |
| const hasBody = !["GET", "HEAD"].includes(request.method ?? "GET"); | |
| const declaredLength = Number(request.headers["content-length"] ?? "0"); | |
| if ( | |
| hasBody && | |
| Number.isFinite(declaredLength) && | |
| declaredLength > MAX_PREVIEW_REQUEST_BODY_BYTES | |
| ) { | |
| throw new PreviewRuntimeError(413, "Preview request body is too large"); | |
| } | |
| const requestBody = hasBody | |
| ? request.pipe(new ByteLimitTransform(MAX_PREVIEW_REQUEST_BODY_BYTES)) | |
| : undefined; | |
| try { | |
| const upstream = await fetchPreviewUpstream(fetchImpl, target, { | |
| method: request.method, | |
| headers, | |
| ...(requestBody ? { body: Readable.toWeb(requestBody), duplex: "half" } : {}), | |
| redirect: "manual", | |
| signal: abort.signal, | |
| }); | |
| const responseHeaders = previewResponseHeaders( | |
| upstream, | |
| config, | |
| scoped.prefix, | |
| scoped.upstreamPath, | |
| resolution | |
| ); | |
| response.writeHead(upstream.status, upstream.statusText, responseHeaders); | |
| if (request.method === "HEAD" || !upstream.body) { | |
| response.end(); | |
| return; | |
| } | |
| const source = Readable.fromWeb(upstream.body); | |
| if (isHtml(upstream.headers.get("content-type"))) { | |
| await pipeline(source, new HtmlShimInjector(previewShim(scoped.prefix)), response); | |
| } else { | |
| await pipeline(source, response); | |
| } | |
| } finally { | |
| clearResolutionGuard(); | |
| } | |
| } finally { | |
| clearResolutionGuard(); | |
| request.off("aborted", onAborted); | |
| } | |
| } | |
| export async function fetchPreviewUpstream( | |
| fetchImpl, | |
| target, | |
| init, | |
| retryDelaysMs = UPSTREAM_ROUTE_RETRY_DELAYS_MS | |
| ) { | |
| const method = String(init.method ?? "GET").toUpperCase(); | |
| const canRetryRouteMiss = method === "GET" || method === "HEAD"; | |
| for (let attempt = 0; ; attempt += 1) { | |
| const upstream = await fetchImpl(target, init); | |
| const retryDelay = retryDelaysMs[attempt]; | |
| if (!canRetryRouteMiss || upstream.status !== 404 || retryDelay === undefined) { | |
| return upstream; | |
| } | |
| await upstream.body?.cancel().catch(() => undefined); | |
| await delay(retryDelay, init.signal); | |
| } | |
| } | |
| async function handleWebSocketUpgrade({ | |
| request, | |
| socket, | |
| head, | |
| config, | |
| resolveCapability, | |
| validateResolution, | |
| WebSocketImpl, | |
| }) { | |
| const requestUrl = new URL(request.url ?? "/", config.publicOrigin); | |
| const scoped = parseScopedRequest(requestUrl); | |
| if (!scoped || scoped.canonicalLocation) { | |
| throw new PreviewRuntimeError(404, "Preview capability required"); | |
| } | |
| const resolution = validateResolution(await resolveCapability(scoped.capability)); | |
| const target = sandboxProxyUrl(resolution, scoped.upstreamPath, "wss:"); | |
| const protocols = parseWebSocketProtocols(request.headers["sec-websocket-protocol"]); | |
| const headers = Object.fromEntries(sanitizedUpstreamHeaders(request.headers, resolution.port)); | |
| headers.authorization = `Bearer ${config.jobsToken}`; | |
| headers["x-sandbox-token"] = resolution.sandboxToken; | |
| const upstream = new WebSocketImpl(target, protocols, { | |
| headers, | |
| followRedirects: false, | |
| handshakeTimeout: UPSTREAM_HANDSHAKE_TIMEOUT_MS, | |
| maxPayload: MAX_WEBSOCKET_PAYLOAD_BYTES, | |
| perMessageDeflate: false, | |
| }); | |
| let browser; | |
| let completed = false; | |
| const reject = (cause) => { | |
| if (completed) return; | |
| completed = true; | |
| try { | |
| upstream.terminate(); | |
| } catch { | |
| // Best-effort cleanup of a failed upstream handshake. | |
| } | |
| rejectUpgrade(socket, cause); | |
| }; | |
| upstream.once("unexpected-response", (_request, response) => { | |
| response.resume(); | |
| reject(new PreviewRuntimeError(502, "Preview WebSocket refused the connection")); | |
| }); | |
| upstream.once("error", reject); | |
| upstream.once("open", () => { | |
| if (completed) return; | |
| void Promise.resolve(resolveCapability(scoped.capability)) | |
| .then(validateResolution) | |
| .then((current) => { | |
| if (completed) return; | |
| if (!samePreviewResolution(current, resolution)) { | |
| throw new PreviewRuntimeError(404, "Preview capability is no longer valid"); | |
| } | |
| completed = true; | |
| upstream.off("error", reject); | |
| const selectedProtocol = upstream.protocol; | |
| const wss = new WebSocketServer({ | |
| noServer: true, | |
| maxPayload: MAX_WEBSOCKET_PAYLOAD_BYTES, | |
| perMessageDeflate: false, | |
| handleProtocols: (offered) => | |
| selectedProtocol && offered.has(selectedProtocol) ? selectedProtocol : false, | |
| }); | |
| wss.handleUpgrade(request, socket, head, (accepted) => { | |
| browser = accepted; | |
| bridgeWebSockets(browser, upstream, resolution, async () => | |
| validateResolution(await resolveCapability(scoped.capability)) | |
| ); | |
| }); | |
| }) | |
| .catch(reject); | |
| }); | |
| socket.once("close", () => { | |
| if (!browser && upstream.readyState < WebSocketImpl.CLOSING) upstream.terminate(); | |
| }); | |
| } | |
| function bridgeWebSockets(browser, upstream, resolution, refreshResolution) { | |
| const closeBoth = (code = 1011, reason = "Preview connection closed") => { | |
| for (const ws of [browser, upstream]) { | |
| if (ws.readyState === WebSocket.OPEN) ws.close(code, reason.slice(0, 123)); | |
| else if (ws.readyState === WebSocket.CONNECTING) ws.terminate(); | |
| } | |
| }; | |
| const expiryDelay = Math.max(0, new Date(resolution.expiresAt).getTime() - Date.now()); | |
| const expiryTimer = setTimeout(() => closeBoth(1008, "Preview capability expired"), expiryDelay); | |
| expiryTimer.unref?.(); | |
| let refreshRunning = false; | |
| const refreshTimer = setInterval(() => { | |
| if (refreshRunning) return; | |
| refreshRunning = true; | |
| void refreshResolution() | |
| .then((current) => { | |
| if (!samePreviewResolution(current, resolution)) { | |
| closeBoth(1008, "Preview capability was replaced"); | |
| } | |
| }) | |
| .catch(() => closeBoth(1008, "Preview capability is no longer valid")) | |
| .finally(() => { | |
| refreshRunning = false; | |
| }); | |
| }, PREVIEW_REVALIDATE_INTERVAL_MS); | |
| refreshTimer.unref?.(); | |
| const clearGuards = () => { | |
| clearTimeout(expiryTimer); | |
| clearInterval(refreshTimer); | |
| }; | |
| browser.on("message", (data, isBinary) => { | |
| if (upstream.readyState === WebSocket.OPEN) upstream.send(data, { binary: isBinary }); | |
| }); | |
| upstream.on("message", (data, isBinary) => { | |
| if (browser.readyState === WebSocket.OPEN) browser.send(data, { binary: isBinary }); | |
| }); | |
| browser.on("ping", (data) => { | |
| if (upstream.readyState === WebSocket.OPEN) upstream.ping(data); | |
| }); | |
| upstream.on("ping", (data) => { | |
| if (browser.readyState === WebSocket.OPEN) browser.ping(data); | |
| }); | |
| browser.once("close", (code, reason) => { | |
| clearGuards(); | |
| if (upstream.readyState === WebSocket.OPEN) | |
| upstream.close(forwardableCloseCode(code), reason.toString()); | |
| else if (upstream.readyState === WebSocket.CONNECTING) upstream.terminate(); | |
| }); | |
| upstream.once("close", (code, reason) => { | |
| clearGuards(); | |
| if (browser.readyState === WebSocket.OPEN) | |
| browser.close(forwardableCloseCode(code), reason.toString()); | |
| else if (browser.readyState === WebSocket.CONNECTING) browser.terminate(); | |
| }); | |
| for (const ws of [browser, upstream]) { | |
| ws.on("error", () => closeBoth()); | |
| } | |
| } | |
| function samePreviewResolution(left, right) { | |
| return ( | |
| left.sandboxId === right.sandboxId && | |
| left.sandboxGeneration === right.sandboxGeneration && | |
| left.processId === right.processId && | |
| left.baseUrl === right.baseUrl && | |
| left.port === right.port && | |
| left.sandboxToken === right.sandboxToken && | |
| left.expiresAt === right.expiresAt | |
| ); | |
| } | |
| function guardHttpResolution({ resolution, refresh, abort }) { | |
| let refreshRunning = false; | |
| const expire = () => { | |
| if (!abort.signal.aborted) { | |
| abort.abort(new PreviewRuntimeError(404, "Preview capability expired")); | |
| } | |
| }; | |
| const expiryDelay = Math.max(0, new Date(resolution.expiresAt).getTime() - Date.now()); | |
| const expiryTimer = setTimeout(expire, expiryDelay); | |
| expiryTimer.unref?.(); | |
| const refreshTimer = setInterval(() => { | |
| if (refreshRunning || abort.signal.aborted) return; | |
| refreshRunning = true; | |
| void refresh() | |
| .then((current) => { | |
| if (!samePreviewResolution(current, resolution)) expire(); | |
| }) | |
| .catch(expire) | |
| .finally(() => { | |
| refreshRunning = false; | |
| }); | |
| }, PREVIEW_REVALIDATE_INTERVAL_MS); | |
| refreshTimer.unref?.(); | |
| return () => { | |
| clearTimeout(expiryTimer); | |
| clearInterval(refreshTimer); | |
| }; | |
| } | |
| function forwardableCloseCode(code) { | |
| return [1004, 1005, 1006, 1015].includes(code) ? 1000 : code; | |
| } | |
| async function resolvePreviewCapability(capability, signal, config, fetchImpl) { | |
| const resolverTimeout = AbortSignal.timeout(10_000); | |
| const response = await fetchImpl(config.resolverUrl, { | |
| method: "POST", | |
| headers: { | |
| authorization: `Bearer ${config.resolverSecret}`, | |
| "content-type": "application/json", | |
| accept: "application/json", | |
| }, | |
| body: JSON.stringify({ capability }), | |
| redirect: "error", | |
| signal: signal ? AbortSignal.any([signal, resolverTimeout]) : resolverTimeout, | |
| }); | |
| if (!response.ok) { | |
| await response.body?.cancel().catch(() => undefined); | |
| if ([400, 404, 410].includes(response.status)) { | |
| throw new PreviewRuntimeError(404, "Preview capability is no longer valid"); | |
| } | |
| throw new PreviewRuntimeError(502, "Preview resolver is unavailable"); | |
| } | |
| const declared = Number(response.headers.get("content-length") ?? "0"); | |
| if (Number.isFinite(declared) && declared > MAX_RESOLVER_BODY_BYTES) { | |
| await response.body?.cancel().catch(() => undefined); | |
| throw new PreviewRuntimeError(502, "Preview resolver returned an invalid response"); | |
| } | |
| const bytes = new Uint8Array(await response.arrayBuffer()); | |
| if (bytes.byteLength > MAX_RESOLVER_BODY_BYTES) { | |
| throw new PreviewRuntimeError(502, "Preview resolver returned an invalid response"); | |
| } | |
| try { | |
| return JSON.parse(new TextDecoder().decode(bytes)); | |
| } catch { | |
| throw new PreviewRuntimeError(502, "Preview resolver returned an invalid response"); | |
| } | |
| } | |
| export function validatePreviewResolution(value) { | |
| if (!value || typeof value !== "object" || Array.isArray(value)) { | |
| throw new PreviewRuntimeError(502, "Preview resolver returned an invalid target"); | |
| } | |
| const sandboxId = value.sandboxId; | |
| const sandboxGeneration = value.sandboxGeneration; | |
| const processId = value.processId; | |
| const baseUrl = value.baseUrl; | |
| const port = value.port; | |
| const sandboxToken = value.sandboxToken; | |
| const expiresAt = value.expiresAt; | |
| if ( | |
| typeof sandboxId !== "string" || | |
| !/^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/.test(sandboxId) || | |
| typeof sandboxGeneration !== "string" || | |
| !sandboxGeneration || | |
| typeof processId !== "string" || | |
| !/^[A-Za-z0-9._~-]{1,256}$/.test(processId) || | |
| !Number.isSafeInteger(port) || | |
| port < 1_024 || | |
| port > 65_535 || | |
| port === SANDBOX_CONTROL_PORT || | |
| typeof sandboxToken !== "string" || | |
| !/^[a-f0-9]{64}$/.test(sandboxToken) || | |
| typeof expiresAt !== "string" || | |
| !Number.isFinite(Date.parse(expiresAt)) || | |
| Date.parse(expiresAt) <= Date.now() | |
| ) { | |
| throw new PreviewRuntimeError(502, "Preview resolver returned an invalid target"); | |
| } | |
| let target; | |
| try { | |
| target = new URL(baseUrl); | |
| } catch { | |
| throw new PreviewRuntimeError(502, "Preview resolver returned an invalid target"); | |
| } | |
| const expectedHostname = `${sandboxId}--${SANDBOX_CONTROL_PORT}.hf.jobs`; | |
| if ( | |
| target.protocol !== "https:" || | |
| target.username || | |
| target.password || | |
| target.hostname !== expectedHostname || | |
| target.port || | |
| target.pathname !== "/" || | |
| target.search || | |
| target.hash || | |
| (baseUrl !== target.origin && baseUrl !== `${target.origin}/`) | |
| ) { | |
| throw new PreviewRuntimeError(502, "Preview resolver returned an invalid target"); | |
| } | |
| return { | |
| sandboxId, | |
| sandboxGeneration, | |
| processId, | |
| baseUrl: target.origin, | |
| port, | |
| sandboxToken, | |
| expiresAt, | |
| }; | |
| } | |
| function sandboxProxyUrl(resolution, upstreamPath, protocol) { | |
| const target = new URL(resolution.baseUrl); | |
| target.protocol = protocol; | |
| const path = new URL(upstreamPath, "https://preview.invalid"); | |
| target.pathname = `/v1/proxy/${resolution.port}${path.pathname}`; | |
| target.search = path.search; | |
| return target.toString(); | |
| } | |
| function parseScopedRequest(url) { | |
| const match = /^\/p\/([A-Za-z0-9_-]{43})(\/.*)?$/.exec(url.pathname); | |
| if (!match || !CAPABILITY_PATTERN.test(match[1])) return undefined; | |
| const capability = match[1]; | |
| const prefix = `/p/${capability}`; | |
| if (!match[2]) { | |
| return { capability, prefix, canonicalLocation: `${prefix}/${url.search}` }; | |
| } | |
| const pathname = validateUpstreamPath(match[2]); | |
| return { | |
| capability, | |
| prefix, | |
| upstreamPath: `${pathname}${url.search}`, | |
| }; | |
| } | |
| function validateUpstreamPath(pathname) { | |
| if (pathname.length > 8_192 || pathname.includes("\\")) { | |
| throw new PreviewRuntimeError(400, "Invalid preview path"); | |
| } | |
| let decoded = pathname; | |
| for (let index = 0; index < 4; index += 1) { | |
| let next; | |
| try { | |
| next = decodeURIComponent(decoded); | |
| } catch { | |
| throw new PreviewRuntimeError(400, "Invalid preview path"); | |
| } | |
| if (next === decoded) break; | |
| decoded = next; | |
| } | |
| if ( | |
| decoded.includes("\\") || | |
| decoded.split("/").some((segment) => segment === "." || segment === "..") || | |
| [...decoded].some((character) => { | |
| const code = character.charCodeAt(0); | |
| return code === 0 || code < 32 || code === 127; | |
| }) | |
| ) { | |
| throw new PreviewRuntimeError(400, "Invalid preview path"); | |
| } | |
| return pathname; | |
| } | |
| function recoverRootRelativeRequest(request, url, publicOrigin) { | |
| if (url.pathname.startsWith("/p/")) return undefined; | |
| const referer = request.headers.referer; | |
| if (typeof referer !== "string") return undefined; | |
| let source; | |
| try { | |
| source = new URL(referer); | |
| } catch { | |
| return undefined; | |
| } | |
| if (source.origin !== publicOrigin) return undefined; | |
| const match = /^\/p\/([A-Za-z0-9_-]{43})(?:\/|$)/.exec(source.pathname); | |
| if (!match || !CAPABILITY_PATTERN.test(match[1])) return undefined; | |
| return `/p/${match[1]}${url.pathname}${url.search}`; | |
| } | |
| function isServiceWorkerRequest(request) { | |
| return ( | |
| String(request.headers["service-worker"] ?? "").toLowerCase() === "script" || | |
| String(request.headers["sec-fetch-dest"] ?? "").toLowerCase() === "serviceworker" | |
| ); | |
| } | |
| function isCorsPreflight(request) { | |
| return ( | |
| request.method === "OPTIONS" && | |
| typeof request.headers["access-control-request-method"] === "string" | |
| ); | |
| } | |
| function sendCorsPreflight(response, request) { | |
| if (request.headers.origin !== "null") { | |
| throw new PreviewRuntimeError(403, "Preview preflight requires an opaque origin"); | |
| } | |
| const method = String(request.headers["access-control-request-method"] ?? "").toUpperCase(); | |
| if (!/^[A-Z]+$/.test(method) || ["CONNECT", "TRACE", "TRACK"].includes(method)) { | |
| throw new PreviewRuntimeError(403, "Preview preflight method is not allowed"); | |
| } | |
| const requestedHeaders = String(request.headers["access-control-request-headers"] ?? "") | |
| .split(",") | |
| .map((value) => value.trim().toLowerCase()) | |
| .filter(Boolean); | |
| for (const name of requestedHeaders) { | |
| if ( | |
| !/^[a-z0-9!#$%&'*+.^_`|~-]+$/.test(name) || | |
| BLOCKED_REQUEST_HEADERS.has(name) || | |
| name.startsWith("x-forwarded-") || | |
| name.startsWith("x-sandbox-") || | |
| name.startsWith("x-hf-") || | |
| name.startsWith("x-chat-") || | |
| name.startsWith("x-agent-") || | |
| name.startsWith("sec-") | |
| ) { | |
| throw new PreviewRuntimeError(403, "Preview preflight header is not allowed"); | |
| } | |
| } | |
| response.writeHead(204, { | |
| "access-control-allow-origin": "null", | |
| "access-control-allow-credentials": "true", | |
| "access-control-allow-methods": method, | |
| ...(requestedHeaders.length | |
| ? { "access-control-allow-headers": requestedHeaders.join(", ") } | |
| : {}), | |
| "access-control-max-age": "0", | |
| "cache-control": "no-store", | |
| vary: "Origin, Access-Control-Request-Method, Access-Control-Request-Headers", | |
| }); | |
| response.end(); | |
| } | |
| function sanitizedUpstreamHeaders(source, port) { | |
| const headers = new Headers(); | |
| for (const [rawName, rawValue] of Object.entries(source)) { | |
| if (rawValue === undefined) continue; | |
| const name = rawName.toLowerCase(); | |
| if ( | |
| BLOCKED_REQUEST_HEADERS.has(name) || | |
| name.startsWith("x-forwarded-") || | |
| name.startsWith("x-sandbox-") || | |
| name.startsWith("x-hf-") || | |
| name.startsWith("x-chat-") || | |
| name.startsWith("x-agent-") || | |
| name.startsWith("sec-") | |
| ) { | |
| continue; | |
| } | |
| for (const value of Array.isArray(rawValue) ? rawValue : [rawValue]) { | |
| headers.append(name, value); | |
| } | |
| } | |
| if (source.origin) headers.set("origin", `http://localhost:${port}`); | |
| return headers; | |
| } | |
| function previewResponseHeaders(upstream, config, prefix, currentPath, resolution) { | |
| const headers = {}; | |
| for (const [name, value] of upstream.headers) { | |
| const lower = name.toLowerCase(); | |
| if (BLOCKED_RESPONSE_HEADERS.has(lower) || lower.startsWith("access-control-")) continue; | |
| if (lower === "set-cookie" || lower === "location") continue; | |
| headers[name] = value; | |
| } | |
| const location = upstream.headers.get("location"); | |
| if (location) | |
| headers.location = rewritePreviewLocation(location, prefix, currentPath, resolution); | |
| const cookies = upstream.headers.getSetCookie?.() ?? []; | |
| const scopedCookies = cookies | |
| .map((cookie) => scopeSetCookie(cookie, `${prefix}/`)) | |
| .filter(Boolean); | |
| if (scopedCookies.length) headers["set-cookie"] = scopedCookies; | |
| headers["content-security-policy"] = previewContentSecurityPolicy(config.chatOrigin); | |
| headers["referrer-policy"] = "same-origin"; | |
| headers["access-control-allow-origin"] = "null"; | |
| headers["access-control-allow-credentials"] = "true"; | |
| headers["cross-origin-resource-policy"] = "cross-origin"; | |
| headers["permissions-policy"] = | |
| "camera=(), geolocation=(), microphone=(), payment=(), usb=(), serial=(), bluetooth=()"; | |
| headers["x-content-type-options"] = "nosniff"; | |
| headers["x-robots-tag"] = "noindex, nofollow, noarchive"; | |
| headers["cache-control"] = "no-store"; | |
| headers.vary = mergeVary(headers.vary, "Origin"); | |
| return headers; | |
| } | |
| function mergeVary(value, token) { | |
| const names = String(value ?? "") | |
| .split(",") | |
| .map((name) => name.trim()) | |
| .filter(Boolean); | |
| if (!names.some((name) => name.toLowerCase() === token.toLowerCase())) names.push(token); | |
| return names.join(", "); | |
| } | |
| function delay(ms, signal) { | |
| if (signal?.aborted) return Promise.reject(signal.reason); | |
| return new Promise((resolve, reject) => { | |
| const timeout = setTimeout(done, ms); | |
| function done() { | |
| signal?.removeEventListener("abort", aborted); | |
| resolve(); | |
| } | |
| function aborted() { | |
| clearTimeout(timeout); | |
| signal?.removeEventListener("abort", aborted); | |
| reject(signal.reason); | |
| } | |
| signal?.addEventListener("abort", aborted, { once: true }); | |
| }); | |
| } | |
| function previewContentSecurityPolicy(chatOrigin) { | |
| return [ | |
| "sandbox allow-downloads allow-forms allow-modals allow-popups allow-scripts", | |
| "default-src * data: blob: 'unsafe-inline' 'unsafe-eval'", | |
| "connect-src * data: blob: ws: wss:", | |
| "img-src * data: blob:", | |
| "media-src * data: blob:", | |
| "style-src * 'unsafe-inline'", | |
| "script-src * 'unsafe-inline' 'unsafe-eval' blob:", | |
| "worker-src 'none'", | |
| "object-src 'none'", | |
| "base-uri 'none'", | |
| `frame-ancestors ${chatOrigin}`, | |
| ].join("; "); | |
| } | |
| function scopeSetCookie(value, path) { | |
| const parts = value.split(";"); | |
| const pair = parts.shift()?.trim(); | |
| if (!pair || !pair.includes("=")) return undefined; | |
| const attributes = []; | |
| let hasSecure = false; | |
| let hasSameSite = false; | |
| for (const raw of parts) { | |
| const attribute = raw.trim(); | |
| const name = attribute.split("=", 1)[0].toLowerCase(); | |
| if (name === "domain" || name === "path") continue; | |
| if (name === "secure") hasSecure = true; | |
| if (name === "samesite") hasSameSite = true; | |
| attributes.push(attribute); | |
| } | |
| attributes.push(`Path=${path}`); | |
| if (!hasSecure) attributes.push("Secure"); | |
| if (!hasSameSite) attributes.push("SameSite=Lax"); | |
| return [pair, ...attributes].join("; "); | |
| } | |
| function rewritePreviewLocation(location, prefix, currentPath, resolution) { | |
| let target; | |
| try { | |
| target = new URL(location, `https://preview.invalid${currentPath}`); | |
| } catch { | |
| return `${prefix}/`; | |
| } | |
| const localHosts = new Set([ | |
| "localhost", | |
| "127.0.0.1", | |
| "0.0.0.0", | |
| "[::1]", | |
| new URL(resolution.baseUrl).hostname, | |
| ]); | |
| if (target.origin === "https://preview.invalid" || localHosts.has(target.hostname)) { | |
| const marker = `/v1/proxy/${resolution.port}`; | |
| const path = target.pathname.startsWith(marker) | |
| ? target.pathname.slice(marker.length) || "/" | |
| : target.pathname; | |
| return `${prefix}${path}${target.search}${target.hash}`; | |
| } | |
| return location; | |
| } | |
| function isHtml(contentType) { | |
| return /(?:text\/html|application\/xhtml\+xml)/i.test(contentType ?? ""); | |
| } | |
| class HtmlShimInjector extends Transform { | |
| constructor(shim) { | |
| super(); | |
| this.shim = Buffer.from(shim, "utf8"); | |
| this.pending = Buffer.alloc(0); | |
| this.injected = false; | |
| } | |
| _transform(chunk, _encoding, callback) { | |
| if (this.injected) { | |
| this.push(chunk); | |
| callback(); | |
| return; | |
| } | |
| this.pending = Buffer.concat([this.pending, chunk]); | |
| const text = this.pending.toString("utf8"); | |
| const head = /<head(?:\s[^>]*)?>/i.exec(text); | |
| if (head) { | |
| const offset = Buffer.byteLength(text.slice(0, head.index + head[0].length), "utf8"); | |
| this.push(this.pending.subarray(0, offset)); | |
| this.push(this.shim); | |
| this.push(this.pending.subarray(offset)); | |
| this.pending = Buffer.alloc(0); | |
| this.injected = true; | |
| } else if (this.pending.byteLength >= HTML_INJECTION_PROBE_BYTES) { | |
| this.push(this.shim); | |
| this.push(this.pending); | |
| this.pending = Buffer.alloc(0); | |
| this.injected = true; | |
| } | |
| callback(); | |
| } | |
| _flush(callback) { | |
| if (!this.injected) this.push(this.shim); | |
| if (this.pending.byteLength) this.push(this.pending); | |
| callback(); | |
| } | |
| } | |
| class ByteLimitTransform extends Transform { | |
| constructor(limit) { | |
| super(); | |
| this.limit = limit; | |
| this.bytes = 0; | |
| } | |
| _transform(chunk, _encoding, callback) { | |
| this.bytes += chunk.byteLength; | |
| if (this.bytes > this.limit) { | |
| callback(new PreviewRuntimeError(413, "Preview request body is too large")); | |
| return; | |
| } | |
| callback(null, chunk); | |
| } | |
| } | |
| function previewShim(prefix) { | |
| const encodedPrefix = JSON.stringify(prefix).replaceAll("<", "\\u003c"); | |
| return `<script>(()=>{"use strict";const P=${encodedPrefix},O=location.origin;const scoped=(input,ws=false)=>{try{const u=new URL(String(input),location.href);if(u.origin===O&&!u.pathname.startsWith(P+"/")&&u.pathname!==P)u.pathname=P+(u.pathname.startsWith("/")?u.pathname:"/"+u.pathname);if(ws){if(u.protocol==="http:")u.protocol="ws:";if(u.protocol==="https:")u.protocol="wss:"}return u.href}catch{return input}};const f=window.fetch;window.fetch=function(input,init){if(input instanceof Request)return f.call(this,new Request(scoped(input.url),input),init);return f.call(this,scoped(input),init)};const xo=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(method,url,...rest){return xo.call(this,method,scoped(url),...rest)};const W=window.WebSocket;function SW(url,protocols){return protocols===undefined?new W(scoped(url,true)):new W(scoped(url,true),protocols)}SW.prototype=W.prototype;Object.setPrototypeOf(SW,W);window.WebSocket=SW;const E=window.EventSource;function SE(url,options){return new E(scoped(url),options)}SE.prototype=E.prototype;Object.setPrototypeOf(SE,E);window.EventSource=SE;for(const name of ["pushState","replaceState"]){const original=history[name];history[name]=function(state,unused,url){return original.call(this,state,unused,url==null?url:scoped(url))}}const rewrite=(root=document)=>{for(const element of root.querySelectorAll?.("[src],[href],form[action]")??[]){for(const attr of ["src","href","action"]){const value=element.getAttribute(attr);if(value?.startsWith("/")&&!value.startsWith("//")&&!value.startsWith(P+"/"))element.setAttribute(attr,P+value)}}};document.addEventListener("submit",event=>{const form=event.target;if(form instanceof HTMLFormElement)form.action=scoped(form.getAttribute("action")||location.href)},true);if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",()=>rewrite(),{once:true});else rewrite();new MutationObserver(records=>{for(const record of records)for(const node of record.addedNodes)if(node instanceof Element){rewrite(node);for(const attr of ["src","href","action"]){const value=node.getAttribute(attr);if(value?.startsWith("/")&&!value.startsWith("//")&&!value.startsWith(P+"/"))node.setAttribute(attr,P+value)}}}).observe(document.documentElement,{childList:true,subtree:true});try{const sw=Object.getPrototypeOf(navigator.serviceWorker);Object.defineProperty(sw,"register",{configurable:false,value:()=>Promise.reject(new DOMException("Service workers are disabled in previews","SecurityError"))})}catch{}document.currentScript?.remove()})();</script>`; | |
| } | |
| function parseWebSocketProtocols(value) { | |
| if (typeof value !== "string") return []; | |
| return value | |
| .split(",") | |
| .map((protocol) => protocol.trim()) | |
| .filter(Boolean); | |
| } | |
| function sendLanding(response) { | |
| response.writeHead(200, { | |
| "content-type": "text/plain; charset=utf-8", | |
| "content-security-policy": "default-src 'none'; frame-ancestors 'none'", | |
| "cache-control": "no-store", | |
| "x-content-type-options": "nosniff", | |
| }); | |
| response.end("A fresh Agent preview link is required.\n"); | |
| } | |
| function sendError(response, cause) { | |
| if (response.headersSent || response.destroyed) { | |
| response.destroy(); | |
| return; | |
| } | |
| const status = cause instanceof PreviewRuntimeError ? cause.status : 502; | |
| const message = cause instanceof PreviewRuntimeError ? cause.message : "Preview proxy failed"; | |
| response.writeHead(status, { | |
| "content-type": "text/plain; charset=utf-8", | |
| "content-security-policy": "default-src 'none'; frame-ancestors 'none'", | |
| "cache-control": "no-store", | |
| "x-content-type-options": "nosniff", | |
| }); | |
| response.end(`${message}\n`); | |
| } | |
| function rejectUpgrade(socket, cause) { | |
| if (!socket.writable) return; | |
| const status = cause instanceof PreviewRuntimeError ? cause.status : 502; | |
| const phrase = status === 404 ? "Not Found" : status === 400 ? "Bad Request" : "Bad Gateway"; | |
| socket.end( | |
| `HTTP/1.1 ${status} ${phrase}\r\nConnection: close\r\nCache-Control: no-store\r\nContent-Length: 0\r\n\r\n` | |
| ); | |
| } | |
| function requireHttpsOrigin(candidate, name) { | |
| let url; | |
| try { | |
| url = new URL(candidate); | |
| } catch { | |
| throw new Error(`${name} must be an absolute HTTPS origin`); | |
| } | |
| if ( | |
| url.protocol !== "https:" || | |
| url.username || | |
| url.password || | |
| url.pathname !== "/" || | |
| url.search || | |
| url.hash || | |
| (candidate !== url.origin && candidate !== `${url.origin}/`) | |
| ) { | |
| throw new Error(`${name} must be a credential-free HTTPS origin`); | |
| } | |
| return url.origin; | |
| } | |
| function requireHttpsBaseUrl(candidate, name) { | |
| let url; | |
| try { | |
| url = new URL(candidate); | |
| } catch { | |
| throw new Error(`${name} must be an absolute HTTPS URL`); | |
| } | |
| if ( | |
| url.protocol !== "https:" || | |
| url.username || | |
| url.password || | |
| url.search || | |
| url.hash || | |
| url.pathname.includes("//") | |
| ) { | |
| throw new Error(`${name} must be a credential-free HTTPS URL with an optional base path`); | |
| } | |
| const pathname = url.pathname === "/" ? "" : url.pathname.replace(/\/$/, ""); | |
| return `${url.origin}${pathname}`; | |
| } | |
| if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { | |
| const config = runtimeConfigFromEnv(); | |
| const server = createPreviewRuntime({ config }); | |
| server.listen(config.port, "0.0.0.0", () => { | |
| process.stdout.write(`Agent preview runtime listening on port ${config.port}\n`); | |
| }); | |
| } | |