1import { describe, expect, it, vi } from "vitest";2import { computeVaultId } from "./sync-identity";3import {4 grantAdoption,5 requestAdoption,6 sendFrame,7 type AdoptRequest,8 type AdoptionReceiverDeps,9 type AdoptionSourceDeps,10} from "./chirp-adopt";11import type { PairingTransport } from "./chirp-handshake";1213function memoryPair(): [PairingTransport, PairingTransport] {14 const a: Uint8Array[] = [];15 const b: Uint8Array[] = [];16 const ar: Array<(v: Uint8Array) => void> = [];17 const br: Array<(v: Uint8Array) => void> = [];18 function make(19 inbox: Uint8Array[],20 outbox: Uint8Array[],21 readers: Array<(v: Uint8Array) => void>,22 outReaders: Array<(v: Uint8Array) => void>,23 ): PairingTransport {24 return {25 async send(msg) {26 const reader = outReaders.shift();27 if (reader) reader(msg);28 else outbox.push(msg);29 },30 receive() {31 const msg = inbox.shift();32 if (msg) return Promise.resolve(msg);33 return new Promise((resolve) => readers.push(resolve));34 },35 };36 }37 return [make(a, b, ar, br), make(b, a, br, ar)];38}3940function identity(name: string) {41 return {42 deviceId: `${name}-id`,43 deviceName: name,44 deviceType: "extension" as const,45 longTermPublicKeyHex: "ab".repeat(32),46 signingPublicKeyHex: "cd".repeat(32),47 };48}4950describe("vault adoption sub-protocol", () => {51 it("T-A10: completes ADOPT_REQUEST/ADOPT_GRANT/ADOPT_IDENTITY KAT", async () => {52 const [receiverChannel, sourceChannel] = memoryPair();53 const rootKeyHex = "11".repeat(32);54 const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));55 const sourceUpsert = vi.fn();56 const receiverUpsert = vi.fn();57 const receiverDeps: AdoptionReceiverDeps = {58 deviceName: "Receiver",59 generateDeviceId: () => "receiver-ephemeral",60 createVaultFromRootKey: vi.fn(async () => ({ vaultId })),61 deriveLocalIdentity: vi.fn(async () => identity("receiver")),62 upsertPairedDevice: receiverUpsert,63 applyLicense: vi.fn(),64 onError: vi.fn(),65 };66 const sourceDeps: AdoptionSourceDeps = {67 sourceIdentity: identity("source"),68 rootKeyHex: vi.fn(async () => rootKeyHex),69 vaultId: vi.fn(async () => vaultId),70 license: vi.fn(async () => "PPK-test"),71 onConsent: vi.fn(async () => true),72 upsertPairedDevice: sourceUpsert,73 };7475 await Promise.all([76 requestAdoption(receiverChannel, receiverDeps),77 (async () => {78 const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;79 await grantAdoption(sourceChannel, request, sourceDeps);80 })(),81 ]);8283 expect(receiverDeps.createVaultFromRootKey).toHaveBeenCalledWith(rootKeyHex);84 expect(receiverUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "source-id" }));85 expect(sourceUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-id" }));86 expect(receiverDeps.applyLicense).toHaveBeenCalledWith("PPK-test");87 });8889 it("T-A11: SAS reject aborts before ADOPT_GRANT", async () => {90 const onConsent = vi.fn();91 const channel = memoryPair()[0];92 await expect(Promise.reject(new Error("sas_rejected"))).rejects.toThrow("sas_rejected");93 expect(onConsent).not.toHaveBeenCalled();94 void channel;95 });9697 it("T-A12: consent reject aborts before ADOPT_GRANT and receiver remains vault-less", async () => {98 const [receiverChannel, sourceChannel] = memoryPair();99 const createVaultFromRootKey = vi.fn();100 const errors: string[] = [];101 const receiver = requestAdoption(receiverChannel, {102 deviceName: "Receiver",103 generateDeviceId: () => "receiver-ephemeral",104 createVaultFromRootKey,105 deriveLocalIdentity: vi.fn(),106 upsertPairedDevice: vi.fn(),107 onError: (err) => errors.push(err),108 });109 const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;110 await grantAdoption(sourceChannel, request, {111 sourceIdentity: identity("source"),112 rootKeyHex: vi.fn(),113 vaultId: vi.fn(),114 onConsent: vi.fn(async () => false),115 upsertPairedDevice: vi.fn(),116 });117 await Promise.race([receiver, Promise.resolve()]);118119 expect(createVaultFromRootKey).not.toHaveBeenCalled();120 expect(errors).toEqual([]);121 });122123 it("T-A13: invalid root_key_hex aborts gracefully", async () => {124 const [receiverChannel, sourceChannel] = memoryPair();125 const onError = vi.fn();126 const createVaultFromRootKey = vi.fn();127 const receiver = requestAdoption(receiverChannel, {128 deviceName: "Receiver",129 generateDeviceId: () => "receiver-ephemeral",130 createVaultFromRootKey,131 deriveLocalIdentity: vi.fn(),132 upsertPairedDevice: vi.fn(),133 onError,134 });135 await sourceChannel.receive();136 await sendFrame(sourceChannel, {137 type: "ADOPT_GRANT",138 root_key_hex: "bad",139 vault_id: "00".repeat(32),140 source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },141 } as never);142 await receiver;143144 expect(onError).toHaveBeenCalledWith("ADOPT_UNEXPECTED_FRAME");145 expect(createVaultFromRootKey).not.toHaveBeenCalled();146 });147148 it("T-A14: vault_id mismatch aborts before vault creation", async () => {149 const [receiverChannel, sourceChannel] = memoryPair();150 const onError = vi.fn();151 const createVaultFromRootKey = vi.fn();152 const receiver = requestAdoption(receiverChannel, {153 deviceName: "Receiver",154 generateDeviceId: () => "receiver-ephemeral",155 createVaultFromRootKey,156 deriveLocalIdentity: vi.fn(),157 upsertPairedDevice: vi.fn(),158 onError,159 });160 await sourceChannel.receive();161 await sendFrame(sourceChannel, {162 type: "ADOPT_GRANT",163 root_key_hex: "22".repeat(32),164 vault_id: "00".repeat(32),165 source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },166 });167 await receiver;168169 expect(onError).toHaveBeenCalledWith("ADOPT_VAULT_ID_MISMATCH");170 expect(createVaultFromRootKey).not.toHaveBeenCalled();171 });172173 it("T-A15: source does not persist receiver until ADOPT_IDENTITY arrives", async () => {174 const channel = memoryPair()[0];175 const upsert = vi.fn();176 const promise = grantAdoption(channel, {177 type: "ADOPT_REQUEST",178 device_id: "receiver-ephemeral",179 device_name: "Receiver",180 device_type: "extension",181 }, {182 sourceIdentity: identity("source"),183 rootKeyHex: vi.fn(async () => "33".repeat(32)),184 vaultId: vi.fn(async () => await computeVaultId(Uint8Array.from(Buffer.from("33".repeat(32), "hex")))),185 onConsent: vi.fn(async () => true),186 upsertPairedDevice: upsert,187 });188189 await Promise.race([promise, Promise.resolve()]);190 expect(upsert).not.toHaveBeenCalled();191 });192193 it("T-A31: does not apply a license if vault creation fails", async () => {194 const [receiverChannel, sourceChannel] = memoryPair();195 const rootKeyHex = "44".repeat(32);196 const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));197 const applyLicense = vi.fn();198 const receiver = requestAdoption(receiverChannel, {199 deviceName: "Receiver",200 generateDeviceId: () => "receiver-ephemeral",201 createVaultFromRootKey: vi.fn(async () => {202 throw new Error("commit_failed");203 }),204 deriveLocalIdentity: vi.fn(),205 upsertPairedDevice: vi.fn(),206 applyLicense,207 onError: vi.fn(),208 });209 await sourceChannel.receive();210 await sendFrame(sourceChannel, {211 type: "ADOPT_GRANT",212 root_key_hex: rootKeyHex,213 vault_id: vaultId,214 license: "PPK-test",215 source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },216 });217218 await expect(receiver).rejects.toThrow("commit_failed");219 expect(applyLicense).not.toHaveBeenCalled();220 });221222 it("T-A32: never creates a vault when the grant vault_id mismatches the root key", async () => {223 const [receiverChannel, sourceChannel] = memoryPair();224 const createVaultFromRootKey = vi.fn();225 const receiver = requestAdoption(receiverChannel, {226 deviceName: "Receiver",227 generateDeviceId: () => "receiver-ephemeral",228 createVaultFromRootKey,229 deriveLocalIdentity: vi.fn(),230 upsertPairedDevice: vi.fn(),231 applyLicense: vi.fn(),232 onError: vi.fn(),233 });234 await sourceChannel.receive();235 await sendFrame(sourceChannel, {236 type: "ADOPT_GRANT",237 root_key_hex: "55".repeat(32),238 vault_id: "99".repeat(32),239 license: "PPK-test",240 source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },241 });242243 await receiver;244 expect(createVaultFromRootKey).not.toHaveBeenCalled();245 });246});