123456789101112131415161718192021222324252627import { noiseNKInitiator, noiseNKResponder } from "./noise-js";28import type { DeviceType, PairedDevice } from "./sync-types";2930export interface PairingTransport {31 send(msg: Uint8Array): Promise<void>;32 receive(): Promise<Uint8Array>;33}3435export interface LocalIdentity {36 deviceId: string;37 deviceName: string;38 deviceType: DeviceType;39 longTermPublicKeyHex: string;40 41 wsPort?: number;42}4344interface PairingFrame {45 type: "PAIR_IDENTITY";46 device_id: string;47 device_name: string;48 device_type: DeviceType;49 long_term_public_key_hex: string;50 ws_port?: number;51}5253function encodeFrame(frame: PairingFrame): Uint8Array {54 return new TextEncoder().encode(JSON.stringify(frame));55}5657function decodeFrame(bytes: Uint8Array): PairingFrame {58 const parsed = JSON.parse(new TextDecoder().decode(bytes));59 if (parsed?.type !== "PAIR_IDENTITY") throw new Error("expected PAIR_IDENTITY frame");60 if (typeof parsed.device_id !== "string" || !parsed.device_id) throw new Error("device_id missing");61 if (typeof parsed.device_name !== "string") throw new Error("device_name missing");62 if (typeof parsed.device_type !== "string") throw new Error("device_type missing");63 if (typeof parsed.long_term_public_key_hex !== "string") throw new Error("long_term_public_key_hex missing");64 65 if (!/^[0-9a-f]{64}$/.test(parsed.long_term_public_key_hex)) {66 throw new Error("long_term_public_key_hex is not 32-byte hex");67 }68 return parsed as PairingFrame;69}707172737475767778export async function runPairingInitiator(79 remoteEphemeralPublicKey: Uint8Array,80 ourIdentity: LocalIdentity,81 transport: PairingTransport,82): Promise<PairedDevice> {83 const session = await noiseNKInitiator(84 remoteEphemeralPublicKey,85 transport.send,86 transport.receive,87 );8889 90 const ourFrame = encodeFrame({91 type: "PAIR_IDENTITY",92 device_id: ourIdentity.deviceId,93 device_name: ourIdentity.deviceName,94 device_type: ourIdentity.deviceType,95 long_term_public_key_hex: ourIdentity.longTermPublicKeyHex,96 ws_port: ourIdentity.wsPort,97 });98 await transport.send(session.encrypt(ourFrame));99100 const theirCipher = await transport.receive();101 const theirPlain = session.decrypt(theirCipher);102 const theirFrame = decodeFrame(theirPlain);103 return frameToPairedDevice(theirFrame);104}105106107108109110111112113export async function runPairingResponder(114 ourEphemeralPrivateKey: Uint8Array,115 ourEphemeralPublicKey: Uint8Array,116 ourIdentity: LocalIdentity,117 transport: PairingTransport,118): Promise<PairedDevice> {119 const session = await noiseNKResponder(120 ourEphemeralPrivateKey,121 ourEphemeralPublicKey,122 transport.send,123 transport.receive,124 );125126 127 const theirCipher = await transport.receive();128 const theirPlain = session.decrypt(theirCipher);129 const theirFrame = decodeFrame(theirPlain);130131 const ourFrame = encodeFrame({132 type: "PAIR_IDENTITY",133 device_id: ourIdentity.deviceId,134 device_name: ourIdentity.deviceName,135 device_type: ourIdentity.deviceType,136 long_term_public_key_hex: ourIdentity.longTermPublicKeyHex,137 ws_port: ourIdentity.wsPort,138 });139 await transport.send(session.encrypt(ourFrame));140141 return frameToPairedDevice(theirFrame);142}143144function frameToPairedDevice(frame: PairingFrame): PairedDevice {145 return {146 deviceId: frame.device_id,147 deviceName: frame.device_name,148 deviceType: frame.device_type,149 longTermPublicKeyHex: frame.long_term_public_key_hex,150 wsPort: typeof frame.ws_port === "number" ? frame.ws_port : undefined,151 pairedAt: Date.now(),152 };153}154155156157158159export function memoryTransportPair(): [PairingTransport, PairingTransport] {160 const a: Uint8Array[] = [];161 const b: Uint8Array[] = [];162 const aReaders: Array<(b: Uint8Array) => void> = [];163 const bReaders: Array<(b: Uint8Array) => void> = [];164165 function makeTransport(166 inbox: Uint8Array[],167 outbox: Uint8Array[],168 inboxReaders: Array<(b: Uint8Array) => void>,169 outboxReaders: Array<(b: Uint8Array) => void>,170 ): PairingTransport {171 return {172 async send(msg: Uint8Array) {173 const r = outboxReaders.shift();174 if (r) r(msg);175 else outbox.push(msg);176 },177 receive() {178 const msg = inbox.shift();179 if (msg) return Promise.resolve(msg);180 return new Promise<Uint8Array>((resolve) => inboxReaders.push(resolve));181 },182 };183 }184 return [makeTransport(a, b, aReaders, bReaders), makeTransport(b, a, bReaders, aReaders)];185}