import { describe, expect, it, vi } from "vitest"; import type { PairingTransport } from "./pairing-handshake"; import { computeVaultId } from "./sync-identity"; import { ADOPTION_VERSION, type AdoptionV2ReceiverDeps, type AdoptionV2SourceDeps, grantAdoptionV2, isAdoptRequestV2, requestAdoptionV2, } from "./adoption-protocol"; function memoryPair(): [PairingTransport, PairingTransport] { const left: Uint8Array[] = []; const right: Uint8Array[] = []; const leftReaders: Array<(value: Uint8Array) => void> = []; const rightReaders: Array<(value: Uint8Array) => void> = []; const make = ( inbox: Uint8Array[], outbox: Uint8Array[], readers: Array<(value: Uint8Array) => void>, outReaders: Array<(value: Uint8Array) => void>, ): PairingTransport => ({ async send(message) { const reader = outReaders.shift(); if (reader) reader(message); else outbox.push(message); }, receive() { const message = inbox.shift(); return message ? Promise.resolve(message) : new Promise((resolve) => readers.push(resolve)); }, }); return [ make(left, right, leftReaders, rightReaders), make(right, left, rightReaders, leftReaders), ]; } function identity(name: string) { return { deviceId: `${name}-id`, deviceName: name, deviceType: "extension" as const, longTermPublicKeyHex: "ab".repeat(32), signingPublicKeyHex: "cd".repeat(32), }; } describe("adoption transcript v2", () => { it("persists the validated receiver before reading or sending the root", async () => { const [receiverChannel, sourceChannel] = memoryPair(); const rootKeyHex = "11".repeat(32); const vaultId = await computeVaultId( Uint8Array.from({ length: 32 }, () => 0x11), ); const order: string[] = []; const receiverDeps: AdoptionV2ReceiverDeps = { deviceName: "receiver", localIdentity: vi.fn(async () => identity("receiver")), rotateLocalIdentity: vi.fn(async () => identity("receiver-rotated")), license: vi.fn(async () => "PPK-receiver"), confirmSas: vi.fn(async () => true), validateSource: vi.fn(async () => undefined), hasSourceIdentityCollision: vi.fn(async () => false), createVaultFromRootKey: vi.fn(async () => { order.push("create-vault"); return { vaultId }; }), applyLicense: vi.fn(async () => undefined), upsertPairedDeviceAndClearRevocation: vi.fn(async () => undefined), onError: vi.fn(), }; const sourceDeps: AdoptionV2SourceDeps = { sourceIdentity: identity("source"), rootKeyHex: vi.fn(async () => { order.push("read-root"); return rootKeyHex; }), vaultId: vi.fn(async () => vaultId), license: vi.fn(async () => "PPK-source"), applyLicense: vi.fn(async () => undefined), onConsent: vi.fn(async () => true), persistPendingIdentity: vi.fn(async () => { order.push("persist-pending"); return true; }), completePendingIdentity: vi.fn(async () => { order.push("complete-pending"); }), hasDeviceIdentityCollision: vi.fn(async () => false), rotateSourceIdentity: vi.fn(async () => identity("source-rotated")), upsertPairedDevice: vi.fn(async () => undefined), snapshotCapable: () => true, }; const [result] = await Promise.all([ requestAdoptionV2(receiverChannel, receiverDeps), (async () => { const request = JSON.parse( new TextDecoder().decode(await sourceChannel.receive()), ) as unknown; expect(isAdoptRequestV2(request)).toBe(true); if (isAdoptRequestV2(request)) { await grantAdoptionV2(sourceChannel, request, sourceDeps); } })(), ]); expect(result?.device.deviceId).toBe("source-id"); expect(order).toEqual([ "persist-pending", "read-root", "create-vault", "complete-pending", ]); expect(ADOPTION_VERSION).toBe(2); }); });