12345678910111213141516171819202122import { describe, expect, it } from "vitest";23import { generateKeyPair } from "@stablelib/x25519";24import {25 memoryTransportPair,26 runPairingInitiator,27 runPairingResponder,28 type LocalIdentity,29} from "../../shared/chirp-handshake";30import { noiseNKResponder } from "../../shared/noise-js";31import {32 computeVaultId,33 deriveSyncIdentityKeypair,34 pubkeyToHex,35} from "../../shared/sync-identity";3637const MASTER = Uint8Array.from(Array.from({ length: 32 }, (_, i) => (i * 5 + 3) & 0xff));3839async function identityFor(40 deviceId: string,41 deviceName: string,42 type: LocalIdentity["deviceType"],43 opts: { withVaultId?: boolean } = {},44): Promise<LocalIdentity> {45 const kp = deriveSyncIdentityKeypair(MASTER, deviceId);46 return {47 deviceId,48 deviceName,49 deviceType: type,50 longTermPublicKeyHex: pubkeyToHex(kp.publicKey),51 signingPublicKeyHex: "b".repeat(64),52 vaultId: opts.withVaultId ? await computeVaultId(MASTER) : undefined,53 };54}5556575859606162async function captureInitiatorFrame(initIdent: LocalIdentity): Promise<Record<string, unknown>> {63 const respEphemeral = generateKeyPair();64 const [t1, t2] = memoryTransportPair();6566 let captured: Record<string, unknown> | undefined;6768 const responder = (async () => {69 const session = await noiseNKResponder(70 respEphemeral.secretKey,71 respEphemeral.publicKey,72 t2.send,73 t2.receive,74 );75 76 const cipher = await t2.receive();77 const plain = session.decrypt(cipher);78 captured = JSON.parse(new TextDecoder().decode(plain));79 80 const back = JSON.stringify({81 type: "PAIR_IDENTITY",82 device_id: "desktop-001",83 device_name: "peach-desktop",84 device_type: "desktop",85 long_term_public_key_hex: "a".repeat(64),86 ws_port: 8765,87 });88 await t2.send(session.encrypt(new TextEncoder().encode(back)));89 })();9091 await runPairingInitiator(respEphemeral.publicKey, initIdent, t1);92 await responder;93 if (!captured) throw new Error("did not capture initiator frame");94 return captured;95}9697describe("PAIR_IDENTITY vault_id (Change 1)", () => {98 it("PAIR_IDENTITY frame includes vault_id field when identity carries a vaultId", async () => {99 const initIdent = await identityFor("ext-001", "Firefox", "extension", { withVaultId: true });100 const frame = await captureInitiatorFrame(initIdent);101102 expect(frame.type).toBe("PAIR_IDENTITY");103 expect(frame).toHaveProperty("vault_id");104 expect(typeof frame.vault_id).toBe("string");105 });106107 it("vault_id is a non-empty 64-char lowercase hex string", async () => {108 const initIdent = await identityFor("ext-002", "Firefox", "extension", { withVaultId: true });109 const frame = await captureInitiatorFrame(initIdent);110111 const vaultId = frame.vault_id as string;112 expect(vaultId.length).toBeGreaterThan(0);113 114 expect(vaultId).toMatch(/^[0-9a-f]{64}$/);115 116 expect(vaultId).toBe(await computeVaultId(MASTER));117 });118119 it("omits vault_id entirely when the identity has no vaultId (locked vault / chirp peer)", async () => {120 const initIdent = await identityFor("ext-003", "Firefox", "extension", { withVaultId: false });121 const frame = await captureInitiatorFrame(initIdent);122123 124 125 expect(frame).not.toHaveProperty("vault_id");126 });127});