adoption-protocol.test.ts

3.9 KB · raw

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});