1234567891011121314import { beforeEach, describe, expect, it, vi } from "vitest";151617import {18 pairWithDesktopHelper,19 DESKTOP_PAIRING_PORT,20 type DesktopPairingDeps,21 type DesktopPairingSocket,22 type PairWithDesktopCallbacks,23} from "../../background/desktop-pairing";242526import { derivePairingSAS } from "../../shared/chirp-handshake";27import type { LocalIdentity } from "../../shared/chirp-handshake";28import type { PairedDevice } from "../../shared/sync-types";29303132const enc = (o: unknown) => new TextEncoder().encode(JSON.stringify(o));33const dec = (b: Uint8Array) => JSON.parse(new TextDecoder().decode(b));3435const HELPER_EPHEMERAL_HEX = "a".repeat(64);36const HELPER_LONGTERM_HEX = "b".repeat(64);3738const OUR_IDENTITY: LocalIdentity = {39 deviceId: "ext-local-001",40 deviceName: "Firefox on MacBook",41 deviceType: "extension",42 longTermPublicKeyHex: "c".repeat(64),43 signingPublicKeyHex: "d".repeat(64),44 vaultId: "e".repeat(64),45};46474849505152function makeFakeSocket(inbound: Array<Uint8Array | null>) {53 const sent: Uint8Array[] = [];54 let closed = false;55 let lastWasClose = false;56 const queue = [...inbound];57 const sock: DesktopPairingSocket = {58 send(bytes) { sent.push(bytes); },59 async receive() {60 const next = queue.shift();61 if (next === undefined || next === null) {62 closed = true;63 lastWasClose = true;64 throw new Error("socket closed");65 }66 lastWasClose = false;67 return next;68 },69 close() { closed = true; },70 closedBeforeFrame() { return lastWasClose; },71 };72 return { sock, sent, isClosed: () => closed };73}74757677787980const FAKE_HANDSHAKE_HASH = Uint8Array.from([81 0x0f, 0xba, 0xb8, 0xcd, ...new Array(28).fill(0),82]);8384858687vi.mock("../../shared/noise-js", async (orig) => {88 const actual = await orig<typeof import("../../shared/noise-js")>();89 return {90 ...actual,91 noiseNKInitiator: vi.fn(async (_responderStatic, _send, _receive) => ({92 encrypt: (pt: Uint8Array) => pt,93 decrypt: (ct: Uint8Array) => ct,94 handshakeHash: FAKE_HANDSHAKE_HASH,95 })),96 };97});9899function makeDeps(overrides: Partial<DesktopPairingDeps> = {}): {100 deps: DesktopPairingDeps;101 persisted: PairedDevice[];102 sockets: ReturnType<typeof makeFakeSocket>[];103} {104 const persisted: PairedDevice[] = [];105 const sockets: ReturnType<typeof makeFakeSocket>[] = [];106 const deps: DesktopPairingDeps = {107 findHost: vi.fn(async () => "192.168.1.50"),108 openSocket: vi.fn(async () => {109 110 const s = makeFakeSocket([111 enc({ type: "PEACH_HELLO_ACK", ephemeralPublicKeyHex: HELPER_EPHEMERAL_HEX }),112 enc({113 type: "PAIR_IDENTITY",114 device_id: "desktop-001",115 device_name: "June's iMac",116 device_type: "desktop",117 long_term_public_key_hex: HELPER_LONGTERM_HEX,118 ws_port: 8765,119 }),120 ]);121 sockets.push(s);122 return s.sock;123 }),124 getLocalIdentity: vi.fn(async () => OUR_IDENTITY),125 persistDevice: vi.fn(async (d: PairedDevice) => { persisted.push(d); }),126 ...overrides,127 };128 return { deps, persisted, sockets };129}130131function makeCallbacks(): {132 cb: PairWithDesktopCallbacks;133 sas: { value?: string };134 errors: string[];135 completed: PairedDevice[];136} {137 const sas: { value?: string } = {};138 const errors: string[] = [];139 const completed: PairedDevice[] = [];140 const cb: PairWithDesktopCallbacks = {141 onSasReady: (s) => { sas.value = s; },142 onError: (e) => { errors.push(e); },143 onComplete: (d) => { completed.push(d); },144 };145 return { cb, sas, errors, completed };146}147148beforeEach(() => {149 vi.clearAllMocks();150});151152153154describe("pairWithDesktopHelper", () => {155 it("sends PEACH_HELLO with a 6-digit zero-padded code to port 49142", async () => {156 const { deps, sockets } = makeDeps();157 const { cb } = makeCallbacks();158159 await pairWithDesktopHelper("123", cb, deps, async () => true);160161 expect(deps.openSocket).toHaveBeenCalledWith("192.168.1.50", DESKTOP_PAIRING_PORT);162 const firstFrame = dec(sockets[0].sent[0]);163 expect(firstFrame.type).toBe("PEACH_HELLO");164 165 expect(firstFrame.code).toBe("000123");166 expect(firstFrame.code).toMatch(/^\d{6}$/);167 });168169 it("treats a socket close without PEACH_HELLO_ACK as invalid_code", async () => {170 171 172 173 174 const closeImmediately = makeFakeSocket([null]); 175 const { deps } = makeDeps({176 openSocket: vi.fn(async () => closeImmediately.sock),177 });178 const { cb, errors, completed } = makeCallbacks();179180 await pairWithDesktopHelper("654321", cb, deps, async () => true);181182 expect(errors).toEqual(["invalid_code"]);183 expect(completed).toHaveLength(0);184 expect(deps.persistDevice).not.toHaveBeenCalled();185 });186187 it("derives the SAS via the real derivePairingSAS — KAT 0fbab8cd… → 895245", () => {188 189 190 191 const hash = Uint8Array.from([192 0x0f, 0xba, 0xb8, 0xcd, ...new Array(28).fill(0),193 ]);194 expect(derivePairingSAS(hash)).toBe("895245");195 });196197 it("surfaces that same SAS through the pairing flow", async () => {198 199 const { deps } = makeDeps();200 const { cb, sas } = makeCallbacks();201202 await pairWithDesktopHelper("000123", cb, deps, async () => true);203204 expect(sas.value).toBe("895245");205 });206207 it("persists the device with deviceType 'desktop' on SAS confirm", async () => {208 const { deps, persisted } = makeDeps();209 const { cb, completed, errors } = makeCallbacks();210211 await pairWithDesktopHelper("000123", cb, deps, async () => true);212213 expect(errors).toHaveLength(0);214 expect(persisted).toHaveLength(1);215 expect(persisted[0].deviceType).toBe("desktop");216 expect(persisted[0].deviceId).toBe("desktop-001");217 expect(persisted[0].longTermPublicKeyHex).toBe(HELPER_LONGTERM_HEX);218 expect(persisted[0].trustSource).toBe("direct");219 expect(persisted[0].wsPort).toBe(8765);220 expect(completed).toHaveLength(1);221 });222223 it("aborts with sas_mismatch and persists nothing when the user rejects", async () => {224 const { deps, persisted } = makeDeps();225 const { cb, errors } = makeCallbacks();226227 await pairWithDesktopHelper("000123", cb, deps, async () => false);228229 expect(errors).toEqual(["sas_mismatch"]);230 expect(persisted).toHaveLength(0);231 expect(deps.persistDevice).not.toHaveBeenCalled();232 });233234 it("includes vault_id in the outbound PAIR_IDENTITY (Change 1)", async () => {235 const { deps, sockets } = makeDeps();236 const { cb } = makeCallbacks();237238 await pairWithDesktopHelper("000123", cb, deps, async () => true);239240 241 242 const ourPairIdentity = dec(sockets[0].sent[1]);243 expect(ourPairIdentity.type).toBe("PAIR_IDENTITY");244 expect(ourPairIdentity.vault_id).toBe(OUR_IDENTITY.vaultId);245 });246247 it("reports no_host_found when the probe returns null", async () => {248 const { deps } = makeDeps({ findHost: vi.fn(async () => null) });249 const { cb, errors } = makeCallbacks();250251 await pairWithDesktopHelper("000123", cb, deps, async () => true);252253 expect(errors).toEqual(["no_host_found"]);254 expect(deps.openSocket).not.toHaveBeenCalled();255 });256257 it("reports vault_locked when local identity is unavailable", async () => {258 const { deps } = makeDeps({259 getLocalIdentity: vi.fn(async () => { throw new Error("vault_locked"); }),260 });261 const { cb, errors } = makeCallbacks();262263 await pairWithDesktopHelper("000123", cb, deps, async () => true);264265 expect(errors).toEqual(["vault_locked"]);266 expect(deps.findHost).not.toHaveBeenCalled();267 });268});