1234567891011121314import {15 pairWithDesktopHelper,16 type DesktopPairingDeps,17 type DesktopPairingSocket,18 type PairWithDesktopCallbacks,19} from "./desktop-pairing";20import { upsertPairedDevice, setLastKnownIp } from "./paired-devices";21import { buildLocalIdentity } from "./chirp-identity";22import { computeVaultId } from "../shared/sync-identity";23import { isUnlocked, getRootKeyHex } from "./vault";24import { probeSubnetForPort } from "./desktop-pairing-probe";25import { discoverPeachSync } from "./desktop-pairing-mdns";26import { getLocalSubnet } from "../shared/local-network";27import type { LocalIdentity } from "../shared/chirp-handshake";2829function hexToBytesLocal(hex: string): Uint8Array {30 const out = new Uint8Array(hex.length / 2);31 for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);32 return out;33}343536function push(type: string, payload: unknown): void {37 try {38 const maybe = chrome.runtime.sendMessage({ type, payload }) as unknown;39 if (maybe && typeof (maybe as Promise<unknown>).catch === "function") {40 (maybe as Promise<unknown>).catch(() => undefined);41 }42 } catch {43 44 }45}464748export function openBinaryWebSocket(ip: string, port: number): Promise<DesktopPairingSocket> {49 return new Promise((resolve, reject) => {50 let ws: WebSocket;51 try {52 ws = new WebSocket(`ws://${ip}:${port}/`);53 } catch (err) {54 reject(err instanceof Error ? err : new Error("websocket open failed"));55 return;56 }57 ws.binaryType = "arraybuffer";5859 const inbox: Uint8Array[] = [];60 const readers: Array<{ resolve: (b: Uint8Array) => void; reject: (e: Error) => void }> = [];61 let opened = false;62 let closed = false;63 64 65 let sawCloseBeforeFrame = false;6667 ws.onopen = () => {68 opened = true;69 resolve(sock);70 };71 ws.onmessage = (ev: MessageEvent) => {72 const bytes = new Uint8Array(ev.data as ArrayBuffer);73 const r = readers.shift();74 if (r) { sawCloseBeforeFrame = false; r.resolve(bytes); }75 else inbox.push(bytes);76 };77 ws.onerror = () => {78 if (!opened) reject(new Error("websocket connection failed"));79 };80 ws.onclose = () => {81 closed = true;82 if (readers.length) {83 sawCloseBeforeFrame = true;84 while (readers.length) readers.shift()!.reject(new Error("socket closed"));85 }86 if (!opened) reject(new Error("socket closed before open"));87 };8889 const sock: DesktopPairingSocket = {90 send(bytes: Uint8Array) { ws.send(bytes as Uint8Array<ArrayBuffer>); },91 receive(): Promise<Uint8Array> {92 const queued = inbox.shift();93 if (queued) { sawCloseBeforeFrame = false; return Promise.resolve(queued); }94 if (closed) { sawCloseBeforeFrame = true; return Promise.reject(new Error("socket closed")); }95 return new Promise<Uint8Array>((res, rej) => readers.push({ resolve: res, reject: rej }));96 },97 close() { try { ws.close(); } catch { } },98 closedBeforeFrame() { return sawCloseBeforeFrame; },99 };100 });101}102103104export function productionDesktopPairingDeps(): DesktopPairingDeps {105 return {106 async findHost(port) {107 108 const discovered = await discoverPeachSync();109 if (discovered) return discovered.ip;110 111 const subnet = await getLocalSubnet();112 if (!subnet) return null;113 return probeSubnetForPort(subnet, port);114 },115 openSocket: (ip, port) => openBinaryWebSocket(ip, port),116 async getLocalIdentity(): Promise<LocalIdentity> {117 if (!isUnlocked()) throw new Error("vault_locked");118 const masterKeyHex = getRootKeyHex();119 if (!masterKeyHex) throw new Error("vault_locked");120 121 122 const masterKey = hexToBytesLocal(masterKeyHex);123 try {124 const base = await buildLocalIdentity(masterKey);125 const vaultId = await computeVaultId(masterKey);126 return { ...base, vaultId };127 } finally {128 masterKey.fill(0);129 }130 },131 persistDevice: (d) => upsertPairedDevice(d),132 cacheLastKnownIp: (pubkeyHex, ip) => setLastKnownIp(pubkeyHex, ip),133 };134}135136137138139let pendingConfirm: ((confirmed: boolean) => void) | null = null;140let inFlight = false;141142function clearPairingState(): void {143 pendingConfirm = null;144 inFlight = false;145}146147export function isDesktopPairingInFlight(): boolean {148 return inFlight;149}150151152153154155156export function startDesktopPairing(157 code: string,158 deps: DesktopPairingDeps = productionDesktopPairingDeps(),159): void {160 inFlight = true;161 const cb: PairWithDesktopCallbacks = {162 onSasReady: (sas) => push("DESKTOP_PAIR_SAS", { sas }),163 onComplete: (device) => {164 push("DESKTOP_PAIR_RESULT", { status: "complete", device });165 clearPairingState();166 },167 onError: (error) => {168 push("DESKTOP_PAIR_RESULT", { status: "error", error });169 clearPairingState();170 },171 };172 173 const confirm = (_sas: string): Promise<boolean> =>174 new Promise<boolean>((resolve) => { pendingConfirm = resolve; });175176 void pairWithDesktopHelper(code, cb, deps, confirm).finally(() => {177 178 179 if (inFlight) clearPairingState();180 });181}182183184export function resolveDesktopPairingConfirm(confirmed: boolean): void {185 const resolve = pendingConfirm;186 pendingConfirm = null;187 if (resolve) resolve(confirmed);188}