// Background wiring for code pairing. // // Keeps `code-pairing.ts` a PURE, dependency-injected client (so its unit tests // run without chrome / vault / WebSocket). This module supplies the production // `CodePairingDeps` and the single-flight SAS/confirm round-trip that bridges // the blocking `confirm(sas)` callback to the popup via runtime messages: // // PAIR_WITH_CODE → startCodePairing() (popup → bg, messages.ts) // CODE_PAIR_SAS ← push when onSasReady fires (bg → popup) // CODE_PAIR_CONFIRM → resolveCodePairingConfirm() (popup → bg, messages.ts) // CODE_PAIR_RESULT ← push on complete/error (bg → popup) import { pairWithCode, type CodePairingDeps, type CodePairingCallbacks, } from "./code-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. } } /** Production dependency set — real probe, WebSocket, identity, and storage. */ export function productionCodePairingDeps(): CodePairingDeps { 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; CODE_PAIR_CONFIRM resolves it. let pendingConfirm: ((confirmed: boolean) => void) | null = null; let inFlight = false; function clearPairingState(): void { pendingConfirm = null; inFlight = false; } export function isCodePairingInFlight(): boolean { return inFlight; } /** * Kick off a code pairing. Returns immediately; the SAS and the terminal * result are delivered to the popup as CODE_PAIR_SAS / CODE_PAIR_RESULT * pushes. `deps` is injectable for tests. */ export function startCodePairing( code: string, deps: CodePairingDeps = productionCodePairingDeps(), ): void { inFlight = true; const cb: CodePairingCallbacks = { onSasReady: (sas) => push("CODE_PAIR_SAS", { sas }), onComplete: (device) => { push("CODE_PAIR_RESULT", { status: "complete", device }); clearPairingState(); }, onError: (error) => { push("CODE_PAIR_RESULT", { status: "error", error }); clearPairingState(); }, }; // The confirm callback blocks on the popup's CODE_PAIR_CONFIRM reply. const confirm = (_sas: string): Promise => new Promise((resolve) => { pendingConfirm = resolve; }); void pairWithCode(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 resolveCodePairingConfirm(confirmed: boolean): void { const resolve = pendingConfirm; pendingConfirm = null; if (resolve) resolve(confirmed); }