/** * Capacitor pairing handshake — WebSocket variant of the desktop * INITIATE_PAIR_WITH_PEER flow. Used by the Android entering-side path * after `Sync.resolvePeerByCode` returns the desktop's connection info. * * Bytes-on-wire match the desktop TCP path: Noise NK + auth proof, then a * single PAIR_REQUEST frame and a single PAIR_ACCEPT/PAIR_REJECT frame. * The transport returned by `createCapacitorSyncTransport` already wraps * Noise + framing; this module just sends/receives the two pairing frames. */ import { createCapacitorSyncTransport } from "./capacitor-sync-transport"; import { encodeSyncMessage, decodeSyncMessage } from "./sync-wire"; import type { DiscoveredPeer, SyncMessage } from "./sync-types"; const PAIR_RESPONSE_TIMEOUT_MS = 30_000; export interface CapacitorPairingResult { publicKeyHex: string; deviceName: string; } export async function runCapacitorPairingHandshake( peer: DiscoveredPeer, ourPrivateKey: Uint8Array, ourPublicKey: Uint8Array, ourPublicKeyHex: string, ourDeviceName: string, ): Promise { const transport = await createCapacitorSyncTransport( peer, ourPrivateKey, ourPublicKey, ); try { const responsePromise = new Promise((resolve, reject) => { const timer = setTimeout(() => { cleanup(); reject(new Error("pairing response timeout")); }, PAIR_RESPONSE_TIMEOUT_MS); const offFrame = transport.onFrame((frame) => { try { const message = decodeSyncMessage(frame); cleanup(); resolve(message); } catch (err) { cleanup(); reject(err instanceof Error ? err : new Error(String(err))); } }); const offError = transport.onError((message) => { cleanup(); reject(new Error(message)); }); const offClose = transport.onClose(() => { cleanup(); reject(new Error("connection closed before pairing response")); }); function cleanup() { clearTimeout(timer); offFrame(); offError(); offClose(); } }); await transport.send( encodeSyncMessage({ type: "PAIR_REQUEST", public_key_hex: ourPublicKeyHex, device_name: ourDeviceName, }), ); const response = await responsePromise; if (response.type === "PAIR_ACCEPT") { return { publicKeyHex: response.public_key_hex, deviceName: response.device_name, }; } if (response.type === "PAIR_REJECT") { throw new Error(`pair_rejected:${response.reason}`); } throw new Error(`unexpected_response:${response.type}`); } finally { try { transport.close(); } catch { // best-effort } } }