12345678910111213import { describe, expect, it } from "vitest";14import { generateKeyPair } from "@stablelib/x25519";15import {16 memoryTransportPair,17 runPairingInitiator,18 type LocalIdentity,19} from "./pairing-handshake";20import { noiseNKResponder } from "./noise-js";21import {22 computeVaultId,23 deriveSyncIdentityKeypair,24 pubkeyToHex,25} from "./sync-identity";2627const MASTER = Uint8Array.from(Array.from({ length: 32 }, (_, i) => (i * 5 + 3) & 0xff));2829async function identityFor(30 deviceId: string,31 deviceName: string,32 type: LocalIdentity["deviceType"],33 opts: { withVaultId?: boolean } = {},34): Promise<LocalIdentity> {35 const kp = deriveSyncIdentityKeypair(MASTER, deviceId);36 return {37 deviceId,38 deviceName,39 deviceType: type,40 longTermPublicKeyHex: pubkeyToHex(kp.publicKey),41 signingPublicKeyHex: "b".repeat(64),42 vaultId: opts.withVaultId ? await computeVaultId(MASTER) : undefined,43 };44}454647484950async function captureInitiatorFrame(initIdent: LocalIdentity): Promise<Record<string, unknown>> {51 const respEphemeral = generateKeyPair();52 const [t1, t2] = memoryTransportPair();5354 let captured: Record<string, unknown> | undefined;5556 const responder = (async () => {57 const session = await noiseNKResponder(58 respEphemeral.secretKey,59 respEphemeral.publicKey,60 t2.send,61 t2.receive,62 );63 64 const cipher = await t2.receive();65 const plain = session.decrypt(cipher);66 captured = JSON.parse(new TextDecoder().decode(plain));67 68 const back = JSON.stringify({69 type: "PAIR_IDENTITY",70 device_id: "android-001",71 device_name: "Pixel 8",72 device_type: "android",73 long_term_public_key_hex: "a".repeat(64),74 ws_port: 49142,75 });76 await t2.send(session.encrypt(new TextEncoder().encode(back)));77 })();7879 await runPairingInitiator(respEphemeral.publicKey, initIdent, t1);80 await responder;81 if (!captured) throw new Error("did not capture initiator frame");82 return captured;83}8485describe("PAIR_IDENTITY vault_id (Change 1)", () => {86 it("PAIR_IDENTITY frame includes vault_id field when identity carries a vaultId", async () => {87 const initIdent = await identityFor("ext-001", "Firefox", "extension", { withVaultId: true });88 const frame = await captureInitiatorFrame(initIdent);8990 expect(frame.type).toBe("PAIR_IDENTITY");91 expect(frame).toHaveProperty("vault_id");92 expect(typeof frame.vault_id).toBe("string");93 });9495 it("vault_id is a non-empty 64-char lowercase hex string", async () => {96 const initIdent = await identityFor("ext-002", "Firefox", "extension", { withVaultId: true });97 const frame = await captureInitiatorFrame(initIdent);9899 const vaultId = frame.vault_id as string;100 expect(vaultId.length).toBeGreaterThan(0);101 102 expect(vaultId).toMatch(/^[0-9a-f]{64}$/);103 104 expect(vaultId).toBe(await computeVaultId(MASTER));105 });106107 it("omits vault_id entirely when the identity has no vaultId (locked vault / mobile peer)", async () => {108 const initIdent = await identityFor("ext-003", "Firefox", "extension", { withVaultId: false });109 const frame = await captureInitiatorFrame(initIdent);110111 112 113 expect(frame).not.toHaveProperty("vault_id");114 });115});