import dgram from 'node:dgram'; import { networkInterfaces } from 'node:os'; export const DISCOVERY_PORT = 3378; export const DISCOVERY_MULTICAST = '239.255.42.99'; export const DISCOVERY_PREFIX = 'AGENTHUB|'; export function getLanIPv4(): string | undefined { for (const iface of Object.values(networkInterfaces())) { if (!iface) continue; for (const entry of iface) { if (entry.family === 'IPv4' && !entry.internal) { return entry.address; } } } return undefined; } export function resolveAdvertiseUrl(host: string, port: number): string { const advertiseHost = host === '0.0.0.0' || host === '::' ? getLanIPv4() ?? '127.0.0.1' : host; return `http://${advertiseHost}:${port}`; } export function startDiscoveryBroadcaster(serverUrl: string, options?: { port?: number; intervalMs?: number }) { const port = options?.port ?? DISCOVERY_PORT; const intervalMs = options?.intervalMs ?? 2000; const socket = dgram.createSocket('udp4'); const message = Buffer.from(`${DISCOVERY_PREFIX}${serverUrl}`); socket.on('error', () => { // Discovery is best-effort; ignore network errors. }); const send = () => { try { socket.send(message, 0, message.length, port, DISCOVERY_MULTICAST); } catch { try { socket.setBroadcast(true); socket.send(message, 0, message.length, port, '255.255.255.255'); } catch { // Ignore send failures. } } }; send(); const interval = setInterval(send, intervalMs); return { socket, interval, stop: () => { clearInterval(interval); socket.close(); }, }; } export function discoverServer(timeoutMs = 3000, port = DISCOVERY_PORT): Promise { return new Promise((resolve) => { const socket = dgram.createSocket('udp4'); let timer: NodeJS.Timeout; const cleanup = () => { try { socket.close(); } catch { // Already closed. } clearTimeout(timer); }; socket.on('message', (msg) => { const text = msg.toString(); if (text.startsWith(DISCOVERY_PREFIX)) { cleanup(); resolve(text.slice(DISCOVERY_PREFIX.length)); } }); socket.on('error', () => { cleanup(); resolve(undefined); }); socket.bind(port, () => { try { socket.addMembership(DISCOVERY_MULTICAST); } catch { // Multicast may be blocked; still try to receive broadcasts. } }); timer = setTimeout(() => { cleanup(); resolve(undefined); }, timeoutMs); }); }