1import { describe, expect, it, vi } from "vitest";2import type { PairingTransport } from "./pairing-handshake";3import { computeVaultId } from "./sync-identity";4import {5 ADOPTION_VERSION,6 type AdoptionV2ReceiverDeps,7 type AdoptionV2SourceDeps,8 grantAdoptionV2,9 isAdoptRequestV2,10 requestAdoptionV2,11} from "./adoption-protocol";1213function memoryPair(): [PairingTransport, PairingTransport] {14 const left: Uint8Array[] = [];15 const right: Uint8Array[] = [];16 const leftReaders: Array<(value: Uint8Array) => void> = [];17 const rightReaders: Array<(value: Uint8Array) => void> = [];18 const make = (19 inbox: Uint8Array[],20 outbox: Uint8Array[],21 readers: Array<(value: Uint8Array) => void>,22 outReaders: Array<(value: Uint8Array) => void>,23 ): PairingTransport => ({24 async send(message) {25 const reader = outReaders.shift();26 if (reader) reader(message);27 else outbox.push(message);28 },29 receive() {30 const message = inbox.shift();31 return message ? Promise.resolve(message) : new Promise((resolve) => readers.push(resolve));32 },33 });34 return [35 make(left, right, leftReaders, rightReaders),36 make(right, left, rightReaders, leftReaders),37 ];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("adoption transcript v2", () => {51 it("persists the validated receiver before reading or sending the root", async () => {52 const [receiverChannel, sourceChannel] = memoryPair();53 const rootKeyHex = "11".repeat(32);54 const vaultId = await computeVaultId(55 Uint8Array.from({ length: 32 }, () => 0x11),56 );57 const order: string[] = [];58 const receiverDeps: AdoptionV2ReceiverDeps = {59 deviceName: "receiver",60 localIdentity: vi.fn(async () => identity("receiver")),61 rotateLocalIdentity: vi.fn(async () => identity("receiver-rotated")),62 license: vi.fn(async () => "PPK-receiver"),63 confirmSas: vi.fn(async () => true),64 validateSource: vi.fn(async () => undefined),65 hasSourceIdentityCollision: vi.fn(async () => false),66 createVaultFromRootKey: vi.fn(async () => {67 order.push("create-vault");68 return { vaultId };69 }),70 applyLicense: vi.fn(async () => undefined),71 upsertPairedDeviceAndClearRevocation: vi.fn(async () => undefined),72 onError: vi.fn(),73 };74 const sourceDeps: AdoptionV2SourceDeps = {75 sourceIdentity: identity("source"),76 rootKeyHex: vi.fn(async () => {77 order.push("read-root");78 return rootKeyHex;79 }),80 vaultId: vi.fn(async () => vaultId),81 license: vi.fn(async () => "PPK-source"),82 applyLicense: vi.fn(async () => undefined),83 onConsent: vi.fn(async () => true),84 persistPendingIdentity: vi.fn(async () => {85 order.push("persist-pending");86 return true;87 }),88 completePendingIdentity: vi.fn(async () => {89 order.push("complete-pending");90 }),91 hasDeviceIdentityCollision: vi.fn(async () => false),92 rotateSourceIdentity: vi.fn(async () => identity("source-rotated")),93 upsertPairedDevice: vi.fn(async () => undefined),94 snapshotCapable: () => true,95 };9697 const [result] = await Promise.all([98 requestAdoptionV2(receiverChannel, receiverDeps),99 (async () => {100 const request = JSON.parse(101 new TextDecoder().decode(await sourceChannel.receive()),102 ) as unknown;103 expect(isAdoptRequestV2(request)).toBe(true);104 if (isAdoptRequestV2(request)) {105 await grantAdoptionV2(sourceChannel, request, sourceDeps);106 }107 })(),108 ]);109110 expect(result?.device.deviceId).toBe("source-id");111 expect(order).toEqual([112 "persist-pending",113 "read-root",114 "create-vault",115 "complete-pending",116 ]);117 expect(ADOPTION_VERSION).toBe(2);118 });119});