UNPKG

@interopio/bridge

Version:

`io.Bridge` is a distributed scalable application that provides connectivity between `io.Connect` platforms.

1,597 lines (1,561 loc) 70.6 kB
// src/utils/uuid.ts import { nanoid } from "nanoid"; function newUUID() { return nanoid(); } // src/utils/network.ts import { networkInterfaces } from "node:os"; import { ADDRCONFIG } from "node:dns"; import { lookup, lookupService } from "node:dns/promises"; // src/utils/internal/IpAddressUtil.ts var DOT = ".".charCodeAt(0); var COLON = ":".charCodeAt(0); function textToNumericFormatV4(address) { const result = new Uint8Array(4); const len = address.length; if (len == 0 || len > 15) { return; } let currentValue = 0; let currentByte = 0; let newByte = true; for (let i = 0; i < len; i++) { const ch = address.charCodeAt(i); if (ch === DOT) { if (newByte || currentValue < 0 || currentValue > 255 || currentByte > 3) { return; } result[currentByte++] = currentValue & 255; currentValue = 0; newByte = true; } else { const digit = ch - 48; if (digit < 0) { return; } currentValue *= 10; currentValue += digit; newByte = false; } } if (newByte || currentValue < 0 || currentValue >= 1 << (4 - currentByte) * 8) { return; } switch (currentByte) { case 0: result[0] = currentValue >> 24 & 255; //fall through case 1: result[1] = currentValue >> 16 & 255; //fall through case 2: result[2] = currentValue >> 8 & 255; //fall through case 3: result[3] = currentValue >> 0 & 255; } return result; } // src/utils/network.ts import { readFileSync } from "node:fs"; var IpAddress = class { address; family; _hostname; constructor(host, address, family) { this._hostname = host; this.address = address; this.family = family; } get addressBytes() { if (this.family === 4) { return textToNumericFormatV4(this.address); } } async getHost() { if (!this._hostname) { this._hostname = (await lookupService(this.address, 0)).hostname; } return this._hostname; } async getIpAddress(nonLoopback = false) { const hints = nonLoopback ? ADDRCONFIG : void 0; return await lookup(this.address, { family: this.family, hints }); } }; function isLoopback(address) { return address.family === 4 && address.address.startsWith("127.") || address.family === 6 && address.address.startsWith("::1"); } async function getByName(host) { const { address, family } = await lookup(host, { family: 4 }); if (family !== 4 && family !== 6) { throw new Error("unexpected address type"); } return new IpAddress(void 0, address, family); } function getNetworkInterfaces() { return Object.entries(networkInterfaces()).map(([name, addresses]) => { return { name, loopback: addresses.some((info) => info.internal), addresses: addresses.map((info) => { return new IpAddress(void 0, info.address, info.family === "IPv6" ? 6 : 4); }) }; }); } function soMaxConn() { let somaxconn; if (process.platform === "win32") { somaxconn = 200; } else if (process.platform === "darwin") { somaxconn = 128; } else { somaxconn = 4096; } try { somaxconn = parseInt(readFileSync("/proc/sys/net/core/somaxconn").toString()); } catch (error) { } return somaxconn; } var SOMAXCONN = soMaxConn(); // src/cluster/Address.ts async function toIpAddress(address) { return await getByName(scopedHost(address)); } async function fromHostname(hostname, port) { return new AddressImpl(hostname, await getByName(hostname), port); } function fromIpAddress(address, port) { return new AddressImpl(void 0, address, port); } var AddressImpl = class { type; host; port; address; constructor(hostname, ipAddress, port) { const type = ipAddress.family; if (type !== 4 && type !== 6) { throw new Error("unexpected address type"); } this.type = type; this.host = hostname ?? ipAddress.address; this.port = port; this.address = ipAddress; } }; function scopedHost(address) { if (address.type === 6) { return `[${address.host}]`; } return address.host; } // src/config/Properties.ts var BOOLEAN = new class { convert(value) { if (typeof value === "boolean") { return value; } else if (typeof value === "string") { return value.toLowerCase() === "true"; } throw new Error("Cannot convert value to boolean: " + value); } }(); var STRING = new class { convert(value) { if (value === void 0) { throw new Error("Cannot convert undefined "); } return String(value); } }(); var NUMBER = new class { convert(value) { if (value === void 0) { throw new Error("Cannot convert undefined "); } if (typeof value === "object") { throw new Error("Cannot convert object/array to number: " + value); } return Number(value); } }(); function property(name, converter) { return { key: name, optional: true, converter }; } // src/config/DiscoveryConfig.ts var DefaultDiscoveryConfig = class { discoveryStrategyConfigs = []; discoveryServiceFactory; get enabled() { return this.discoveryStrategyConfigs.length > 0; } }; // src/logging.ts import { IOGateway } from "@interopio/gateway"; function getLogger(name) { return IOGateway.Logging.getLogger(`gateway.bridge.${name}`); } // src/kubernetes/KubernetesDiscoveryStrategyFactory.ts import { stat } from "node:fs/promises"; import { lookup as lookup2 } from "node:dns/promises"; // src/kubernetes/KubernetesConfig.ts import { readFileSync as readFileSync2 } from "node:fs"; // src/discovery/settings.ts function getProperty(prefix, property2) { let s = prefix; if (!s.endsWith(".")) { s += "."; } return s + property2.key; } function readProperty(prefix, property2) { if (prefix) { const p = getProperty(prefix, property2); const v = process.env[p]; return v; } } function getOrUndefined(properties, prefix, property2) { return getOrDefault(properties, prefix, property2); } function getOrDefault(properties, prefix, property2, defaultValue) { if (property2 === void 0) { return defaultValue; } let value = readProperty(prefix, property2); if (value === void 0) { value = properties[property2.key]; } if (value === void 0) { return defaultValue; } return value; } // src/kubernetes/KubernetesProperties.ts var KUBERNETES_ENV_PREFIX = "io.bridge.kubernetes"; var SERVICE_DNS = property("service-dns", STRING); var SERVICE_NAME = property("service-name", STRING); var SERVICE_LABEL_NAME = property("service-label-name", STRING); var SERVICE_LABEL_VALUE = property("service-label-value", STRING); var NAMESPACE = property("namespace", STRING); var POD_LABEL_NAME = property("pod-label-name", STRING); var POD_LABEL_VALUE = property("pod-label-value", STRING); var EXPOSE_EXTERNALLY = property("expose-externally", BOOLEAN); var SERVICE_PER_POD_LABEL_NAME = property("service-per-pod-label-name", STRING); var SERVICE_PER_POD_LABEL_VALUE = property("service-per-pod-label-value", STRING); var RESOLVE_NOT_READY_ADDRESSES = property("resolve-not-ready-addresses", BOOLEAN); var KUBERNETES_MASTER_URL = property("kubernetes-master", STRING); var KUBERNETES_API_TOKEN = property("api-token", STRING); var SERVICE_PORT = property("service-port", NUMBER); // src/kubernetes/KubernetesConfig.ts function readFileContents(fileName) { return readFileSync2(fileName, "utf-8").toString(); } var KubernetesConfig = class { serviceDns; serviceName; serviceLabelName; serviceLabelValue; namespace; podLabelName; podLabelValue; resolveNotReadyAddresses; exposeExternallyMode; servicePerPodLabelName; servicePerPodLabelValue; kubernetesMasterUrl; kubernetesApiToken; servicePort; fileContentsReader; tokenProvider; constructor(properties, fileContentsReader = readFileContents) { this.fileContentsReader = fileContentsReader; this.serviceDns = getOrDefault(properties, KUBERNETES_ENV_PREFIX, SERVICE_DNS); this.serviceName = getOrDefault(properties, KUBERNETES_ENV_PREFIX, SERVICE_NAME); this.serviceLabelName = getOrUndefined(properties, KUBERNETES_ENV_PREFIX, SERVICE_LABEL_NAME); this.serviceLabelValue = getOrDefault(properties, KUBERNETES_ENV_PREFIX, SERVICE_LABEL_VALUE, "true"); this.resolveNotReadyAddresses = getOrDefault(properties, KUBERNETES_ENV_PREFIX, RESOLVE_NOT_READY_ADDRESSES, true); this.podLabelName = getOrUndefined(properties, KUBERNETES_ENV_PREFIX, POD_LABEL_NAME); this.podLabelValue = getOrUndefined(properties, KUBERNETES_ENV_PREFIX, POD_LABEL_VALUE); this.exposeExternallyMode = this.getExposeExternallyMode(properties); this.servicePerPodLabelName = getOrUndefined(properties, KUBERNETES_ENV_PREFIX, SERVICE_PER_POD_LABEL_NAME); this.servicePerPodLabelValue = getOrUndefined(properties, KUBERNETES_ENV_PREFIX, SERVICE_PER_POD_LABEL_VALUE); this.kubernetesMasterUrl = getOrDefault(properties, KUBERNETES_ENV_PREFIX, KUBERNETES_MASTER_URL, "https://kubernetes.default.svc"); this.tokenProvider = this.buildTokenProvider(properties); this.servicePort = getOrDefault(properties, KUBERNETES_ENV_PREFIX, SERVICE_PORT, 0); this.namespace = this.getNamespaceWithFallbacks(properties); } getExposeExternallyMode(properties) { const exposeExternally = getOrUndefined(properties, KUBERNETES_ENV_PREFIX, EXPOSE_EXTERNALLY); if (exposeExternally === void 0) { return "auto"; } else if (exposeExternally) { return "enabled"; } else { return "disabled"; } } getNamespaceWithFallbacks(properties) { let namespace = getOrDefault(properties, KUBERNETES_ENV_PREFIX, NAMESPACE); if (!namespace) { namespace = process.env["KUBERNETES_NAMESPACE"]; } if (!namespace && this.mode === "kubernetes-api") { namespace = this.readNamespace(); } return namespace; } buildTokenProvider(properties) { const apiToken = getOrUndefined(properties, KUBERNETES_ENV_PREFIX, KUBERNETES_API_TOKEN); if (!apiToken && this.mode === "kubernetes-api") { return () => readFileContents("/var/run/secrets/kubernetes.io/serviceaccount/token"); } else { return () => apiToken; } } readNamespace() { return this.fileContentsReader("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); } get mode() { if (this.serviceDns) { return "dns-lookup"; } return "kubernetes-api"; } toString() { return "KubernetesConfig: {service-dns: " + this.serviceDns + ", service-name: " + this.serviceName + ", service-port: " + this.servicePort + ", service-label-name: " + this.serviceLabelName + ", service-label-value: " + this.serviceLabelValue + ", namespace: " + this.namespace + ", pod-label-name: " + this.podLabelName + ", pod-label-value: " + this.podLabelValue + ", resolve-not-ready-addresses: " + this.resolveNotReadyAddresses + ", expose-externally-mode: " + this.exposeExternallyMode + ", service-per-pod-label-name: " + this.servicePerPodLabelName + ", service-per-pod-label-value: " + this.servicePerPodLabelValue + ", kubernetes-master-url: " + this.kubernetesMasterUrl + "}"; } }; // src/kubernetes/KubernetesApiProvider.ts var KubernetesApiEndpointSlicesProvider = class { getEndpointsByServiceLabelUrlString(kubernetesMaster, namespace, param) { return `${kubernetesMaster}/apis/discovery.k8s.io/v1/namespaces/${namespace}/endpointslices?${param}`; } getEndpointsByNameUrlString(kubernetesMaster, namespace, endpointName) { return `${kubernetesMaster}/apis/discovery.k8s.io/v1/namespaces/${namespace}/endpointslices?labelSelector=kubernetes.io/service-name=${endpointName}`; } parseEndpoints(param) { const endpoints = new Array(); for (const item of param.items) { endpoints.push(...this.parseEndpointSlices(item)); } return endpoints; } parseEndpointSlices(item) { const addresses = new Array(); const endpointPort = this.extractPort(item); for (const endpoint of item.endpoints) { const ready = endpoint.conditions.ready; for (const address of endpoint.addresses) { addresses.push(new Endpoint(new EndpointAddress({ ip: address, port: endpointPort }), void 0, ready)); } } return addresses; } extractPort(item) { for (const port of item.ports) { const bridgeServicePort = port.name; if (bridgeServicePort && bridgeServicePort === "io-bridge") { const servicePort = port.port; if (servicePort !== void 0) { return servicePort; } } } if (item.ports.length === 1) { const port = item.ports[0]; const servicePort = port.port; if (servicePort !== void 0) { return servicePort; } } } }; // src/kubernetes/KubernetesClient.ts var EndpointAddress = class { targetRefName; address; constructor(address, targetRefName) { this.address = address; this.targetRefName = targetRefName; } get ip() { return this.address.ip; } get port() { return this.address.port; } }; var Endpoint = class { privateAddress; publicAddress = void 0; ready; additionalProperties = /* @__PURE__ */ new Map(); constructor(privateAddress, publicAddress = void 0, ready, additionalProperties = /* @__PURE__ */ new Map()) { this.privateAddress = privateAddress; this.publicAddress = publicAddress; this.ready = ready; this.additionalProperties = additionalProperties; } }; var READ_TIMEOUT_SECONDS = 10; function isReady(podItemStatus) { for (const containerStatus of podItemStatus.containerStatuses) { if (containerStatus.ready !== true) { return false; } } return true; } function containerPort(container) { const ports = container.ports; if (ports.length > 0) { const port = ports[0]; return port.containerPort; } } function extractContainerPort(item) { const containers = item.spec.containers; if (containers.length === 1) { const container = containers[0]; return containerPort(container); } else { for (const container of containers) { if (container.name === "io-bridge") { return containerPort(container); } } } } function parsePodsList(podList) { const addresses = new Array(); for (const item of podList.items) { const podName = item.metadata.name; const status = item.status; const ip = status.podIP; if (ip) { const port = extractContainerPort(item); addresses.push(new Endpoint(new EndpointAddress({ ip, port }, podName), void 0, isReady(status))); } } return addresses; } function getLabelSelectorParameter(labelNames, labelValues) { const labelNameArray = labelNames.split(","); const labelValueArray = labelValues.split(","); const selectorList = []; for (let i = 0; i < labelNameArray.length; i++) { selectorList[i] = `${labelNameArray[i]}=${labelValueArray[i]}`; } return `labelSelector=${selectorList.join(",")}`; } var KubernetesClient = class { namespace; kubernetesMaster; exposeExternallyMode; tokenProvider; servicePerPodLabelName; servicePerPodLabelValue; clientTopologyIntentTracker; apiProvider; /*testing*/ constructor(namespace, kubernetesMaster, exposeExternallyMode, tokenProvider, servicePerPodLabelName, servicePerPodLabelValue, clientTopologyIntentTracker, apiProvider) { this.namespace = namespace; this.kubernetesMaster = kubernetesMaster; this.exposeExternallyMode = exposeExternallyMode; this.tokenProvider = tokenProvider; this.servicePerPodLabelName = servicePerPodLabelName; this.servicePerPodLabelValue = servicePerPodLabelValue; if (clientTopologyIntentTracker) { clientTopologyIntentTracker.init(); } this.apiProvider = apiProvider ?? new KubernetesApiEndpointSlicesProvider(); } start() { } destroy() { if (this.clientTopologyIntentTracker) { this.clientTopologyIntentTracker.destroy(); } } /** * Returns POD addresses in the specified namespace. * */ async endpoints() { try { const url = `${this.kubernetesMaster}/api/v1/namespaces/${this.namespace}/pods`; return this.enrichWithPublicAddresses(parsePodsList(await this.callGet(url))); } catch (e) { return this.handleUnknownError(e); } } /** * Retrieves POD addresses for all services in the specified namespace filtered by service labels and values. * @param serviceLabels comma separated list of service labels * @param serviceLabelValues comma separated list of service label values * @return all POD addresses from the specified namespace filtered by labels */ async endpointsByServiceLabel(serviceLabels, serviceLabelValues) { try { const param = getLabelSelectorParameter(serviceLabels, serviceLabelValues); const url = this.apiProvider.getEndpointsByServiceLabelUrlString(this.kubernetesMaster, this.namespace, param); return this.enrichWithPublicAddresses(this.apiProvider.parseEndpoints(this.callGet(url))); } catch (e) { return this.handleUnknownError(e); } } /** * Retrieves POD addresses from the specified namespace and given endpointName. * @param endpointName endpoint name * @return all POD addresses from the specified namespace and the given endpointName */ async endpointsByName(endpointName) { try { const url = this.apiProvider.getEndpointsByNameUrlString(this.kubernetesMaster, this.namespace, endpointName); return this.enrichWithPublicAddresses(this.apiProvider.parseEndpoints(await this.callGet(url))); } catch (e) { return this.handleUnknownError(e); } } /** * Retrieves POD addresses for all services in the specified namespace filtered by pod labels and values. * @param podLabels comma separated list of pod labels * @param podLabelValues comma separated list of pod label values * @return all POD addresses from the specified namespace filtered by the labels */ async endpointsByPodLabel(podLabels, podLabelValues) { try { const param = getLabelSelectorParameter(podLabels, podLabelValues); const url = `${this.kubernetesMaster}/api/v1/namespaces/${this.namespace}/pods?${param}`; return this.enrichWithPublicAddresses(parsePodsList(await this.callGet(url))); } catch (e) { return this.handleUnknownError(e); } } enrichWithPublicAddresses(endpoints) { if (this.exposeExternallyMode === "disabled") { return endpoints; } } handleUnknownError(e) { return []; } async callGet(url) { const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(`request to ${url} timed out`); }, READ_TIMEOUT_SECONDS * 1e3); try { const response = await fetch(url, { signal: controller.signal, headers: [["Authorization", `Bearer ${this.tokenProvider()}`]] }).catch((err) => { throw err; }); return await response.json(); } finally { clearTimeout(timeout); } } }; // src/kubernetes/KubernetesEndpointResolver.ts var DNS_RETRY = 5; var KubernetesEndpointResolver = class { logger; constructor(logger) { this.logger = logger; } async start() { } async close() { } async mapAddress(address) { if (!address) { return; } try { let retry = 0; try { return await getByName(address); } catch (e) { if (retry < DNS_RETRY) { retry++; } else { throw e; } } } catch (e) { } } }; // src/config/NamedDiscoveryConfig.ts var NamedDiscoveryConfig = class { tag; _enabled = false; constructor(tag) { this.tag = tag; } setEnabled(enabled) { this._enabled = enabled; return this; } get enabled() { return this._enabled; } }; // src/config/KubernetesConfig.ts var KubernetesConfig2 = class extends NamedDiscoveryConfig { constructor() { super("kubernetes"); } }; // src/config/Config.ts var AutoDetectionConfig = class { enabled = true; }; var DefaultMulticastConfig = class { enabled = false; group = "239.1.2.3"; port = 34567; }; var DefaultTcpIpConfig = class { enabled = false; members = []; }; var JoinConfig = class { _multicast = new DefaultMulticastConfig(); _tcpIp = new DefaultTcpIpConfig(); _kubernetes = new KubernetesConfig2(); _discovery = new DefaultDiscoveryConfig(); _autoDetection = new AutoDetectionConfig(); get discovery() { return this._discovery; } get multicast() { return this._multicast; } get autoDetectionEnabled() { return this._autoDetection.enabled && !this._multicast.enabled && !this._tcpIp.enabled && !this._kubernetes.enabled && !this._discovery.enabled; } }; var NetworkConfig = class _NetworkConfig { static DEFAULT_PORT = 8383; port = _NetworkConfig.DEFAULT_PORT; reuseAddress; publicAddress; join = new JoinConfig(); constructor() { this.reuseAddress = process.platform !== "win32"; } }; var CorsConfig = class { #allowOrigin = void 0; set allowOrigin(value) { if (value !== void 0) { if (value === "*") { this.#allowOrigin = value; } else if (Array.isArray(value)) { this.#allowOrigin = value; } else { this.#allowOrigin = value.split(","); } } } get allowOrigin() { return this.#allowOrigin; } disabled; allowCredentials = false; }; var ServerConfig = class { port = void 0; host = void 0; cors = new CorsConfig(); auth = { type: "none", basic: { realm: "io.Bridge" } }; wsPingInterval = 3e4; // 30 seconds }; var MeshConfig = class { timeout; }; var GatewayConfig = class { enabled; contexts = { lifetime: "retained" }; }; var Config = class { properties = {}; network = new NetworkConfig(); license; server; mesh; gateway; constructor() { this.server = new ServerConfig(); this.mesh = new MeshConfig(); this.gateway = new GatewayConfig(); } get(name) { const value = this.properties[name]; return value; } set(name, value) { this.properties[name] = value; return this; } }; // src/discovery/SimpleDiscoveryNode.ts var SimpleDiscoveryNode = class { privateAddress; publicAddress; properties; constructor(privateAddress, publicAddress, properties) { this.privateAddress = privateAddress; this.publicAddress = publicAddress ?? privateAddress; this.properties = properties ?? /* @__PURE__ */ new Map(); } }; // src/kubernetes/KubernetesApiEndpointResolver.ts function buildKubernetesClient(config, clusterTopologyIntentTracker) { return new KubernetesClient(config.namespace, config.kubernetesMasterUrl, config.exposeExternallyMode, config.tokenProvider, config.servicePerPodLabelName, config.servicePerPodLabelValue, clusterTopologyIntentTracker); } var KubernetesApiEndpointResolver = class _KubernetesApiEndpointResolver extends KubernetesEndpointResolver { client; serviceName; port = 0; serviceLabel; serviceLabelValue; podLabel; podLabelValue; resolveNotReadyAddresses; static of(logger, config, tracker) { return new _KubernetesApiEndpointResolver( logger, buildKubernetesClient(config, tracker), config.serviceName, config.servicePort, config.serviceLabelName, config.serviceLabelValue, config.podLabelName, config.podLabelValue, config.resolveNotReadyAddresses ); } /*testing*/ constructor(logger, client, serviceName, port = 0, serviceLabel, serviceLabelValue, podLabel, podLabelValue, resolveNotReadyAddresses) { super(logger); this.client = client; this.serviceName = serviceName; this.port = port; this.serviceLabel = serviceLabel; this.serviceLabelValue = serviceLabelValue; this.podLabel = podLabel; this.podLabelValue = podLabelValue; this.resolveNotReadyAddresses = resolveNotReadyAddresses; } async resolve() { if (this.serviceName) { return await this.getSimpleDiscoveryNodes(await this.client.endpointsByName(this.serviceName)); } else if (this.serviceLabel) { return await this.getSimpleDiscoveryNodes(await this.client.endpointsByServiceLabel(this.serviceLabel, this.serviceLabelValue)); } else if (this.podLabel) { return await this.getSimpleDiscoveryNodes(await this.client.endpointsByPodLabel(this.podLabel, this.podLabelValue)); } return await this.getSimpleDiscoveryNodes(await this.client.endpoints()); } async getSimpleDiscoveryNodes(endpoints) { const discoveredNodes = new Array(); for (const endpoint of endpoints) { await this.addAddress(discoveredNodes, endpoint); } return discoveredNodes; } async addAddress(discoveredNodes, endpoint) { if (this.resolveNotReadyAddresses === true || endpoint.ready) { const privateAddress = await this.createAddress(endpoint.privateAddress, this.resolvePort.bind(this)); const publicAddress = await this.createAddress(endpoint.publicAddress, this.resolvePortPublic.bind(this)); discoveredNodes.push(new SimpleDiscoveryNode(privateAddress, publicAddress, /* @__PURE__ */ new Map())); } } async createAddress(address, portResolver) { if (address === void 0) { return void 0; } const ip = address.ip; const ipAddress = await this.mapAddress(ip); const port = portResolver(address); return fromIpAddress(ipAddress, port); } resolvePort(address) { if (this.port > 0) { return this.port; } if (address.port) { return address.port; } return NetworkConfig.DEFAULT_PORT; } resolvePortPublic(address) { if (address.port) { return address.port; } if (this.port > 0) { return this.port; } return NetworkConfig.DEFAULT_PORT; } }; // src/kubernetes/KubernetesDiscoveryStrategy.ts var KubernetesDiscoveryStrategy = class { logger; endpointResolver; constructor(logger, properties, intentTracker) { this.logger = logger; const config = new KubernetesConfig(properties); this.logger.info(`${config}`); this.endpointResolver = KubernetesApiEndpointResolver.of(this.logger, config, intentTracker); this.logger.info(`Kubernetes Discovery activated with mode: ${config.mode}`); } async start() { await this.endpointResolver.start(); } async discover() { return void 0; } async close() { await this.endpointResolver.close(); } }; // src/kubernetes/KubernetesDiscoveryStrategyFactory.ts var PROPERTY_DEFINITIONS = [ SERVICE_DNS, SERVICE_NAME, SERVICE_LABEL_NAME, SERVICE_LABEL_VALUE, NAMESPACE, POD_LABEL_NAME, POD_LABEL_VALUE, RESOLVE_NOT_READY_ADDRESSES, EXPOSE_EXTERNALLY, KUBERNETES_MASTER_URL, KUBERNETES_API_TOKEN, SERVICE_PORT ]; var KubernetesDiscoveryStrategyFactory = class { tokenPath; priority = DiscoveryStrategyPriorities.PLATFORM; constructor(tokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token") { this.tokenPath = tokenPath; } async defaultKubernetesMasterReachable() { try { await lookup2("kubernetes.default.svc"); return true; } catch (e) { return false; } } async tokenFileExists() { try { await stat(this.tokenPath); return true; } catch (e) { return false; } } async isAutoDetectionApplicable() { return await this.tokenFileExists() && await this.defaultKubernetesMasterReachable(); } newDiscoveryStrategy(node, logger, properties) { const tracker = node?.properties.get("internal.discovery.cluster.topology.intent.tracker"); return new KubernetesDiscoveryStrategy(logger, properties, tracker); } getConfigurationProperties() { return PROPERTY_DEFINITIONS; } }; // src/discovery/multicast/MulticastDiscoveryStrategy.ts import { createSocket } from "node:dgram"; // src/discovery/multicast/MulticastProperties.ts var PORT = property("port", NUMBER); var GROUP = property("group", STRING); // src/discovery/multicast/MulticastDiscoveryStrategy.ts var DATA_OUTPUT_BUFFER_SIZE = 64 * 1024; var DEFAULT_MULTICAST_PORT = 34567; var DEFAULT_MULTICAST_GROUP = "239.1.2.3"; var SOCKET_TIME_TO_LIVE = 255; function multicastDiscoverySender(discoveryNode, socket, group, port) { let multicastMemberInfo; if (discoveryNode) { const address = discoveryNode.publicAddress; multicastMemberInfo = { host: address.host, port: address.port }; } const msg = Buffer.from(JSON.stringify(multicastMemberInfo)); return () => { socket.send(msg, port, group, (error) => { console.error(error); }); }; } function multicastDiscoveryReceiver(socket) { return () => { return new Promise((resolve, reject) => { socket.on("message", (message, remote) => { resolve(JSON.parse(message)); }); socket.on("error", (err) => { reject(err); }); }); }; } var MulticastDiscoveryStrategy = class { logger; properties; discoveryNode; isClient; discoverySender; discoveryReceiver; sendIntervalId; socket; constructor(discoveryNode, logger, properties) { this.logger = logger; this.properties = properties; this.discoveryNode = discoveryNode; } async initializeMulticastSocket() { const port = getOrDefault(this.properties, "", PORT, DEFAULT_MULTICAST_PORT); if (port < 0 || port > 65535) { throw new Error("port number must be between 0 and 65535"); } const group = getOrDefault(this.properties, "", GROUP, DEFAULT_MULTICAST_GROUP); this.socket = createSocket({ type: "udp4", reuseAddr: true, recvBufferSize: DATA_OUTPUT_BUFFER_SIZE, sendBufferSize: DATA_OUTPUT_BUFFER_SIZE }); if (this.discoveryNode) { const address = await toIpAddress(this.discoveryNode.privateAddress); if (!isLoopback(address)) { this.socket.setMulticastInterface(address.address); } } this.socket.bind(port, () => { this.socket.setMulticastTTL(SOCKET_TIME_TO_LIVE); this.socket.addMembership(group); }); this.discoverySender = multicastDiscoverySender(this.discoveryNode, this.socket, group, port); this.discoveryReceiver = multicastDiscoveryReceiver(this.socket); if (this.discoveryNode == null) { this.isClient = true; } } async discover() { const multicastMemberInfo = await this.discoveryReceiver(); if (multicastMemberInfo) { const list = []; try { const discoveryNode = new SimpleDiscoveryNode(await fromHostname(multicastMemberInfo.host, multicastMemberInfo.port)); list.push(discoveryNode); } catch (e) { if (this.logger.enabledFor("trace")) { this.logger.debug(e.message); } } return list; } } async start() { await this.initializeMulticastSocket(); if (!this.isClient) { this.sendIntervalId = setInterval(() => { this.discoverySender(); }, 2e3); } } async close() { if (this.sendIntervalId) { clearInterval(this.sendIntervalId); delete this.sendIntervalId; } return new Promise((resolve, reject) => { if (this.socket) { this.socket.close(() => { resolve(); }); } else { resolve(); } }); } }; // src/discovery/multicast/MulticastDiscoveryStrategyFactory.ts var PROPERTY_DEFINITIONS2 = [ GROUP, PORT ]; var MulticastDiscoveryStrategyFactory = class { priority = DiscoveryStrategyPriorities.UNKNOWN; newDiscoveryStrategy(discoveryNode, logger, prop) { return new MulticastDiscoveryStrategy(discoveryNode, logger, prop); } async isAutoDetectionApplicable() { return false; } getConfigurationProperties() { return PROPERTY_DEFINITIONS2; } }; // src/discovery/index.ts var DefaultDiscoveryServiceFactory = class { newDiscoveryService(settings) { return new DefaultDiscoveryService(settings); } }; function verifyNoUnknownProperties(mappedProperties, allProperties) { const unknownProperties = Object.keys(allProperties).filter((key) => !mappedProperties.hasOwnProperty(key)); if (unknownProperties.length > 0) { throw new Error(`Unknown properties: ${unknownProperties.join(", ")} on discovery strategy`); } } function prepareProperties(properties, propertyDefinitions) { const mappedProperties = {}; for (const propertyDefinition of propertyDefinitions) { const propertyKey = propertyDefinition.key; if (properties[propertyKey] == void 0) { if (!propertyDefinition.optional) { throw new Error(`Missing required property: ${propertyKey} on discovery strategy`); } continue; } const value = properties[propertyKey]; const converter = propertyDefinition.converter; const mappedValue = converter.convert(value); mappedProperties[propertyKey] = mappedValue; } verifyNoUnknownProperties(mappedProperties, properties); return mappedProperties; } var DefaultDiscoveryService = class { settings; strategies = []; constructor(settings) { this.settings = settings; } async start() { this.strategies.push(...await this.loadDiscoveryStrategies()); for (const strategy of this.strategies) { await strategy.start(); } } async discover() { const discoveryNodes = /* @__PURE__ */ new Set(); for (const strategy of this.strategies) { const candidates = await strategy.discover(); if (candidates) { for (const candidate of candidates) { if (this.validateCandidate(candidate)) { discoveryNodes.add(candidate); } } } } return discoveryNodes; } async close() { for (const strategy of this.strategies) { await strategy.close(); } } validateCandidate(candidate) { return true; } async loadDiscoveryStrategies() { const discoveryStrategyConfigs = this.settings.conf.discoveryStrategyConfigs; const factories = this.collectFactories(discoveryStrategyConfigs); const discoveryStrategies = new Array(); for (const config of discoveryStrategyConfigs) { const discoveryStrategy = await this.buildDiscoveryStrategy(config, factories); discoveryStrategies.push(discoveryStrategy); } const logger = this.settings.logger; if (discoveryStrategies.length == 0 && this.settings.auto) { logger.debug(`Discovery auto-detection enabled, trying to detect discovery strategy`); const autoDetectedFactory = await this.detectDiscoveryStrategyFactory(factories); if (autoDetectedFactory) { logger.debug(`Auto-detected discovery strategy: ${autoDetectedFactory.constructor.name}`); discoveryStrategies.push(autoDetectedFactory.newDiscoveryStrategy(await this.settings.node, logger, {})); } else { logger.debug(`No discovery strategy auto-detected`); } } return discoveryStrategies; } collectFactories(strategyConfigs) { const knownFactories = [new MulticastDiscoveryStrategyFactory(), new KubernetesDiscoveryStrategyFactory()]; const factories = new Array(); for (const factory of knownFactories) { factories.push(factory); } for (const config of strategyConfigs) { const factory = config.discoveryStrategyFactory; if (factory) { factories.push(factory); } } return factories; } async buildDiscoveryStrategy(config, factories) { const discoveryNode = await this.settings.node; const logger = this.settings.logger; for (const factory of factories) { if (factory.constructor.name == config.discoveryStrategyFactory.constructor.name) { const properties = prepareProperties(config.properties, factory.getConfigurationProperties()); return factory.newDiscoveryStrategy(discoveryNode, logger, properties); } } throw new Error("DiscoveryStrategyFactory not found"); } async detectDiscoveryStrategyFactory(factories) { let highestPriorityFactory; for (const factory of factories) { try { if (await factory.isAutoDetectionApplicable()) { if (highestPriorityFactory === void 0 || factory.priority > highestPriorityFactory.priority) { highestPriorityFactory = factory; } } } catch (error) { } } return highestPriorityFactory; } }; var DiscoveryStrategyPriorities = { UNKNOWN: 0, PLATFORM: 20, CUSTOM: 50 }; // src/version/MemberVersion.ts function parse(version) { const pattern = /^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g; const { groups } = pattern.exec(version); const major = parseInt(groups["major"]); const minor = parseInt(groups["minor"]); const patch = parseInt(groups["patch"]); return [major, minor, patch]; } var MemberVersion = class { major; minor; patch; get isUnknown() { return this.major === 0 && this.minor === 0 && this.patch === 0; } toString() { return `${this.major}.${this.minor}.${this.patch}`; } constructor(major, minor, patch) { this.major = major; this.minor = minor; this.patch = patch; } }; var UNKNOWN_VERSION = new MemberVersion(0, 0, 0); function parseVersion(version) { if (!version || version.startsWith("0.0.0")) { return UNKNOWN_VERSION; } return new MemberVersion(...parse(version)); } // package.json var package_default = { name: "@interopio/bridge", version: "0.0.6-beta.0", license: "see license in license.md", author: "interop.io", homepage: "https://docs.interop.io/bridge", keywords: [ "io.bridge", "bridge", "io.connect", "glue42", "interop.io" ], type: "module", exports: { "./package.json": "./package.json", ".": { default: "./dist/index.js" }, "./main": { import: { types: "./types/main.d.ts", default: "./dist/main.js" } } }, bin: { bridge: "./bin/bridge.js" }, scripts: { test: "mocha test --recursive", "build:types": "tsc --declaration --emitDeclarationOnly --outDir types", "build:main": "esbuild src/main.mts --outdir=dist --bundle --packages=external --sourcemap --platform=node --format=esm --target=node20.15", "build:index": "esbuild src/index.ts --outdir=dist --target=node20.15", build: "npm run build:main && npm run build:index" }, dependencies: { "@interopio/gateway-server": "^0.10.0-beta.0", dotenv: "^17.2.2", jsrsasign: "^11.1.0", nanoid: "^5.0.9" }, devDependencies: { "@types/jsrsasign": "^10.5.15", "@types/ws": "^8.18.1", "rand-seed": "^3.0.0" }, engines: { node: ">= 20.15 || >=22.2.0 || >=24.0" } }; // src/instance/AddressPicker.ts import { createServer } from "node:net"; async function createAddress(addressDef, port) { if (addressDef.host) { return { type: addressDef.ipAddress.family, host: addressDef.host, port, address: addressDef.ipAddress }; } return { type: addressDef.ipAddress.family, host: await addressDef.ipAddress.getHost(), port, address: addressDef.ipAddress }; } function createNetServer(logger, host, range) { const ports = portRange(range); return new Promise((resolve, reject) => { let server = createServer({}, () => { }); server.on("error", (err) => { if (err["code"] === "EADDRINUSE") { logger.debug(`port ${err["port"]} already in use on address ${err["address"]}`); const { value: port } = ports.next(); if (port) { logger.info(`retry starting server on port ${port} and host ${host ?? "<unspecified>"}`); server.close(); server.listen(port, host); } else { logger.warn(`all configured port(s) ${range} are in use. closing...`); server.close(); reject(err); } } else { logger.error(`server error: ${err.message}`, err); reject(err); } }); server.on("listening", () => { const info = server.address(); logger.info(`listening on ${info.address}:${info.port}`); resolve(server); }); try { const { value: port } = ports.next(); server.listen(port, host); } catch (e) { logger.error(`error starting socket server`, e); reject(e instanceof Error ? e : new Error(`listen failed: ${e}`)); } }); } var InterfaceAddress = class { address; host; constructor(address, host) { this.address = address; this.host = host; } }; var AddressDef = class extends InterfaceAddress { ipAddress; port; constructor(ipAddress, host, port) { super(ipAddress.address, host); this.ipAddress = ipAddress; this.port = port ?? 0; } }; var DefaultAddressPicker = class { #logger; #config; #publicAddressConfig; #publicAddress; #bindAddress; server; #port; constructor(config, logger) { this.#logger = logger; this.#config = config; this.#port = config.network.port; this.#publicAddressConfig = config.network.publicAddress; } async pickAddress() { if (this.#publicAddress || this.#bindAddress) { return; } const publicAddressDef = await this.getPublicAddressByPortSearch(); this.#publicAddress = this.#bindAddress; } getPublicAddressFor(endpoint) { return this.#publicAddress; } getServers() { return /* @__PURE__ */ new Map([["member", this.server]]); } async getPublicAddressByPortSearch() { const bindAddressDef = await this.pickAddressDef(); this.server = await createNetServer(this.#logger, "0.0.0.0", bindAddressDef.port === 0 ? this.#port : bindAddressDef.port); const port = this.server.address().port; this.#bindAddress = await createAddress(bindAddressDef, port); return this.getPublicAddress(port); } async getPublicAddress(port) { let address = this.#publicAddressConfig; if (address) { address = address.trim(); if (!address) { throw new Error("Public address is empty"); } else if ("127.0.0.1" === address) { return this.pickLoopbackAddressDef(address, port); } else { const ipAddress = await getByName(address); return new AddressDef(ipAddress, address, port); } } } async pickAddressDef() { let addressDef = this.pickInterfaceAddressDef(); if (addressDef) { } if (addressDef === void 0) { addressDef = await this.pickLoopbackAddressDef(); } return addressDef; } pickInterfaceAddressDef() { const interfaces = this.getInterfaces(); if (interfaces) { const address = this.pickMatchingAddress(interfaces); if (address) { return address; } } return this.pickMatchingAddress(); } getInterfaces() { return []; } async pickLoopbackAddressDef(host, defaultPort) { const address = await getByName("127.0.0.1"); return new AddressDef(address, host, defaultPort); } match(ipAddress, interfaces) { for (const iface of interfaces) { if (ipAddress.address === iface.address) { return iface; } } return void 0; } getMatchingAddress(ipAddress, interfaces) { if (interfaces) { return this.match(ipAddress, interfaces); } else { return ipAddress; } } pickMatchingAddress(interfaces) { const preferIPv4 = true; const networkInterfaces2 = getNetworkInterfaces(); let matchingAddress; for (const ni of networkInterfaces2) { if (!interfaces && ni.loopback) { continue; } for (const ipAddress of ni.addresses) { if (preferIPv4 && ipAddress.family === 6) { continue; } const address = this.getMatchingAddress(ipAddress, interfaces); break; } } return matchingAddress; } }; var PORT_RANGE_MATCHER = /^(\d+|(0x[\da-f]+))(-(\d+|(0x[\da-f]+)))?$/i; function validPort(port) { if (port > 65535) throw new Error(`bad port ${port}`); return port; } function* portRange(port) { if (typeof port === "string") { for (const portRange2 of port.split(",")) { const trimmed = portRange2.trim(); const matchResult = PORT_RANGE_MATCHER.exec(trimmed); if (matchResult) { const start = parseInt(matchResult[1]); const end = parseInt(matchResult[4] ?? matchResult[1]); for (let i = validPort(start); i < validPort(end) + 1; i++) { yield i; } } else { throw new Error(`'${portRange2}' is not a valid port or range.`); } } } else { yield validPort(port); } } // src/instance/BridgeNode.ts import gatewayServer from "@interopio/gateway-server"; // src/license/types.ts var licenseValidationResults = { TRIAL_LICENSE_EXPIRED: { licenseType: "trial", expired: true, fatal: true }, TRIAL_LICENSE_VALID: { licenseType: "trial", expired: false, fatal: false }, PAID_LICENSE_EXPIRED: { licenseType: "paid", expired: true, fatal: false }, PAID_LICENSE_VALID: { licenseType: "paid", expired: false, fatal: false }, NO_LICENSE: { licenseType: "none", expired: false, fatal: true } }; // src/license/LicenseValidator.ts import { KJUR } from "jsrsasign"; var LicenseError = class extends Error { licensePayload; constructor(message, licensePayload) { super(message); this.licensePayload = licensePayload; } }; var LicenseEvaluator = class { tokenResultMap = { "trial-false": licenseValidationResults.TRIAL_LICENSE_VALID, "trial-true": licenseValidationResults.TRIAL_LICENSE_EXPIRED, "paid-false": licenseValidationResults.PAID_LICENSE_VALID, "paid-true": licenseValidationResults.PAID_LICENSE_EXPIRED }; validationKey; constructor(validationKey2) { if (!validationKey2 || typeof validationKey2 !== "string") { throw new Error(`Validation key must be a non-empty string`); } this.validationKey = validationKey2; } decrypt(token) { if (KJUR.jws.JWS.verifyJWT(token, this.validationKey, { alg: ["RS256"] })) { return KJUR.jws.JWS.parse(token).payloadObj; } else { throw new LicenseError("invalid jwt token", token); } } validateBasicTokenData(token) { const decoded = this.decrypt(token); if (["paid", "trial"].indexOf(decoded.type) === -1) { throw new LicenseError("Invalid license type", token); } return decoded; } getLicenseStatus(token, now = /* @__PURE__ */ new Date()) { try { const license = this.validateBasicTokenData(token); const isExpired = license.expiration < now.getTime(); const licenseValidationResult = this.tokenResultMap[`${license.type}-${isExpired}`]; return { ...licenseValidationResult, license }; } catch (e) { return { ...licenseValidationResults.NO_LICENSE, error: e.message }; } } }; var LicenseValidator = class { licenseHelper; logger; constructor({ validationKey: validationKey2, logger }) { this.licenseHelper = new LicenseEvaluator(validationKey2); this.logger = logger ?? getLogger("LicenseValidator"); } validate(licenseKeyString, options) { if (typeof licenseKeyString !== "string") { throw new LicenseError("Invalid license key format", licenseKeyString); } const licenseStatus = this.licenseHelper.getLicenseStatus(licenseKeyString); this.printLicenseMessage(licenseStatus, options?.logSuccessMessages ?? true); if (licenseStatus.fatal) { throw new LicenseError("License validation failed", licenseStatus.license); } } printLicenseMessage(licenseStatus, logSuccessMessages) { if (!licenseStatus || licenseStatus.licenseType === "none") { this.logger.error("This license is invalid. Please contact us at sales@interop.io to get a valid license"); return; } const message = [ licenseStatus.licenseType === "paid" ? `This is a paid license that was issued for client ` : `This is a trial license that was issued for `, licenseStatus.license.organization.displayName, licenseStatus.expired ? `, but it has expired at ` : ` and is valid until `, new Date(licenseStatus.license.expiration).toString(), "." ].join(""); if (licenseStatus.fatal) { this.logger.error(message); } else if (licenseStatus.expired) { this.logger.warn(message); } else if (logSuccessMessages) { this.logger.info(message); } } }; // src/license/BridgeLicenseValidator.ts var validationKey = `-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyyZrtwcQCMfyOkZDcjxZ +07BrvgcAvoQETF+ho8VLfaDxgNPZd6Q7jZEfrtv0+wn0pxx8pUDeBh8mCYvkwmb 9tkFH47GJuDhWasNgyp4ugRX4bzoPV3SnHsBR4KJ/F76kBbike6sdIfO2lYGZ36s LJPHV7epOYsfhoc4343/vOxEPvNbwacbXWl4NsCGev4qUmWhe9iMpb/JPd3o4hIU SR31AmR2xoh3BkAJc2q/cSeQK6Kn9nZYocyW367+7FOvYsrOYotsuUASp0OWp+Lh WGfR7F6d016+u6kbvLvcceGztiP1u25QmPNCUmw49cTatthiEwwGHb01CR58mStZ xg5t9+N7X9hPO2K59b8EbOnnFTlwtMMF7MKR56S4YwMamCChr9WGgpOQV+lrqyx2 yn9lwn8Yf4gLoLPKBEhHTEy6r/we9qymmlSSe4wr5Fctov2odSE535nvtdRYZkKk t32gOXuwnKg2kRlRSAErpyou1mz7/mWEt1H3sGTRArjNTP2KqZkc14vPToEEJt93 ZKjhD1pVEchDiWBOMj9o12pq80tGZ8PhGJasJVVi0JPUiaznG4r12JdyDAjuXMru 6f4Tx0ULkdwn9ia7lchcq7xC2PlTnYz+fGpfU7V0Ci56QDTp6oP567L1EIeddkaI nIsi4KHT7Ctp047FTTelntUCAwEAAQ== -----END PUBLIC KEY-----`; var BridgeLicenseValidator = (logger) => new LicenseValidator({ validationKey, logger }); // src/instance/BridgeNode.ts import { userInfo } from "node:os"; // ../bridge-mesh/src/mesh/connections.ts import "@interopio/gateway"; var InMemoryNodeConnections = class { #logger; #nodes = /* @__PURE__ */ new Map(); #nodesByEndpoint = /* @__PURE__ */ new Map(); #memberIds = 0; #timeout; constructor(logger, timeout = 6e4) { this.#logger = logger; this.#timeout = timeout; } announce(nodes) { for (const node of nodes) { const { node: nodeId, users, endpoint } = node; const foundId = this.#nodesByEndpoint.get(endpoin