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 getLocalSubnet(): Promise<string | null>;53 54 openPairingTransport(endpoint: { ip: string; port: number }): Promise<{55 send(msg: Uint8Array): Promise<void>;56 receive(): Promise<Uint8Array>;57 close(): Promise<void>;58 }>;59 60 openAudio?(): Promise<Awaited<ReturnType<typeof openBrowserAudioIO>>>;61 62 chirpTimeoutMs?: number;63 onProgress(p: PairingProgress): void;64}6566export class ChirpPairingFailure extends Error {67 readonly phase: PairingProgress["phase"];68 constructor(phase: PairingProgress["phase"], message: string) {69 super(message);70 this.phase = phase;71 }72}7374export async function runChirpPairing(deps: ChirpPairingDeps): Promise<PairedDevice> {75 deps.onProgress({ phase: "idle" });7677 const localIdentity = await deps.fetchLocalIdentity();78 if (localIdentity.deviceType !== "extension") {79 80 81 throw new ChirpPairingFailure("handshake_failed", "wrong-platform orchestrator");82 }8384 deps.onProgress({ phase: "listening" });8586 const attempt = createPairingAttempt(DEVICE_TYPE_EXTENSION);87 try {88 const audio = await (deps.openAudio ?? openBrowserAudioIO)();89 await runChirpExchange(audio, { attempt, timeoutMs: deps.chirpTimeoutMs });90 } catch (err) {91 if (err instanceof ChirpExchangeError) {92 const phase: PairingProgress["phase"] =93 err.kind === "timeout" ? "timeout" :94 err.kind === "version_mismatch" ? "version_mismatch" :95 err.kind === "mic_denied" ? "mic_denied" :96 "handshake_failed";97 deps.onProgress({ phase, errorMessage: err.message });98 throw new ChirpPairingFailure(phase, err.message);99 }100 throw err;101 }102103 deps.onProgress({ phase: "handshake" });104105 106 107 108 const subnet = await deps.getLocalSubnet();109 if (!subnet) {110 deps.onProgress({ phase: "handshake_failed", errorMessage: "no_subnet" });111 throw new ChirpPairingFailure("handshake_failed", "could not determine local subnet");112 }113114 const peerEndpoint = await firstResponsivePeer(deps, subnet, CHIRP_PAIRING_PORT);115 if (!peerEndpoint) {116 deps.onProgress({ phase: "handshake_failed", errorMessage: "peer_not_found" });117 throw new ChirpPairingFailure("handshake_failed", "could not reach paired peer on subnet");118 }119120 const transport = await deps.openPairingTransport(peerEndpoint);121 let paired: PairedDevice;122 try {123 paired = await runPairingResponder(124 attempt.ephemeralPrivateKey,125 attempt.ephemeralPublicKey,126 localIdentity,127 transport,128 );129 } catch (err) {130 await transport.close();131 deps.onProgress({ phase: "handshake_failed", errorMessage: (err as Error).message });132 throw new ChirpPairingFailure("handshake_failed", (err as Error).message);133 }134 await transport.close();135136 137 138 paired = { ...paired, lastKnownIp: peerEndpoint.ip };139 await deps.completePairing(paired);140 deps.onProgress({ phase: "paired", peerDeviceName: paired.deviceName });141 return paired;142}143144async function firstResponsivePeer(145 deps: ChirpPairingDeps,146 subnet: string,147 port: number,148): Promise<{ ip: string; port: number } | null> {149 150 151 152 153 154 const batch = 16;155 for (let start = 1; start <= 254; start += batch) {156 const end = Math.min(start + batch - 1, 254);157 const ips: string[] = [];158 for (let i = start; i <= end; i++) ips.push(`${subnet}.${i}`);159160 const results = await Promise.all(161 ips.map(async (ip) => {162 try {163 const t = await deps.openPairingTransport({ ip, port });164 await t.close();165 return { ip, port };166 } catch {167 return null;168 }169 }),170 );171 const hit = results.find((r): r is { ip: string; port: number } => r !== null);172 if (hit) return hit;173 }174 return null;175}