fix: make opencode iframe auth traceable

This commit is contained in:
UniDesk Codex
2026-06-30 09:32:26 +08:00
parent 8423316ebe
commit 4e2aae4d4d
3 changed files with 482 additions and 25 deletions
+208 -18
View File
@@ -20,10 +20,15 @@ const CLOUD_WEB_ERROR_HANDLER_INSTALLED = Symbol.for("hwlab.cloud-web.error-hand
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_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();
@@ -68,6 +73,10 @@ export function createCloudWebServer({
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, serviceId, sendJson });
return;
@@ -164,12 +173,83 @@ async function opencodeRuntimeConfigFromEnv(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 session = await requestCloudApiJson({ request, method: "GET", pathname: "/auth/session", cloudApiBaseUrl, cloudApiProxyTimeoutMs });
if (!session.ok || session.body?.authenticated !== true) 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 target.toString();
return { ok: true, url: target.toString() };
}
function displayTimeConfigFromEnv() {
@@ -345,13 +425,15 @@ export function isClientDisconnectError(error) {
return CLIENT_DISCONNECT_ERROR_CODES.has(code) || CLIENT_DISCONNECT_ERROR_PATTERN.test(candidate.message || "");
}
function logCloudWebRuntimeEvent(event, { serviceId, request = null, error = null, source = null } = {}) {
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",
@@ -359,6 +441,7 @@ function logCloudWebRuntimeEvent(event, { serviceId, request = null, error = nul
message: error.message || String(error)
}
: null,
details: details && typeof details === "object" ? details : null,
observedAt: new Date().toISOString()
};
process.stderr.write(JSON.stringify(payload) + "\n");
@@ -581,29 +664,51 @@ export async function proxyCloudApi({ request, response, url, cloudApiBaseUrl, c
}
async function proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, cloudApiProxyTimeoutMs, opencodeUpstreamUrl, opencodeProxyTimeoutMs, opencodeUsername, opencodePassword, serviceId, sendJson }) {
const traceContext = cloudWebTraceContext(request);
if (!cloudApiBaseUrl) {
sendJson(response, 503, { error: "cloud_api_base_url_missing", serviceId, path: url.pathname });
sendJsonWithTraceContext(response, 503, { error: "cloud_api_base_url_missing", serviceId, path: url.pathname, diagnostic: opencodeTraceDiagnostic(traceContext) }, traceContext, sendJson);
return;
}
if (!opencodeUpstreamUrl) {
sendJson(response, 503, { error: "opencode_upstream_url_missing", serviceId, path: url.pathname });
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) {
sendJson(response, 503, { error: "opencode_credentials_missing", serviceId, path: url.pathname });
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", cloudApiBaseUrl, cloudApiProxyTimeoutMs });
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) {
sendJson(response, session.statusCode === 403 ? 403 : 401, {
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
}
});
sendJsonWithTraceContext(response, session.statusCode === 403 ? 403 : 401, {
authenticated: false,
error: "opencode_auth_required",
serviceId,
path: url.pathname
});
path: url.pathname,
diagnostic: opencodeTraceDiagnostic(traceContext, { sessionStatusCode: session.statusCode || 0 })
}, traceContext, sendJson);
return;
}
@@ -613,10 +718,13 @@ async function proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, c
}
const target = opencodeTargetUrl(url, opencodeUpstreamUrl);
const extraResponseHeaders = ticketAuth.ticket ? { "set-cookie": opencodeTicketSetCookie(ticketAuth.ticket) } : {};
const extraResponseHeaders = {
...cloudWebTraceHeaders(traceContext, serviceId),
...(ticketAuth.ticket ? { "set-cookie": opencodeTicketSetCookie(ticketAuth.ticket) } : {})
};
const upstreamRequest = {
method: request.method,
headers: opencodeUpstreamHeaders(request, body, authorization)
headers: opencodeUpstreamHeaders({ headers: { ...request.headers, ...cloudWebTraceHeaders(traceContext, serviceId) } }, body, authorization)
};
try {
await proxyCloudApiRequest({
@@ -632,13 +740,14 @@ async function proxyOpencodeRequest({ request, response, url, cloudApiBaseUrl, c
if (!response.writableEnded) response.destroy(error);
return;
}
sendJson(response, 502, {
sendJsonWithTraceContext(response, 502, {
status: "failed",
error: "opencode_proxy_unavailable",
serviceId,
path: url.pathname,
reason: UPSTREAM_UNAVAILABLE_MESSAGE
});
reason: UPSTREAM_UNAVAILABLE_MESSAGE,
diagnostic: opencodeTraceDiagnostic(traceContext)
}, traceContext, sendJson);
}
}
@@ -673,10 +782,11 @@ function issueOpencodeTicket(cookieHeader) {
function opencodeTicketAuthRequest(request, url) {
const ticket = opencodeTicketFromRequest(request, url);
const record = ticket ? opencodeTicketRecord(ticket) : null;
if (!record) return { request, ticket: "" };
if (!record) return { request, ticket: "", ticketPresent: Boolean(ticket) };
return {
request: { headers: { ...request.headers, cookie: record.cookieHeader } },
ticket
ticket,
ticketPresent: true
};
}
@@ -724,6 +834,86 @@ function opencodeBasicAuthorization(username, password) {
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 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]);
}