import type { NodeBridgeServer } from "../infra/bridge/server.js"; import { startNodeBridgeServer } from "../infra/bridge/server.js"; import { listSystemPresence, upsertPresence, } from "../infra/system-presence.js"; import { loadVoiceWakeConfig } from "../infra/voicewake.js"; import { isLoopbackAddress } from "./net.js"; import { getHealthVersion, getPresenceVersion, incrementPresenceVersion, } from "./server/health-state.js"; import type { BridgeEvent, BridgeRequest, BridgeResponse, } from "./server-bridge-types.js"; export type GatewayNodeBridgeRuntime = { bridge: NodeBridgeServer | null; nodePresenceTimers: Map>; }; export async function startGatewayNodeBridge(params: { bridgeEnabled: boolean; bridgePort: number; bridgeHost: string | null; machineDisplayName: string; canvasHostPort?: number; canvasHostHost?: string; broadcast: ( event: string, payload: unknown, opts?: { dropIfSlow?: boolean; stateVersion?: { presence?: number; health?: number }; }, ) => void; bridgeUnsubscribeAll: (nodeId: string) => void; handleBridgeRequest: ( nodeId: string, req: BridgeRequest, ) => Promise; handleBridgeEvent: (nodeId: string, evt: BridgeEvent) => Promise | void; logBridge: { info: (msg: string) => void; warn: (msg: string) => void }; }): Promise { const nodePresenceTimers = new Map>(); const stopNodePresenceTimer = (nodeId: string) => { const timer = nodePresenceTimers.get(nodeId); if (timer) { clearInterval(timer); } nodePresenceTimers.delete(nodeId); }; const beaconNodePresence = ( node: { nodeId: string; displayName?: string; remoteIp?: string; version?: string; platform?: string; deviceFamily?: string; modelIdentifier?: string; }, reason: string, ) => { const host = node.displayName?.trim() || node.nodeId; const rawIp = node.remoteIp?.trim(); const ip = rawIp && !isLoopbackAddress(rawIp) ? rawIp : undefined; const version = node.version?.trim() || "unknown"; const platform = node.platform?.trim() || undefined; const deviceFamily = node.deviceFamily?.trim() || undefined; const modelIdentifier = node.modelIdentifier?.trim() || undefined; const text = `Node: ${host}${ip ? ` (${ip})` : ""} · app ${version} · last input 0s ago · mode remote · reason ${reason}`; upsertPresence(node.nodeId, { host, ip, version, platform, deviceFamily, modelIdentifier, mode: "remote", reason, lastInputSeconds: 0, instanceId: node.nodeId, text, }); incrementPresenceVersion(); params.broadcast( "presence", { presence: listSystemPresence() }, { dropIfSlow: true, stateVersion: { presence: getPresenceVersion(), health: getHealthVersion(), }, }, ); }; const startNodePresenceTimer = (node: { nodeId: string }) => { stopNodePresenceTimer(node.nodeId); nodePresenceTimers.set( node.nodeId, setInterval(() => { beaconNodePresence(node, "periodic"); }, 180_000), ); }; if (params.bridgeEnabled && params.bridgePort > 0 && params.bridgeHost) { try { const started = await startNodeBridgeServer({ host: params.bridgeHost, port: params.bridgePort, serverName: params.machineDisplayName, canvasHostPort: params.canvasHostPort, canvasHostHost: params.canvasHostHost, onRequest: (nodeId, req) => params.handleBridgeRequest(nodeId, req), onAuthenticated: async (node) => { beaconNodePresence(node, "node-connected"); startNodePresenceTimer(node); try { const cfg = await loadVoiceWakeConfig(); started.sendEvent({ nodeId: node.nodeId, event: "voicewake.changed", payloadJSON: JSON.stringify({ triggers: cfg.triggers }), }); } catch { // Best-effort only. } }, onDisconnected: (node) => { params.bridgeUnsubscribeAll(node.nodeId); stopNodePresenceTimer(node.nodeId); beaconNodePresence(node, "node-disconnected"); }, onEvent: params.handleBridgeEvent, onPairRequested: (request) => { params.broadcast("node.pair.requested", request, { dropIfSlow: true, }); }, }); if (started.port > 0) { params.logBridge.info( `listening on tcp://${params.bridgeHost}:${started.port} (node)`, ); return { bridge: started, nodePresenceTimers }; } } catch (err) { params.logBridge.warn(`failed to start: ${String(err)}`); } } else if ( params.bridgeEnabled && params.bridgePort > 0 && !params.bridgeHost ) { params.logBridge.warn( "bind policy requested tailnet IP, but no tailnet interface was found; refusing to start bridge", ); } return { bridge: null, nodePresenceTimers }; }