const SYNC_PORT = 44100; const PROBE_TIMEOUT_MS = 400; // Detect the local subnet via WebRTC ICE candidate parsing. // Returns e.g. "192.168.1" or null if unavailable. export async function getLocalSubnet(): Promise { return new Promise((resolve) => { try { const pc = new RTCPeerConnection({ iceServers: [] }); pc.createDataChannel(''); pc.onicecandidate = (e) => { if (!e.candidate) { pc.close(); resolve(null); return; } const m = e.candidate.candidate.match(/(\d+\.\d+\.\d+)\.\d+/); if (m) { pc.close(); resolve(m[1]); } }; pc.createOffer().then(o => pc.setLocalDescription(o)).catch(() => { pc.close(); resolve(null); }); setTimeout(() => { try { pc.close(); } catch { /* ignore */ } resolve(null); }, 2000); } catch { resolve(null); } }); } // Probe a single IP to see if it responds to PEACH_DISCOVER_PROBE with our // publicKeyHex. Returns true if it's our paired device. async function probeDiscover(ip: string, publicKeyHex: string): Promise { return new Promise((resolve) => { try { const ws = new WebSocket(`ws://${ip}:${SYNC_PORT}`); const timer = setTimeout(() => { try { ws.close(); } catch { /* ignore */ } resolve(false); }, PROBE_TIMEOUT_MS); ws.onopen = () => ws.send(JSON.stringify({ type: 'PEACH_DISCOVER_PROBE', publicKeyHex })); ws.onmessage = (e) => { clearTimeout(timer); try { const msg = JSON.parse(e.data as string); resolve(msg?.type === 'PEACH_DISCOVER_ACK' && msg?.publicKeyHex === publicKeyHex); } catch { resolve(false); } ws.close(); }; ws.onerror = () => { clearTimeout(timer); resolve(false); }; ws.onclose = () => { clearTimeout(timer); }; } catch { resolve(false); } }); } // Scan the subnet for a device with the given publicKeyHex. // Returns the IP if found, null otherwise. export async function discoverDevice(subnet: string, publicKeyHex: string): Promise { const batch = 16; for (let start = 1; start <= 254; start += batch) { const end = Math.min(start + batch - 1, 254); const ips = Array.from({ length: end - start + 1 }, (_, i) => `${subnet}.${start + i}`); const results = await Promise.all(ips.map(async (ip) => { const found = await probeDiscover(ip, publicKeyHex); return found ? ip : null; })); const hit = results.find(r => r !== null); if (hit) return hit; } return null; } // Probe a single IP to see if it's a phone in pairing mode with the given token. // Returns { ip, publicKeyHex } if found, null otherwise. async function probePair(ip: string, token: string): Promise<{ ip: string; publicKeyHex: string } | null> { return new Promise((resolve) => { try { const ws = new WebSocket(`ws://${ip}:${SYNC_PORT}`); const timer = setTimeout(() => { try { ws.close(); } catch { /* ignore */ } resolve(null); }, PROBE_TIMEOUT_MS); ws.onopen = () => ws.send(JSON.stringify({ type: 'PEACH_PAIR_PROBE', token })); ws.onmessage = (e) => { clearTimeout(timer); try { const msg = JSON.parse(e.data as string); if (msg?.type === 'PEACH_PAIR_ACK' && typeof msg?.publicKeyHex === 'string') { resolve({ ip, publicKeyHex: msg.publicKeyHex as string }); } else { resolve(null); } } catch { resolve(null); } ws.close(); }; ws.onerror = () => { clearTimeout(timer); resolve(null); }; ws.onclose = () => { clearTimeout(timer); }; } catch { resolve(null); } }); } // Scan the subnet for a phone in pairing mode with the given token. // Returns { ip, publicKeyHex } of the first phone that accepts the token. export async function scanForPairingPhone( subnet: string, token: string, ): Promise<{ ip: string; publicKeyHex: string } | null> { const batch = 16; for (let start = 1; start <= 254; start += batch) { const end = Math.min(start + batch - 1, 254); const ips = Array.from({ length: end - start + 1 }, (_, i) => `${subnet}.${start + i}`); const results = await Promise.all(ips.map(ip => probePair(ip, token))); const hit = results.find(r => r !== null); if (hit) return hit; } return null; }