local-network.ts

4.2 KB · raw

1const SYNC_PORT = 44100;2const PROBE_TIMEOUT_MS = 400;34// Detect the local subnet via WebRTC ICE candidate parsing.5// Returns e.g. "192.168.1" or null if unavailable.6export async function getLocalSubnet(): Promise<string | null> {7  return new Promise((resolve) => {8    try {9      const pc = new RTCPeerConnection({ iceServers: [] });10      pc.createDataChannel('');11      pc.onicecandidate = (e) => {12        if (!e.candidate) { pc.close(); resolve(null); return; }13        const m = e.candidate.candidate.match(/(\d+\.\d+\.\d+)\.\d+/);14        if (m) { pc.close(); resolve(m[1]); }15      };16      pc.createOffer().then(o => pc.setLocalDescription(o)).catch(() => { pc.close(); resolve(null); });17      setTimeout(() => { try { pc.close(); } catch { /* ignore */ } resolve(null); }, 2000);18    } catch {19      resolve(null);20    }21  });22}2324// Probe a single IP to see if it responds to PEACH_DISCOVER_PROBE with our25// publicKeyHex. Returns true if it's our paired device.26async function probeDiscover(ip: string, publicKeyHex: string): Promise<boolean> {27  return new Promise((resolve) => {28    try {29      const ws = new WebSocket(`ws://${ip}:${SYNC_PORT}`);30      const timer = setTimeout(() => { try { ws.close(); } catch { /* ignore */ } resolve(false); }, PROBE_TIMEOUT_MS);31      ws.onopen = () => ws.send(JSON.stringify({ type: 'PEACH_DISCOVER_PROBE', publicKeyHex }));32      ws.onmessage = (e) => {33        clearTimeout(timer);34        try {35          const msg = JSON.parse(e.data as string);36          resolve(msg?.type === 'PEACH_DISCOVER_ACK' && msg?.publicKeyHex === publicKeyHex);37        } catch { resolve(false); }38        ws.close();39      };40      ws.onerror = () => { clearTimeout(timer); resolve(false); };41      ws.onclose = () => { clearTimeout(timer); };42    } catch {43      resolve(false);44    }45  });46}4748// Scan the subnet for a device with the given publicKeyHex.49// Returns the IP if found, null otherwise.50export async function discoverDevice(subnet: string, publicKeyHex: string): Promise<string | null> {51  const batch = 16;52  for (let start = 1; start <= 254; start += batch) {53    const end = Math.min(start + batch - 1, 254);54    const ips = Array.from({ length: end - start + 1 }, (_, i) => `${subnet}.${start + i}`);55    const results = await Promise.all(ips.map(async (ip) => {56      const found = await probeDiscover(ip, publicKeyHex);57      return found ? ip : null;58    }));59    const hit = results.find(r => r !== null);60    if (hit) return hit;61  }62  return null;63}6465// Probe a single IP to see if it's a phone in pairing mode with the given token.66// Returns { ip, publicKeyHex } if found, null otherwise.67async function probePair(ip: string, token: string): Promise<{ ip: string; publicKeyHex: string } | null> {68  return new Promise((resolve) => {69    try {70      const ws = new WebSocket(`ws://${ip}:${SYNC_PORT}`);71      const timer = setTimeout(() => { try { ws.close(); } catch { /* ignore */ } resolve(null); }, PROBE_TIMEOUT_MS);72      ws.onopen = () => ws.send(JSON.stringify({ type: 'PEACH_PAIR_PROBE', token }));73      ws.onmessage = (e) => {74        clearTimeout(timer);75        try {76          const msg = JSON.parse(e.data as string);77          if (msg?.type === 'PEACH_PAIR_ACK' && typeof msg?.publicKeyHex === 'string') {78            resolve({ ip, publicKeyHex: msg.publicKeyHex as string });79          } else {80            resolve(null);81          }82        } catch { resolve(null); }83        ws.close();84      };85      ws.onerror = () => { clearTimeout(timer); resolve(null); };86      ws.onclose = () => { clearTimeout(timer); };87    } catch {88      resolve(null);89    }90  });91}9293// Scan the subnet for a phone in pairing mode with the given token.94// Returns { ip, publicKeyHex } of the first phone that accepts the token.95export async function scanForPairingPhone(96  subnet: string,97  token: string,98): Promise<{ ip: string; publicKeyHex: string } | null> {99  const batch = 16;100  for (let start = 1; start <= 254; start += batch) {101    const end = Math.min(start + batch - 1, 254);102    const ips = Array.from({ length: end - start + 1 }, (_, i) => `${subnet}.${start + i}`);103    const results = await Promise.all(ips.map(ip => probePair(ip, token)));104    const hit = results.find(r => r !== null);105    if (hit) return hit;106  }107  return null;108}