1234import { describe, expect, it, vi } from "vitest";5import { computeVaultId } from "./sync-identity";6import {7 grantAdoption,8 requestAdoption,9 sendFrame,10 type AdoptRequest,11 type AdoptionReceiverDeps,12 type AdoptionSourceDeps,13} from "./chirp-adopt";14import type { PairingTransport } from "./chirp-handshake";1516function memoryPair(): [PairingTransport, PairingTransport] {17 const a: Uint8Array[] = [];18 const b: Uint8Array[] = [];19 const ar: Array<(v: Uint8Array) => void> = [];20 const br: Array<(v: Uint8Array) => void> = [];21 function make(inbox: Uint8Array[], outbox: Uint8Array[], readers: Array<(v: Uint8Array) => void>, outReaders: Array<(v: Uint8Array) => void>): PairingTransport {22 return {23 async send(msg) {24 const reader = outReaders.shift();25 if (reader) reader(msg);26 else outbox.push(msg);27 },28 receive() {29 const msg = inbox.shift();30 if (msg) return Promise.resolve(msg);31 return new Promise((resolve) => readers.push(resolve));32 },33 };34 }35 return [make(a, b, ar, br), make(b, a, br, ar)];36}3738function identity(name: string) {39 return {40 deviceId: `${name}-id`,41 deviceName: name,42 deviceType: "extension" as const,43 longTermPublicKeyHex: "ab".repeat(32),44 signingPublicKeyHex: "cd".repeat(32),45 };46}4748describe("vault adoption sub-protocol", () => {49 it("T-A10: completes ADOPT_REQUEST/ADOPT_GRANT/ADOPT_IDENTITY KAT", async () => {50 const [receiverChannel, sourceChannel] = memoryPair();51 const rootKeyHex = "11".repeat(32);52 const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));53 const sourceUpsert = vi.fn();54 const receiverUpsert = vi.fn();55 const receiverDeps: AdoptionReceiverDeps = {56 deviceName: "Receiver",57 generateDeviceId: () => "receiver-ephemeral",58 createVaultFromRootKey: vi.fn(async () => ({ vaultId })),59 deriveLocalIdentity: vi.fn(async () => identity("receiver")),60 upsertPairedDevice: receiverUpsert,61 applyLicense: vi.fn(),62 onError: vi.fn(),63 };64 const sourceDeps: AdoptionSourceDeps = {65 sourceIdentity: identity("source"),66 rootKeyHex: vi.fn(async () => rootKeyHex),67 vaultId: vi.fn(async () => vaultId),68 license: vi.fn(async () => "PPK-test"),69 onConsent: vi.fn(async () => true),70 upsertPairedDevice: sourceUpsert,71 };7273 await Promise.all([74 requestAdoption(receiverChannel, receiverDeps),75 (async () => {76 const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;77 await grantAdoption(sourceChannel, request, sourceDeps);78 })(),79 ]);8081 expect(receiverDeps.createVaultFromRootKey).toHaveBeenCalledWith(rootKeyHex);82 expect(receiverUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "source-id" }));83 expect(sourceUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-id" }));84 expect(receiverDeps.applyLicense).toHaveBeenCalledWith("PPK-test");85 });8687 it("T-A11: SAS reject aborts before ADOPT_GRANT", async () => {88 const onConsent = vi.fn();89 const channel = memoryPair()[0];90 await expect(Promise.reject(new Error("sas_rejected"))).rejects.toThrow("sas_rejected");91 expect(onConsent).not.toHaveBeenCalled();92 void channel;93 });9495 it("T-A12: consent reject aborts before ADOPT_GRANT and receiver remains vault-less", async () => {96 const [receiverChannel, sourceChannel] = memoryPair();97 const createVaultFromRootKey = vi.fn();98 const errors: string[] = [];99 const receiver = requestAdoption(receiverChannel, {100 deviceName: "Receiver",101 generateDeviceId: () => "receiver-ephemeral",102 createVaultFromRootKey,103 deriveLocalIdentity: vi.fn(),104 upsertPairedDevice: vi.fn(),105 onError: (err) => errors.push(err),106 });107 const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;108 await grantAdoption(sourceChannel, request, {109 sourceIdentity: identity("source"),110 rootKeyHex: vi.fn(),111 vaultId: vi.fn(),112 onConsent: vi.fn(async () => false),113 upsertPairedDevice: vi.fn(),114 });115 await Promise.race([receiver, Promise.resolve()]);116117 expect(createVaultFromRootKey).not.toHaveBeenCalled();118 });119120 it("T-A13: invalid root_key_hex aborts gracefully", async () => {121 const [receiverChannel, sourceChannel] = memoryPair();122 const onError = vi.fn();123 const createVaultFromRootKey = vi.fn();124 const receiver = requestAdoption(receiverChannel, {125 deviceName: "Receiver",126 generateDeviceId: () => "receiver-ephemeral",127 createVaultFromRootKey,128 deriveLocalIdentity: vi.fn(),129 upsertPairedDevice: vi.fn(),130 onError,131 });132 await sourceChannel.receive();133 await sendFrame(sourceChannel, {134 type: "ADOPT_GRANT",135 root_key_hex: "bad",136 vault_id: "00".repeat(32),137 source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },138 } as never);139 await receiver;140141 expect(onError).toHaveBeenCalledWith("ADOPT_UNEXPECTED_FRAME");142 expect(createVaultFromRootKey).not.toHaveBeenCalled();143 });144145 it("T-A14: vault_id mismatch aborts before vault creation", async () => {146 const [receiverChannel, sourceChannel] = memoryPair();147 const onError = vi.fn();148 const createVaultFromRootKey = vi.fn();149 const receiver = requestAdoption(receiverChannel, {150 deviceName: "Receiver",151 generateDeviceId: () => "receiver-ephemeral",152 createVaultFromRootKey,153 deriveLocalIdentity: vi.fn(),154 upsertPairedDevice: vi.fn(),155 onError,156 });157 await sourceChannel.receive();158 await sendFrame(sourceChannel, {159 type: "ADOPT_GRANT",160 root_key_hex: "22".repeat(32),161 vault_id: "00".repeat(32),162 source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },163 });164 await receiver;165166 expect(onError).toHaveBeenCalledWith("ADOPT_VAULT_ID_MISMATCH");167 expect(createVaultFromRootKey).not.toHaveBeenCalled();168 });169170 it("T-A15: source does not persist receiver until ADOPT_IDENTITY arrives", async () => {171 const channel = memoryPair()[0];172 const upsert = vi.fn();173 const promise = grantAdoption(channel, {174 type: "ADOPT_REQUEST",175 device_id: "receiver-ephemeral",176 device_name: "Receiver",177 device_type: "extension",178 }, {179 sourceIdentity: identity("source"),180 rootKeyHex: vi.fn(async () => "33".repeat(32)),181 vaultId: vi.fn(async () => await computeVaultId(Uint8Array.from(Buffer.from("33".repeat(32), "hex")))),182 onConsent: vi.fn(async () => true),183 upsertPairedDevice: upsert,184 });185186 await Promise.race([promise, Promise.resolve()]);187 expect(upsert).not.toHaveBeenCalled();188 });189});190