1234567891011121314151617import { dialPairingChannel, PAIRING_PORT, type PairingDialDeps } from "../shared/pairing-dial";18import type { LocalIdentity } from "../shared/pairing-handshake";19import { parsePairingCode } from "../shared/pairing-code";20import type { DeviceType, PairedDevice } from "../shared/sync-types";21222324const DEFAULT_SYNC_PORT = PAIRING_PORT;2526export type CodePairingError =27 | "invalid_code" 28 | "no_host_found" 29 | "handshake_failed" 30 | "sas_mismatch" 31 | "vault_locked"; 3233export interface CodePairingDeps extends PairingDialDeps {34 35 getLocalIdentity(): Promise<LocalIdentity>;36 37 persistDevice(device: PairedDevice): Promise<void>;38 39 cacheLastKnownIp?(pubkeyHex: string, ip: string): Promise<void>;40}4142export interface CodePairingCallbacks {43 44 onSasReady(sas: string): void;45 46 onComplete(device: PairedDevice): void;47 48 onError(error: CodePairingError): void;49}5051interface PairingFrame {52 type: "PAIR_IDENTITY";53 device_id: string;54 device_name: string;55 device_type: DeviceType;56 long_term_public_key_hex: string;57 signing_public_key_hex?: string;58 ws_port?: number;59 vault_id?: string; 60}6162function encodeJson(obj: unknown): Uint8Array {63 return new TextEncoder().encode(JSON.stringify(obj));64}6566function decodePairIdentity(bytes: Uint8Array): PairingFrame {67 const p = JSON.parse(new TextDecoder().decode(bytes)) as Record<string, unknown>;68 if (p?.type !== "PAIR_IDENTITY") throw new Error("expected PAIR_IDENTITY");69 if (typeof p.device_id !== "string" || !p.device_id) throw new Error("device_id missing");70 if (typeof p.device_name !== "string") throw new Error("device_name missing");71 if (typeof p.device_type !== "string") throw new Error("device_type missing");72 if (typeof p.long_term_public_key_hex !== "string" ||73 !/^[0-9a-f]{64}$/.test(p.long_term_public_key_hex)) {74 throw new Error("long_term_public_key_hex is not 32-byte hex");75 }76 return p as unknown as PairingFrame;77}7879808182838485868788export async function pairWithCode(89 code: string,90 cb: CodePairingCallbacks,91 deps: CodePairingDeps,92 confirm: (sas: string) => Promise<boolean>,93): Promise<void> {94 let identity: LocalIdentity;95 try {96 identity = await deps.getLocalIdentity();97 } catch {98 cb.onError("vault_locked");99 return;100 }101102 const dial = await dialPairingChannel(code, deps);103 if (!dial.ok) {104 cb.onError(dial.error);105 return;106 }107 const { channel, sas, host, socket } = dial.dialled;108109 try {110 111 const ourFrame: PairingFrame = {112 type: "PAIR_IDENTITY",113 device_id: identity.deviceId,114 device_name: identity.deviceName,115 device_type: identity.deviceType,116 long_term_public_key_hex: identity.longTermPublicKeyHex,117 signing_public_key_hex: identity.signingPublicKeyHex,118 ws_port: identity.wsPort,119 vault_id: identity.vaultId, 120 };121 await channel.send(encodeJson(ourFrame));122123 const theirFrame = decodePairIdentity(await channel.receive());124125 cb.onSasReady(sas);126 const confirmed = await confirm(sas);127 if (!confirmed) {128 cb.onError("sas_mismatch");129 return;130 }131132 133 134 135 const device: PairedDevice = {136 deviceId: theirFrame.device_id,137 deviceName: theirFrame.device_name,138 deviceType:139 (theirFrame.device_type as DeviceType) ||140 (parsePairingCode(code) ? "android" : "desktop"),141 longTermPublicKeyHex: theirFrame.long_term_public_key_hex,142 signingPublicKeyHex: theirFrame.signing_public_key_hex,143 trustSource: "direct",144 wsPort: typeof theirFrame.ws_port === "number" ? theirFrame.ws_port : DEFAULT_SYNC_PORT,145 pairedAt: Date.now(),146 };147 await deps.persistDevice(device);148 if (deps.cacheLastKnownIp && device.longTermPublicKeyHex) {149 await deps.cacheLastKnownIp(device.longTermPublicKeyHex, host).catch(() => undefined);150 }151 cb.onComplete(device);152 } catch (err) {153 if (typeof console !== "undefined") console.error("[desktop-pairing] handshake failed:", err);154 cb.onError("handshake_failed");155 } finally {156 socket.close();157 }158}