import { randomBytes } from "node:crypto"; import { createServer, request as httpRequest } from "node:http"; import { readFile, stat } from "node:fs/promises"; import path from "node:path"; import { gzip } from "node:zlib"; import { promisify } from "node:util"; import { copyProxyResponseHeaders, isCloudWebSseRoute, proxyCloudApiRequest, upstreamRequestHeaders } from "./cloud-web-proxy.mjs"; import { cloudWebProxyRoutePolicy } from "./cloud-web-routes.mjs"; import { UPSTREAM_UNAVAILABLE_ERROR_CODE, UPSTREAM_UNAVAILABLE_MESSAGE } from "./http.mjs"; const gzipAsync = promisify(gzip); const GZIP_MIN_BYTES = 1024; const CLIENT_DISCONNECT_ERROR_CODES = new Set([ "ECONNRESET", "EPIPE", "ERR_STREAM_PREMATURE_CLOSE" ]); const CLIENT_DISCONNECT_ERROR_PATTERN = /\b(?:socket hang up|client disconnected|premature close)\b/iu; const CLOUD_WEB_ERROR_HANDLER_INSTALLED = Symbol.for("hwlab.cloud-web.error-handler-installed"); const CLIENT_ROUTE_FALLBACK_PATHS = new Set(["/workbench", "/workspace", "/opencode", "/gate", "/diagnostics/gate", "/help", "/skills", "/access"]); const STATIC_ASSET_EXTENSION_PATTERN = /\.[A-Za-z0-9][A-Za-z0-9_-]*$/u; const OPENCODE_PROXY_PREFIX = "/_opencode"; const OPENCODE_FRAME_URL_PATH = "/opencode/frame-url"; const OPENCODE_DEFAULT_PROJECT_BOOTSTRAP_SCRIPT_PATH = "/__hwlab/opencode-default-project.js"; const OPENCODE_TICKET_QUERY = "hwlab_opencode_ticket"; const OPENCODE_TICKET_COOKIE = "hwlab_opencode_ticket"; const OPENCODE_TICKET_TTL_MS = 5 * 60 * 1000; const OPENCODE_TICKET_MAX_AGE_SECONDS = Math.floor(OPENCODE_TICKET_TTL_MS / 1000); const ZERO_OTEL_TRACE_ID = "00000000000000000000000000000000"; const ZERO_OTEL_SPAN_ID = "0000000000000000"; const OTEL_TRACEPARENT_PATTERN = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/u; const SAFE_REQUEST_ID_PATTERN = /^[A-Za-z0-9_.:-]{1,160}$/u; const TRACE_EXPLORER_TEMPLATE_TOKEN = "{trace_id}"; const TRACE_EXPLORER_TEMPLATE_PLACEHOLDER_PATTERN = /\{([A-Za-z0-9_:-]+)\}/gu; const opencodeTickets = new Map(); export async function serveCloudWeb(options) { installCloudWebProcessErrorHandlers({ serviceId: options.serviceId }); await validateCloudWebRuntimeConfig(); const server = createCloudWebServer(options); server.listen(options.port, "0.0.0.0", () => { process.stdout.write(JSON.stringify({ serviceId: options.serviceId, environment: process.env.HWLAB_ENVIRONMENT || "dev", status: "listening", port: options.port }) + "\n"); }); } export function createCloudWebServer({ serviceId, healthPayload, sendJson, roots = ["/app/web/hwlab-cloud-web/dist", "/app/web/hwlab-cloud-web"], cloudApiBaseUrl = process.env.HWLAB_API_BASE_URL || "", cloudApiProxyTimeoutMs = parseTimeout(process.env.HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS, 1260000, { min: 1000, max: 2400000 }), opencodeUpstreamUrl = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_UPSTREAM_URL") || "", opencodeProxyHost = opencodeProxyHostFromEnv(), opencodeProxyTimeoutMs = parseTimeout(process.env.HWLAB_CLOUD_WEB_OPENCODE_PROXY_TIMEOUT_MS, cloudApiProxyTimeoutMs, { min: 1000, max: 2400000 }), opencodeUsername = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_USERNAME") || "", opencodePassword = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_PASSWORD") || "", opencodeDefaultProject = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_DEFAULT_PROJECT") || "", opencodeEventDirectoryRewrite = opencodeEventDirectoryRewriteFromEnv() }) { return createServer(async (request, response) => { attachRequestErrorHandlers({ request, response, serviceId }); const url = new URL(request.url || "/", "http://hwlab-cloud-web.local"); try { if (url.pathname === "/health/live" || url.pathname === "/health") { sendJson(response, 200, healthPayload()); return; } if (url.pathname === OPENCODE_FRAME_URL_PATH && request.method === "GET") { await handleOpencodeFrameUrlRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson }); return; } if (isOpencodeProxyRequest(request, url, opencodeProxyHost)) { await proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, opencodeUpstreamUrl, opencodeProxyTimeoutMs, opencodeUsername, opencodePassword, opencodeDefaultProject, opencodeEventDirectoryRewrite, serviceId, sendJson }); return; } if (await handleCloudWebAuth({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson })) { return; } const proxyPolicy = cloudWebProxyRoutePolicy(request.method, url.pathname); if (proxyPolicy.proxy) { await proxyCloudApi({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson }); return; } const routePath = normalizeCloudWebRoutePath(url.pathname); const relativePath = routePath === "/" || CLIENT_ROUTE_FALLBACK_PATHS.has(routePath) || isCloudWebWorkbenchSessionRoute(routePath) || isCloudWebClientRouteRequest(request, routePath) ? "index.html" : url.pathname.slice(1); for (const root of roots) { const candidate = path.resolve(root, relativePath); if (!candidate.startsWith(root)) continue; try { const info = await stat(candidate); if (!info.isFile()) continue; const type = contentType(candidate); const body = await staticResponseBody({ candidate, relativePath, type, request, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs }); const encoded = await encodeStaticBody(request, body, type); response.writeHead(200, { "content-type": type, ...encoded.headers, "content-length": encoded.body.length }); response.end(encoded.body); return; } catch (error) { if (isClientDisconnectError(error)) { logCloudWebRuntimeEvent("cloud-web-client-disconnect", { serviceId, request, error }); return; } if (!isStaticFileMiss(error)) throw error; } } sendJson(response, 404, { error: "not_found", serviceId, path: url.pathname }); } catch (error) { if (isClientDisconnectError(error)) { logCloudWebRuntimeEvent("cloud-web-client-disconnect", { serviceId, request, error }); return; } if (!response.headersSent && !response.writableEnded) { sendJson(response, 500, { error: "internal_error", serviceId, message: error instanceof Error ? error.message : String(error) }); return; } response.destroy(error); } }); } export async function validateCloudWebRuntimeConfig() { await runtimeConfigFromEnv(); } function isStaticFileMiss(error) { return error?.code === "ENOENT" || error?.code === "ENOTDIR"; } async function staticResponseBody({ candidate, relativePath, type, request, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs }) { const body = await readFile(candidate); if (relativePath !== "index.html" || !String(type).startsWith("text/html")) return body; return await injectCloudWebRuntimeConfig(body, { request, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs }); } async function injectCloudWebRuntimeConfig(body, options = {}) { const config = await runtimeConfigFromEnv(options); if (Object.keys(config).length === 0) return body; const html = body.toString("utf8"); const script = ``; if (html.includes("")) return Buffer.from(html.replace("", `${script}`), "utf8"); if (html.includes("")) return Buffer.from(html.replace("", `${script}`), "utf8"); return Buffer.from(`${html}${script}`, "utf8"); } async function runtimeConfigFromEnv(options = {}) { const config = { displayTime: displayTimeConfigFromEnv() }; const workbench = workbenchRuntimeConfigFromEnv(); if (workbench) config.workbench = workbench; const opencode = await opencodeRuntimeConfigFromEnv(options); if (opencode) config.opencode = opencode; return config; } async function opencodeRuntimeConfigFromEnv(options = {}) { const url = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_URL"); if (!url) return null; validateOpencodeUrl(url); return { url: await opencodeFrameUrl(url, options) }; } async function opencodeFrameUrl(value, { request = null, url = null, cloudApiBaseUrl = "", cloudApiProxyTimeoutMs = 0 } = {}) { if (!request || normalizeCloudWebRoutePath(url?.pathname || "") !== "/opencode" || !cloudApiBaseUrl || !hasCookieHeader(request.headers.cookie)) return value; const result = await mintOpencodeFrameUrl(value, { request, cloudApiBaseUrl, cloudApiProxyTimeoutMs, traceContext: cloudWebTraceContext(request) }); return result.ok ? result.url : value; } async function handleOpencodeFrameUrlRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson }) { const traceContext = cloudWebTraceContext(request); response.setHeader("cache-control", "no-store"); if (!cloudApiBaseUrl) { sendJsonWithTraceContext(response, 503, { error: "cloud_api_base_url_missing", serviceId, path: url.pathname, diagnostic: opencodeTraceDiagnostic(traceContext) }, traceContext, sendJson); return; } const configuredUrl = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_URL"); if (!configuredUrl) { sendJsonWithTraceContext(response, 503, { error: "opencode_url_missing", serviceId, path: url.pathname, diagnostic: opencodeTraceDiagnostic(traceContext) }, traceContext, sendJson); return; } try { validateOpencodeUrl(configuredUrl); } catch (error) { sendJsonWithTraceContext(response, 503, { error: "opencode_url_invalid", serviceId, path: url.pathname, message: error instanceof Error ? error.message : String(error), diagnostic: opencodeTraceDiagnostic(traceContext) }, traceContext, sendJson); return; } const result = await mintOpencodeFrameUrl(configuredUrl, { request, cloudApiBaseUrl, cloudApiProxyTimeoutMs, traceContext, serviceId }); if (!result.ok) { logCloudWebRuntimeEvent("opencode_auth_required", { serviceId, request, traceContext, details: { route: url.pathname, sessionStatusCode: result.sessionStatusCode, sessionAuthenticated: result.sessionAuthenticated, ticketPresent: false, ticketAccepted: false, valuesPrinted: false } }); sendJsonWithTraceContext(response, result.statusCode, { authenticated: false, error: "opencode_auth_required", serviceId, path: url.pathname, diagnostic: opencodeTraceDiagnostic(traceContext, { sessionStatusCode: result.sessionStatusCode }) }, traceContext, sendJson); return; } sendJsonWithTraceContext(response, 200, { authenticated: true, url: result.url, expiresInMs: OPENCODE_TICKET_TTL_MS, diagnostic: opencodeTraceDiagnostic(traceContext) }, traceContext, sendJson); } async function mintOpencodeFrameUrl(value, { request, cloudApiBaseUrl, cloudApiProxyTimeoutMs, traceContext = null, serviceId = "hwlab-cloud-web" } = {}) { const session = await requestCloudApiJson({ request, method: "GET", pathname: "/auth/session", headers: cloudWebTraceHeaders(traceContext, serviceId), cloudApiBaseUrl, cloudApiProxyTimeoutMs }); if (!session.ok || session.body?.authenticated !== true) { return { ok: false, statusCode: session.statusCode === 403 ? 403 : 401, sessionStatusCode: session.statusCode || 0, sessionAuthenticated: session.body?.authenticated === true }; } const ticket = issueOpencodeTicket(request.headers.cookie); const target = new URL(value); target.searchParams.set(OPENCODE_TICKET_QUERY, ticket); return { ok: true, url: target.toString() }; } function displayTimeConfigFromEnv() { const timeZone = requiredRuntimeConfigEnv("HWLAB_CLOUD_WEB_DISPLAY_TIME_ZONE"); const locale = requiredRuntimeConfigEnv("HWLAB_CLOUD_WEB_DISPLAY_TIME_LOCALE"); const label = requiredRuntimeConfigEnv("HWLAB_CLOUD_WEB_DISPLAY_TIME_LABEL"); validateDisplayTimeZone(timeZone); validateDisplayLocale(locale); return { timeZone, locale, label }; } function requiredRuntimeConfigEnv(name) { const value = process.env[name]; if (typeof value !== "string" || value.trim().length === 0) { throw new Error(`missing required Cloud Web runtime config env: ${name}`); } return value.trim(); } function optionalRuntimeConfigEnv(name) { const value = process.env[name]; if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function validateOpencodeUrl(value) { let url; try { url = new URL(value); } catch { throw new Error("invalid HWLAB_CLOUD_WEB_OPENCODE_URL: must be an absolute http(s) URL"); } if (url.protocol !== "http:" && url.protocol !== "https:") { throw new Error("invalid HWLAB_CLOUD_WEB_OPENCODE_URL: only http(s) URLs are allowed"); } if (url.username || url.password || url.hash) { throw new Error("invalid HWLAB_CLOUD_WEB_OPENCODE_URL: credentials and fragments are not allowed"); } } function opencodeProxyHostFromEnv() { const explicit = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_PROXY_HOST"); if (explicit) return normalizeHostName(explicit); const frameUrl = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_URL"); if (!frameUrl) return ""; try { const url = new URL(frameUrl); return normalizeHostName(url.hostname); } catch { return ""; } } function opencodeEventDirectoryRewriteFromEnv() { const from = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_EVENT_DIRECTORY_FROM"); const to = optionalRuntimeConfigEnv("HWLAB_CLOUD_WEB_OPENCODE_EVENT_DIRECTORY_TO"); if (!from || !to || from === to) return null; return { from, to }; } function opencodeEventDirectoryTransformForTarget(target, rewrite) { if (!rewrite || target?.pathname !== "/global/event") return null; return opencodeEventDirectoryRewriteTransform(rewrite); } function opencodeEventDirectoryRewriteTransform(rewrite) { const stats = { enabled: true, from: rewrite.from, to: rewrite.to, dataLines: 0, rewrittenEvents: 0, jsonErrors: 0 }; let pending = ""; return { stats, write(chunk) { pending += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk ?? ""); const output = []; let newlineIndex = pending.indexOf("\n"); while (newlineIndex >= 0) { const line = pending.slice(0, newlineIndex + 1); pending = pending.slice(newlineIndex + 1); output.push(rewriteOpencodeEventDirectoryLine(line, rewrite, stats)); newlineIndex = pending.indexOf("\n"); } return output; }, end() { if (!pending) return ""; const line = pending; pending = ""; return rewriteOpencodeEventDirectoryLine(line, rewrite, stats); } }; } function rewriteOpencodeEventDirectoryLine(line, rewrite, stats) { const match = String(line).match(/^(data:\s*)(.*?)(\r?\n)?$/u); if (!match) return line; const [, prefix, payload, lineEnding = ""] = match; const text = payload.trim(); if (!text) return line; stats.dataLines += 1; try { const parsed = JSON.parse(text); if (parsed && typeof parsed === "object" && parsed.directory === rewrite.from) { parsed.directory = rewrite.to; stats.rewrittenEvents += 1; return `${prefix}${JSON.stringify(parsed)}${lineEnding}`; } } catch { stats.jsonErrors += 1; } return line; } function validateDisplayTimeZone(timeZone) { try { new Intl.DateTimeFormat("en-US", { timeZone }).format(new Date(0)); } catch { throw new Error(`invalid HWLAB_CLOUD_WEB_DISPLAY_TIME_ZONE: ${timeZone}`); } } function validateDisplayLocale(locale) { try { new Intl.DateTimeFormat(locale).format(new Date(0)); } catch { throw new Error(`invalid HWLAB_CLOUD_WEB_DISPLAY_TIME_LOCALE: ${locale}`); } } function workbenchRuntimeConfigFromEnv() { const traceTimeline = {}; const result = { debugCapabilities: { isolatedKafka: requiredWorkbenchRealtimeFeature(process.env.HWLAB_WORKBENCH_KAFKA_DEBUG_REPLAY_ENABLED, "HWLAB_WORKBENCH_KAFKA_DEBUG_REPLAY_ENABLED"), rawHwlabEventWindow: { enabled: requiredWorkbenchRealtimeFeature(process.env.HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_ENABLED, "HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_ENABLED"), maxEntries: requiredPositiveWorkbenchInteger(process.env.HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_ENTRIES, "HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_ENTRIES"), maxRetainedBytes: requiredPositiveWorkbenchInteger(process.env.HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_RETAINED_BYTES, "HWLAB_WORKBENCH_RAW_HWLAB_EVENT_WINDOW_MAX_RETAINED_BYTES") } } }; const autoExpandRunning = parseEnvBoolean(process.env.HWLAB_WORKBENCH_TRACE_AUTO_EXPAND_RUNNING); const autoCollapseTerminal = parseEnvBoolean(process.env.HWLAB_WORKBENCH_TRACE_AUTO_COLLAPSE_TERMINAL); const traceExplorerUrlTemplate = traceExplorerUrlTemplateFromEnv(process.env.HWLAB_WORKBENCH_TRACE_EXPLORER_URL_TEMPLATE); if (typeof autoExpandRunning === "boolean") traceTimeline.autoExpandRunning = autoExpandRunning; if (typeof autoCollapseTerminal === "boolean") traceTimeline.autoCollapseTerminal = autoCollapseTerminal; if (Object.keys(traceTimeline).length > 0) result.traceTimeline = traceTimeline; if (traceExplorerUrlTemplate) result.traceExplorerUrlTemplate = traceExplorerUrlTemplate; return Object.keys(result).length > 0 ? result : null; } function requiredWorkbenchRealtimeFeature(value, name) { const normalized = String(value ?? "").trim().toLowerCase(); if (normalized === "true" || normalized === "1") return true; if (normalized === "false" || normalized === "0") return false; throw new Error(`${name} is required and must be explicitly true or false`); } function requiredPositiveWorkbenchInteger(value, name) { const number = Number(value); if (!Number.isInteger(number) || number <= 0) throw new Error(`${name} is required and must be a positive integer`); return number; } function traceExplorerUrlTemplateFromEnv(value) { if (value === undefined || value === null || value === "") return null; const template = String(value).trim(); if (!isSafeTraceExplorerUrlTemplate(template)) { throw new Error("invalid HWLAB_WORKBENCH_TRACE_EXPLORER_URL_TEMPLATE: template must be http(s) or same-origin relative URL and contain only {trace_id}"); } return template; } function isSafeTraceExplorerUrlTemplate(template) { if (!template.includes(TRACE_EXPLORER_TEMPLATE_TOKEN)) return false; const placeholders = Array.from(template.matchAll(TRACE_EXPLORER_TEMPLATE_PLACEHOLDER_PATTERN), (match) => match[0]); if (placeholders.length === 0 || placeholders.some((placeholder) => placeholder !== TRACE_EXPLORER_TEMPLATE_TOKEN)) return false; try { const rendered = template.replaceAll(TRACE_EXPLORER_TEMPLATE_TOKEN, "trc_template_probe"); const url = new URL(rendered, "http://hwlab-cloud-web.local"); return (url.protocol === "http:" || url.protocol === "https:") && !template.startsWith("//"); } catch { return false; } } function parseEnvBoolean(value) { if (value === undefined || value === null || value === "") return null; const normalized = String(value).trim().toLowerCase(); if (["1", "true", "yes", "on"].includes(normalized)) return true; if (["0", "false", "no", "off"].includes(normalized)) return false; return null; } function safeInlineJson(value) { return JSON.stringify(value).replace(/ { if (isClientDisconnectError(error)) { logCloudWebRuntimeEvent("cloud-web-client-disconnect", { serviceId, error, source: "uncaughtException" }); return; } logCloudWebRuntimeEvent("cloud-web-runtime-fatal", { serviceId, error, source: "uncaughtException" }); process.exit(1); }); process.on("unhandledRejection", (reason) => { const error = reason instanceof Error ? reason : new Error(String(reason)); if (isClientDisconnectError(error)) { logCloudWebRuntimeEvent("cloud-web-client-disconnect", { serviceId, error, source: "unhandledRejection" }); return; } logCloudWebRuntimeEvent("cloud-web-runtime-fatal", { serviceId, error, source: "unhandledRejection" }); process.exit(1); }); } function attachRequestErrorHandlers({ request, response, serviceId }) { const handle = (error) => { if (isClientDisconnectError(error)) { logCloudWebRuntimeEvent("cloud-web-client-disconnect", { serviceId, request, error }); return; } throw error; }; request.on("error", handle); response.on("error", handle); } export function isClientDisconnectError(error) { const candidate = error instanceof Error ? error : new Error(String(error)); const code = typeof candidate.code === "string" ? candidate.code : ""; return CLIENT_DISCONNECT_ERROR_CODES.has(code) || CLIENT_DISCONNECT_ERROR_PATTERN.test(candidate.message || ""); } function logCloudWebRuntimeEvent(event, { serviceId, request = null, error = null, source = null, traceContext = null, details = null } = {}) { const payload = { event, serviceId: serviceId || "hwlab-cloud-web", source, method: request?.method || null, path: request?.url || null, traceId: traceContext?.traceId || null, requestId: traceContext?.requestId || null, error: error ? { name: error.name || "Error", code: typeof error.code === "string" ? error.code : null, message: error.message || String(error) } : null, details: details && typeof details === "object" ? details : null, observedAt: new Date().toISOString() }; process.stderr.write(JSON.stringify(payload) + "\n"); } export async function handleCloudWebAuth(options) { const { request, response, url } = options; if (url.pathname === "/auth/bootstrap" && request.method === "GET") { await handleAuthBootstrap(options); return true; } if (url.pathname === "/auth/session" && request.method === "GET") { await proxyCloudApi(options); return true; } if (url.pathname === "/auth/login" && request.method === "POST") { await proxyCloudApi(options); return true; } if (url.pathname === "/auth/register" && request.method === "POST") { await proxyCloudApi(options); return true; } if (url.pathname === "/auth/logout" && request.method === "POST") { await proxyCloudApi(options); return true; } if (!url.pathname.startsWith("/auth/")) return false; await proxyCloudApi(options); return true; } async function handleAuthBootstrap(options) { const { request, response, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson } = options; if (!cloudApiBaseUrl) { sendJson(response, 503, { error: "cloud_api_base_url_missing", serviceId, path: "/auth/bootstrap" }); return; } const auth = await resolveAuthBootstrap({ request, cloudApiBaseUrl, cloudApiProxyTimeoutMs }); sendJsonWithForwardedCookies(response, auth.statusCode || 502, auth.body ?? { authenticated: false, error: "auth_bootstrap_failed" }, auth.headers); } async function resolveAuthBootstrap({ request, cloudApiBaseUrl, cloudApiProxyTimeoutMs }) { const session = await requestCloudApiJson({ request, method: "GET", pathname: "/auth/session", cloudApiBaseUrl, cloudApiProxyTimeoutMs }); return withCookieHeader(request, session); } function requestCloudApiJson({ request, method, pathname, body = "", headers = {}, cloudApiBaseUrl, cloudApiProxyTimeoutMs }) { return new Promise((resolve) => { let settled = false; const target = new URL(pathname, cloudApiBaseUrl); const proxyRequest = { method, headers: { ...upstreamRequestHeaders({ headers: request.headers }, body), ...headers } }; const upstream = httpRequest(target, { method, headers: proxyRequest.headers }, (upstreamResponse) => { const chunks = []; upstreamResponse.on("data", (chunk) => chunks.push(chunk)); upstreamResponse.on("end", () => { if (settled) return; settled = true; const rawBody = Buffer.concat(chunks).toString("utf8"); let parsed = null; try { parsed = rawBody ? JSON.parse(rawBody) : null; } catch { parsed = { error: "invalid_json", rawBody: rawBody.slice(0, 200) }; } resolve({ ok: (upstreamResponse.statusCode || 0) >= 200 && (upstreamResponse.statusCode || 0) < 300, statusCode: upstreamResponse.statusCode || 502, body: parsed, headers: upstreamResponse.headers }); }); upstreamResponse.on("error", (error) => { if (settled) return; settled = true; resolve({ ok: false, statusCode: 502, body: { authenticated: false, error: error.message }, headers: {} }); }); }); const timeout = setTimeout(() => { if (settled) return; settled = true; upstream.destroy(); resolve({ ok: false, statusCode: 504, body: { authenticated: false, error: "auth_bootstrap_timeout" }, headers: {} }); }, cloudApiProxyTimeoutMs); upstream.on("error", (error) => { if (settled) return; settled = true; clearTimeout(timeout); resolve({ ok: false, statusCode: 502, body: { authenticated: false, error: error.message }, headers: {} }); }); upstream.on("close", () => clearTimeout(timeout)); if (body) upstream.write(body); upstream.end(); }); } function withCookieHeader(request, result) { return { ...result, cookieHeader: cookieHeaderForUpstream(request.headers.cookie, result.headers?.["set-cookie"]) }; } function cookieHeaderForUpstream(existingCookie, setCookie) { const pairs = new Map(); for (const pair of cookiePairsFromHeader(existingCookie)) pairs.set(pair.name, pair.value); for (const pair of cookiePairsFromSetCookie(setCookie)) pairs.set(pair.name, pair.value); return Array.from(pairs.entries()).map(([name, value]) => `${name}=${value}`).join("; "); } function cookiePairsFromHeader(header) { if (!header) return []; return String(header).split(";").map((part) => cookiePair(part)).filter(Boolean); } function hasCookieHeader(header) { return cookiePairsFromHeader(header).length > 0; } function cookiePairsFromSetCookie(setCookie) { const values = Array.isArray(setCookie) ? setCookie : setCookie ? [setCookie] : []; return values.map((value) => cookiePair(String(value).split(";")[0])).filter(Boolean); } function cookiePair(value) { const index = String(value).indexOf("="); if (index <= 0) return null; const name = String(value).slice(0, index).trim(); const cookieValue = String(value).slice(index + 1).trim(); if (!name || !cookieValue) return null; return { name, value: cookieValue }; } function sendJsonWithForwardedCookies(response, statusCode, body, headers = {}) { const payload = JSON.stringify(body); const setCookie = headers["set-cookie"]; const responseHeaders = { "content-type": "application/json", "content-length": Buffer.byteLength(payload) }; if (setCookie) responseHeaders["set-cookie"] = setCookie; response.writeHead(statusCode, responseHeaders); response.end(payload); } async function readRequestBody(request) { let body = ""; for await (const chunk of request) { body += chunk; } return body; } function parseTimeout(value, fallback, { min, max }) { const parsed = Number.parseInt(value || "", 10); if (!Number.isInteger(parsed)) return fallback; return Math.min(Math.max(parsed, min), max); } function requestUpstream({ target, request, response, body, url, cloudApiProxyTimeoutMs }) { return proxyCloudApiRequest({ target, request, response, body, timeoutMs: cloudApiProxyTimeoutMs, forceStream: isCloudWebSseRoute(url.pathname) }); } export async function proxyCloudApi({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, serviceId, sendJson }) { if (!cloudApiBaseUrl) { sendJson(response, 503, { error: "cloud_api_base_url_missing", serviceId, path: url.pathname }); return; } let body = ""; if (request.method !== "GET") { body = await readRequestBody(request); } const target = new URL(url.pathname + url.search, cloudApiBaseUrl); try { await requestUpstream({ target, request, response, body: request.method === "GET" ? "" : body, url, cloudApiProxyTimeoutMs }); } catch (error) { if (response.headersSent || response.writableEnded) { if (!response.writableEnded) response.destroy(error); return; } const timedOut = error.timedOut === true || /timed out after \d+ms/iu.test(error.message); const code = UPSTREAM_UNAVAILABLE_ERROR_CODE; const category = timedOut ? "timeout" : "proxy"; const traceId = request.headers["x-trace-id"] || null; const userMessage = UPSTREAM_UNAVAILABLE_MESSAGE; sendJson(response, 502, { status: "failed", error: { code, layer: "proxy", category, retryable: true, userMessage, message: userMessage, timeoutMs: timedOut ? cloudApiProxyTimeoutMs : null, traceId, route: url.pathname, blocker: { code, layer: "proxy", category, retryable: true, summary: userMessage, userMessage, traceId, route: url.pathname } }, serviceId, target: target.href.replace(/\/\/[^/@]*@/u, "//***@"), traceId, reason: userMessage }); } } async function proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, opencodeUpstreamUrl, opencodeProxyTimeoutMs, opencodeUsername, opencodePassword, opencodeDefaultProject = "", opencodeEventDirectoryRewrite = null, serviceId, sendJson }) { const traceContext = cloudWebTraceContext(request); const startedAtMs = Date.now(); if (!cloudApiBaseUrl) { emitOpencodeProxySpanAsync({ traceContext, serviceId, request, url, startedAtMs, statusCode: 503, errorCode: "cloud_api_base_url_missing" }); sendJsonWithTraceContext(response, 503, { error: "cloud_api_base_url_missing", serviceId, path: url.pathname, diagnostic: opencodeTraceDiagnostic(traceContext) }, traceContext, sendJson); return; } if (!opencodeUpstreamUrl) { emitOpencodeProxySpanAsync({ traceContext, serviceId, request, url, startedAtMs, statusCode: 503, errorCode: "opencode_upstream_url_missing" }); sendJsonWithTraceContext(response, 503, { error: "opencode_upstream_url_missing", serviceId, path: url.pathname, diagnostic: opencodeTraceDiagnostic(traceContext) }, traceContext, sendJson); return; } const authorization = opencodeBasicAuthorization(opencodeUsername, opencodePassword); if (!authorization) { emitOpencodeProxySpanAsync({ traceContext, serviceId, request, url, startedAtMs, statusCode: 503, errorCode: "opencode_credentials_missing" }); sendJsonWithTraceContext(response, 503, { error: "opencode_credentials_missing", serviceId, path: url.pathname, diagnostic: opencodeTraceDiagnostic(traceContext) }, traceContext, sendJson); return; } const ticketAuth = opencodeTicketAuthRequest(request, url); const session = await requestCloudApiJson({ request: ticketAuth.request, method: "GET", pathname: "/auth/session", headers: cloudWebTraceHeaders(traceContext, serviceId), cloudApiBaseUrl, cloudApiProxyTimeoutMs }); if (!session.ok || session.body?.authenticated !== true) { logCloudWebRuntimeEvent("opencode_auth_required", { serviceId, request, traceContext, details: { route: url.pathname, sessionStatusCode: session.statusCode || 0, sessionAuthenticated: session.body?.authenticated === true, ticketPresent: ticketAuth.ticketPresent, ticketAccepted: Boolean(ticketAuth.ticket), valuesPrinted: false } }); emitOpencodeProxySpanAsync({ traceContext, serviceId, request, url, startedAtMs, statusCode: session.statusCode === 403 ? 403 : 401, errorCode: "opencode_auth_required", ticketAuth, sessionStatusCode: session.statusCode || 0 }); sendJsonWithTraceContext(response, session.statusCode === 403 ? 403 : 401, { authenticated: false, error: "opencode_auth_required", serviceId, path: url.pathname, diagnostic: opencodeTraceDiagnostic(traceContext, { sessionStatusCode: session.statusCode || 0 }) }, traceContext, sendJson); return; } let body = ""; if (request.method !== "GET" && request.method !== "HEAD") { body = await readRequestBody(request); } const target = opencodeTargetUrl(url, opencodeUpstreamUrl); const streamTransform = opencodeEventDirectoryTransformForTarget(target, opencodeEventDirectoryRewrite); if (streamTransform?.stats?.enabled) { emitOpencodeProxyStreamStartSpanAsync({ traceContext, serviceId, request, url, target, ticketAuth, sessionStatusCode: session.statusCode || 0, streamTransformStats: streamTransform.stats }); } const extraResponseHeaders = { ...cloudWebTraceHeaders(traceContext, serviceId), ...(ticketAuth.ticket ? { "set-cookie": opencodeTicketSetCookie(ticketAuth.ticket) } : {}) }; const upstreamRequest = { method: request.method, headers: opencodeUpstreamHeaders({ headers: { ...request.headers, ...cloudWebTraceHeaders(traceContext, serviceId) } }, body, authorization) }; try { let proxyResult; if (shouldServeOpencodeDefaultProjectBootstrapScript({ request, target, opencodeDefaultProject })) { proxyResult = sendOpencodeDefaultProjectBootstrapScript({ response, extraResponseHeaders, defaultProject: opencodeDefaultProject }); } else if (shouldInjectOpencodeDefaultProject({ request, target, opencodeDefaultProject })) { proxyResult = await proxyOpencodeHtmlRequest({ target, request: upstreamRequest, response, timeoutMs: opencodeProxyTimeoutMs, extraResponseHeaders, defaultProject: opencodeDefaultProject }); } else { proxyResult = await proxyCloudApiRequest({ target, request: upstreamRequest, response, body: request.method === "GET" || request.method === "HEAD" ? "" : body, timeoutMs: opencodeProxyTimeoutMs, extraResponseHeaders, streamTransform }); } emitOpencodeProxySpanAsync({ traceContext, serviceId, request, url, target, startedAtMs, statusCode: proxyResult?.statusCode || 0, errorCode: proxyResult?.errorCode || "", ticketAuth, sessionStatusCode: session.statusCode || 0, streaming: proxyResult?.streaming === true, streamTransformStats: proxyResult?.streamTransformStats }); } catch (error) { if (response.headersSent || response.writableEnded) { if (!response.writableEnded) response.destroy(error); return; } emitOpencodeProxySpanAsync({ traceContext, serviceId, request, url, target, startedAtMs, statusCode: 502, errorCode: error?.timedOut ? "opencode_proxy_timeout" : "opencode_proxy_unavailable", ticketAuth, sessionStatusCode: session.statusCode || 0 }); sendJsonWithTraceContext(response, 502, { status: "failed", error: "opencode_proxy_unavailable", serviceId, path: url.pathname, reason: UPSTREAM_UNAVAILABLE_MESSAGE, diagnostic: opencodeTraceDiagnostic(traceContext) }, traceContext, sendJson); } } function isOpencodeProxyRequest(request, url, opencodeProxyHost) { if (url.pathname === OPENCODE_PROXY_PREFIX || url.pathname.startsWith(`${OPENCODE_PROXY_PREFIX}/`)) return true; const targetHost = normalizeHostName(opencodeProxyHost); if (!targetHost) return false; return requestHostName(request) === targetHost; } function opencodeTargetUrl(url, opencodeUpstreamUrl) { const pathname = url.pathname === OPENCODE_PROXY_PREFIX ? "/" : url.pathname.startsWith(`${OPENCODE_PROXY_PREFIX}/`) ? url.pathname.slice(OPENCODE_PROXY_PREFIX.length) || "/" : url.pathname; const target = new URL(`${pathname}${url.search}`, opencodeUpstreamUrl); target.searchParams.delete(OPENCODE_TICKET_QUERY); return target; } function shouldInjectOpencodeDefaultProject({ request, target, opencodeDefaultProject }) { if (!opencodeDefaultProject) return false; if (request.method !== "GET") return false; const accept = String(request.headers.accept || ""); if (accept && !accept.includes("text/html") && !accept.includes("*/*")) return false; if (target.pathname.startsWith("/assets/")) return false; if (STATIC_ASSET_EXTENSION_PATTERN.test(target.pathname)) return false; return true; } function shouldServeOpencodeDefaultProjectBootstrapScript({ request, target, opencodeDefaultProject }) { return Boolean(opencodeDefaultProject) && request.method === "GET" && target.pathname === OPENCODE_DEFAULT_PROJECT_BOOTSTRAP_SCRIPT_PATH; } function sendOpencodeDefaultProjectBootstrapScript({ response, extraResponseHeaders, defaultProject }) { const outputBody = Buffer.from(opencodeDefaultProjectBootstrapJavaScript(defaultProject), "utf8"); response.writeHead(200, mergeOpencodeProxyResponseHeaders({ "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store", "content-length": outputBody.length }, extraResponseHeaders)); response.end(outputBody); return { statusCode: 200, streaming: false, bodyBytes: outputBody.length, opencodeDefaultProjectBootstrapScript: true }; } async function proxyOpencodeHtmlRequest({ target, request, response, timeoutMs, extraResponseHeaders, defaultProject }) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const upstream = await fetch(target, { method: request.method, headers: request.headers, signal: controller.signal }); const upstreamHeaders = Object.fromEntries(upstream.headers.entries()); const contentType = String(upstream.headers.get("content-type") || ""); const inputBody = Buffer.from(await upstream.arrayBuffer()); const outputBody = /^text\/html(?:\s*;|$)/iu.test(contentType) ? Buffer.from(opencodeInjectDefaultProjectHtml(inputBody.toString("utf8"), defaultProject), "utf8") : inputBody; response.writeHead(upstream.status, { ...mergeOpencodeProxyResponseHeaders(opencodeDecodedFetchResponseHeaders(upstreamHeaders), extraResponseHeaders), "content-length": outputBody.length }); response.end(outputBody); return { statusCode: upstream.status, streaming: false, bodyBytes: outputBody.length, opencodeDefaultProjectInjected: outputBody.length !== inputBody.length }; } catch (error) { if (error?.name === "AbortError") error.timedOut = true; throw error; } finally { clearTimeout(timeout); } } function opencodeInjectDefaultProjectHtml(html, defaultProject) { if (!html || html.includes("data-hwlab-opencode-default-project")) return html; const script = opencodeDefaultProjectBootstrapScriptTag(); if (html.includes("")) return html.replace("", `${script}`); if (html.includes("")) return html.replace("", `${script}`); return `${script}${html}`; } function opencodeDefaultProjectBootstrapScriptTag() { return ``; } function opencodeDefaultProjectBootstrapJavaScript(defaultProject) { return `(function(){try{var project=${safeInlineJson(defaultProject)};if(!project)return;var serverKey="opencode.global.dat:server";var server=JSON.parse(localStorage.getItem(serverKey)||"{}");var projects=server.projects&&typeof server.projects==="object"?server.projects:{};var local=Array.isArray(projects.local)?projects.local.slice():[];var found=false;local=local.map(function(item){if(!item||item.worktree!==project)return item;found=true;return Object.assign({},item,{expanded:true});});if(!found)local.unshift({worktree:project,expanded:true});projects.local=local;server.projects=projects;server.lastProject=Object.assign({},server.lastProject||{},{local:project});localStorage.setItem(serverKey,JSON.stringify(server));var layoutKey="opencode.global.dat:layout.page";var layout=JSON.parse(localStorage.getItem(layoutKey)||"{}");layout.workspaceExpanded=Object.assign({},layout.workspaceExpanded||{});layout.workspaceExpanded[project]=true;localStorage.setItem(layoutKey,JSON.stringify(layout));}catch(error){console.warn("HWLAB OpenCode default project bootstrap failed",error);}})();\n`; } function opencodeDecodedFetchResponseHeaders(headers = {}) { const result = copyProxyResponseHeaders(headers); for (const key of Object.keys(result)) { const lowerKey = key.toLowerCase(); if (lowerKey === "content-encoding" || lowerKey === "content-md5" || lowerKey === "etag") delete result[key]; } return result; } function mergeOpencodeProxyResponseHeaders(baseHeaders = {}, extraHeaders = {}) { const result = { ...baseHeaders }; for (const [key, value] of Object.entries(extraHeaders)) { if (value === undefined || value === null || value === "") continue; const existingKey = Object.keys(result).find((item) => item.toLowerCase() === key.toLowerCase()); if (existingKey && existingKey.toLowerCase() === "set-cookie") { const existing = Array.isArray(result[existingKey]) ? result[existingKey] : [result[existingKey]]; const next = Array.isArray(value) ? value : [value]; result[existingKey] = [...existing.filter(Boolean), ...next.filter(Boolean)]; continue; } result[existingKey || key] = value; } return result; } function issueOpencodeTicket(cookieHeader) { pruneOpencodeTickets(); const ticket = randomBytes(24).toString("base64url"); opencodeTickets.set(ticket, { cookieHeader: String(cookieHeader || ""), expiresAtMs: Date.now() + OPENCODE_TICKET_TTL_MS }); return ticket; } function opencodeTicketAuthRequest(request, url) { const ticket = opencodeTicketFromRequest(request, url); const record = ticket ? opencodeTicketRecord(ticket) : null; if (!record) return { request, ticket: "", ticketPresent: Boolean(ticket) }; return { request: { headers: { ...request.headers, cookie: record.cookieHeader } }, ticket, ticketPresent: true }; } function opencodeTicketFromRequest(request, url) { const fromQuery = url.searchParams.get(OPENCODE_TICKET_QUERY) || ""; if (fromQuery) return fromQuery; return cookiePairsFromHeader(request.headers.cookie).find((pair) => pair.name === OPENCODE_TICKET_COOKIE)?.value || ""; } function opencodeTicketRecord(ticket) { pruneOpencodeTickets(); const record = opencodeTickets.get(String(ticket || "")); if (!record || record.expiresAtMs <= Date.now() || !record.cookieHeader) { opencodeTickets.delete(String(ticket || "")); return null; } return record; } function pruneOpencodeTickets() { const now = Date.now(); for (const [ticket, record] of opencodeTickets.entries()) { if (!record || record.expiresAtMs <= now) opencodeTickets.delete(ticket); } } function opencodeTicketSetCookie(ticket) { return `${OPENCODE_TICKET_COOKIE}=${encodeURIComponent(ticket)}; Path=/; HttpOnly; Secure; SameSite=None; Max-Age=${OPENCODE_TICKET_MAX_AGE_SECONDS}`; } function opencodeUpstreamHeaders(request, body, authorization) { const headers = { accept: request.headers.accept || "*/*", authorization }; for (const name of ["content-type", "prefer", "traceparent", "tracestate", "x-hwlab-otel-trace-id", "x-trace-id", "x-request-id"]) { if (request.headers[name] !== undefined) headers[name] = request.headers[name]; } if (body) headers["content-length"] = Buffer.byteLength(body); return headers; } function opencodeBasicAuthorization(username, password) { if (!username || !password) return ""; return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`; } function cloudWebTraceContext(request) { const parsed = parseOtelTraceparent(request?.headers?.traceparent); const explicitTraceId = normalizedOtelTraceId(request?.headers?.["x-hwlab-otel-trace-id"]); const traceId = parsed?.traceId || explicitTraceId || randomOtelTraceId(); const spanId = randomOtelSpanId(); return { traceId, parentSpanId: parsed?.spanId || null, spanId, traceparent: `00-${traceId}-${spanId}-01`, requestId: safeRequestId(request?.headers?.["x-request-id"]) || `req_${randomBytes(16).toString("hex")}`, valuesPrinted: false }; } function cloudWebTraceHeaders(traceContext, serviceId = "hwlab-cloud-web") { if (!traceContext?.traceId || !traceContext?.traceparent) return {}; return { traceparent: traceContext.traceparent, "x-hwlab-otel-trace-id": traceContext.traceId, "x-request-id": traceContext.requestId, "x-source-service-id": serviceId || "hwlab-cloud-web" }; } function sendJsonWithTraceContext(response, statusCode, body, traceContext, sendJson) { for (const [name, value] of Object.entries(cloudWebTraceHeaders(traceContext))) { if (value !== undefined && value !== null && value !== "") response.setHeader(name, value); } sendJson(response, statusCode, body); } function opencodeTraceDiagnostic(traceContext, extra = {}) { return { traceId: traceContext?.traceId || null, requestId: traceContext?.requestId || null, traceparent: traceContext?.traceparent || null, valuesPrinted: false, ...extra }; } function emitOpencodeProxySpanAsync({ traceContext, serviceId, request, url, target = null, startedAtMs, statusCode = 0, errorCode = "", ticketAuth = null, sessionStatusCode = 0, streaming = false, streamTransformStats = null } = {}) { const endpoint = otelTracesEndpoint(process.env); if (!endpoint || !traceContext?.traceId || !traceContext?.spanId) return; const endedAtMs = Date.now(); const route = opencodeProxyRoute(url?.pathname || ""); const span = { traceId: traceContext.traceId, spanId: traceContext.spanId, parentSpanId: traceContext.parentSpanId || "", name: "opencode.proxy.request", kind: 2, startTimeMs: startedAtMs || endedAtMs, endTimeMs: endedAtMs, statusCode, errorCode, attributes: { "http.request.method": request?.method || "GET", "http.route": route, "http.response.status_code": statusCode || undefined, "http.response.status_class": statusCode ? `${Math.floor(statusCode / 100)}xx` : "unknown", "url.scheme": target?.protocol ? target.protocol.replace(":", "") : undefined, "server.address": target?.hostname || undefined, "server.port": target ? Number(target.port || (target.protocol === "https:" ? 443 : 80)) : undefined, "opencode.proxy.duration_ms": Math.max(0, endedAtMs - (startedAtMs || endedAtMs)), "opencode.proxy.ticket_present": ticketAuth?.ticketPresent === true, "opencode.proxy.ticket_accepted": Boolean(ticketAuth?.ticket), "opencode.proxy.session_status_code": sessionStatusCode || undefined, "opencode.proxy.streaming": streaming === true, ...opencodeStreamTransformStatsAttributes(streamTransformStats), valuesPrinted: false } }; setTimeout(() => { emitOtelSpan(endpoint, span, serviceId || "hwlab-cloud-web", "hwlab.cloud_web.opencode_proxy").catch((error) => { process.stderr.write(JSON.stringify({ event: "cloud-web-opencode-otel-emit-failed", serviceId: serviceId || "hwlab-cloud-web", traceId: traceContext.traceId, requestId: traceContext.requestId, error: error instanceof Error ? error.message : String(error), valuesPrinted: false }) + "\n"); }); }, 0); } function opencodeStreamTransformStatsAttributes(stats) { if (!stats?.enabled) return {}; return { "opencode.proxy.sse.directory_rewrite_enabled": true, "opencode.proxy.sse.directory_rewrite_from": stats.from, "opencode.proxy.sse.directory_rewrite_to": stats.to, "opencode.proxy.sse.directory_rewrite_data_lines": stats.dataLines, "opencode.proxy.sse.directory_rewrite_events": stats.rewrittenEvents, "opencode.proxy.sse.directory_rewrite_json_errors": stats.jsonErrors }; } function emitOpencodeProxyStreamStartSpanAsync({ traceContext, serviceId, request, url, target = null, ticketAuth = null, sessionStatusCode = 0, streamTransformStats = null } = {}) { const endpoint = otelTracesEndpoint(process.env); if (!endpoint || !traceContext?.traceId) return; const observedAtMs = Date.now(); const span = { traceId: traceContext.traceId, spanId: randomOtelSpanId(), parentSpanId: traceContext.spanId || traceContext.parentSpanId || "", name: "opencode.proxy.stream.start", kind: 2, startTimeMs: observedAtMs, endTimeMs: observedAtMs, statusCode: 0, errorCode: "", attributes: { "http.request.method": request?.method || "GET", "http.route": opencodeProxyRoute(url?.pathname || ""), "url.scheme": target?.protocol ? target.protocol.replace(":", "") : undefined, "server.address": target?.hostname || undefined, "server.port": target ? Number(target.port || (target.protocol === "https:" ? 443 : 80)) : undefined, "opencode.proxy.phase": "stream_start", "opencode.proxy.ticket_present": ticketAuth?.ticketPresent === true, "opencode.proxy.ticket_accepted": Boolean(ticketAuth?.ticket), "opencode.proxy.session_status_code": sessionStatusCode || undefined, "opencode.proxy.streaming": true, ...opencodeStreamTransformStatsAttributes(streamTransformStats), valuesPrinted: false } }; setTimeout(() => { emitOtelSpan(endpoint, span, serviceId || "hwlab-cloud-web", "hwlab.cloud_web.opencode_proxy").catch((error) => { process.stderr.write(JSON.stringify({ event: "cloud-web-opencode-otel-emit-failed", serviceId: serviceId || "hwlab-cloud-web", traceId: traceContext.traceId, requestId: traceContext.requestId, span: "opencode.proxy.stream.start", error: error instanceof Error ? error.message : String(error), valuesPrinted: false }) + "\n"); }); }, 0); } function opencodeProxyRoute(pathname) { const path = String(pathname || "/"); if (path === "/global/event") return "/global/event"; if (path === "/project" || path === "/project/current" || path === "/project/git/init") return path; if (path === "/provider" || path === "/path" || path === "/global/health") return path; if (/^\/session\/[^/]+\/prompt_async$/u.test(path)) return "/session/:sessionID/prompt_async"; if (/^\/session\/[^/]+\/message$/u.test(path)) return "/session/:sessionID/message"; if (/^\/session\/[^/]+\/abort$/u.test(path)) return "/session/:sessionID/abort"; if (/^\/session\/[^/]+(?:\/.*)?$/u.test(path)) return "/session/:sessionID/*"; if (path.startsWith("/assets/")) return "/assets/*"; return path; } async function emitOtelSpan(endpoint, span, serviceId, scopeName) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 1500); try { await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(otelPayload(span, serviceId, scopeName)), signal: controller.signal }); } finally { clearTimeout(timeout); } } function otelPayload(span, serviceId, scopeName) { return { resourceSpans: [{ resource: { attributes: otelAttributes({ "service.name": serviceId, "deployment.environment": process.env.HWLAB_ENVIRONMENT || "v03" }) }, scopeSpans: [{ scope: { name: scopeName, version: "1" }, spans: [{ traceId: span.traceId, spanId: span.spanId, parentSpanId: span.parentSpanId || undefined, name: span.name, kind: span.kind, startTimeUnixNano: unixNano(span.startTimeMs), endTimeUnixNano: unixNano(span.endTimeMs), attributes: otelAttributes({ ...span.attributes, "otel.trace_id": span.traceId }), status: span.errorCode || (span.statusCode && span.statusCode >= 500) ? { code: 2, message: span.errorCode || `http_${span.statusCode}` } : { code: 1 } }] }] }] }; } function otelAttributes(values) { const result = []; for (const [key, value] of Object.entries(values || {})) { if (value === undefined || value === null || value === "") continue; if (typeof value === "number" && Number.isFinite(value)) result.push({ key, value: Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value } }); else if (typeof value === "boolean") result.push({ key, value: { boolValue: value } }); else result.push({ key, value: { stringValue: String(value) } }); } return result; } function unixNano(ms) { return String(BigInt(Math.max(0, Math.trunc(ms))) * 1000000n); } function otelTracesEndpoint(env) { const explicit = firstNonEmpty(env.HWLAB_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT); if (explicit) return explicit; const base = firstNonEmpty(env.HWLAB_OTEL_EXPORTER_OTLP_ENDPOINT, env.OTEL_EXPORTER_OTLP_ENDPOINT); return base ? `${base.replace(/\/+$/u, "")}/v1/traces` : ""; } function firstNonEmpty(...values) { for (const value of values) { const text = String(value || "").trim(); if (text) return text; } return ""; } function parseOtelTraceparent(value) { const match = firstHeaderValue(value).trim().match(OTEL_TRACEPARENT_PATTERN); if (!match) return null; const traceId = normalizedOtelTraceId(match[1]); const spanId = normalizedOtelSpanId(match[2]); return traceId && spanId ? { traceId, spanId } : null; } function normalizedOtelTraceId(value) { const text = firstHeaderValue(value).trim().toLowerCase(); return /^[0-9a-f]{32}$/u.test(text) && text !== ZERO_OTEL_TRACE_ID ? text : ""; } function normalizedOtelSpanId(value) { const text = firstHeaderValue(value).trim().toLowerCase(); return /^[0-9a-f]{16}$/u.test(text) && text !== ZERO_OTEL_SPAN_ID ? text : ""; } function randomOtelTraceId() { const value = randomBytes(16).toString("hex"); return value === ZERO_OTEL_TRACE_ID ? "00000000000000000000000000000001" : value; } function randomOtelSpanId() { const value = randomBytes(8).toString("hex"); return value === ZERO_OTEL_SPAN_ID ? "0000000000000001" : value; } function safeRequestId(value) { const text = firstHeaderValue(value).trim(); return SAFE_REQUEST_ID_PATTERN.test(text) ? text : ""; } function firstHeaderValue(value) { if (Array.isArray(value)) return String(value[0] ?? ""); return String(value ?? ""); } function requestHostName(request) { return normalizeHostName(String(request.headers.host || "").split(":")[0]); } function normalizeHostName(value) { return String(value || "").trim().toLowerCase().replace(/\.+$/u, ""); } function contentType(filePath) { if (filePath.endsWith(".html")) return "text/html; charset=utf-8"; if (filePath.endsWith(".css")) return "text/css; charset=utf-8"; if (filePath.endsWith(".mjs") || filePath.endsWith(".js")) return "text/javascript; charset=utf-8"; if (filePath.endsWith(".svg")) return "image/svg+xml; charset=utf-8"; if (filePath.endsWith(".md")) return "text/markdown; charset=utf-8"; if (filePath.endsWith(".json")) return "application/json; charset=utf-8"; return "application/octet-stream"; } async function encodeStaticBody(request, body, type) { if (!isGzipCandidate(type, body.length) || !acceptsEncoding(request.headers["accept-encoding"], "gzip")) { return { body, headers: isCompressibleType(type) ? { "vary": "Accept-Encoding" } : {} }; } const compressed = await gzipAsync(body, { level: 6 }); return { body: compressed, headers: { "content-encoding": "gzip", "vary": "Accept-Encoding" } }; } function isGzipCandidate(type, sizeBytes) { return sizeBytes >= GZIP_MIN_BYTES && isCompressibleType(type); } function isCompressibleType(type) { return /^(?:text\/|application\/json\b)/u.test(String(type || "")); } function acceptsEncoding(header, encoding) { const value = Array.isArray(header) ? header.join(",") : String(header || ""); for (const segment of value.split(",")) { const [name, ...params] = segment.trim().split(";"); const token = name.trim().toLowerCase(); if (token !== encoding && token !== "*") continue; const q = params.map((param) => param.trim().toLowerCase()).find((param) => param.startsWith("q=")); if (q && Number.parseFloat(q.slice(2)) <= 0) continue; return true; } return false; }