// 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 PairWithDesktopCallbacks, } from "./desktop-pairing"; import { openPairingSocket } from "../shared/pairing-dial"; import { upsertPairedDevice, setLastKnownIp } from "./paired-devices"; import { buildLocalIdentity } from "./chirp-identity"; import { computeVaultId } from "../shared/sync-identity"; import { isUnlocked, getRootKeyHex } from "./vault"; import { probeSubnetForPort, wsPortOpen } 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. } } /** * The WebSocket adapter moved to `shared/pairing-dial.ts` alongside the seam it * implements, so vault adoption (which runs in the popup, before a vault * exists) dials through the same code. Re-exported under its original name * because that is what existing callers import. */ export { openPairingSocket as openBinaryWebSocket }; /** Production dependency set — real probe, WebSocket, identity, and storage. */ export function productionDesktopPairingDeps(): DesktopPairingDeps { return { async findHost(port) { // Localhost first: peach-sync runs on the same machine as the extension. // This works in every browser without mDNS or WebRTC, and is the fastest // path. The daemon binds 0.0.0.0 so 127.0.0.1 is always reachable. if (await wsPortOpen("127.0.0.1", port, 800)) return "127.0.0.1"; // mDNS next — instant when chrome.mdns is available (Chrome/Chromium). const discovered = await discoverPeachSync(); if (discovered && (await wsPortOpen(discovered.ip, port, 800))) return discovered.ip; // Fallback: /24 subnet probe (Firefox, Chrome without mDNS, or a stale hit). const subnet = await getLocalSubnet(); if (!subnet) return null; return probeSubnetForPort(subnet, port); }, openSocket: (ip, port) => openPairingSocket(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); }