import { createServer } from "node:http"; import { pathToFileURL } from "node:url"; import { DEVICE_ADAPTER_CATALOG, } from "../../packages/device-adapter-catalog/src/index.mjs"; import { createDeviceEdgeChannelServer, } from "../../services/device-edge-channel/src/runtime.mjs"; import { readRuntimeConfiguration, } from "../../services/device-edge-channel/src/server.mjs"; import { createDeviceGatewayRuntime, } from "../../services/device-gateway/src/runtime.mjs"; if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { await main(); } export async function main(environment = process.env) { const base = await readRuntimeConfiguration(environment); const tracker = normalizeTrackerIngressConfiguration( environment, base.channel.edgeRegistrationId, ); const channel = createDeviceEdgeChannelServer({ ...base.channel, commandTransport: "typed-service-ping-v1", }); const gateway = createDeviceGatewayRuntime({ listenEnabled: true, publicIngressEnabled: true, coreChannelAuthenticated: true, adapterRegistry: DEVICE_ADAPTER_CATALOG.registry, protocolProfileRef: tracker.protocolProfileRef, edgeRef: tracker.edgeRef, healthHost: tracker.healthHost, healthPort: tracker.healthPort, tcpHost: tracker.tcpHost, tcpPort: tracker.tcpPort, maxBufferedBytes: tracker.maxBufferedBytes, maxAggregateBufferedBytes: tracker.maxAggregateBufferedBytes, maxConcurrentSessions: tracker.maxConcurrentSessions, maxSessionsPerAddress: tracker.maxSessionsPerAddress, maxConnectionsPerMinutePerAddress: tracker.maxConnectionsPerMinutePerAddress, maxTrackedSourceAddresses: tracker.maxTrackedSourceAddresses, sessionTimeoutMs: tracker.sessionTimeoutMs, onDiscovery: (signal) => channel.submitDiscovery(signal), onMessage: (message) => channel.submitAdapterMessage(message), onCommandStatus: (status) => channel.submitCommandStatus(status), }); const health = createCombinedHealthServer(channel, gateway, base.health); let stopping = false; try { await channel.start(); await gateway.start(); await listen(health, base.health.port, base.health.host); } catch (error) { await Promise.allSettled([ gateway.stop(), channel.stop(), closeServer(health), ]); throw error; } console.log(JSON.stringify({ event: "device_edge_runtime_started", channel: `${base.channel.host}:${base.channel.port}`, health: `${base.health.host}:${base.health.port}`, trackerIngress: `${tracker.tcpHost}:${tracker.tcpPort}`, adapterProfile: tracker.protocolProfileRef, edgeRegistrationId: base.channel.edgeRegistrationId, channelGeneration: base.channel.channelGeneration, trustGeneration: base.channel.trustGeneration, commandTransport: "typed-service-ping-v1", })); process.on("SIGTERM", shutdown); process.on("SIGINT", shutdown); async function shutdown() { if (stopping) return; stopping = true; await Promise.allSettled([ gateway.stop(), channel.stop(), closeServer(health), ]); process.exit(0); } } export function normalizeTrackerIngressConfiguration(environment = {}, edgeRef) { return Object.freeze({ edgeRef: normalizeRef(edgeRef, "device_edge_runtime_edge_ref_invalid"), protocolProfileRef: normalizeProfileRef( environment.DEVICE_GATEWAY_PROTOCOL_PROFILE_REF ?? DEVICE_ADAPTER_CATALOG.defaultProfileRef, ), healthHost: normalizeLoopbackHost( environment.DEVICE_GATEWAY_HEALTH_HOST ?? "127.0.0.1", ), healthPort: normalizePort(environment.DEVICE_GATEWAY_HEALTH_PORT, 18221), tcpHost: normalizePublicHost( environment.DEVICE_GATEWAY_TCP_HOST ?? "0.0.0.0", ), tcpPort: normalizePort(environment.DEVICE_GATEWAY_TCP_PORT, 9921), maxBufferedBytes: normalizeInteger( environment.DEVICE_GATEWAY_MAX_BUFFERED_BYTES, 64 * 1024, 1024, 256 * 1024, "device_edge_runtime_session_buffer_invalid", ), maxAggregateBufferedBytes: normalizeInteger( environment.DEVICE_GATEWAY_MAX_AGGREGATE_BUFFERED_BYTES, 32 * 1024 * 1024, 1024, 32 * 1024 * 1024, "device_edge_runtime_aggregate_buffer_invalid", ), maxConcurrentSessions: normalizeInteger( environment.DEVICE_GATEWAY_MAX_SESSIONS, 128, 1, 128, "device_edge_runtime_session_limit_invalid", ), maxSessionsPerAddress: normalizeInteger( environment.DEVICE_GATEWAY_MAX_SESSIONS_PER_ADDRESS, 16, 1, 16, "device_edge_runtime_address_session_limit_invalid", ), maxConnectionsPerMinutePerAddress: normalizeInteger( environment.DEVICE_GATEWAY_MAX_CONNECTIONS_PER_MINUTE_PER_ADDRESS, 60, 1, 60, "device_edge_runtime_address_rate_limit_invalid", ), maxTrackedSourceAddresses: normalizeInteger( environment.DEVICE_GATEWAY_MAX_TRACKED_SOURCE_ADDRESSES, 2048, 1, 2048, "device_edge_runtime_source_tracking_limit_invalid", ), sessionTimeoutMs: normalizeInteger( environment.DEVICE_GATEWAY_SESSION_TIMEOUT_MS, 10_000, 100, 60_000, "device_edge_runtime_session_timeout_invalid", ), }); } function createCombinedHealthServer(channel, gateway, healthConfig) { return createServer((request, response) => { response.setHeader("Content-Type", "application/json; charset=utf-8"); response.setHeader("Cache-Control", "no-store"); response.setHeader("X-Content-Type-Options", "nosniff"); if (request.method !== "GET" || request.url !== "/healthz") { response.statusCode = 404; response.end(JSON.stringify({ ok: false, error: "not_found" })); return; } response.statusCode = 200; response.end(JSON.stringify({ ok: true, service: "nodedc-device-edge-runtime", health: `${healthConfig.host}:${healthConfig.port}`, ...channel.status(), trackerIngress: "telemetry-ingest", tracker: gateway.status(), commandTransport: "typed-service-ping-v1", })); }); } function normalizeRef(value, errorCode) { if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) { throw new TypeError(errorCode); } return value; } function normalizeProfileRef(value) { if (typeof value !== "string" || !/^[a-z][a-z0-9._-]{2,127}$/.test(value)) { throw new TypeError("device_edge_runtime_profile_ref_invalid"); } DEVICE_ADAPTER_CATALOG.registry.resolveProfile(value); return value; } function normalizeLoopbackHost(value) { if (!["127.0.0.1", "::1"].includes(value)) { throw new TypeError("device_edge_runtime_health_host_invalid"); } return value; } function normalizePublicHost(value) { if (!["0.0.0.0", "::"].includes(value)) { throw new TypeError("device_edge_runtime_public_host_invalid"); } return value; } function normalizePort(value, fallback) { return normalizeInteger( value, fallback, 1, 65_535, "device_edge_runtime_port_invalid", ); } function normalizeInteger(value, fallback, minimum, maximum, errorCode) { const normalized = Number(value ?? fallback); if (!Number.isSafeInteger(normalized) || normalized < minimum || normalized > maximum) { throw new TypeError(errorCode); } return normalized; } function listen(server, port, host) { return new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, host, () => { server.off("error", reject); resolve(); }); }); } function closeServer(server) { if (!server.listening) return Promise.resolve(); return new Promise((resolve) => server.close(() => resolve())); }