123456789101112131415161718192021import {22 ChirpExchangeError,23 createPairingAttempt,24 openBrowserAudioIO,25 runChirpExchange,26} from "../shared/chirp-pairing";27import {28 runPairingResponder,29 type LocalIdentity,30} from "../shared/chirp-handshake";31import {32 DEVICE_TYPE_EXTENSION,33 type PairedDevice,34 type PairingProgress,35} from "../shared/sync-types";363738394041424344export const CHIRP_PAIRING_PORT = 49142;4546export interface ChirpPairingDeps {47 48 fetchLocalIdentity(): Promise<LocalIdentity>;49 50 completePairing(paired: PairedDevice): Promise<void>;51 52 cacheLastKnownIp?(pubkeyHex: string, ip: string): Promise<void>;53 54 getLocalSubnet(): Promise<string | null>;55 56 openPairingTransport(endpoint: { ip: string; port: number }): Promise<{57 send(msg: Uint8Array): Promise<void>;58 receive(): Promise<Uint8Array>;59 close(): Promise<void>;60 }>;61 62 openAudio?(): Promise<Awaited<ReturnType<typeof openBrowserAudioIO>>>;63 64 chirpTimeoutMs?: number;65 onProgress(p: PairingProgress): void;66}6768export class ChirpPairingFailure extends Error {69 readonly phase: PairingProgress["phase"];70 constructor(phase: PairingProgress["phase"], message: string) {71 super(message);72 this.phase = phase;73 }74}7576export async function runChirpPairing(deps: ChirpPairingDeps): Promise<PairedDevice> {77 deps.onProgress({ phase: "idle" });7879 const localIdentity = await deps.fetchLocalIdentity();80 if (localIdentity.deviceType !== "extension") {81 82 83 throw new ChirpPairingFailure("handshake_failed", "wrong-platform orchestrator");84 }8586 deps.onProgress({ phase: "listening" });8788 const attempt = createPairingAttempt(DEVICE_TYPE_EXTENSION);89 try {90 const audio = await (deps.openAudio ?? openBrowserAudioIO)();91 await runChirpExchange(audio, { attempt, timeoutMs: deps.chirpTimeoutMs });92 } catch (err) {93 if (err instanceof ChirpExchangeError) {94 const phase: PairingProgress["phase"] =95 err.kind === "timeout" ? "timeout" :96 err.kind === "version_mismatch" ? "version_mismatch" :97 err.kind === "mic_denied" ? "mic_denied" :98 "handshake_failed";99 deps.onProgress({ phase, errorMessage: err.message });100 throw new ChirpPairingFailure(phase, err.message);101 }102 throw err;103 }104105 deps.onProgress({ phase: "handshake" });106107 108 109 110 const subnet = await deps.getLocalSubnet();111 if (!subnet) {112 deps.onProgress({ phase: "handshake_failed", errorMessage: "no_subnet" });113 throw new ChirpPairingFailure("handshake_failed", "could not determine local subnet");114 }115116 const peerEndpoint = await firstResponsivePeer(deps, subnet, CHIRP_PAIRING_PORT);117 if (!peerEndpoint) {118 deps.onProgress({ phase: "handshake_failed", errorMessage: "peer_not_found" });119 throw new ChirpPairingFailure("handshake_failed", "could not reach paired peer on subnet");120 }121122 const transport = await deps.openPairingTransport(peerEndpoint);123 let paired: PairedDevice;124 try {125 paired = await runPairingResponder(126 attempt.ephemeralPrivateKey,127 attempt.ephemeralPublicKey,128 localIdentity,129 transport,130 );131 } catch (err) {132 await transport.close();133 deps.onProgress({ phase: "handshake_failed", errorMessage: (err as Error).message });134 throw new ChirpPairingFailure("handshake_failed", (err as Error).message);135 }136 await transport.close();137138 139 140 141 142 await deps.completePairing(paired);143 await deps.cacheLastKnownIp?.(paired.longTermPublicKeyHex ?? "", peerEndpoint.ip);144 deps.onProgress({ phase: "paired", peerDeviceName: paired.deviceName });145 return paired;146}147148async function firstResponsivePeer(149 deps: ChirpPairingDeps,150 subnet: string,151 port: number,152): Promise<{ ip: string; port: number } | null> {153 154 155 156 157 158 159 160 161 162 163 164 165 166 const PROBE_TIMEOUT_MS = 250;167 const batch = 16;168 for (let start = 1; start <= 254; start += batch) {169 const end = Math.min(start + batch - 1, 254);170 const ips: string[] = [];171 for (let i = start; i <= end; i++) ips.push(`${subnet}.${i}`);172173 const results = await Promise.all(174 ips.map(async (ip) => {175 let timer: ReturnType<typeof setTimeout> | undefined;176 const timeout = new Promise<null>((resolve) => {177 timer = setTimeout(() => resolve(null), PROBE_TIMEOUT_MS);178 });179 const probe = (async () => {180 try {181 const t = await deps.openPairingTransport({ ip, port });182 return { transport: t, ip, port };183 } catch {184 return null;185 }186 })();187 const winner = await Promise.race([probe, timeout]);188 if (winner && "transport" in winner) {189 if (timer !== undefined) clearTimeout(timer);190 await winner.transport.close();191 return { ip: winner.ip, port: winner.port };192 }193 194 195 probe.then((late) => {196 if (late) void late.transport.close();197 }).catch(() => { });198 return null;199 }),200 );201 const hit = results.find((r): r is { ip: string; port: number } => r !== null);202 if (hit) return hit;203 }204 return null;205}