// Background wiring for desktop helper code pairing (Change 3 §Wiring). // // Keeps `desktop-pairing.ts` a PURE, dependency-injected client (so its unit // tests run without chrome / vault / WebSocket). This module supplies the // production `DesktopPairingDeps`, the real `WebSocket` adapter, and the // single-flight SAS/confirm round-trip that bridges the blocking // `confirm(sas)` callback to the popup via runtime messages: // // PAIR_WITH_DESKTOP → startDesktopPairing() (popup → bg, messages.ts) // DESKTOP_PAIR_SAS ← push when onSasReady fires (bg → popup) // DESKTOP_PAIR_CONFIRM → resolveDesktopPairingConfirm (popup → bg, messages.ts) // DESKTOP_PAIR_RESULT ← push on complete/error (bg → popup) import { pairWithDesktopHelper, type DesktopPairingDeps, type DesktopPairingSocket, type PairWithDesktopCallbacks, } from "./desktop-pairing"; import { upsertPairedDevice, setLastKnownIp } from "./paired-devices"; import { buildLocalIdentity } from "./chirp-identity"; import { computeVaultId } from "../shared/sync-identity"; import { isUnlocked, getRootKeyHex } from "./vault"; import { probeSubnetForPort } from "./desktop-pairing-probe"; import { discoverPeachSync } from "./desktop-pairing-mdns"; import { getLocalSubnet } from "../shared/local-network"; import type { LocalIdentity } from "../shared/chirp-handshake"; function hexToBytesLocal(hex: string): Uint8Array { const out = new Uint8Array(hex.length / 2); for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); return out; } /** Best-effort push to the popup. The popup may be closed — pushes are advisory. */ function push(type: string, payload: unknown): void { try { const maybe = chrome.runtime.sendMessage({ type, payload }) as unknown; if (maybe && typeof (maybe as Promise).catch === "function") { (maybe as Promise).catch(() => undefined); } } catch { // Popup closed / no receiver — fine. } } /** Adapt a real `WebSocket` to the `DesktopPairingSocket` seam (binary frames). */ export function openBinaryWebSocket(ip: string, port: number): Promise { return new Promise((resolve, reject) => { let ws: WebSocket; try { ws = new WebSocket(`ws://${ip}:${port}/`); } catch (err) { reject(err instanceof Error ? err : new Error("websocket open failed")); return; } ws.binaryType = "arraybuffer"; const inbox: Uint8Array[] = []; const readers: Array<{ resolve: (b: Uint8Array) => void; reject: (e: Error) => void }> = []; let opened = false; let closed = false; // True iff the socket closed with no frame delivered for the pending read — // the wrong/expired-code signal (peach-sync sends no error frame). let sawCloseBeforeFrame = false; ws.onopen = () => { opened = true; resolve(sock); }; ws.onmessage = (ev: MessageEvent) => { const bytes = new Uint8Array(ev.data as ArrayBuffer); const r = readers.shift(); if (r) { sawCloseBeforeFrame = false; r.resolve(bytes); } else inbox.push(bytes); }; ws.onerror = () => { if (!opened) reject(new Error("websocket connection failed")); }; ws.onclose = () => { closed = true; if (readers.length) { sawCloseBeforeFrame = true; while (readers.length) readers.shift()!.reject(new Error("socket closed")); } if (!opened) reject(new Error("socket closed before open")); }; const sock: DesktopPairingSocket = { send(bytes: Uint8Array) { ws.send(bytes as Uint8Array); }, receive(): Promise { const queued = inbox.shift(); if (queued) { sawCloseBeforeFrame = false; return Promise.resolve(queued); } if (closed) { sawCloseBeforeFrame = true; return Promise.reject(new Error("socket closed")); } return new Promise((res, rej) => readers.push({ resolve: res, reject: rej })); }, close() { try { ws.close(); } catch { /* ignore */ } }, closedBeforeFrame() { return sawCloseBeforeFrame; }, }; }); } /** Production dependency set — real probe, WebSocket, identity, and storage. */ export function productionDesktopPairingDeps(): DesktopPairingDeps { return { async findHost(port) { // mDNS first — instant and reliable when chrome.mdns is available. const discovered = await discoverPeachSync(); if (discovered) return discovered.ip; // Fallback: /24 subnet probe (Firefox, or Chrome without mDNS). const subnet = await getLocalSubnet(); if (!subnet) return null; return probeSubnetForPort(subnet, port); }, openSocket: (ip, port) => openBinaryWebSocket(ip, port), async getLocalIdentity(): Promise { if (!isUnlocked()) throw new Error("vault_locked"); const masterKeyHex = getRootKeyHex(); if (!masterKeyHex) throw new Error("vault_locked"); // SEV-2-B: derive from the stable ROOT key so pairing survives a // master-password change. Zero the copy when done. const masterKey = hexToBytesLocal(masterKeyHex); try { const base = await buildLocalIdentity(masterKey); const vaultId = await computeVaultId(masterKey); return { ...base, vaultId }; } finally { masterKey.fill(0); } }, persistDevice: (d) => upsertPairedDevice(d), cacheLastKnownIp: (pubkeyHex, ip) => setLastKnownIp(pubkeyHex, ip), }; } // ── Single in-flight pairing per worker ────────────────────────────────────── // One pairing at a time. The blocking confirm() resolver is parked here while // the popup shows the SAS; DESKTOP_PAIR_CONFIRM resolves it. let pendingConfirm: ((confirmed: boolean) => void) | null = null; let inFlight = false; function clearPairingState(): void { pendingConfirm = null; inFlight = false; } export function isDesktopPairingInFlight(): boolean { return inFlight; } /** * Kick off a desktop pairing. Returns immediately; the SAS and the terminal * result are delivered to the popup as DESKTOP_PAIR_SAS / DESKTOP_PAIR_RESULT * pushes. `deps` is injectable for tests. */ export function startDesktopPairing( code: string, deps: DesktopPairingDeps = productionDesktopPairingDeps(), ): void { inFlight = true; const cb: PairWithDesktopCallbacks = { onSasReady: (sas) => push("DESKTOP_PAIR_SAS", { sas }), onComplete: (device) => { push("DESKTOP_PAIR_RESULT", { status: "complete", device }); clearPairingState(); }, onError: (error) => { push("DESKTOP_PAIR_RESULT", { status: "error", error }); clearPairingState(); }, }; // The confirm callback blocks on the popup's DESKTOP_PAIR_CONFIRM reply. const confirm = (_sas: string): Promise => new Promise((resolve) => { pendingConfirm = resolve; }); void pairWithDesktopHelper(code, cb, deps, confirm).finally(() => { // onComplete/onError already cleared state; this is the backstop for an // unexpected throw that skipped the callbacks. if (inFlight) clearPairingState(); }); } /** Resolve the parked confirm() with the user's decision from the popup. */ export function resolveDesktopPairingConfirm(confirmed: boolean): void { const resolve = pendingConfirm; pendingConfirm = null; if (resolve) resolve(confirmed); }