#!/usr/bin/env node import { createAuditEvent, createEvidenceRecord, operationContext } from "../../internal/sim/l2-runtime.mjs"; import { createGatewayState, createHealth, resourceIdForBox, selectInstanceValue } from "../../internal/sim/model.mjs"; import { fetchJson, createJsonServer, listen, parsePort, readJson } from "../../internal/sim/http.mjs"; import { createRpcErrorBody, ERROR_CODES } from "../../internal/protocol/index.mjs"; const gatewayId = selectInstanceValue(process.env.HWLAB_GATEWAY_ID, "gateway-a", { requireIndexForMultiple: true, label: "HWLAB_GATEWAY_ID" }); const port = parsePort(process.env.PORT, 7101); const endpoint = `http://127.0.0.1:${port}`; const boxUrls = parseBoxUrls(process.env.HWLAB_BOX_URLS); const boxes = parseBoxes({ resources: process.env.HWLAB_BOX_RESOURCES, ids: process.env.HWLAB_BOX_IDS, urls: boxUrls, instanceScoped: parseList(process.env.HWLAB_GATEWAY_ID).length > 1 }); const state = createGatewayState({ gatewayId, endpoint, boxes: boxes.map((box) => box.resourceId) }); state.registry = { gatewayId, gatewaySessionId: state.session.gatewaySessionId, endpoint, boxes }; state.lastHeartbeatAt = state.session.lastSeenAt; const routes = new Map(); routes.set("GET /health/live", () => ({ ...createHealth({ serviceId: "hwlab-gateway-simu", name: gatewayId }), gatewayId, gatewaySessionId: state.session.gatewaySessionId }) ); routes.set("GET /status", () => state); routes.set("GET /boxes", () => ({ gatewayId, gatewaySessionId: state.session.gatewaySessionId, boxes: state.registry.boxes })); routes.set("POST /register", async ({ req }) => { const body = await readJson(req); const now = new Date().toISOString(); const registered = normalizeBoxRegistration(body); upsertBox(registered); state.boxes = state.registry.boxes.map((box) => box.resourceId); state.updatedAt = now; state.session.lastSeenAt = now; return { accepted: true, serviceId: "hwlab-gateway-simu", gatewayId, gatewaySessionId: state.session.gatewaySessionId, registered, registry: state.registry }; }); routes.set("POST /heartbeat", async ({ req }) => { const body = await readJson(req); const now = new Date().toISOString(); state.lastHeartbeatAt = now; state.session.lastSeenAt = now; state.updatedAt = now; return { accepted: true, serviceId: "hwlab-gateway-simu", gatewayId, gatewaySessionId: state.session.gatewaySessionId, heartbeat: { observedAt: now, boxCount: state.registry.boxes.length, reportedBy: body.reportedBy ?? gatewayId } }; }); routes.set("GET /capabilities", async () => capabilityReport()); routes.set("POST /capabilities/report", async () => capabilityReport()); routes.set("POST /invoke", async ({ req }) => { const body = await readJson(req); return invokeBox(body); }); function rpcHttpError(statusCode, { code, message, data = {} }) { return Object.assign(new Error(message), { statusCode, body: createRpcErrorBody({ code, message, data }) }); } function parseBoxUrls(value) { return String(value ?? "") .split(",") .map((item) => item.trim()) .filter(Boolean); } function parseList(value) { return String(value ?? "") .split(",") .map((item) => item.trim()) .filter(Boolean); } function parseBoxes({ resources, ids, urls, instanceScoped = false }) { const explicitResources = instanceScoped ? selectScopedList(resources, "HWLAB_BOX_RESOURCES") : parseList(resources); const explicitIds = instanceScoped ? selectScopedList(ids, "HWLAB_BOX_IDS") : parseList(ids); const explicitUrls = instanceScoped ? selectScopedList(urls.join(","), "HWLAB_BOX_URLS") : urls; const count = Math.max(explicitResources.length, explicitIds.length, explicitUrls.length, 1); const parsed = []; for (let index = 0; index < count; index += 1) { const boxId = explicitIds[index] ?? explicitResources[index]?.replace(/^res_/, "") ?? (count === 1 ? "boxsim_alpha" : `box-${index + 1}`); const resourceId = explicitResources[index] ?? resourceIdForBox(boxId); parsed.push({ boxId, resourceId, url: explicitUrls[index] ?? null, state: "registered" }); } return parsed; } function selectScopedList(value, label = "scoped list") { const items = parseList(value); if (items.length <= 1) { return items; } const selected = selectInstanceValue(items.join(","), "", { requireIndexForMultiple: true, label }); return selected ? [selected] : []; } function normalizeBoxRegistration(body) { const boxId = body.boxId ?? body.identity?.boxId; const resourceId = body.resourceId ?? body.resource?.resourceId ?? (boxId ? resourceIdForBox(boxId) : null); if (!boxId || !resourceId) { throw rpcHttpError(400, { code: ERROR_CODES.invalidParams, message: "register requires boxId or resourceId", data: { reason: "identity_required" } }); } return { boxId, resourceId, url: body.url ?? body.endpoint ?? null, state: "registered", capabilities: Array.isArray(body.capabilities) ? body.capabilities : undefined, registeredAt: new Date().toISOString() }; } function upsertBox(box) { const index = state.registry.boxes.findIndex((item) => item.resourceId === box.resourceId); if (index >= 0) { state.registry.boxes[index] = { ...state.registry.boxes[index], ...box }; return; } state.registry.boxes.push(box); } async function capabilityReport() { const reports = []; for (const box of state.registry.boxes) { if (box.url) { try { const response = await fetchJson(new URL("/capabilities", box.url)); if (response.ok) { const body = response.body; const capabilities = body?.capabilities ?? []; upsertBox({ ...box, boxId: body?.boxId ?? box.boxId, resourceId: body?.resourceId ?? box.resourceId, capabilities }); reports.push({ boxId: body?.boxId ?? box.boxId, resourceId: body?.resourceId ?? box.resourceId, url: box.url, status: "available", capabilities }); continue; } } catch (error) { reports.push({ boxId: box.boxId, resourceId: box.resourceId, url: box.url, status: "unreachable", capabilities: box.capabilities ?? [], error: error instanceof Error ? error.message : String(error) }); continue; } } reports.push({ boxId: box.boxId, resourceId: box.resourceId, url: box.url, status: box.capabilities ? "cached" : "unreachable", capabilities: box.capabilities ?? [] }); } state.capabilityReport = { serviceId: "hwlab-gateway-simu", gatewayId, gatewaySessionId: state.session.gatewaySessionId, reportedAt: new Date().toISOString(), boxes: reports }; return state.capabilityReport; } function findBoxTarget(body) { const resourceId = body.resourceId; const boxId = body.boxId; return state.registry.boxes.find( (box) => (resourceId && box.resourceId === resourceId) || (boxId && box.boxId === boxId) ); } async function invokeBox(body) { const box = findBoxTarget(body); if (!box) { throw rpcHttpError(404, { code: ERROR_CODES.sessionNotFound, message: "Target box is not registered with this gateway", data: { gatewayId, gatewaySessionId: state.session.gatewaySessionId, boxId: body.boxId ?? null, resourceId: body.resourceId ?? null } }); } if (!box.url) { throw rpcHttpError(409, { code: ERROR_CODES.operationRejected, message: "Target box has no invoke URL", data: { gatewayId, resourceId: box.resourceId, reason: "box_url_missing" } }); } const context = operationContext(body, { serviceId: "hwlab-gateway-simu", actorId: "svc_hwlab-gateway-simu" }); const forwarded = { ...body, operationId: context.operationId, traceId: context.traceId, actorType: "service", actorId: "svc_hwlab-gateway-simu", source: body.source ?? "gateway-simu" }; const response = await fetchJson(new URL("/invoke", box.url), { method: "POST", body: forwarded }); if (!response.ok) { throw Object.assign(new Error("box invoke failed"), { statusCode: response.status, body: response.body ?? createRpcErrorBody({ code: ERROR_CODES.hwlabUnknown, message: "Box invoke failed", data: { status: response.status } }) }); } const now = new Date().toISOString(); const audit = createAuditEvent({ serviceId: "hwlab-gateway-simu", action: "hardware.operation.dispatched", targetType: "box_resource", targetId: box.resourceId, projectId: state.projectId, gatewaySessionId: state.session.gatewaySessionId, operationId: context.operationId, traceId: context.traceId, actorType: "service", actorId: "svc_hwlab-gateway-simu", outcome: "succeeded", metadata: { gatewayId, boxId: box.boxId, resourceId: box.resourceId, capabilityId: body.capabilityId ?? null, port: body.port ?? null, forwardedTo: box.url }, occurredAt: now }); const evidence = createEvidenceRecord({ serviceId: "hwlab-gateway-simu", projectId: state.projectId, operationId: context.operationId, traceId: context.traceId, kind: "trace", payload: { gatewayId, boxId: box.boxId, resourceId: box.resourceId, result: response.body }, metadata: { gatewaySessionId: state.session.gatewaySessionId, forwardedTo: box.url, producerServiceId: "hwlab-gateway-simu" }, createdAt: now }); state.lastDispatch = { operationId: context.operationId, traceId: context.traceId, resourceId: box.resourceId, observedAt: now, accepted: true }; state.updatedAt = now; return { accepted: true, serviceId: "hwlab-gateway-simu", gatewayId, gatewaySessionId: state.session.gatewaySessionId, operationId: context.operationId, traceId: context.traceId, target: { boxId: box.boxId, resourceId: box.resourceId, url: box.url }, result: response.body, auditId: audit.auditId, evidenceId: evidence.evidenceId, audit, evidence }; } listen(createJsonServer({ routes }), port);