import { randomUUID, X509Certificate } from "node:crypto"; import { createSecureServer } from "node:http2"; import { DEVICE_EDGE_CHANNEL_LIMITS, DEVICE_EDGE_CHANNEL_PATH, createChannelEnvelope, createChannelEnvelopeDecoder, encodeChannelEnvelope, normalizeCertificateFingerprint, } from "../../../packages/device-edge-channel-contract/src/index.mjs"; import { normalizeAdapterAcceptance, normalizeAdapterMessage, normalizeDiscoverySignal, } from "../../../packages/device-protocol-contract/src/index.mjs"; const CHANNEL_TRACKER_SESSION_ID = "channel:control"; const CHANNEL_PROFILE_REF = "channel.control.v1"; export function createDeviceEdgeChannelServer(options = {}) { const config = normalizeConfig(options); let trust = config.trust; const pending = new Map(); let active = null; let started = false; let heartbeatTimer = null; let totalChannelsAccepted = 0; let totalChannelsRejected = 0; let totalEventsSubmitted = 0; let totalEventsAccepted = 0; let totalEventsRejected = 0; let totalProtocolFailures = 0; const server = createSecureServer({ key: trust.key, cert: trust.cert, ca: trust.ca, minVersion: "TLSv1.3", maxVersion: "TLSv1.3", allowHTTP1: false, requestCert: true, rejectUnauthorized: true, settings: { enablePush: false, maxConcurrentStreams: 1, initialWindowSize: 1024 * 1024, }, }); server.on("stream", (stream, headers) => { if (headers[":method"] !== "POST" || headers[":path"] !== DEVICE_EDGE_CHANNEL_PATH) { stream.respond({ ":status": 404, "cache-control": "no-store" }); stream.end(); return; } if (!isApprovedCorePeer(stream)) { totalChannelsRejected += 1; stream.respond({ ":status": 403, "cache-control": "no-store" }); stream.end(); return; } if (active) { totalChannelsRejected += 1; stream.respond({ ":status": 409, "cache-control": "no-store" }); stream.end(); return; } const state = { stream, decoder: createChannelEnvelopeDecoder({ direction: "core-to-edge", maxEnvelopeBytes: config.maxEnvelopeBytes, }), accepted: false, edgeSequence: 0, coreSequence: 0, lastCoreActivityAt: config.clock(), closed: false, }; active = state; stream.respond({ ":status": 200, "content-type": "application/x-ndjson", "cache-control": "no-store", "x-content-type-options": "nosniff", }); send(state, "channel.hello", { status: "ready", transport: "http2-mtls", trustGeneration: trust.generationRef, commandTransport: config.commandTransport, }, { trackerSessionId: CHANNEL_TRACKER_SESSION_ID, adapterProfileRef: CHANNEL_PROFILE_REF, correlationId: `correlation:${randomUUID()}`, }); let processing = Promise.resolve(); stream.on("data", (chunk) => { stream.pause(); processing = processing .then(async () => { const envelopes = state.decoder.push(chunk); for (const envelope of envelopes) { await handleCoreEnvelope(state, envelope); } }) .catch(() => protocolFailure(state)) .finally(() => { if (!state.closed) stream.resume(); }); }); stream.on("aborted", () => closeActive(state)); stream.on("close", () => closeActive(state)); stream.on("error", () => closeActive(state)); }); server.on("tlsClientError", () => { totalChannelsRejected += 1; }); server.on("sessionError", () => { totalProtocolFailures += 1; }); return Object.freeze({ async start() { if (started) return server.address(); await listen(server, config.port, config.host); started = true; heartbeatTimer = setInterval(checkChannelHealth, config.keepaliveMs); heartbeatTimer.unref?.(); return server.address(); }, async stop() { clearInterval(heartbeatTimer); heartbeatTimer = null; const current = active; if (current) { current.stream.close(); closeActive(current); } rejectAllPending("device_edge_channel_stopped"); if (started) await closeServer(server); started = false; }, async submitDiscovery(signal) { const normalized = normalizeDiscoverySignal(signal); const result = await submitEvent("discovery.observed", { signal: normalized, }, { trackerSessionId: normalized.sessionRef, adapterProfileRef: normalized.modelProfileRef, eventAt: normalized.observedAt, }); if (!result?.discovery) { throw new Error("device_edge_channel_discovery_acceptance_invalid"); } return Object.freeze({ ...result.discovery, ...(result.commandOffer ? { commandOffer: result.commandOffer } : {}), }); }, async submitAdapterMessage(message) { const normalized = normalizeAdapterMessage(message, { maxBytes: config.maxEnvelopeBytes, }); const result = await submitEvent("adapter.message", { message: normalized, }, { trackerSessionId: normalized.sessionRef, adapterProfileRef: normalized.protocolProfileRef, eventAt: normalized.observedAt, }); const acceptance = normalizeAdapterAcceptance(result?.acceptance); if (acceptance.idempotencyKey !== normalized.idempotencyKey) { throw new Error("device_edge_channel_acceptance_mismatch"); } return Object.freeze({ ...acceptance, ...(result.commandOffer ? { commandOffer: result.commandOffer } : {}), }); }, async submitCommandStatus(status) { const normalized = normalizeCommandStatus(status); const result = await submitEvent("command.status", { status: normalized, }, { trackerSessionId: normalized.sessionRef, adapterProfileRef: normalized.adapterProfileRef, eventAt: normalized.observedAt, }); if (result?.status !== "recorded") { throw new Error("device_edge_channel_command_status_invalid"); } return Object.freeze({ status: "recorded" }); }, status() { return Object.freeze({ listening: started, channel: active?.accepted ? "accepted" : active ? "negotiating" : "absent", edgeRegistrationId: config.edgeRegistrationId, channelGeneration: config.channelGeneration, trustGeneration: trust.generationRef, edgeCertificateFingerprint: trust.certificateFingerprint, pendingAcceptances: pending.size, channelsAccepted: totalChannelsAccepted, channelsRejected: totalChannelsRejected, eventsSubmitted: totalEventsSubmitted, eventsAccepted: totalEventsAccepted, eventsRejected: totalEventsRejected, protocolFailures: totalProtocolFailures, trackerIngress: "disabled", commandTransport: config.commandTransport, }); }, rotateTrust(next) { const nextTrust = normalizeTrust(next); if (nextTrust.generationRef === trust.generationRef) { throw new TypeError("device_edge_channel_trust_generation_unchanged"); } server.setSecureContext({ key: nextTrust.key, cert: nextTrust.cert, ca: nextTrust.ca, minVersion: "TLSv1.3", maxVersion: "TLSv1.3", }); trust = nextTrust; const current = active; if (current) { current.stream.close(); closeActive(current); } return Object.freeze({ trustGeneration: trust.generationRef, edgeCertificateFingerprint: trust.certificateFingerprint, channel: "reconnect-required", }); }, disconnectActiveChannel() { active?.stream.close(); }, }); async function submitEvent(messageKind, payload, metadata) { const state = active; if (!state?.accepted || state.closed) { throw new Error("device_edge_channel_unavailable"); } if (pending.size >= config.maxPendingAcceptances) { throw new Error("device_edge_channel_acceptance_window_full"); } const correlationId = `correlation:${randomUUID()}`; totalEventsSubmitted += 1; return new Promise((resolve, reject) => { const timer = setTimeout(() => { pending.delete(correlationId); totalEventsRejected += 1; reject(new Error("device_edge_channel_acceptance_timeout")); }, config.acceptanceTimeoutMs); timer.unref?.(); pending.set(correlationId, { resolve, reject, timer }); try { send(state, messageKind, payload, { ...metadata, correlationId, }); } catch (error) { clearTimeout(timer); pending.delete(correlationId); totalEventsRejected += 1; reject(error); } }); } async function handleCoreEnvelope(state, envelope) { assertActiveState(state); if ( envelope.edgeRegistrationId !== config.edgeRegistrationId || envelope.channelGeneration !== config.channelGeneration || envelope.sequence !== state.coreSequence + 1 ) { throw new Error("device_edge_channel_core_envelope_mismatch"); } state.coreSequence = envelope.sequence; state.lastCoreActivityAt = config.clock(); if (!state.accepted) { if (envelope.messageKind !== "channel.accepted") { throw new Error("device_edge_channel_acceptance_required"); } if ( envelope.payload?.status !== "accepted" || envelope.payload?.commandTransport !== config.commandTransport ) { throw new Error("device_edge_channel_acceptance_invalid"); } state.accepted = true; totalChannelsAccepted += 1; return; } if (envelope.messageKind === "channel.heartbeat") return; if (!["event.accepted", "event.rejected"].includes(envelope.messageKind)) { throw new Error("device_edge_channel_core_message_unhandled"); } const receipt = pending.get(envelope.correlationId); if (!receipt) { throw new Error("device_edge_channel_correlation_unknown"); } clearTimeout(receipt.timer); pending.delete(envelope.correlationId); if (envelope.messageKind === "event.accepted") { totalEventsAccepted += 1; receipt.resolve(envelope.payload?.result); return; } totalEventsRejected += 1; receipt.reject(new Error(normalizeRejectionCode(envelope.payload?.errorCode))); } function send(state, messageKind, payload, metadata) { assertActiveState(state); state.edgeSequence += 1; const now = config.now(); const envelope = createChannelEnvelope({ edgeRegistrationId: config.edgeRegistrationId, channelGeneration: config.channelGeneration, trackerSessionId: metadata.trackerSessionId, adapterProfileRef: metadata.adapterProfileRef, sequence: state.edgeSequence, eventAt: metadata.eventAt ?? now, receivedAt: now, messageKind, correlationId: metadata.correlationId, payload, }, { direction: "edge-to-core", maxEnvelopeBytes: config.maxEnvelopeBytes, }); const encoded = encodeChannelEnvelope(envelope, { direction: "edge-to-core", maxEnvelopeBytes: config.maxEnvelopeBytes, }); if (!state.stream.write(encoded)) { state.stream.once("drain", () => {}); } } function checkChannelHealth() { const state = active; if (!state || state.closed) return; if (config.clock() - state.lastCoreActivityAt >= config.deadPeerMs) { protocolFailure(state); return; } if (state.accepted) { try { send(state, "channel.heartbeat", { status: "alive" }, { trackerSessionId: CHANNEL_TRACKER_SESSION_ID, adapterProfileRef: CHANNEL_PROFILE_REF, correlationId: `correlation:${randomUUID()}`, }); } catch { protocolFailure(state); } } } function isApprovedCorePeer(stream) { const socket = stream.session?.socket; if (!socket?.authorized) return false; let fingerprint; try { fingerprint = normalizeCertificateFingerprint( socket.getPeerCertificate()?.fingerprint256, ); } catch { return false; } return trust.allowedCoreFingerprints.has(fingerprint); } function protocolFailure(state) { totalProtocolFailures += 1; state.stream.close(); closeActive(state); } function closeActive(state) { if (state.closed) return; state.closed = true; if (active === state) active = null; rejectAllPending("device_edge_channel_disconnected"); } function rejectAllPending(code) { for (const receipt of pending.values()) { clearTimeout(receipt.timer); receipt.reject(new Error(code)); } pending.clear(); } function assertActiveState(state) { if (!state || state.closed || active !== state) { throw new Error("device_edge_channel_unavailable"); } } } function normalizeConfig(options) { const edgeRegistrationId = normalizeRef( options.edgeRegistrationId, "edge_registration_id", ); const channelGeneration = normalizeRef( options.channelGeneration, "channel_generation", ); const trust = normalizeTrust({ ...options.tls, generationRef: options.trustGeneration, }); const keepaliveMs = normalizeDuration(options.keepaliveMs, 10, DEVICE_EDGE_CHANNEL_LIMITS.keepaliveMs, "keepalive"); const deadPeerMs = normalizeDuration(options.deadPeerMs, keepaliveMs * 2, DEVICE_EDGE_CHANNEL_LIMITS.deadPeerMs, "dead_peer"); if (deadPeerMs < keepaliveMs * 2) { throw new TypeError("device_edge_channel_dead_peer_invalid"); } return Object.freeze({ edgeRegistrationId, channelGeneration, commandTransport: normalizeCommandTransport(options.commandTransport), trust, host: normalizeHost(options.host ?? "127.0.0.1"), port: normalizePort(options.port ?? 443), keepaliveMs, deadPeerMs, acceptanceTimeoutMs: normalizeDuration( options.acceptanceTimeoutMs, 10, 5_000, "acceptance_timeout", ), maxEnvelopeBytes: normalizeInteger( options.maxEnvelopeBytes, 256, DEVICE_EDGE_CHANNEL_LIMITS.maxEnvelopeBytes, DEVICE_EDGE_CHANNEL_LIMITS.maxEnvelopeBytes, "max_envelope_bytes", ), maxPendingAcceptances: normalizeInteger( options.maxPendingAcceptances, 1, DEVICE_EDGE_CHANNEL_LIMITS.maxPendingAcceptances, DEVICE_EDGE_CHANNEL_LIMITS.maxPendingAcceptances, "max_pending_acceptances", ), clock: typeof options.clock === "function" ? options.clock : Date.now, now: typeof options.now === "function" ? () => new Date(options.now()).toISOString() : () => new Date().toISOString(), }); } function normalizeCommandTransport(value) { const normalized = value ?? "disabled"; if (!["disabled", "typed-service-ping-v1"].includes(normalized)) { throw new TypeError("device_edge_channel_command_transport_invalid"); } return normalized; } function normalizeCommandStatus(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new TypeError("device_edge_channel_command_status_invalid"); } const commandRef = normalizeRef(value.commandRef, "command_ref"); const transportMessageRef = normalizeRef( value.transportMessageRef, "transport_message_ref", ); const sessionRef = normalizeRef(value.sessionRef, "tracker_session_ref"); const adapterProfileRef = normalizeRef( value.adapterProfileRef, "adapter_profile_ref", ); if (!["acknowledged", "unknown"].includes(value.lifecycleState)) { throw new TypeError("device_edge_channel_command_lifecycle_invalid"); } const resultCode = String(value.resultCode || ""); if (!/^[a-z][a-z0-9._-]{1,63}$/.test(resultCode)) { throw new TypeError("device_edge_channel_command_result_invalid"); } const observedAt = new Date(value.observedAt); if (Number.isNaN(observedAt.getTime())) { throw new TypeError("device_edge_channel_command_observed_at_invalid"); } return Object.freeze({ commandRef, transportMessageRef, sessionRef, adapterProfileRef, lifecycleState: value.lifecycleState, resultCode, observedAt: observedAt.toISOString(), }); } function normalizeTrust(value) { if (!value || typeof value !== "object") { throw new TypeError("device_edge_channel_tls_invalid"); } for (const key of ["key", "cert", "ca"]) { if (!(typeof value[key] === "string" || Buffer.isBuffer(value[key]))) { throw new TypeError(`device_edge_channel_tls_${key}_invalid`); } } if ( !Array.isArray(value.allowedCoreFingerprints) || value.allowedCoreFingerprints.length < 1 || value.allowedCoreFingerprints.length > 2 ) { throw new TypeError("device_edge_channel_core_identity_allowlist_invalid"); } const generationRef = normalizeRef( value.generationRef, "trust_generation", ); let certificateFingerprint; try { certificateFingerprint = normalizeCertificateFingerprint( new X509Certificate(value.cert).fingerprint256, ); } catch { throw new TypeError("device_edge_channel_tls_cert_invalid"); } const allowedCoreFingerprints = new Set( value.allowedCoreFingerprints.map(normalizeCertificateFingerprint), ); if (allowedCoreFingerprints.size !== value.allowedCoreFingerprints.length) { throw new TypeError("device_edge_channel_core_identity_allowlist_duplicate"); } return Object.freeze({ generationRef, key: value.key, cert: value.cert, ca: value.ca, certificateFingerprint, allowedCoreFingerprints, }); } function normalizeRejectionCode(value) { if (typeof value !== "string" || !/^[a-z][a-z0-9._:-]{2,127}$/.test(value)) { return "device_edge_channel_event_rejected"; } return value; } function normalizeRef(value, field) { if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) { throw new TypeError(`device_edge_channel_${field}_invalid`); } return value; } function normalizeHost(value) { if (typeof value !== "string" || value.length < 1 || value.length > 253) { throw new TypeError("device_edge_channel_host_invalid"); } return value; } function normalizePort(value) { const number = Number(value); if (!Number.isSafeInteger(number) || number < 0 || number > 65_535) { throw new TypeError("device_edge_channel_port_invalid"); } return number; } function normalizeDuration(value, minimum, fallback, field) { return normalizeInteger(value, minimum, 120_000, fallback, field); } function normalizeInteger(value, minimum, maximum, fallback, field) { const number = value == null ? fallback : Number(value); if (!Number.isSafeInteger(number) || number < minimum || number > maximum) { throw new TypeError(`device_edge_channel_${field}_invalid`); } return number; } function listen(server, port, host) { return new Promise((resolve, reject) => { const onError = (error) => { server.off("listening", onListening); reject(error); }; const onListening = () => { server.off("error", onError); resolve(); }; server.once("error", onError); server.once("listening", onListening); server.listen(port, host); }); } function closeServer(server) { return new Promise((resolve) => server.close(() => resolve())); }