import { readFile } from "node:fs/promises"; import { createCoreDiscoveryClient } from "./core-client.mjs"; import { createDeviceGatewayRuntime } from "./runtime.mjs"; const config = await readConfig(); const onDiscovery = config.listenEnabled ? createCoreDiscoveryClient({ coreUrl: config.coreUrl, gatewayToken: config.gatewayToken, timeoutMs: config.coreTimeoutMs, }) : undefined; const runtime = createDeviceGatewayRuntime({ listenEnabled: config.listenEnabled, publicIngressEnabled: config.publicIngressEnabled, healthHost: config.healthHost, healthPort: config.healthPort, tcpHost: config.tcpHost, tcpPort: config.tcpPort, maxBufferedBytes: config.maxBufferedBytes, maxConcurrentSessions: config.maxConcurrentSessions, maxSessionsPerAddress: config.maxSessionsPerAddress, maxConnectionsPerMinutePerAddress: config.maxConnectionsPerMinutePerAddress, sessionTimeoutMs: config.sessionTimeoutMs, onDiscovery, }); const addresses = await runtime.start(); console.log(JSON.stringify({ event: "device_gateway_started", health: addresses.healthAddress, tcp: addresses.tcpAddress, publicIngress: config.publicIngressEnabled ? "discovery-only" : "disabled", commandTransport: "disabled", })); process.on("SIGTERM", shutdown); process.on("SIGINT", shutdown); async function shutdown() { await runtime.stop(); process.exit(0); } async function readConfig() { const listenEnabled = parseBoolean( process.env.DEVICE_GATEWAY_LISTEN_ENABLED, false, ); const publicIngressEnabled = parseBoolean( process.env.DEVICE_GATEWAY_PUBLIC_INGRESS_ENABLED, false, ); return { listenEnabled, publicIngressEnabled, healthHost: String( process.env.DEVICE_GATEWAY_HEALTH_HOST || "127.0.0.1", ), healthPort: parsePort(process.env.DEVICE_GATEWAY_HEALTH_PORT, 18121), tcpHost: String( process.env.DEVICE_GATEWAY_TCP_HOST || (publicIngressEnabled ? "0.0.0.0" : "127.0.0.1"), ), tcpPort: parsePort(process.env.DEVICE_GATEWAY_TCP_PORT, 9921), maxBufferedBytes: parsePositiveInt( process.env.DEVICE_GATEWAY_MAX_BUFFERED_BYTES, 65536, ), maxConcurrentSessions: parsePositiveInt( process.env.DEVICE_GATEWAY_MAX_SESSIONS, 100, ), maxSessionsPerAddress: parsePositiveInt( process.env.DEVICE_GATEWAY_MAX_SESSIONS_PER_ADDRESS, 10, ), maxConnectionsPerMinutePerAddress: parsePositiveInt( process.env.DEVICE_GATEWAY_MAX_CONNECTIONS_PER_MINUTE_PER_ADDRESS, 30, ), sessionTimeoutMs: parsePositiveInt( process.env.DEVICE_GATEWAY_SESSION_TIMEOUT_MS, 10000, ), coreUrl: listenEnabled ? requiredValue( process.env.DEVICE_GATEWAY_CORE_URL, "device_gateway_core_url_required", ) : "", gatewayToken: listenEnabled ? await readRequiredSecretFile( process.env.DEVICE_GATEWAY_CORE_TOKEN_FILE, "device_gateway_core_token_file_required", ) : "", coreTimeoutMs: parsePositiveInt( process.env.DEVICE_GATEWAY_CORE_TIMEOUT_MS, 5000, ), }; } async function readRequiredSecretFile(path, errorCode) { const normalized = requiredValue(path, errorCode); const value = (await readFile(normalized, "utf8")).trim(); if (value.length < 32) throw new Error(errorCode); return value; } function requiredValue(value, errorCode) { if (typeof value !== "string" || value.trim() === "") { throw new Error(errorCode); } return value.trim(); } function parsePort(value, fallback) { const parsed = Number(value || fallback); if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 65535) { throw new Error("device_gateway_port_invalid"); } return parsed; } function parsePositiveInt(value, fallback) { const parsed = Number(value || fallback); if (!Number.isSafeInteger(parsed) || parsed < 1) { throw new Error("device_gateway_positive_integer_invalid"); } return parsed; } function parseBoolean(value, fallback) { if (value === undefined || value === null || value === "") return fallback; const normalized = String(value).trim().toLowerCase(); if (["1", "true", "yes", "on"].includes(normalized)) return true; if (["0", "false", "no", "off"].includes(normalized)) return false; throw new Error("device_gateway_boolean_invalid"); }