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 { getLocalSubnet } from "../shared/local-network";26import type { LocalIdentity } from "../shared/chirp-handshake";2728function hexToBytesLocal(hex: string): Uint8Array {29 const out = new Uint8Array(hex.length / 2);30 for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);31 return out;32}333435function push(type: string, payload: unknown): void {36 try {37 const maybe = chrome.runtime.sendMessage({ type, payload }) as unknown;38 if (maybe && typeof (maybe as Promise<unknown>).catch === "function") {39 (maybe as Promise<unknown>).catch(() => undefined);40 }41 } catch {42 43 }44}454647export function openBinaryWebSocket(ip: string, port: number): Promise<DesktopPairingSocket> {48 return new Promise((resolve, reject) => {49 let ws: WebSocket;50 try {51 ws = new WebSocket(`ws://${ip}:${port}/`);52 } catch (err) {53 reject(err instanceof Error ? err : new Error("websocket open failed"));54 return;55 }56 ws.binaryType = "arraybuffer";5758 const inbox: Uint8Array[] = [];59 const readers: Array<{ resolve: (b: Uint8Array) => void; reject: (e: Error) => void }> = [];60 let opened = false;61 let closed = false;62 63 64 let sawCloseBeforeFrame = false;6566 ws.onopen = () => {67 opened = true;68 resolve(sock);69 };70 ws.onmessage = (ev: MessageEvent) => {71 const bytes = new Uint8Array(ev.data as ArrayBuffer);72 const r = readers.shift();73 if (r) { sawCloseBeforeFrame = false; r.resolve(bytes); }74 else inbox.push(bytes);75 };76 ws.onerror = () => {77 if (!opened) reject(new Error("websocket connection failed"));78 };79 ws.onclose = () => {80 closed = true;81 if (readers.length) {82 sawCloseBeforeFrame = true;83 while (readers.length) readers.shift()!.reject(new Error("socket closed"));84 }85 if (!opened) reject(new Error("socket closed before open"));86 };8788 const sock: DesktopPairingSocket = {89 send(bytes: Uint8Array) { ws.send(bytes as Uint8Array<ArrayBuffer>); },90 receive(): Promise<Uint8Array> {91 const queued = inbox.shift();92 if (queued) { sawCloseBeforeFrame = false; return Promise.resolve(queued); }93 if (closed) { sawCloseBeforeFrame = true; return Promise.reject(new Error("socket closed")); }94 return new Promise<Uint8Array>((res, rej) => readers.push({ resolve: res, reject: rej }));95 },96 close() { try { ws.close(); } catch { } },97 closedBeforeFrame() { return sawCloseBeforeFrame; },98 };99 });100}101102103export function productionDesktopPairingDeps(): DesktopPairingDeps {104 return {105 async findHost(port) {106 const subnet = await getLocalSubnet();107 if (!subnet) return null;108 return probeSubnetForPort(subnet, port);109 },110 openSocket: (ip, port) => openBinaryWebSocket(ip, port),111 async getLocalIdentity(): Promise<LocalIdentity> {112 if (!isUnlocked()) throw new Error("vault_locked");113 const masterKeyHex = getRootKeyHex();114 if (!masterKeyHex) throw new Error("vault_locked");115 116 117 const masterKey = hexToBytesLocal(masterKeyHex);118 try {119 const base = await buildLocalIdentity(masterKey);120 const vaultId = await computeVaultId(masterKey);121 return { ...base, vaultId };122 } finally {123 masterKey.fill(0);124 }125 },126 persistDevice: (d) => upsertPairedDevice(d),127 cacheLastKnownIp: (pubkeyHex, ip) => setLastKnownIp(pubkeyHex, ip),128 };129}130131132133134let pendingConfirm: ((confirmed: boolean) => void) | null = null;135let inFlight = false;136137function clearPairingState(): void {138 pendingConfirm = null;139 inFlight = false;140}141142export function isDesktopPairingInFlight(): boolean {143 return inFlight;144}145146147148149150151export function startDesktopPairing(152 code: string,153 deps: DesktopPairingDeps = productionDesktopPairingDeps(),154): void {155 inFlight = true;156 const cb: PairWithDesktopCallbacks = {157 onSasReady: (sas) => push("DESKTOP_PAIR_SAS", { sas }),158 onComplete: (device) => {159 push("DESKTOP_PAIR_RESULT", { status: "complete", device });160 clearPairingState();161 },162 onError: (error) => {163 push("DESKTOP_PAIR_RESULT", { status: "error", error });164 clearPairingState();165 },166 };167 168 const confirm = (_sas: string): Promise<boolean> =>169 new Promise<boolean>((resolve) => { pendingConfirm = resolve; });170171 void pairWithDesktopHelper(code, cb, deps, confirm).finally(() => {172 173 174 if (inFlight) clearPairingState();175 });176}177178179export function resolveDesktopPairingConfirm(confirmed: boolean): void {180 const resolve = pendingConfirm;181 pendingConfirm = null;182 if (resolve) resolve(confirmed);183}