Files
pikasTech-HWLAB/internal/cloud/server.ts
T
2026-07-21 21:38:12 +02:00

1973 lines
84 KiB
TypeScript

/*
* SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p0-projector-resume; PJ2026-010403 API契约 draft-2026-06-20-p0-error-diagnostics; PJ2026-010401 Web工作台 draft-2026-06-20-p0-error-diagnostics; PJ2026-010405 云端控制台 draft-2026-07-13-p0-cloud-console; PJ2026-01060505 Workbench Performance draft-2026-06-20-p0-error-diagnostics
* 职责: Cloud API REST/SSE route dispatcher。Workbench 新资源路由应委托 read model / compat wrapper,不在 dispatcher 内写业务事实。
*/
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { copyFileSync, existsSync, lstatSync, mkdirSync, symlinkSync } from "node:fs";
import path from "node:path";
import {
buildMetadataFromEnv,
normalizeIsoTimestamp
} from "../build-metadata.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, ERROR_CODES, SERVICE_IDS } from "../protocol/index.mjs";
import {
AUDIT_FIELD_NAMES,
CLOUD_API_SERVICE_ID,
createAuditRecord
} from "../audit/index.mjs";
import {
SUPPORTED_RPC_METHODS,
createErrorEnvelope,
createResultEnvelope,
handleJsonRpcRequest
} from "./json-rpc.ts";
import {
describeCodeAgentAvailability,
handleCodeAgentChat
} from "./code-agent-chat.ts";
import { createDurableCodeAgentTraceStore, defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
import { createCodeAgentSessionRegistry } from "./code-agent-session-registry.ts";
import { codeAgentSessionLifecycleSummary } from "./code-agent-session-lifecycle.ts";
import { createCodexStdioSessionManager } from "./codex-stdio-session.ts";
import { buildGateDiagnosticsRows } from "./gate-diagnostics.ts";
import {
applyRuntimeDbReadinessLayers,
buildDbRuntimeReadiness
} from "./db-contract.ts";
import { buildCloudApiReadiness } from "./health-contract.ts";
import {
M3_IO_CONTROL_ROUTE,
M3_STATUS_ROUTE,
describeM3IoControl,
describeM3IoControlLive,
describeM3StatusLive,
handleM3IoControl
} from "./m3-io-control.ts";
import { createConfiguredCloudRuntimeStore } from "../db/runtime-store.ts";
import { createAccessController } from "./access-control.ts";
import { createGatewayDemoRegistry } from "./gateway-demo-registry.ts";
import { buildLiveBuildsPayload } from "./server-rest-payloads.ts";
import {
createWebPerformanceStore,
handleWebPerformanceIngestHttp,
handleWebPerformanceMetricsHttp,
handleWebPerformanceSummaryHttp
} from "./web-performance.ts";
import { createBackendPerformanceStore, withBackendPerformanceContext } from "./backend-performance.ts";
import {
createCodeAgentChatResultStore,
handleCodeAgentCancelHttp,
handleCodeAgentChatHttp,
handleCodeAgentChatResultHttp,
handleCodeAgentInspectHttp,
handleCodeAgentSessionsHttp,
handleWorkbenchTurnAdmissionHttp,
handleCodeAgentSteerHttp,
handleCodeAgentTurnHttp,
handleCodeAgentTraceHttp
} from "./server-code-agent-http.ts";
import { drainWorkbenchRealtimeConnections, handleWorkbenchReadModelHttp, handleWorkbenchRealtimeHttp } from "./server-workbench-http.ts";
import { handleWorkbenchDebugFakeSseHttp } from "./workbench-debug-fake-sse.ts";
import { handleWorkbenchKafkaSseDebugHttp } from "./workbench-kafka-sse-debug.ts";
import { handleWorkbenchLaunchHttp } from "./server-workbench-launch-http.ts";
import { handleAgentObserverHttp } from "./server-agent-observer-http.ts";
import { startWorkbenchEmptySessionGc } from "./workbench-empty-session-gc.ts";
import { startHwlabKafkaEventBridge } from "./kafka-event-bridge.ts";
import { handleM3IoControlHttp } from "./server-m3-http.ts";
import { handleSkillsHttp } from "./server-skills-http.ts";
import { emitHttpServerRequestSpan, httpRequestOtelTraceContext, newOtelSpanId } from "./otel-trace.ts";
import { decorateErrorPayloadForHttp, logErrorEnvelope } from "./error-envelope.ts";
import { handleProviderProfileCatalogHttp, handleProviderProfilesHttp } from "./provider-profile-management.ts";
import { handleAdminSecretsHttp } from "./admin-secrets-readmodel.ts";
import {
configureSkillRuntime,
ensureCodexSkillsAggregationSync
} from "./skills-store.ts";
import { createHwpodNodeWsRegistry } from "./hwpod-node-ws-registry.ts";
import {
handleHwlabNodeDownloadHttp,
handleHwlabNodeUpdateHttp,
handleHwpodNodeOpsHttp,
handleHwpodSpecDiscoveryHttp,
handleHwpodTopologyHttp
} from "./server-hwpod-http.ts";
import { HWPOD_TOPOLOGY_CONTRACT_VERSION } from "./hwpod-topology-read-model.ts";
import { handleCaseRunHttp } from "./server-caserun-http.ts";
import { handleProjectManagementProxyHttp } from "./project-management-proxy.ts";
import { handleTaskTreeProxyHttp } from "./tasktree-proxy.ts";
import { handleDashboardSummaryHttp } from "./server-dashboard-http.ts";
import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "../../tools/src/hwpod-node-ops-contract.ts";
import { proxyWorkbenchCommandHttp, workbenchActivityDispatchRequested, workbenchTemporalProxyEnabled } from "./workbench-command-proxy.ts";
import { normalizeWorkbenchCloudAuthorization } from "../workbench/cloud-authorization.ts";
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
const HWLAB_WEB_SESSION_COOKIE = "hwlab_session";
function runtimeEnvironment(env = process.env) {
const value = env.HWLAB_GITOPS_PROFILE || env.HWLAB_ENVIRONMENT || ENVIRONMENT_DEV;
return typeof value === "string" && value.trim() ? value.trim() : ENVIRONMENT_DEV;
}
async function observeHttpRoutePhase(request, options, phase, operation, attributes = {}) {
const startedAtMs = Date.now();
try {
const value = await operation();
emitHttpRoutePhaseSpan(request, options, phase, startedAtMs, Date.now(), "ok", null, attributes);
return value;
} catch (error) {
emitHttpRoutePhaseSpan(request, options, phase, startedAtMs, Date.now(), "error", error, attributes);
throw error;
}
}
function createHttpRoutePhaseObserver(request, options, namespace) {
return (phase, operation, attributes = {}) => observeHttpRoutePhase(request, options, `${namespace}.${phase}`, operation, attributes);
}
function emitHttpRoutePhaseSpan(request, options, phase, startTimeMs, endTimeMs, outcome, error, attributes = {}) {
const context = request?.hwlabHttpRequestContext;
if (!context?.traceId) return;
const errorMessage = error ? String(error?.message ?? error).slice(0, 300) : null;
void emitHttpServerRequestSpan(context, options?.env ?? process.env, {
name: `${String(context.method ?? "GET").toUpperCase()} ${String(context.route ?? "/")} ${phase}`,
method: context.method,
route: context.route,
parentSpanId: context.spanId,
spanId: newOtelSpanId(),
startTimeMs,
endTimeMs,
statusCode: outcome === "error" ? 500 : 200,
status: outcome === "error" ? "error" : "ok",
statusMessage: errorMessage,
errorCode: error ? phaseErrorCode(error) : null,
attributes: {
"hwlab.http.stage": "http.server.phase",
"hwlab.http.phase": phase,
"hwlab.http.phase.outcome": outcome,
...attributes
}
}).catch(() => undefined);
}
function phaseErrorCode(error) {
return String(error?.code ?? error?.name ?? "phase_error").slice(0, 120);
}
export function createCloudApiServer(options = {}) {
const env = options.env ?? process.env;
ensureCodeAgentRuntimeBase(env);
const sessionRegistry = options.sessionRegistry || createCodeAgentSessionRegistry();
const runtimeStore = options.runtimeStore || createConfiguredCloudRuntimeStore({ ...options, env });
const traceStore = createDurableCodeAgentTraceStore({
traceStore: options.traceStore || defaultCodeAgentTraceStore,
traceEventStore: runtimeStore
});
const codexStdioManager = options.codexStdioManager || createCodexStdioSessionManager({ traceStore });
const codeAgentChatResults = options.codeAgentChatResults || createCodeAgentChatResultStore({
maxResults: parsePositiveInteger(env.HWLAB_CODE_AGENT_CHAT_RESULT_CACHE_SIZE, 256)
});
const webPerformanceStore = options.webPerformanceStore || createWebPerformanceStore({ env });
const backendPerformanceStore = options.backendPerformanceStore || createBackendPerformanceStore({ env });
const gatewayRegistry = options.gatewayRegistry || createGatewayDemoRegistry({
staleMs: parsePositiveInteger(env.HWLAB_GATEWAY_DEMO_STALE_MS, 30000),
dispatchTimeoutMs: parsePositiveInteger(env.HWLAB_GATEWAY_DISPATCH_TIMEOUT_MS, 120000)
});
const hwpodNodeWsRegistry = options.hwpodNodeWsRegistry || createHwpodNodeWsRegistry({ ...options, env, runtimeStore });
const accessController = options.accessController || createAccessController({ ...options, env, runtimeStore, gatewayRegistry, traceStore, codeAgentChatResults });
if (typeof accessController.configureCodeAgentWorkspaceContext === "function") {
accessController.configureCodeAgentWorkspaceContext({ env, fetchImpl: options.fetchImpl, traceStore, codeAgentChatResults });
}
const workbenchEmptySessionGc = startWorkbenchEmptySessionGc({ env, accessController, logger: options.logger ?? console });
const kafkaEventBridge = options.kafkaEventBridge ?? startHwlabKafkaEventBridge({ env, logger: options.logger ?? console, runtimeStore, kafkaFactory: options.kafkaFactory });
const server = createServer(async (request, response) => {
const requestContext = buildHttpRequestContext(request);
attachHttpRequestContext(request, response, requestContext);
const backendPerformance = backendPerformanceStore.beginHttpRequest(request, response);
let otelHttpSpanEmitted = false;
const emitHttpSpan = (closedBy) => {
if (otelHttpSpanEmitted) return;
otelHttpSpanEmitted = true;
const httpCtx = request.hwlabHttpRequestContext;
if (!httpCtx?.traceId) return;
if (isOtelNoisyProbeRoute(httpCtx.route)) return;
const lastError = httpCtx.lastHttpError;
void emitHttpServerRequestSpan(httpCtx, env, {
startTimeMs: httpCtx.startedAtMs,
endTimeMs: Date.now(),
statusCode: Number(response.statusCode ?? backendPerformance.statusCode ?? 0),
method: backendPerformance.method || httpCtx.method,
route: backendPerformance.route || httpCtx.route,
attributes: {
"http.closed_by": closedBy,
...(httpCtx.otelAttributes ?? {}),
"http.response.body.size": Number(backendPerformance.responseBytes ?? 0),
"hwlab.backend.phase_count": Array.isArray(backendPerformance.phases) ? backendPerformance.phases.length : 0
},
errorCode: lastError?.code,
errorCategory: lastError?.category,
errorLayer: lastError?.layer,
statusMessage: lastError?.code
}).catch(() => undefined);
};
response.once("finish", () => emitHttpSpan("finish"));
response.once("close", () => emitHttpSpan("close"));
await withBackendPerformanceContext(backendPerformance, async () => {
try {
await routeRequest(request, response, { ...options, env, runtimeStore, kafkaEventBridge, gatewayRegistry, hwpodNodeWsRegistry, accessController, sessionRegistry, traceStore, codexStdioManager, codeAgentChatResults, webPerformanceStore, backendPerformanceStore, backendPerformance, emitHttpRoutePhaseSpan });
} catch (error) {
if (error?.alreadySent || response.headersSent || response.writableEnded) return;
backendPerformance.recordPhase({ phase: "route_exception", durationMs: 0, outcome: "error" });
sendJson(response, 500, {
error: {
code: "internal_error",
message: "hwlab-cloud-api request handling failed",
reason: error.message
}
});
}
});
});
server.on("close", () => {
workbenchEmptySessionGc.stop();
void kafkaEventBridge.stop();
});
server.hwlabStartupReady = kafkaEventBridge?.ready ?? Promise.resolve();
server.hwlabAbortStartup = async () => { workbenchEmptySessionGc.stop(); await kafkaEventBridge.stop(); };
return server;
}
function buildHttpRequestContext(request) {
const otelContext = httpRequestOtelTraceContext({
traceparent: getHeader(request, "traceparent"),
traceId: getHeader(request, "x-hwlab-otel-trace-id")
});
return {
requestId: safeRequestId(getHeader(request, "x-request-id")) || `req_${randomUUID()}`,
startedAtMs: Date.now(),
traceId: otelContext.traceId,
parentSpanId: otelContext.parentSpanId,
spanId: otelContext.spanId,
traceparent: otelContext.traceparent,
method: String(request.method || "GET").toUpperCase(),
route: String(request.url || "/").split("?")[0] || "/",
valuesPrinted: false
};
}
function isOtelNoisyProbeRoute(route) {
const path = String(route ?? "/").split("?")[0] || "/";
return path === "/metrics" || path.startsWith("/health");
}
function attachHttpRequestContext(request, response, context) {
request.hwlabHttpRequestContext = context;
response.hwlabHttpRequestContext = context;
if (response.headersSent || response.writableEnded) return;
response.setHeader("traceparent", context.traceparent);
response.setHeader("x-hwlab-otel-trace-id", context.traceId);
response.setHeader("x-request-id", context.requestId);
}
export function ensureCodeAgentRuntimeBase(env = process.env) {
const codeAgentAdapter = String(env.HWLAB_CODE_AGENT_ADAPTER ?? env.HWLAB_CODE_AGENT_PROVIDER ?? "").trim().toLowerCase();
const useCodexStdioRuntime = !codeAgentAdapter || codeAgentAdapter === "codex-stdio";
const skillRuntime = configureSkillRuntime(env);
if (!useCodexStdioRuntime) return;
const workspace = env.HWLAB_CODE_AGENT_CODEX_WORKSPACE || env.HWLAB_CODE_AGENT_WORKSPACE || "/workspace/hwlab";
const codexHome = env.CODEX_HOME || env.HWLAB_CODE_AGENT_CODEX_HOME || "/codex-home";
const appCodexCommand = "/app/node_modules/.bin/codex";
const localCodexCommand = path.join(process.cwd(), "node_modules", ".bin", "codex");
const codexCommand = existsSync(appCodexCommand)
? appCodexCommand
: existsSync(localCodexCommand)
? localCodexCommand
: appCodexCommand;
env.HWLAB_CODE_AGENT_WORKSPACE ||= workspace;
env.HWLAB_CODE_AGENT_CODEX_WORKSPACE ||= workspace;
env.HWLAB_CODE_AGENT_CODEX_SANDBOX ||= "danger-full-access";
env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED ||= "1";
env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR ||= "repo-owned";
env.CODEX_HOME ||= codexHome;
env.HWLAB_CODE_AGENT_CODEX_COMMAND ||= codexCommand;
for (const target of [path.dirname(workspace), codexHome]) {
try {
mkdirSync(target, { recursive: true });
} catch {
// Runtime health reports the concrete missing/writable blocker.
}
}
try {
if (!existsSync(workspace)) {
if (workspace === "/workspace/hwlab" && existsSync("/app")) {
symlinkSync("/app", workspace, "dir");
} else {
mkdirSync(workspace, { recursive: true });
}
}
lstatSync(workspace);
ensurePreinstalledHwpodSpec({ workspace });
ensureCodexSkillsAggregationSync(skillRuntime);
} catch {
// Runtime health reports the concrete missing/writable blocker.
}
}
function ensurePreinstalledHwpodSpec({ workspace }) {
const source = path.resolve(process.cwd(), ".hwlab/hwpod-spec.yaml");
const target = path.join(workspace, ".hwlab/hwpod-spec.yaml");
const sourceMeta = path.resolve(process.cwd(), ".hwlab/hwpod-spec.meta.json");
const targetMeta = path.join(workspace, ".hwlab/hwpod-spec.meta.json");
if (!existsSync(source)) return;
mkdirSync(path.dirname(target), { recursive: true });
if (!existsSync(target)) copyFileSync(source, target);
if (existsSync(sourceMeta) && !existsSync(targetMeta)) copyFileSync(sourceMeta, targetMeta);
}
export async function buildHealthPayload(options = {}) {
const serviceId = CLOUD_API_SERVICE_ID;
const env = options.env ?? process.env;
const healthMode = options.healthMode === "live" ? "live" : "readiness";
const liveProbe = healthMode === "live";
ensureCodeAgentRuntimeBase(env);
const metadata = buildMetadataFromEnv(env, {
serviceId,
fallbackImageRepository: "ghcr.io/pikastech/hwlab-cloud-api"
});
const runtimeStore = options.runtimeStore ?? createConfiguredCloudRuntimeStore({ ...options, env });
const dbProbe = await buildDbRuntimeReadiness(env, liveProbe ? { ...(options.dbProbe ?? {}), probe: false } : options.dbProbe);
const codeAgent = await describeCodeAgentAvailability(env, options);
const runtime = liveProbe ? runtimeReadinessSnapshot(runtimeStore) : await runtimeReadiness(runtimeStore);
const kafkaProjector = await kafkaProjectorHealth(options.kafkaEventBridge, { liveProbe });
const db = applyRuntimeDbReadinessLayers(dbProbe, runtime);
const readiness = buildCloudApiReadiness({ db, codeAgent, runtime, kafkaProjector });
return {
serviceId,
environment: runtimeEnvironment(env),
healthMode,
status: readiness.status,
ready: readiness.ready,
revision: metadata.revision,
readiness,
blockers: readiness.blockers,
blockerCodes: readiness.blockerCodes,
service: {
id: serviceId,
role: "cloud-api",
healthPath: "/health",
livePath: "/health/live"
},
commit: metadata.commit,
image: metadata.image,
build: metadata.build,
runtimeIdentity: metadata.runtime,
endpoint: env.HWLAB_PUBLIC_ENDPOINT || DEV_ENDPOINT,
observedAt: new Date().toISOString(),
db,
codeAgent,
runtime,
kafkaProjector
};
}
async function kafkaProjectorHealth(projector, { liveProbe = false } = {}) {
if (!projector?.started) return { started: false, reason: projector?.reason ?? "disabled", valuesRedacted: true };
const identity = {
capabilities: projector.capabilities ?? null,
directPublishGroupId: projector.directPublishGroupId ?? null,
projectorGroupId: projector.projectorGroupId ?? null,
hwlabEventGroupId: projector.hwlabEventGroupId ?? null,
agentRunTopic: projector.agentRunTopic,
hwlabTopic: projector.hwlabTopic
};
if (liveProbe) return { started: true, ...identity, ...(projector.startupStatus?.() ?? { status: "initializing" }), statusSource: "startup-state", valuesRedacted: true };
try {
return { started: true, ...identity, ...(await projector.status()), valuesRedacted: true };
} catch (error) {
return { started: true, status: "blocked", errorCode: error?.code ?? "hwlab_kafka_projector_status_failed", message: error?.message ?? "Kafka projector status query failed.", valuesRedacted: true };
}
}
function runtimeReadinessSnapshot(runtimeStore) {
if (runtimeStore && typeof runtimeStore.summary === "function") {
try {
const summary = runtimeStore.summary();
return {
...summary,
readinessFresh: false,
readinessSource: "cached-summary"
};
} catch (error) {
return {
adapter: "unknown",
durable: false,
ready: false,
status: "degraded",
blocker: "runtime_readiness_snapshot_failed",
reason: error instanceof Error ? error.message : "Runtime readiness snapshot failed",
readinessFresh: false,
readinessSource: "cached-summary",
valuesRedacted: true,
endpointRedacted: true
};
}
}
return {
adapter: "unknown",
durable: false,
ready: false,
status: "degraded",
blocker: "runtime_readiness_snapshot_unavailable",
reason: "Runtime store does not expose a cached summary for /health/live",
readinessFresh: false,
readinessSource: "cached-summary",
valuesRedacted: true,
endpointRedacted: true
};
}
async function routeRequest(request, response, options) {
const url = new URL(request.url || "/", "http://hwlab-cloud-api.local");
if (request.method === "GET" && url.pathname === "/health/live") {
sendJson(response, 200, await buildHealthPayload({ ...options, healthMode: "live" }));
return;
}
if (request.method === "GET" && url.pathname === "/health") {
sendJson(response, 200, await buildHealthPayload({ ...options, healthMode: "readiness" }));
return;
}
if (request.method === "GET" && url.pathname === "/live") {
sendJson(response, 200, {
serviceId: CLOUD_API_SERVICE_ID,
status: "live"
});
return;
}
if (url.pathname === "/auth" || url.pathname.startsWith("/auth/")) {
await options.accessController.handleAuthRoute(request, response, url);
return;
}
if (request.method === "POST" && (url.pathname === "/rpc" || url.pathname === "/json-rpc")) {
await handleRpcHttpRequest(request, response, options);
return;
}
if (url.pathname === "/v1" || url.pathname.startsWith("/v1/")) {
await handleRestAdapter(request, response, url, options);
return;
}
sendRestError(request, response, 404, "not_found", "Route is not part of the L1 cloud-api runtime", {
operation: `${request.method || "GET"} ${url.pathname}`,
target: {
type: "route",
id: url.pathname
}
});
}
async function handleRpcHttpRequest(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let envelope;
try {
envelope = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(
response,
400,
createErrorEnvelope({
id: "req_unassigned",
code: ERROR_CODES.parseError,
message: "Invalid JSON body",
data: {
reason: error.message
},
context: {
operation: "json_rpc.parse",
target: {
type: "http_route",
id: request.url || "/rpc"
},
result: "rejected"
}
})
);
return;
}
const rpcResponse = await handleJsonRpcRequest(envelope, {
adapter: "json-rpc",
runtimeStore: options.runtimeStore,
env: options.env,
dbProbe: options.dbProbe,
m3IoRequestJson: options.m3IoRequestJson,
gatewayRegistry: options.gatewayRegistry
});
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
}
async function handleRestAdapter(request, response, url, options) {
const navId = navIdForRestPath(url.pathname, request.method || "GET");
if (navId && typeof options.accessController?.requireNavAccess === "function" && !(await options.accessController.requireNavAccess(request, response, navId))) return;
if (url.pathname === "/v1/web-performance" && request.method === "POST") {
await handleWebPerformanceIngestHttp(request, response, { store: options.webPerformanceStore, env: options.env });
return;
}
if (url.pathname === "/v1/web-performance/metrics" && request.method === "GET") {
handleWebPerformanceMetricsHttp(request, response, { store: options.webPerformanceStore, extraMetricsText: () => options.backendPerformanceStore.metricsText() });
return;
}
if (url.pathname === "/v1/web-performance/summary" && request.method === "GET") {
handleWebPerformanceSummaryHttp(request, response, { store: options.webPerformanceStore, backendPerformanceStore: options.backendPerformanceStore });
return;
}
if (url.pathname === "/v1/dashboard/summary") {
await handleDashboardSummaryHttp(request, response, options);
return;
}
if ((url.pathname === "/v1/usage/summary" || url.pathname === "/v1/billing/summary") && request.method === "GET") {
await handleUsageSummaryHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/redeem" && request.method === "POST") {
await handleRedeemHttp(request, response, options);
return;
}
if (url.pathname === "/v1/subscription/summary" && request.method === "GET") {
await handleSubscriptionSummaryHttp(request, response, options);
return;
}
if (url.pathname === "/v1/payments/summary" && request.method === "GET") {
await handlePaymentSummaryHttp(request, response, options);
return;
}
if (url.pathname === "/v1/admin/billing/summary" && request.method === "GET") {
await handleAdminBillingSummaryHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/admin/billing/plans" && request.method === "GET") {
await handleAdminBillingPlansHttp(request, response, options);
return;
}
if (url.pathname === "/v1/admin/billing/usage" && request.method === "GET") {
await handleAdminBillingUsageHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/admin/billing/usage/export" && request.method === "GET") {
await handleAdminBillingUsageExportHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/admin/billing/ledger" && request.method === "GET") {
await handleAdminBillingLedgerHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/admin/billing/ledger/export" && request.method === "GET") {
await handleAdminBillingLedgerExportHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/admin/billing/redeem-codes" && (request.method === "GET" || request.method === "POST")) {
await handleAdminBillingRedeemCodesHttp(request, response, url, options);
return;
}
const adminBillingRedeemCodeMatch = url.pathname.match(/^\/v1\/admin\/billing\/redeem-codes\/([^/]+)$/u);
if (adminBillingRedeemCodeMatch && request.method === "PATCH") {
await handleAdminBillingRedeemCodeHttp(request, response, decodeURIComponent(adminBillingRedeemCodeMatch[1]), options);
return;
}
if (url.pathname === "/v1/admin/billing/redeem-redemptions" && request.method === "GET") {
await handleAdminBillingRedeemRedemptionsHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/admin/billing/users" && (request.method === "GET" || request.method === "POST")) {
await handleAdminBillingUsersHttp(request, response, url, options);
return;
}
const adminBillingUserMatch = url.pathname.match(/^\/v1\/admin\/billing\/users\/([^/]+)$/u);
if (adminBillingUserMatch && (request.method === "GET" || request.method === "PATCH")) {
await handleAdminBillingUserHttp(request, response, url, decodeURIComponent(adminBillingUserMatch[1]), options);
return;
}
const adminBillingUserCreditsMatch = url.pathname.match(/^\/v1\/admin\/billing\/users\/([^/]+)\/credits\/adjust$/u);
if (adminBillingUserCreditsMatch && request.method === "POST") {
await handleAdminBillingCreditAdjustHttp(request, response, decodeURIComponent(adminBillingUserCreditsMatch[1]), options);
return;
}
const adminBillingUserStatusMatch = url.pathname.match(/^\/v1\/admin\/billing\/users\/([^/]+)\/status$/u);
if (adminBillingUserStatusMatch && request.method === "PATCH") {
await handleAdminBillingUserStatusHttp(request, response, decodeURIComponent(adminBillingUserStatusMatch[1]), options);
return;
}
if (request.method === "GET" && url.pathname === "/v1") {
const dbProbeOptions = { ...(options.dbProbe ?? {}), timeoutMs: 500 };
const [dbProbe, codeAgent, runtime] = await Promise.all([
observeHttpRoutePhase(request, options, "v1.db_readiness", () => buildDbRuntimeReadiness(options.env ?? process.env, dbProbeOptions)),
observeHttpRoutePhase(request, options, "v1.code_agent_availability", () => describeCodeAgentAvailability(options.env ?? process.env, options)),
observeHttpRoutePhase(request, options, "v1.runtime_readiness", () => runtimeReadinessSnapshot(options.runtimeStore))
]);
const db = applyRuntimeDbReadinessLayers(dbProbe, runtime);
const kafkaProjector = await kafkaProjectorHealth(options.kafkaEventBridge, { liveProbe: false });
const readiness = buildCloudApiReadiness({ db, codeAgent, runtime, kafkaProjector });
sendJson(response, 200, {
serviceId: CLOUD_API_SERVICE_ID,
adapter: "rest",
status: readiness.status,
ready: readiness.ready,
rpcBridge: "POST /v1/rpc/{method}",
codeAgent,
methods: SUPPORTED_RPC_METHODS,
auditFields: AUDIT_FIELD_NAMES,
db,
runtime,
readiness,
blockers: readiness.blockers,
blockerCodes: readiness.blockerCodes,
caserun: {
route: "/v1/caserun",
casesRoute: "/v1/caserun/cases",
runsRoute: "/v1/caserun/runs",
contractVersion: "hwlab-caserun-web-v1",
mode: "compile-only",
caseAuthority: "builtin-or-configured-case-repo"
},
hwpod: {
route: "/v1/hwpod-node-ops",
specDiscoveryRoute: "/v1/hwpod/specs",
topologyRoute: "/v1/hwpod/topology",
topologyContractVersion: HWPOD_TOPOLOGY_CONTRACT_VERSION,
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
specAuthority: "workspace-or-registry",
compiler: "hwpod-compiler-cli",
apiRole: "node-ops-forwarder",
nodeRole: "thin-hwpod-node-executor",
supportedOps: Array.from(HWPOD_NODE_OPS)
},
projectManagement: {
route: "/v1/project-management",
serviceId: "hwlab-project-management",
contractVersion: "project-management-v1",
sourceOfTruth: "mdtodo-markdown-files"
},
hwlabNode: {
updateRoute: "/v1/hwlab-node/update",
downloadRoute: "/v1/hwlab-node/download/hwlab-node.py",
diagnosticsOp: "node.diagnostics",
updateContractVersion: "hwlab-node-update-v1",
defaultUpdateIntervalSeconds: 300
},
m3IoControl: describeM3IoControl(options)
});
return;
}
if (url.pathname === "/v1/hwlab-node/update") {
handleHwlabNodeUpdateHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/hwlab-node/download/hwlab-node.py") {
handleHwlabNodeDownloadHttp(request, response, options);
return;
}
if (url.pathname === "/v1/auth/session" || url.pathname === "/v1/auth/login" || url.pathname === "/v1/auth/logout" || url.pathname === "/v1/auth/register") {
const authUrl = new URL(url.href);
authUrl.pathname = url.pathname.replace(/^\/v1\/auth/u, "/auth");
await options.accessController.handleAuthRoute(request, response, authUrl);
return;
}
if (url.pathname === "/v1/skills" || url.pathname.startsWith("/v1/skills/")) {
await handleSkillsHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/users/me") {
await options.accessController.handleUserRoute(request, response, url);
return;
}
if (url.pathname === "/v1/project-management" || url.pathname.startsWith("/v1/project-management/")) {
await handleProjectManagementProxyHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/tasktree" || url.pathname.startsWith("/v1/tasktree/")) {
await handleTaskTreeProxyHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/users/me/profile" && request.method === "PATCH") {
await handleUserBillingProfileHttp(request, response, options);
return;
}
if (url.pathname === "/v1/users/me/password" && request.method === "POST") {
await handleUserBillingPasswordHttp(request, response, options);
return;
}
if (url.pathname === "/v1/api-keys" || url.pathname === "/v1/api-keys/default" || url.pathname.startsWith("/v1/api-keys/")) {
if (await maybeHandleUserBillingApiKeysHttp(request, response, url, options)) return;
await options.accessController.handleUserRoute(request, response, url);
return;
}
if (url.pathname === "/v1/internal/workbench/drain") {
if (request.method !== "POST") {
sendJson(response, 405, { ok: false, error: { code: "method_not_allowed", message: "POST required." } });
return;
}
if (!isAuthorizedWorkbenchDrainRequest(request, options.env ?? process.env)) {
sendJson(response, 403, { ok: false, error: { code: "workbench_drain_forbidden", message: "Workbench drain endpoint requires pod-local preStop authorization." } });
return;
}
const result = await drainWorkbenchRealtimeConnections({
reason: "k8s_prestop",
signal: "SIGTERM",
timeoutMs: parsePositiveInteger((options.env ?? process.env).HWLAB_WORKBENCH_SSE_DRAIN_TIMEOUT_MS, 2500),
env: options.env ?? process.env
});
sendJson(response, 200, { ok: true, route: "/v1/internal/workbench/drain", result });
return;
}
if (url.pathname === "/v1/workbench/events" || url.pathname === "/v1/workbench/projection-events") {
await handleWorkbenchRealtimeHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/agent-observer/events") {
await handleAgentObserverHttp(request, response, options);
return;
}
if (url.pathname === "/v1/workbench/launches") {
await handleWorkbenchLaunchHttp(request, response, options);
return;
}
if (url.pathname === "/v1/workbench/debug/fake-sse" || url.pathname.startsWith("/v1/workbench/debug/fake-sse/")) {
await handleWorkbenchDebugFakeSseHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/workbench/debug/kafka-sse" || url.pathname.startsWith("/v1/workbench/debug/kafka-sse/")) {
await handleWorkbenchKafkaSseDebugHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/workbench/sessions" || url.pathname.startsWith("/v1/workbench/sessions/") || url.pathname.startsWith("/v1/workbench/turns/") || url.pathname.startsWith("/v1/workbench/traces/")) {
await handleWorkbenchReadModelHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/access/status" || url.pathname === "/v1/setup/status") {
await options.accessController.handleAccessStatusRoute(request, response, url);
return;
}
if (url.pathname === "/v1/setup/first-admin") {
await options.accessController.handleSetupRoute(request, response, url);
return;
}
if (url.pathname === "/v1/admin/provider-profiles" || url.pathname.startsWith("/v1/admin/provider-profiles/")) {
await handleProviderProfilesHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/admin/secrets" || url.pathname.startsWith("/v1/admin/secrets/")) {
await handleAdminSecretsHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/provider-profiles" && request.method === "GET") {
await handleProviderProfileCatalogHttp(request, response, options);
return;
}
if (url.pathname.startsWith("/v1/admin/")) {
await options.accessController.handleAdminRoute(request, response, url);
return;
}
if (url.pathname === "/v1/hwpod-node-ops") {
await handleHwpodNodeOpsHttp(request, response, options);
return;
}
if (url.pathname === "/v1/caserun" || url.pathname.startsWith("/v1/caserun/")) {
await handleCaseRunHttp(request, response, url, options);
return;
}
if (request.method === "GET" && url.pathname === "/v1/hwpod/specs") {
await handleHwpodSpecDiscoveryHttp(request, response, url, options);
return;
}
if (url.pathname === "/v1/hwpod/topology") {
await handleHwpodTopologyHttp(request, response, url, options);
return;
}
if (request.method === "GET" && url.pathname === "/v1/hwpod-node/ws") {
sendJson(response, 426, { ok: false, status: "upgrade_required", route: "/v1/hwpod-node/ws", websocket: options.hwpodNodeWsRegistry.describe() });
return;
}
if (request.method === "GET" && url.pathname === M3_IO_CONTROL_ROUTE) {
sendJson(response, 200, await describeM3IoControlLive(options));
return;
}
if (request.method === "GET" && url.pathname === M3_STATUS_ROUTE) {
sendJson(response, 200, await describeM3StatusLive(options));
return;
}
if (request.method === "GET" && url.pathname === "/v1/diagnostics/gate") {
sendJson(response, 200, await buildGateDiagnosticsRows(options));
return;
}
if (request.method === "GET" && url.pathname === "/v1/live-builds") {
sendJson(response, 200, await buildLiveBuildsPayload({
...options,
observeLiveBuildPhase: createHttpRoutePhaseObserver(request, options, "live_builds")
}, { buildHealthPayload, runtimeEnvironment }));
return;
}
if (request.method === "GET" && url.pathname === "/v1/gateway/sessions") {
sendJson(response, 200, options.gatewayRegistry.describe());
return;
}
if (request.method === "POST" && url.pathname === "/v1/gateway/poll") {
await handleGatewayPollHttp(request, response, options);
return;
}
if (request.method === "POST" && url.pathname === "/v1/gateway/result") {
await handleGatewayResultHttp(request, response, options);
return;
}
if (request.method === "POST" && url.pathname === M3_IO_CONTROL_ROUTE) {
await handleM3IoControlHttp(request, response, options);
return;
}
if (request.method === "POST" && url.pathname === "/v1/agent/chat") {
const nextOptions = await codeAgentOptions(request, response, options);
if (!nextOptions) return;
if (workbenchTemporalProxyEnabled(nextOptions.env) && !workbenchActivityDispatchRequested(request)) {
await proxyWorkbenchCommandHttp(request, response, nextOptions, url.pathname);
return;
}
await handleCodeAgentChatHttp(request, response, nextOptions);
return;
}
if (request.method === "POST" && url.pathname === "/v1/internal/workbench/admit") {
if (!workbenchActivityDispatchRequested(request)) {
sendJson(response, 404, { ok: false, error: { code: "not_found", message: "REST route is not implemented" } });
return;
}
const nextOptions = await codeAgentOptions(request, response, options, { required: true });
if (!nextOptions) return;
await handleWorkbenchTurnAdmissionHttp(request, response, nextOptions);
return;
}
if (url.pathname === "/v1/agent/sessions" || url.pathname.startsWith("/v1/agent/sessions/")) {
const nextOptions = await codeAgentOptions(request, response, options, { required: true });
if (!nextOptions) return;
if (request.method === "POST" && url.pathname === "/v1/agent/sessions" && workbenchTemporalProxyEnabled(nextOptions.env) && !workbenchActivityDispatchRequested(request)) {
await proxyWorkbenchCommandHttp(request, response, nextOptions, url.pathname);
return;
}
await handleCodeAgentSessionsHttp(request, response, url, nextOptions);
return;
}
if (request.method === "POST" && url.pathname === "/v1/workbench/commands") {
const nextOptions = await codeAgentOptions(request, response, options, { required: true });
if (!nextOptions) return;
await proxyWorkbenchCommandHttp(request, response, nextOptions, url.pathname);
return;
}
if (request.method === "GET" && url.pathname === "/v1/agent/chat/inspect") {
const nextOptions = await codeAgentOptions(request, response, options);
if (!nextOptions) return;
await handleCodeAgentInspectHttp(request, response, url, nextOptions);
return;
}
if (request.method === "GET" && url.pathname.startsWith("/v1/agent/chat/result/")) {
const nextOptions = await codeAgentOptions(request, response, options);
if (!nextOptions) return;
await handleCodeAgentChatResultHttp(request, response, url, nextOptions);
return;
}
if (request.method === "GET" && url.pathname.startsWith("/v1/agent/turns/")) {
const nextOptions = await codeAgentOptions(request, response, options);
if (!nextOptions) return;
await handleCodeAgentTurnHttp(request, response, url, nextOptions);
return;
}
if (request.method === "GET" && url.pathname.startsWith("/v1/agent/traces/")) {
const nextOptions = await codeAgentOptions(request, response, options);
if (!nextOptions) return;
await handleCodeAgentTraceHttp(request, response, url, nextOptions);
return;
}
if (request.method === "POST" && url.pathname === "/v1/agent/chat/cancel") {
const nextOptions = await codeAgentOptions(request, response, options);
if (!nextOptions) return;
if (workbenchTemporalProxyEnabled(nextOptions.env) && !workbenchActivityDispatchRequested(request)) {
await proxyWorkbenchCommandHttp(request, response, nextOptions, url.pathname);
return;
}
await handleCodeAgentCancelHttp(request, response, nextOptions);
return;
}
if (request.method === "POST" && url.pathname === "/v1/agent/chat/steer") {
const nextOptions = await codeAgentOptions(request, response, options);
if (!nextOptions) return;
await handleCodeAgentSteerHttp(request, response, nextOptions);
return;
}
if (request.method !== "POST" || !url.pathname.startsWith("/v1/rpc/")) {
sendRestError(request, response, 404, "not_found", "REST route is not implemented in the L1 cloud-api runtime", {
operation: `${request.method || "GET"} ${url.pathname}`,
target: {
type: "route",
id: url.pathname
}
});
return;
}
const method = decodeURIComponent(url.pathname.slice("/v1/rpc/".length));
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendRestError(request, response, 400, "parse_error", "Invalid JSON body", {
operation: method,
target: {
type: "rpc_method",
id: method
},
result: "rejected",
reason: error.message
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendRestError(request, response, 400, "invalid_params", "REST RPC bridge body must be a JSON object", {
operation: method,
target: {
type: "rpc_method",
id: method
},
result: "rejected"
});
return;
}
const rpcResponse = await handleJsonRpcRequest(
{
jsonrpc: "2.0",
id: getHeader(request, "x-request-id") || `req_${randomUUID()}`,
method,
params,
meta: {
traceId: getHeader(request, "x-trace-id") || `trc_${randomUUID()}`,
actorId: getHeader(request, "x-actor-id"),
serviceId: getHeader(request, "x-source-service-id") || CLOUD_API_SERVICE_ID,
environment: runtimeEnvironment(options.env ?? process.env)
}
},
{
adapter: "rest",
runtimeStore: options.runtimeStore,
env: options.env,
dbProbe: options.dbProbe,
m3IoRequestJson: options.m3IoRequestJson,
gatewayRegistry: options.gatewayRegistry
}
);
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
}
function navIdForRestPath(pathname, method = "GET") {
const verb = String(method || "GET").toUpperCase();
if (pathname === "/v1/dashboard/summary") return "user.dashboard";
if (pathname === "/v1/usage/summary") return "user.usage";
if (pathname === "/v1/billing/summary" || pathname === "/v1/redeem" || pathname === "/v1/subscription/summary" || pathname === "/v1/payments/summary") return "user.billing";
if (pathname.startsWith("/v1/admin/billing/")) return "admin.billing";
if (pathname === "/v1/skills" || pathname.startsWith("/v1/skills/")) return "system.skills";
if (pathname === "/v1/project-management" || pathname.startsWith("/v1/project-management/")) return "project.mdtodo";
if (pathname === "/v1/tasktree" || pathname.startsWith("/v1/tasktree/")) return "project.tasktree";
if (pathname === "/v1/api-keys" || pathname === "/v1/api-keys/default" || pathname.startsWith("/v1/api-keys/")) return "user.apiKeys";
if (pathname === "/v1/users/me/profile" || pathname === "/v1/users/me/password") return "system.settings";
if (pathname === "/v1/workbench/debug/fake-sse" || pathname.startsWith("/v1/workbench/debug/fake-sse/") || pathname === "/v1/workbench/debug/kafka-sse" || pathname.startsWith("/v1/workbench/debug/kafka-sse/")) return "workbench.debug";
if (pathname === "/v1/workbench/commands" || pathname === "/v1/workbench/events" || pathname === "/v1/workbench/projection-events" || pathname === "/v1/workbench/launches" || pathname === "/v1/workbench/sessions" || pathname.startsWith("/v1/workbench/sessions/") || pathname.startsWith("/v1/workbench/turns/") || pathname.startsWith("/v1/workbench/traces/")) return "workbench.code";
if (pathname === "/v1/agent-observer/events") return "workbench.code";
if (pathname === "/v1/agent/chat" || pathname === "/v1/agent/sessions" || pathname.startsWith("/v1/agent/sessions/") || pathname === "/v1/agent/chat/inspect" || pathname.startsWith("/v1/agent/chat/result/") || pathname.startsWith("/v1/agent/turns/") || pathname.startsWith("/v1/agent/traces/") || pathname === "/v1/agent/chat/cancel" || pathname === "/v1/agent/chat/steer") return "workbench.code";
if (pathname === "/v1/admin/provider-profiles" || pathname.startsWith("/v1/admin/provider-profiles/")) return "admin.providerProfiles";
if (pathname === "/v1/admin/secrets" || pathname.startsWith("/v1/admin/secrets/")) return "admin.secrets";
if (pathname === "/v1/provider-profiles" && verb === "GET") return "workbench.code";
if (pathname.startsWith("/v1/admin/access")) return "admin.access";
if (pathname.startsWith("/v1/admin/users")) return "admin.users";
if (pathname.startsWith("/v1/admin/")) return "admin.access";
if (pathname === "/v1/hwpod-node-ops" || pathname === "/v1/caserun" || pathname.startsWith("/v1/caserun/") || pathname === "/v1/hwpod/specs" || pathname === "/v1/hwpod/topology") return "admin.hwpodGroups";
if (pathname === "/v1/web-performance/metrics" || pathname === "/v1/web-performance/summary" || pathname === "/v1/live-builds") return "system.performance";
if (pathname === "/v1/diagnostics/gate") return "system.gate";
return "";
}
async function codeAgentOptions(request, response, options, authOptions = {}) {
if (workbenchActivityDispatchRequested(request)) {
const expected = normalizeWorkbenchCloudAuthorization(String(options.env?.WORKBENCH_CLOUD_API_AUTHORIZATION ?? ""));
const actual = String(request.headers?.authorization ?? "").trim();
const actorId = String(request.headers?.["x-hwlab-actor-id"] ?? "").trim();
if (!expected || actual !== expected || !actorId) {
sendJson(response, 401, { ok: false, error: { code: "workbench_internal_auth_invalid", message: "Workbench internal activity authentication failed" } });
return null;
}
return {
...options,
actor: { id: actorId, role: String(request.headers?.["x-hwlab-actor-role"] ?? "user").trim() || "user" },
authSession: null,
authApiKey: null,
actorApiKeySecret: null,
userBillingAuth: { active: true, source: "workbench-internal", valuesRedacted: true },
userBillingToken: null,
userBillingClient: options.userBillingClient ?? options.accessController?.userBilling ?? null
};
}
const auth = await options.accessController.authenticate(request, { required: authOptions.required ?? options.accessController.required });
if (!auth.ok) {
sendJson(response, auth.status, auth);
return null;
}
return {
...options,
actor: auth.actor ?? null,
authSession: auth.session ?? null,
authApiKey: auth.apiKey ?? null,
actorApiKeySecret: auth.apiKey?.displaySecret ?? null,
userBillingAuth: auth.userBilling ?? null,
userBillingToken: userBillingTokenFromAuth(auth),
userBillingClient: options.userBillingClient ?? options.accessController?.userBilling ?? null
};
}
async function handleUsageSummaryHttp(request, response, url, options) {
const route = url.pathname === "/v1/billing/summary" ? "/v1/billing/summary" : "/v1/usage/summary";
const nextOptions = await codeAgentOptions(request, response, options, { required: true });
if (!nextOptions) return;
const client = nextOptions.userBillingClient;
const token = nextOptions.userBillingToken;
if (nextOptions.userBillingAuth?.active !== true || !token) {
sendRestError(request, response, 403, "user_billing_auth_required", "usage summary requires a user-billing session or API key", {
operation: `GET ${route}`,
target: { type: "route", id: route },
result: "rejected"
});
return;
}
if (!client?.configured || typeof client.billingSummary !== "function") {
sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing summary is not configured", {
operation: `GET ${route}`,
target: { type: "service", id: "hwlab-user-billing" }
});
return;
}
const limit = parsePositiveInteger(url.searchParams.get("limit"), 20);
const result = await client.billingSummary(token, { limit: Math.min(limit, 100) });
if (!result.ok) {
sendRestError(request, response, result.status ?? 502, result.error?.code ?? "user_billing_summary_failed", result.error?.message ?? "user-billing summary request failed", {
operation: `GET ${route}`,
target: { type: "service", id: "hwlab-user-billing" },
reason: result.error?.code,
result: "failed"
});
return;
}
sendJson(response, 200, {
...(result.body ?? {}),
proxy: {
serviceId: CLOUD_API_SERVICE_ID,
route,
source: "hwlab-user-billing",
valuesRedacted: true
}
});
}
async function handleAdminBillingSummaryHttp(request, response, url, options) {
const auth = await options.accessController.authenticate(request, { required: true });
if (!auth.ok) {
sendJson(response, auth.status, auth);
return;
}
if (auth.actor?.role !== "admin") {
sendRestError(request, response, 403, "admin_required", "Only admin users can read billing summaries", {
operation: "GET /v1/admin/billing/summary",
target: { type: "route", id: "/v1/admin/billing/summary" },
result: "rejected"
});
return;
}
const client = options.userBillingClient ?? options.accessController?.userBilling ?? null;
if (!client?.configured || typeof client.adminBillingSummary !== "function") {
sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin summary is not configured", {
operation: "GET /v1/admin/billing/summary",
target: { type: "service", id: "hwlab-user-billing" }
});
return;
}
const limit = parsePositiveInteger(url.searchParams.get("limit"), 50);
const result = await client.adminBillingSummary({ limit: Math.min(limit, 100) });
if (!result.ok) {
sendRestError(request, response, result.status ?? 502, result.error?.code ?? "user_billing_admin_summary_failed", result.error?.message ?? "user-billing admin summary request failed", {
operation: "GET /v1/admin/billing/summary",
target: { type: "service", id: "hwlab-user-billing" },
reason: result.error?.code,
result: "failed"
});
return;
}
sendJson(response, 200, {
...(result.body ?? {}),
actor: adminActorSummary(auth.actor),
proxy: {
serviceId: CLOUD_API_SERVICE_ID,
route: "/v1/admin/billing/summary",
source: "hwlab-user-billing",
valuesRedacted: true
}
});
}
async function requireAdminBillingClient(request, response, options, operation) {
const auth = await options.accessController.authenticate(request, { required: true });
if (!auth.ok) {
sendJson(response, auth.status, auth);
return null;
}
if (auth.actor?.role !== "admin") {
sendRestError(request, response, 403, "admin_required", "Only admin users can call user billing admin APIs", {
operation,
target: { type: "route", id: operation },
result: "rejected"
});
return null;
}
const client = options.userBillingClient ?? options.accessController?.userBilling ?? null;
if (!client?.configured) {
sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin API is not configured", {
operation,
target: { type: "service", id: "hwlab-user-billing" }
});
return null;
}
return { auth, client };
}
async function requireUserBillingClient(request, response, options, operation) {
const nextOptions = await codeAgentOptions(request, response, options, { required: true });
if (!nextOptions) return null;
const client = nextOptions.userBillingClient;
const token = nextOptions.userBillingToken;
if (nextOptions.userBillingAuth?.active !== true || !token) {
sendRestError(request, response, 403, "user_billing_auth_required", "user-billing session or API key is required", {
operation,
target: { type: "route", id: operation },
result: "rejected"
});
return null;
}
if (!client?.configured) {
sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing API is not configured", {
operation,
target: { type: "service", id: "hwlab-user-billing" }
});
return null;
}
return { auth: nextOptions, client, token };
}
async function handleRedeemHttp(request, response, options) {
const context = await requireUserBillingClient(request, response, options, "POST /v1/redeem");
if (!context) return;
const { auth, client, token } = context;
if (typeof client.redeem !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing redeem API is not configured", { operation: "POST /v1/redeem", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
const result = await client.redeem(token, parsed.value);
sendUserBillingProxyResult({ request, response, result, operation: "POST /v1/redeem", route: "/v1/redeem", actor: adminActorSummary(auth.actor) });
}
async function handleSubscriptionSummaryHttp(request, response, options) {
const context = await requireUserBillingClient(request, response, options, "GET /v1/subscription/summary");
if (!context) return;
const { auth, client, token } = context;
if (typeof client.subscriptionSummary !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing subscription API is not configured", { operation: "GET /v1/subscription/summary", target: { type: "service", id: "hwlab-user-billing" } });
}
const result = await client.subscriptionSummary(token);
sendUserBillingProxyResult({ request, response, result, operation: "GET /v1/subscription/summary", route: "/v1/subscription/summary", actor: adminActorSummary(auth.actor) });
}
async function handlePaymentSummaryHttp(request, response, options) {
const context = await requireUserBillingClient(request, response, options, "GET /v1/payments/summary");
if (!context) return;
const { auth, client, token } = context;
if (typeof client.paymentSummary !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing payment API is not configured", { operation: "GET /v1/payments/summary", target: { type: "service", id: "hwlab-user-billing" } });
}
const result = await client.paymentSummary(token);
sendUserBillingProxyResult({ request, response, result, operation: "GET /v1/payments/summary", route: "/v1/payments/summary", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingUsersHttp(request, response, url, options) {
const context = await requireAdminBillingClient(request, response, options, `${request.method} /v1/admin/billing/users`);
if (!context) return;
const { auth, client } = context;
let result;
if (request.method === "GET") {
if (typeof client.adminUsers !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin users API is not configured", { operation: "GET /v1/admin/billing/users", target: { type: "service", id: "hwlab-user-billing" } });
}
result = await client.adminUsers({
page: parsePositiveInteger(url.searchParams.get("page"), 1),
pageSize: Math.min(parsePositiveInteger(url.searchParams.get("pageSize") || url.searchParams.get("limit"), 50), 100),
search: url.searchParams.get("search") ?? "",
status: url.searchParams.get("status") ?? "",
role: url.searchParams.get("role") ?? ""
});
} else {
if (typeof client.createAdminUser !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin user create API is not configured", { operation: "POST /v1/admin/billing/users", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
result = await client.createAdminUser(parsed.value);
}
sendUserBillingProxyResult({ request, response, result, operation: `${request.method} /v1/admin/billing/users`, route: "/v1/admin/billing/users", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingPlansHttp(request, response, options) {
const context = await requireAdminBillingClient(request, response, options, "GET /v1/admin/billing/plans");
if (!context) return;
const { auth, client } = context;
if (typeof client.adminBillingPlans !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin plans API is not configured", { operation: "GET /v1/admin/billing/plans", target: { type: "service", id: "hwlab-user-billing" } });
}
const result = await client.adminBillingPlans();
sendUserBillingProxyResult({ request, response, result, operation: "GET /v1/admin/billing/plans", route: "/v1/admin/billing/plans", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingUsageHttp(request, response, url, options) {
const context = await requireAdminBillingClient(request, response, options, "GET /v1/admin/billing/usage");
if (!context) return;
const { auth, client } = context;
if (typeof client.adminUsage !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin usage API is not configured", { operation: "GET /v1/admin/billing/usage", target: { type: "service", id: "hwlab-user-billing" } });
}
const result = await client.adminUsage(adminAuditParams(url));
sendUserBillingProxyResult({ request, response, result, operation: "GET /v1/admin/billing/usage", route: "/v1/admin/billing/usage", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingUsageExportHttp(request, response, url, options) {
const context = await requireAdminBillingClient(request, response, options, "GET /v1/admin/billing/usage/export");
if (!context) return;
const { client } = context;
if (typeof client.adminUsageExport !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin usage export API is not configured", { operation: "GET /v1/admin/billing/usage/export", target: { type: "service", id: "hwlab-user-billing" } });
}
const result = await client.adminUsageExport(adminAuditParams(url));
sendUserBillingCsvProxyResult({ request, response, result, operation: "GET /v1/admin/billing/usage/export", filename: "hwlab-admin-usage.csv" });
}
async function handleAdminBillingLedgerHttp(request, response, url, options) {
const context = await requireAdminBillingClient(request, response, options, "GET /v1/admin/billing/ledger");
if (!context) return;
const { auth, client } = context;
if (typeof client.adminLedger !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin ledger API is not configured", { operation: "GET /v1/admin/billing/ledger", target: { type: "service", id: "hwlab-user-billing" } });
}
const result = await client.adminLedger(adminAuditParams(url));
sendUserBillingProxyResult({ request, response, result, operation: "GET /v1/admin/billing/ledger", route: "/v1/admin/billing/ledger", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingLedgerExportHttp(request, response, url, options) {
const context = await requireAdminBillingClient(request, response, options, "GET /v1/admin/billing/ledger/export");
if (!context) return;
const { client } = context;
if (typeof client.adminLedgerExport !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin ledger export API is not configured", { operation: "GET /v1/admin/billing/ledger/export", target: { type: "service", id: "hwlab-user-billing" } });
}
const result = await client.adminLedgerExport(adminAuditParams(url));
sendUserBillingCsvProxyResult({ request, response, result, operation: "GET /v1/admin/billing/ledger/export", filename: "hwlab-admin-ledger.csv" });
}
async function handleAdminBillingRedeemCodesHttp(request, response, url, options) {
const context = await requireAdminBillingClient(request, response, options, `${request.method} /v1/admin/billing/redeem-codes`);
if (!context) return;
const { auth, client } = context;
let result;
if (request.method === "GET") {
if (typeof client.adminRedeemCodes !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin redeem code API is not configured", { operation: "GET /v1/admin/billing/redeem-codes", target: { type: "service", id: "hwlab-user-billing" } });
}
result = await client.adminRedeemCodes(adminRedeemParams(url));
} else {
if (typeof client.createAdminRedeemCode !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin redeem code create API is not configured", { operation: "POST /v1/admin/billing/redeem-codes", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
result = await client.createAdminRedeemCode({
...parsed.value,
createdByUserId: String(auth.actor?.id ?? ""),
createdByUsername: String(auth.actor?.username ?? ""),
metadata: { ...(parsed.value?.metadata && typeof parsed.value.metadata === "object" && !Array.isArray(parsed.value.metadata) ? parsed.value.metadata : {}), operator: adminActorSummary(auth.actor) }
});
}
sendUserBillingProxyResult({ request, response, result, operation: `${request.method} /v1/admin/billing/redeem-codes`, route: "/v1/admin/billing/redeem-codes", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingRedeemCodeHttp(request, response, codeId, options) {
const context = await requireAdminBillingClient(request, response, options, "PATCH /v1/admin/billing/redeem-codes/{codeId}");
if (!context) return;
const { auth, client } = context;
if (typeof client.updateAdminRedeemCode !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin redeem code update API is not configured", { operation: "PATCH /v1/admin/billing/redeem-codes/{codeId}", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
const result = await client.updateAdminRedeemCode(codeId, parsed.value);
sendUserBillingProxyResult({ request, response, result, operation: "PATCH /v1/admin/billing/redeem-codes/{codeId}", route: "/v1/admin/billing/redeem-codes/{codeId}", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingRedeemRedemptionsHttp(request, response, url, options) {
const context = await requireAdminBillingClient(request, response, options, "GET /v1/admin/billing/redeem-redemptions");
if (!context) return;
const { auth, client } = context;
if (typeof client.adminRedeemRedemptions !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin redeem redemptions API is not configured", { operation: "GET /v1/admin/billing/redeem-redemptions", target: { type: "service", id: "hwlab-user-billing" } });
}
const result = await client.adminRedeemRedemptions(adminRedeemParams(url));
sendUserBillingProxyResult({ request, response, result, operation: "GET /v1/admin/billing/redeem-redemptions", route: "/v1/admin/billing/redeem-redemptions", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingUserHttp(request, response, url, userId, options) {
const context = await requireAdminBillingClient(request, response, options, `${request.method} /v1/admin/billing/users/{userId}`);
if (!context) return;
const { auth, client } = context;
let result;
if (request.method === "GET") {
if (typeof client.adminUser !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin user detail API is not configured", { operation: "GET /v1/admin/billing/users/{userId}", target: { type: "service", id: "hwlab-user-billing" } });
}
result = await client.adminUser(userId);
} else {
if (typeof client.updateAdminUser !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin user update API is not configured", { operation: "PATCH /v1/admin/billing/users/{userId}", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
result = await client.updateAdminUser(userId, parsed.value);
}
sendUserBillingProxyResult({ request, response, result, operation: `${request.method} /v1/admin/billing/users/{userId}`, route: "/v1/admin/billing/users/{userId}", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingCreditAdjustHttp(request, response, userId, options) {
const context = await requireAdminBillingClient(request, response, options, "POST /v1/admin/billing/users/{userId}/credits/adjust");
if (!context) return;
const { auth, client } = context;
if (typeof client.adjustAdminCredits !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin credit adjust API is not configured", { operation: "POST /v1/admin/billing/users/{userId}/credits/adjust", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
const result = await client.adjustAdminCredits({
...parsed.value,
userId,
metadata: { ...(parsed.value?.metadata && typeof parsed.value.metadata === "object" && !Array.isArray(parsed.value.metadata) ? parsed.value.metadata : {}), operator: adminActorSummary(auth.actor) }
});
sendUserBillingProxyResult({ request, response, result, operation: "POST /v1/admin/billing/users/{userId}/credits/adjust", route: "/v1/admin/billing/users/{userId}/credits/adjust", actor: adminActorSummary(auth.actor) });
}
async function handleAdminBillingUserStatusHttp(request, response, userId, options) {
const auth = await options.accessController.authenticate(request, { required: true });
if (!auth.ok) {
sendJson(response, auth.status, auth);
return;
}
if (auth.actor?.role !== "admin") {
sendRestError(request, response, 403, "admin_required", "Only admin users can update user status", {
operation: "PATCH /v1/admin/billing/users/{userId}/status",
target: { type: "route", id: "/v1/admin/billing/users/{userId}/status" },
result: "rejected"
});
return;
}
const client = options.userBillingClient ?? options.accessController?.userBilling ?? null;
if (!client?.configured || typeof client.updateAdminUserStatus !== "function") {
sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing admin user status API is not configured", {
operation: "PATCH /v1/admin/billing/users/{userId}/status",
target: { type: "service", id: "hwlab-user-billing" }
});
return;
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) {
sendJson(response, 400, parsed.error);
return;
}
const status = String(parsed.value?.status ?? "").trim().toLowerCase();
if (!["active", "disabled", "pending"].includes(status)) {
sendRestError(request, response, 400, "invalid_status", "status must be active, disabled or pending", {
operation: "PATCH /v1/admin/billing/users/{userId}/status",
target: { type: "user", id: userId },
result: "rejected"
});
return;
}
const result = await client.updateAdminUserStatus(userId, status);
if (!result.ok) {
sendRestError(request, response, result.status ?? 502, result.error?.code ?? "user_billing_admin_user_status_failed", result.error?.message ?? "user-billing admin user status request failed", {
operation: "PATCH /v1/admin/billing/users/{userId}/status",
target: { type: "service", id: "hwlab-user-billing" },
reason: result.error?.code,
result: "failed"
});
return;
}
sendJson(response, 200, {
...(result.body ?? {}),
actor: adminActorSummary(auth.actor),
proxy: {
serviceId: CLOUD_API_SERVICE_ID,
route: "/v1/admin/billing/users/{userId}/status",
source: "hwlab-user-billing",
valuesRedacted: true
},
valuesRedacted: true
});
}
async function maybeHandleUserBillingApiKeysHttp(request, response, url, options) {
const client = options.userBillingClient ?? options.accessController?.userBilling ?? null;
if (!client?.configured) return false;
const auth = await options.accessController.authenticate(request, { required: true });
if (!auth.ok || !String(auth.authMethod ?? "").startsWith("user-billing")) return false;
const token = userBillingTokenFromAuth(auth);
if (!token) return false;
if (url.pathname === "/v1/api-keys/default") {
sendRestError(request, response, 404, "user_billing_default_key_unsupported", "user-billing API keys do not support legacy default-key regeneration", {
operation: `${request.method} /v1/api-keys/default`,
target: { type: "route", id: "/v1/api-keys/default" },
result: "rejected"
});
return true;
}
let result = null;
const path = url.pathname;
if (request.method === "GET" && path === "/v1/api-keys" && typeof client.listApiKeys === "function") {
result = await client.listApiKeys(token);
} else if (request.method === "POST" && path === "/v1/api-keys" && typeof client.createApiKey === "function") {
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) {
sendJson(response, 400, parsed.error);
return true;
}
result = await client.createApiKey(token, parsed.value);
} else {
const match = path.match(/^\/v1\/api-keys\/([^/]+)$/u);
if (match && request.method === "PATCH" && typeof client.updateApiKey === "function") {
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) {
sendJson(response, 400, parsed.error);
return true;
}
result = await client.updateApiKey(token, decodeURIComponent(match[1]), parsed.value);
} else if (match && request.method === "DELETE" && typeof client.revokeApiKey === "function") {
result = await client.revokeApiKey(token, decodeURIComponent(match[1]));
}
}
if (!result) return false;
sendUserBillingProxyResult({ request, response, result, operation: `${request.method} /v1/api-keys`, route: path, actor: adminActorSummary(auth.actor) });
return true;
}
async function handleUserBillingProfileHttp(request, response, options) {
const client = options.userBillingClient ?? options.accessController?.userBilling ?? null;
const auth = await options.accessController.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
const token = userBillingTokenFromAuth(auth);
if (!String(auth.authMethod ?? "").startsWith("user-billing")) {
return sendRestError(request, response, 403, "user_billing_auth_required", "profile update requires a user-billing bearer token", { operation: "PATCH /v1/users/me/profile", target: { type: "route", id: "/v1/users/me/profile" } });
}
if (!token || !client?.configured || typeof client.updateProfile !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing profile API is not configured", { operation: "PATCH /v1/users/me/profile", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
const result = await client.updateProfile(token, parsed.value);
sendUserBillingProxyResult({ request, response, result, operation: "PATCH /v1/users/me/profile", route: "/v1/users/me/profile", actor: adminActorSummary(auth.actor) });
}
async function handleUserBillingPasswordHttp(request, response, options) {
const client = options.userBillingClient ?? options.accessController?.userBilling ?? null;
const auth = await options.accessController.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
const token = userBillingTokenFromAuth(auth);
if (auth.authMethod !== "user-billing-session") {
return sendRestError(request, response, 403, "user_billing_session_required", "password update requires a user-billing session token", { operation: "POST /v1/users/me/password", target: { type: "route", id: "/v1/users/me/password" } });
}
if (!token || !client?.configured || typeof client.changePassword !== "function") {
return sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing password API is not configured", { operation: "POST /v1/users/me/password", target: { type: "service", id: "hwlab-user-billing" } });
}
const parsed = await readJsonObject(request, DEFAULT_BODY_LIMIT_BYTES);
if (!parsed.ok) return sendJson(response, 400, parsed.error);
const result = await client.changePassword(token, parsed.value);
sendUserBillingProxyResult({ request, response, result, operation: "POST /v1/users/me/password", route: "/v1/users/me/password", actor: adminActorSummary(auth.actor) });
}
function sendUserBillingProxyResult({ request, response, result, operation, route, actor = null }) {
if (!result?.ok) {
sendRestError(request, response, result?.status ?? 502, result?.error?.code ?? "user_billing_request_failed", result?.error?.message ?? "user-billing request failed", {
operation,
target: { type: "service", id: "hwlab-user-billing" },
reason: result?.error?.code,
result: "failed"
});
return;
}
sendJson(response, result.status ?? 200, {
...(result.body ?? {}),
actor,
proxy: {
serviceId: CLOUD_API_SERVICE_ID,
route,
source: "hwlab-user-billing",
valuesRedacted: true
},
valuesRedacted: true
});
}
function sendUserBillingCsvProxyResult({ request, response, result, operation, filename }) {
if (!result?.ok) {
sendRestError(request, response, result?.status ?? 502, result?.error?.code ?? "user_billing_request_failed", result?.error?.message ?? "user-billing export request failed", {
operation,
target: { type: "service", id: "hwlab-user-billing" },
reason: result?.error?.code,
result: "failed"
});
return;
}
response.writeHead(result.status ?? 200, {
"content-type": "text/csv; charset=utf-8",
"cache-control": "no-store",
"content-disposition": `attachment; filename="${filename}"`
});
response.end(String(result.body ?? ""));
}
function adminAuditParams(url) {
const params = {};
for (const key of ["page", "pageSize", "limit", "userId", "search", "serviceId", "resourceType", "apiKeyId", "status", "kind", "source", "idempotencyKey", "from", "to"]) {
const value = String(url.searchParams.get(key) ?? "").trim();
if (value) params[key] = value;
}
return params;
}
function adminRedeemParams(url) {
const params = {};
for (const key of ["limit", "status", "userId", "codeId"]) {
const value = String(url.searchParams.get(key) ?? "").trim();
if (value) params[key] = value;
}
return params;
}
function adminActorSummary(actor) {
return actor ? {
id: String(actor.id ?? ""),
username: String(actor.username ?? ""),
displayName: String(actor.displayName ?? ""),
role: String(actor.role ?? ""),
status: String(actor.status ?? ""),
valuesRedacted: true
} : null;
}
function bearerTokenFromRequest(request) {
const header = String(getHeader(request, "authorization") ?? "").trim();
return header.toLowerCase().startsWith("bearer ") ? header.slice(7).trim() : "";
}
function userBillingTokenFromRequest(request) {
const bearer = bearerTokenFromRequest(request);
if (bearer) return bearer;
const cookieToken = cookieValueFromRequest(request, HWLAB_WEB_SESSION_COOKIE);
return cookieToken.startsWith("hws_") ? cookieToken : "";
}
function userBillingTokenFromAuth(auth) {
const token = String(auth?.userBilling?.token ?? "").trim();
return token ? token : "";
}
function cookieValueFromRequest(request, name) {
const target = String(name ?? "");
if (!target) return "";
for (const part of String(getHeader(request, "cookie") ?? "").split(";")) {
const trimmed = part.trim();
const index = trimmed.indexOf("=");
if (index <= 0) continue;
if (trimmed.slice(0, index) !== target) continue;
try {
return decodeURIComponent(trimmed.slice(index + 1));
} catch {
return trimmed.slice(index + 1);
}
}
return "";
}
async function readJsonObject(request, limitBytes) {
const body = await readBody(request, limitBytes);
try {
const value = body ? JSON.parse(body) : {};
if (!value || typeof value !== "object" || Array.isArray(value)) {
return { ok: false, error: { accepted: false, error: { code: "invalid_params", message: "body must be a JSON object" } } };
}
return { ok: true, value };
} catch (error) {
return { ok: false, error: { accepted: false, error: { code: "parse_error", message: "Invalid JSON body", reason: error.message } } };
}
}
async function handleGatewayPollHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(response, 400, {
accepted: false,
error: {
code: "parse_error",
message: "Invalid JSON body",
reason: error.message
}
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendJson(response, 400, {
accepted: false,
error: {
code: "invalid_params",
message: "gateway poll body must be a JSON object"
}
});
return;
}
const session = options.gatewayRegistry.updateSession(params);
await persistGatewayDemoSession({
runtimeStore: options.runtimeStore,
session,
request,
env: options.env
});
const outbound = options.gatewayRegistry.nextRequest(session.gatewaySessionId);
sendJson(response, 200, {
accepted: true,
serviceId: CLOUD_API_SERVICE_ID,
mode: "gateway-outbound-poll-demo",
gatewayId: session.gatewayId,
gatewaySessionId: session.gatewaySessionId,
queueDepth: session.queue.length,
request: outbound,
type: outbound ? "request" : "noop",
observedAt: new Date().toISOString()
});
}
async function handleGatewayResultHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(response, 400, {
accepted: false,
error: {
code: "parse_error",
message: "Invalid JSON body",
reason: error.message
}
});
return;
}
const result = options.gatewayRegistry.complete(params);
sendJson(response, result.accepted ? 200 : 404, {
...result,
serviceId: CLOUD_API_SERVICE_ID,
mode: "gateway-outbound-poll-demo",
observedAt: new Date().toISOString()
});
}
async function persistGatewayDemoSession({ runtimeStore, session, request, env = process.env }) {
if (!runtimeStore) return;
const refreshMs = parsePositiveInteger(env.HWLAB_GATEWAY_REGISTRATION_REFRESH_MS, 60000);
if (session.runtimeRegisteredEpochMs && Date.now() - session.runtimeRegisteredEpochMs < refreshMs) {
return;
}
const meta = {
traceId: getHeader(request, "x-trace-id") || `trc_gateway_poll_${randomUUID()}`,
actorId: `svc_${session.serviceId}`,
serviceId: CLOUD_API_SERVICE_ID,
environment: runtimeEnvironment(env)
};
try {
await runtimeStore.registerGatewaySession?.({
projectId: session.projectId,
gatewaySessionId: session.gatewaySessionId,
serviceId: "hwlab-gateway",
gatewayId: session.gatewayId,
endpoint: session.endpoint,
status: "connected",
labels: {
demoOutboundPoll: "true",
os: String(session.system?.platform ?? "unknown")
}
}, meta);
await runtimeStore.registerBoxResource?.({
projectId: session.projectId,
gatewaySessionId: session.gatewaySessionId,
resourceId: session.resourceId,
boxId: session.boxId,
resourceType: "simulator_endpoint",
state: "available",
metadata: {
serviceId: "hwlab-gateway",
demoShellHost: true
}
}, meta);
if (session.capabilities.length > 0) {
await runtimeStore.reportBoxCapabilities?.({
capabilities: session.capabilities.map((capability) => ({
...capability,
resourceId: capability.resourceId ?? session.resourceId,
projectId: capability.projectId ?? session.projectId
}))
}, meta);
}
session.runtimeRegisteredAt = new Date().toISOString();
session.runtimeRegisteredEpochMs = Date.now();
} catch {
// Demo polling must keep the gateway online even when durable runtime writes are not ready.
}
}
function sendRestError(request, response, statusCode, code, message, context = {}) {
const requestId = request.hwlabHttpRequestContext?.requestId || getHeader(request, "x-request-id") || "req_unassigned";
const environment = context.environment || runtimeEnvironment(context.env ?? process.env);
sendJson(response, statusCode, {
error: {
code,
message,
reason: context.reason,
audit: createAuditRecord({
requestId,
actor: {
type: "user",
id: getHeader(request, "x-actor-id") || `system_${CLOUD_API_SERVICE_ID}`
},
source: {
serviceId: CLOUD_API_SERVICE_ID,
environment,
traceId: getHeader(request, "x-trace-id"),
adapter: "rest"
},
operation: context.operation || `${request.method || "GET"} ${request.url || "/"}`,
target: context.target || {
type: "route",
id: request.url || "/"
},
result: context.result || "failed"
})
}
});
}
function statusForRpcResponse(rpcResponse) {
if (!rpcResponse.error) {
return 200;
}
if (rpcResponse.error.code === ERROR_CODES.invalidRequest || rpcResponse.error.code === ERROR_CODES.parseError) {
return 400;
}
if (rpcResponse.error.code === ERROR_CODES.methodNotFound) {
return 404;
}
return 200;
}
function readBody(request, limitBytes = DEFAULT_BODY_LIMIT_BYTES) {
const limit = Number.isInteger(limitBytes) && limitBytes > 0 ? limitBytes : DEFAULT_BODY_LIMIT_BYTES;
return new Promise((resolve, reject) => {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
if (Buffer.byteLength(body, "utf8") > limit) {
request.destroy(new Error(`request body exceeds ${limit} bytes`));
}
});
request.on("end", () => resolve(body));
request.on("error", reject);
});
}
function sendJson(response, statusCode, body) {
if (response.headersSent || response.writableEnded) return false;
const payload = decorateErrorPayloadForHttp(body, { response, statusCode });
logErrorEnvelope(payload, { statusCode });
response.writeHead(statusCode, {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store"
});
response.end(`${JSON.stringify(payload)}\n`);
return true;
}
function getHeader(request, name) {
const value = request.headers[name.toLowerCase()];
if (Array.isArray(value)) {
return value[0];
}
return value;
}
function safeRequestId(value) {
const text = String(value ?? "").trim();
return /^[A-Za-z0-9_.:-]{3,180}$/u.test(text) ? text : null;
}
function firstHeaderValue(request, name) {
return getHeader(request, name);
}
function truthyFlag(value) {
return /^(?:1|true|yes|on|enabled)$/iu.test(String(value ?? "").trim());
}
function safeTraceId(value) {
const text = String(value ?? "").trim();
return /^trc_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
}
function safeSessionId(value) {
const text = String(value ?? "").trim();
return /^ses_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
}
function safeOpaqueId(value) {
const text = String(value ?? "").trim();
return /^[A-Za-z0-9_.:-]{3,180}$/u.test(text) ? text : null;
}
function isAuthorizedWorkbenchDrainRequest(request, env = process.env) {
const expected = String(env.HOSTNAME ?? "").trim();
if (!expected) return false;
return getHeader(request, "x-hwlab-drain-hostname") === expected;
}
function parsePositiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
function positiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
async function runtimeReadiness(runtimeStore) {
if (typeof runtimeStore?.readiness === "function") {
return runtimeStore.readiness();
}
return runtimeStore.summary();
}