// Handshake-free /24 port probe for desktop pairing (Change 5). // // Unlike `probeSubnetForPeer` (chirp-sync-transport.ts), this does NOT run // Noise — during code pairing we have no long-term pubkey, and the pairing // server on port 49142 speaks the plaintext PEACH_HELLO protocol, not Noise // XX. This probe answers only "is a WebSocket server reachable here?"; the // 6-digit code + user-confirmed SAS are the actual trust boundary. const DEFAULT_PROBE_TIMEOUT_MS = 500; const DEFAULT_PROBE_BATCH = 16; export interface PortProbeOptions { batch?: number; timeoutMs?: number; /** Injection seam for tests — defaults to the real WebSocket open check. */ probeOne?: (ip: string, port: number, timeoutMs: number) => Promise; } /** * Scan `subnet` (e.g. "192.168.1") for the first host accepting a WebSocket * connection on `port`. Returns the IP string, or null if none answered. */ export async function probeSubnetForPort( subnet: string, port: number, options: PortProbeOptions = {}, ): Promise { const batchSize = options.batch ?? DEFAULT_PROBE_BATCH; const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS; const probe = options.probeOne ?? wsPortOpen; for (let start = 1; start <= 254; start += batchSize) { const end = Math.min(start + batchSize - 1, 254); const ips: string[] = []; for (let i = start; i <= end; i++) ips.push(`${subnet}.${i}`); const results = await Promise.all( ips.map(async (ip) => ((await probe(ip, port, timeoutMs)) ? ip : null)), ); const hit = results.find((r): r is string => r !== null); if (hit) return hit; } return null; } /** True if a WebSocket to ws://ip:port/ reaches onopen within timeoutMs. */ export function wsPortOpen(ip: string, port: number, timeoutMs: number): Promise { return new Promise((resolve) => { let ws: WebSocket; let settled = false; const done = (ok: boolean) => { if (settled) return; settled = true; try { ws.close(); } catch { /* ignore */ } resolve(ok); }; try { ws = new WebSocket(`ws://${ip}:${port}/`); } catch { resolve(false); return; } const t = setTimeout(() => done(false), timeoutMs); ws.onopen = () => { clearTimeout(t); done(true); }; ws.onerror = () => { clearTimeout(t); done(false); }; ws.onclose = () => { clearTimeout(t); done(false); }; }); }