1import { noiseNKInitiator, performInitiatorAuth } from './noise-js';2import { bytesToBase64, base64ToBytes } from './sync-wire';34export interface SyncTransport {5 send(data: Uint8Array): Promise<void>;6 receive(): Promise<Uint8Array>;7 close(): Promise<void>;8}91011121314export async function createWebSocketSyncTransport(15 url: string,16 remotePublicKeyHex: string,17 ourPrivateKey: Uint8Array,18 ourPublicKey: Uint8Array,19): Promise<SyncTransport> {20 const remotePublicKey = hexToBytes(remotePublicKeyHex);2122 return new Promise((resolve, reject) => {23 const ws = new WebSocket(url);2425 const messageQueue: string[] = [];26 const waiters: ((frame: string) => void)[] = [];2728 const enqueue = (frame: string) => {29 if (waiters.length > 0) {30 waiters.shift()!(frame);31 } else {32 messageQueue.push(frame);33 }34 };3536 ws.onmessage = (event) => enqueue(event.data as string);37 ws.onerror = () => reject(new Error('WebSocket connection failed'));38 ws.onclose = () => { waiters.forEach(w => w('')); waiters.length = 0; };3940 ws.onopen = async () => {41 try {42 const rawSend = (msg: Uint8Array) => {43 ws.send(bytesToBase64(msg));44 return Promise.resolve();45 };46 const rawReceive = (): Promise<Uint8Array> => new Promise((res) => {47 const handle = (frame: string) => {48 if (frame === '') { res(new Uint8Array(0)); return; }49 res(base64ToBytes(frame));50 };51 if (messageQueue.length > 0) handle(messageQueue.shift()!);52 else waiters.push(handle);53 });5455 56 const session = await noiseNKInitiator(remotePublicKey, rawSend, rawReceive);5758 59 60 61 const encryptedSend = (data: Uint8Array) =>62 rawSend(session.encrypt(data));63 const encryptedReceive = async () => {64 const frame = await rawReceive();65 if (frame.length === 0) return frame;66 return session.decrypt(frame);67 };68 await performInitiatorAuth(69 session,70 ourPrivateKey,71 ourPublicKey,72 remotePublicKey,73 encryptedSend,74 encryptedReceive,75 );7677 resolve({78 send: async (plaintext) => {79 ws.send(bytesToBase64(session.encrypt(plaintext)));80 },81 receive: () => new Promise((res) => {82 const handle = (frame: string) => {83 if (frame === '') { res(new Uint8Array(0)); return; }84 res(session.decrypt(base64ToBytes(frame)));85 };86 if (messageQueue.length > 0) handle(messageQueue.shift()!);87 else waiters.push(handle);88 }),89 close: () => {90 ws.close();91 return Promise.resolve();92 },93 });94 } catch (err) {95 ws.close();96 reject(err);97 }98 };99 });100}101102function hexToBytes(hex: string): Uint8Array {103 const bytes = new Uint8Array(hex.length / 2);104 for (let i = 0; i < hex.length; i += 2) {105 bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);106 }107 return bytes;108}