import { readFile } from "node:fs/promises"; import { request as httpRequest } from "node:http"; import { request as httpsRequest } from "node:https"; import { ENVIRONMENT_DEV } from "../protocol/index.mjs"; import { DEFAULT_GATEWAY_SESSION_ID, DEFAULT_PROJECT_ID } from "../sim/model.mjs"; import { PATCH_PANEL_SERVICE_ID, createPatchPanel, createWiringConfig, endpointKey } from "./model.mjs"; const DEFAULT_APPLIED_AT = "not_applied"; function isoNow() { return new Date().toISOString(); } function clone(value) { return JSON.parse(JSON.stringify(value)); } function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function ensureObject(value, label) { if (!isObject(value)) { throw new Error(`${label} must be an object`); } } function normalizeEndpointMap(raw = {}) { ensureObject(raw, "endpoint map"); const byResourceId = new Map(); const diagnostics = []; for (const [resourceId, endpoint] of Object.entries(raw)) { if (typeof endpoint !== "string" || endpoint.trim().length === 0) { diagnostics.push({ level: "error", code: "endpoint_url_invalid", message: `endpoint map entry ${resourceId} must be a non-empty URL string`, path: `$.endpointMap.${resourceId}` }); continue; } try { const normalized = new URL(endpoint).toString(); byResourceId.set(resourceId, normalized); } catch { diagnostics.push({ level: "error", code: "endpoint_url_invalid", message: `endpoint map entry ${resourceId} is not a valid URL: ${endpoint}`, path: `$.endpointMap.${resourceId}` }); } } return { byResourceId, diagnostics }; } function wiringFromRuntimeConfig(config, now) { if (isObject(config.wiringConfig)) { return clone(config.wiringConfig); } return createWiringConfig({ wiringConfigId: config.wiringConfigId ?? "wir_patch_panel_runtime", projectId: config.projectId ?? DEFAULT_PROJECT_ID, gatewaySessionId: config.gatewaySessionId ?? DEFAULT_GATEWAY_SESSION_ID, name: config.name ?? "Patch panel runtime wiring", status: config.status ?? "active", connections: config.connections, constraints: config.constraints, now }); } export async function readPatchPanelRuntimeConfig(path) { const raw = await readFile(path, "utf8"); return JSON.parse(raw); } export function createPatchPanelRuntime({ wiringConfig, endpointMap = {}, configSource = "inline", now = isoNow, requestJson = postJson }) { let appliedAt = DEFAULT_APPLIED_AT; let activeWiringConfig = null; let patchPanel = null; let endpoints = new Map(); let configDiagnostics = []; let evidenceSummary = { configApplied: false, configSource, appliedAt, lastSyncAt: "not_run", routeCount: 0, tickCount: 0, deliveryCount: 0, failedDeliveryCount: 0 }; function applyConfig(config) { const observedAt = now(); const source = config.configSource ?? configSource; const runtimeConfig = isObject(config.wiringConfig) || Array.isArray(config.connections) ? config : {}; const nextWiringConfig = wiringFromRuntimeConfig(runtimeConfig, observedAt); const endpointResult = normalizeEndpointMap(runtimeConfig.endpointMap ?? config.endpointMap ?? endpointMap); const nextPatchPanel = createPatchPanel({ wiringConfig: nextWiringConfig, now }); const endpointDiagnostics = endpointResult.diagnostics; const targetDiagnostics = []; for (const connection of nextPatchPanel.normalizedConnections) { const targetResourceId = connection.to.resourceId; if (connection.syncable && !endpointResult.byResourceId.has(targetResourceId)) { targetDiagnostics.push({ level: "error", code: "target_endpoint_missing", message: `sync target ${targetResourceId}:${connection.to.port} has no endpoint URL in endpointMap`, path: `$.endpointMap.${targetResourceId}` }); } } activeWiringConfig = nextWiringConfig; patchPanel = nextPatchPanel; endpoints = endpointResult.byResourceId; configDiagnostics = [ ...nextPatchPanel.diagnostics, ...endpointDiagnostics, ...targetDiagnostics ]; appliedAt = observedAt; evidenceSummary = { ...evidenceSummary, configApplied: true, configSource: source, appliedAt, wiringConfigId: activeWiringConfig.wiringConfigId, connectionCount: activeWiringConfig.connections.length, syncableConnectionCount: nextPatchPanel.normalizedConnections.filter((connection) => connection.syncable).length, diagnostics: diagnosticSummary(configDiagnostics) }; return { accepted: diagnosticSummary(configDiagnostics).errorCount === 0, appliedAt, wiringConfigId: activeWiringConfig.wiringConfigId, diagnostics: configDiagnostics, summary: evidenceSummary }; } async function reloadFromFile(path) { const config = await readPatchPanelRuntimeConfig(path); return applyConfig({ ...config, configSource: path }); } function diagnostics() { return { serviceId: PATCH_PANEL_SERVICE_ID, environment: ENVIRONMENT_DEV, state: diagnosticSummary(configDiagnostics).errorCount > 0 ? "faulted" : "active", observedAt: now(), configSource: evidenceSummary.configSource, appliedAt, wiringConfigId: activeWiringConfig?.wiringConfigId ?? null, endpointMap: Object.fromEntries(endpoints.entries()), diagnostics: configDiagnostics, summary: evidenceSummary }; } function status() { const state = patchPanel.status(); const summary = diagnosticSummary(configDiagnostics); return { ...state, state: summary.errorCount > 0 ? "faulted" : state.state, metadata: { ...state.metadata, configSource: evidenceSummary.configSource, appliedAt, diagnostics: summary, evidenceSummary } }; } async function routeSignal(signal) { const result = patchPanel.routeSignal(signal); const diagnostics = [...(result.diagnostics ?? [])]; if (!result.accepted) { evidenceSummary = { ...evidenceSummary, routeCount: evidenceSummary.routeCount + 1, lastSyncAt: result.observedAt, diagnostics: diagnosticSummary([...configDiagnostics, ...diagnostics]) }; return { ...result, ...(diagnostics.length > 0 ? { diagnostics } : {}) }; } const hasEndpointDeliveries = result.deliveries.some((delivery) => { const connection = findDeliveryConnection(delivery); return connection?.syncable === true; }); if (!hasEndpointDeliveries) { evidenceSummary = { ...evidenceSummary, routeCount: evidenceSummary.routeCount + 1, lastSyncAt: result.observedAt, deliveryCount: evidenceSummary.deliveryCount + result.deliveries.length, diagnostics: diagnosticSummary([...configDiagnostics, ...diagnostics]) }; return { ...result, ...(diagnostics.length > 0 ? { diagnostics } : {}) }; } const deliveries = []; for (const delivery of result.deliveries) { const connection = findDeliveryConnection(delivery); const write = await deliverToEndpoint(delivery, { requireEndpoint: connection?.syncable === true }); deliveries.push({ ...delivery, deliveryStatus: write.deliveryStatus ?? (write.accepted ? "applied" : "failed"), endpoint: write.endpoint ?? null, diagnostic: write.diagnostic ?? null }); if (write.diagnostic) { diagnostics.push(write.diagnostic); } } const failedDeliveryCount = deliveries.filter((delivery) => delivery.deliveryStatus === "failed").length; evidenceSummary = { ...evidenceSummary, routeCount: evidenceSummary.routeCount + 1, lastSyncAt: result.observedAt, deliveryCount: evidenceSummary.deliveryCount + deliveries.length, failedDeliveryCount: evidenceSummary.failedDeliveryCount + failedDeliveryCount, diagnostics: diagnosticSummary([...configDiagnostics, ...diagnostics]) }; return { ...result, accepted: result.accepted && failedDeliveryCount === 0, deliveryCount: deliveries.length, deliveries, ...(diagnostics.length > 0 ? { diagnostics } : {}) }; } async function tick({ signals = [] } = {}) { if (!Array.isArray(signals)) { throw new Error("tick signals must be an array"); } const startedAt = now(); const routed = []; for (const signal of signals) { routed.push(await routeSignal(signal)); } evidenceSummary = { ...evidenceSummary, tickCount: evidenceSummary.tickCount + 1, lastSyncAt: now() }; return { accepted: routed.every((result) => result.accepted), propagatedBy: PATCH_PANEL_SERVICE_ID, startedAt, observedAt: evidenceSummary.lastSyncAt, routeCount: routed.length, deliveryCount: routed.reduce((sum, result) => sum + result.deliveryCount, 0), routes: routed, diagnostics: routed.flatMap((result) => result.diagnostics ?? []), evidenceSummary }; } function findDeliveryConnection(delivery) { return patchPanel.normalizedConnections.find( (connection) => connection.from.resourceId === delivery.sourceResourceId && connection.from.port === delivery.sourcePort && connection.to.resourceId === delivery.resourceId && connection.to.port === delivery.port ); } async function deliverToEndpoint(delivery, { requireEndpoint }) { const endpoint = endpoints.get(delivery.resourceId); if (!endpoint) { if (!requireEndpoint) { return { accepted: true, deliveryStatus: "recorded" }; } return { accepted: false, diagnostic: { level: "error", code: "target_endpoint_missing", message: `cannot deliver ${delivery.sourceResourceId}:${delivery.sourcePort} -> ${delivery.resourceId}:${delivery.port}; endpointMap is missing ${delivery.resourceId}`, path: `$.endpointMap.${delivery.resourceId}`, endpointKey: endpointKey(delivery.resourceId, delivery.port) } }; } const url = new URL("/internal/ports/write", endpoint.endsWith("/") ? endpoint : `${endpoint}/`).toString(); try { const response = await requestJson(url, { port: delivery.port, value: delivery.value, source: "patch-panel", sourceResourceId: delivery.sourceResourceId, sourcePort: delivery.sourcePort, propagatedBy: PATCH_PANEL_SERVICE_ID, observedAt: delivery.observedAt }); if (response.status < 200 || response.status >= 300 || response.json?.accepted !== true) { return { accepted: false, endpoint: url, diagnostic: { level: "error", code: "target_write_rejected", message: `target endpoint rejected ${delivery.resourceId}:${delivery.port} write with status ${response.status}`, path: `$.endpointMap.${delivery.resourceId}`, response: response.json ?? response.bodyPreview } }; } return { accepted: true, endpoint: url, response: response.json }; } catch (error) { return { accepted: false, endpoint: url, diagnostic: { level: "error", code: "target_write_failed", message: `failed to deliver ${delivery.resourceId}:${delivery.port}: ${error instanceof Error ? error.message : String(error)}`, path: `$.endpointMap.${delivery.resourceId}` } }; } } applyConfig({ wiringConfig, endpointMap, configSource }); return { applyConfig, reloadFromFile, diagnostics, status, routeSignal, tick, get wiringConfig() { return activeWiringConfig; }, get endpointMap() { return Object.fromEntries(endpoints.entries()); }, get evidenceSummary() { return evidenceSummary; } }; } export async function createPatchPanelRuntimeFromFile({ path, now = isoNow, requestJson = postJson }) { const config = await readPatchPanelRuntimeConfig(path); return createPatchPanelRuntime({ ...config, configSource: path, now, requestJson }); } export function diagnosticSummary(diagnostics) { return { errorCount: diagnostics.filter((item) => item.level === "error").length, warningCount: diagnostics.filter((item) => item.level === "warning").length, infoCount: diagnostics.filter((item) => item.level === "info").length }; } export async function postJson(url, body, { timeoutMs = 5000 } = {}) { return new Promise((resolve, reject) => { const parsed = new URL(url); const payload = JSON.stringify(body); const requestFn = parsed.protocol === "https:" ? httpsRequest : httpRequest; const req = requestFn( parsed, { method: "POST", headers: { "content-type": "application/json", "content-length": Buffer.byteLength(payload) }, timeout: timeoutMs }, (response) => { let text = ""; response.setEncoding("utf8"); response.on("data", (chunk) => { text += chunk; }); response.on("end", () => { let json = null; try { json = text ? JSON.parse(text) : null; } catch { json = null; } resolve({ status: response.statusCode ?? 0, json, bodyPreview: json ? undefined : text.slice(0, 500) }); }); } ); req.on("timeout", () => { req.destroy(new Error(`request timed out after ${timeoutMs}ms`)); }); req.on("error", reject); req.write(payload); req.end(); }); }