capacitor-pairing-handshake.ts

2.7 KB · raw

1/**2 * Capacitor pairing handshake — WebSocket variant of the desktop3 * INITIATE_PAIR_WITH_PEER flow. Used by the Android entering-side path4 * after `Sync.resolvePeerByCode` returns the desktop's connection info.5 *6 * Bytes-on-wire match the desktop TCP path: Noise NK + auth proof, then a7 * single PAIR_REQUEST frame and a single PAIR_ACCEPT/PAIR_REJECT frame.8 * The transport returned by `createCapacitorSyncTransport` already wraps9 * Noise + framing; this module just sends/receives the two pairing frames.10 */1112import { createCapacitorSyncTransport } from "./capacitor-sync-transport";13import { encodeSyncMessage, decodeSyncMessage } from "./sync-wire";14import type { DiscoveredPeer, SyncMessage } from "./sync-types";1516const PAIR_RESPONSE_TIMEOUT_MS = 30_000;1718export interface CapacitorPairingResult {19  publicKeyHex: string;20  deviceName: string;21}2223export async function runCapacitorPairingHandshake(24  peer: DiscoveredPeer,25  ourPrivateKey: Uint8Array,26  ourPublicKey: Uint8Array,27  ourPublicKeyHex: string,28  ourDeviceName: string,29): Promise<CapacitorPairingResult> {30  const transport = await createCapacitorSyncTransport(31    peer,32    ourPrivateKey,33    ourPublicKey,34  );3536  try {37    const responsePromise = new Promise<SyncMessage>((resolve, reject) => {38      const timer = setTimeout(() => {39        cleanup();40        reject(new Error("pairing response timeout"));41      }, PAIR_RESPONSE_TIMEOUT_MS);4243      const offFrame = transport.onFrame((frame) => {44        try {45          const message = decodeSyncMessage(frame);46          cleanup();47          resolve(message);48        } catch (err) {49          cleanup();50          reject(err instanceof Error ? err : new Error(String(err)));51        }52      });53      const offError = transport.onError((message) => {54        cleanup();55        reject(new Error(message));56      });57      const offClose = transport.onClose(() => {58        cleanup();59        reject(new Error("connection closed before pairing response"));60      });6162      function cleanup() {63        clearTimeout(timer);64        offFrame();65        offError();66        offClose();67      }68    });6970    await transport.send(71      encodeSyncMessage({72        type: "PAIR_REQUEST",73        public_key_hex: ourPublicKeyHex,74        device_name: ourDeviceName,75      }),76    );7778    const response = await responsePromise;79    if (response.type === "PAIR_ACCEPT") {80      return {81        publicKeyHex: response.public_key_hex,82        deviceName: response.device_name,83      };84    }85    if (response.type === "PAIR_REJECT") {86      throw new Error(`pair_rejected:${response.reason}`);87    }88    throw new Error(`unexpected_response:${response.type}`);89  } finally {90    try {91      transport.close();92    } catch {93      // best-effort94    }95  }96}