1import { afterEach, describe, expect, it, vi } from "vitest";2import { generateKeyPair } from "@stablelib/x25519";3import { noiseKKResponder } from "./noise-js";4import {5 openRelaySyncSession,6 openSyncSession,7 type SyncPeerIdentity,8} from "./sync-transport";910const encode = (value: unknown) => new TextEncoder().encode(JSON.stringify(value));11const decode = (bytes: Uint8Array) => JSON.parse(new TextDecoder().decode(bytes));12const hex = (bytes: Uint8Array) =>13 Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");14const RENDEZVOUS_ID = "Q".repeat(43);1516function installPeer(peerIdentityKeyHex?: string) {17 const initiator = generateKeyPair();18 const responder = generateKeyPair();19 const clientFrames: Uint8Array[] = [];20 const clientReaders: Array<(frame: Uint8Array) => void> = [];21 let socket: {22 binaryType: string;23 onopen: ((event: Event) => void) | null;24 onmessage: ((event: MessageEvent) => void) | null;25 onerror: (() => void) | null;26 onclose: ((event: CloseEvent) => void) | null;27 send(data: Uint8Array | ArrayBuffer): void;28 close(): void;29 };30 let resolveUrl!: (url: string) => void;31 const urlSeen = new Promise<string>((resolve) => { resolveUrl = resolve; });32 const receiveClient = async () => {33 const queued = clientFrames.shift();34 if (queued) return queued;35 return new Promise<Uint8Array>((resolve) => clientReaders.push(resolve));36 };37 const sendServer = async (frame: Uint8Array) => {38 queueMicrotask(() => socket.onmessage?.({ data: frame.buffer } as MessageEvent));39 };4041 const selectorSeen = new Promise<Record<string, unknown>>((resolve, reject) => {42 const WebSocketMock = vi.fn().mockImplementation(function(url: string) {43 resolveUrl(url);44 socket = {45 binaryType: "arraybuffer",46 onopen: null,47 onmessage: null,48 onerror: null,49 onclose: null,50 send(data) {51 const frame = data instanceof Uint8Array ? data.slice() : new Uint8Array(data.slice(0));52 const reader = clientReaders.shift();53 if (reader) reader(frame);54 else clientFrames.push(frame);55 },56 close() {57 queueMicrotask(() => socket.onclose?.({ code: 1000 } as CloseEvent));58 },59 };60 queueMicrotask(() => socket.onopen?.(new Event("open")));61 return socket;62 });63 vi.stubGlobal("WebSocket", WebSocketMock);6465 void (async () => {66 try {67 const selector = decode(await receiveClient()) as Record<string, unknown>;68 resolve(selector);69 const session = await noiseKKResponder(70 responder.secretKey,71 responder.publicKey,72 initiator.publicKey,73 sendServer,74 receiveClient,75 );76 const theirIdentity = decode(session.decrypt(await receiveClient()));77 if (theirIdentity.long_term_public_key_hex !== hex(initiator.publicKey)) {78 throw new Error("initiator identity was not bound to KK key");79 }80 await sendServer(session.encrypt(encode({81 type: "PAIR_IDENTITY",82 device_id: "phone",83 device_name: "Pixel",84 device_type: "android",85 long_term_public_key_hex: peerIdentityKeyHex ?? hex(responder.publicKey),86 })));87 } catch (error) {88 reject(error);89 }90 })();91 });9293 const identity: SyncPeerIdentity = {94 deviceId: "browser",95 deviceName: "Firefox",96 deviceType: "extension",97 longTermPublicKeyHex: hex(initiator.publicKey),98 longTermPrivateKey: initiator.secretKey,99 };100 return { identity, responder, selectorSeen, urlSeen };101}102103afterEach(() => vi.unstubAllGlobals());104105describe("KK WebSocket reconnect transport", () => {106 it("selects paired state then completes KK with both keys pinned", async () => {107 const peer = installPeer();108 const session = await openSyncSession(109 { ip: "127.0.0.1", port: 49142 },110 peer.identity,111 hex(peer.responder.publicKey),112 RENDEZVOUS_ID,113 );114115 await expect(peer.selectorSeen).resolves.toEqual({116 type: "PEACH_RECONNECT",117 rendezvous_id: RENDEZVOUS_ID,118 });119 expect(hex(session.remoteStaticPublicKey)).toBe(hex(peer.responder.publicKey));120 await session.close();121 });122123 it("rejects a post-KK identity that claims a different responder key", async () => {124 const peer = installPeer("ff".repeat(32));125 await expect(126 openSyncSession(127 { ip: "127.0.0.1", port: 49142 },128 peer.identity,129 hex(peer.responder.publicKey),130 RENDEZVOUS_ID,131 ),132 ).rejects.toThrow(/identity does not match authenticated KK key/);133 });134135 it("uses the same KK wire over a credential-free wss relay target", async () => {136 const peer = installPeer();137 const session = await openRelaySyncSession(138 "wss://relay.example.test/base/",139 peer.identity,140 hex(peer.responder.publicKey),141 RENDEZVOUS_ID,142 );143144 await expect(peer.urlSeen).resolves.toBe(145 `wss://relay.example.test/base/r/${RENDEZVOUS_ID}`,146 );147 await expect(peer.selectorSeen).resolves.toEqual({148 type: "PEACH_RECONNECT",149 rendezvous_id: RENDEZVOUS_ID,150 });151 await session.close();152 });153});