1const SYNC_PORT = 44100;2const PROBE_TIMEOUT_MS = 400;3456export 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 { } resolve(null); }, 2000);18 } catch {19 resolve(null);20 }21 });22}23242526async 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 { } 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}47484950export 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}64656667async 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 { } 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}92939495export 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}