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 "./vault-adopt";11import type { PairingTransport } from "./pairing-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 confirmSas: vi.fn(async () => true),61 createVaultFromRootKey: vi.fn(async () => ({ vaultId })),62 deriveLocalIdentity: vi.fn(async () => identity("receiver")),63 upsertPairedDevice: receiverUpsert,64 applyLicense: vi.fn(),65 onError: vi.fn(),66 };67 const sourceDeps: AdoptionSourceDeps = {68 sourceIdentity: identity("source"),69 rootKeyHex: vi.fn(async () => rootKeyHex),70 vaultId: vi.fn(async () => vaultId),71 license: vi.fn(async () => "PPK-test"),72 onConsent: vi.fn(async () => true),73 upsertPairedDevice: sourceUpsert,74 };7576 await Promise.all([77 requestAdoption(receiverChannel, receiverDeps),78 (async () => {79 const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;80 await grantAdoption(sourceChannel, request, sourceDeps);81 })(),82 ]);8384 expect(receiverDeps.createVaultFromRootKey).toHaveBeenCalledWith(rootKeyHex);85 expect(receiverUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "source-id" }));86 expect(sourceUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-id" }));87 expect(receiverDeps.applyLicense).toHaveBeenCalledWith("PPK-test");88 });8990 it("T-A11: SAS reject aborts before ADOPT_GRANT", async () => {91 const [receiverChannel, sourceChannel] = memoryPair();92 const rootKeyHex = vi.fn(async () => "11".repeat(32));93 const createVaultFromRootKey = vi.fn();94 const receiver = requestAdoption(receiverChannel, {95 deviceName: "Receiver",96 generateDeviceId: () => "receiver-ephemeral",97 confirmSas: vi.fn(async () => false),98 createVaultFromRootKey,99 deriveLocalIdentity: vi.fn(),100 upsertPairedDevice: vi.fn(),101 onError: vi.fn(),102 });103 const request = JSON.parse(104 new TextDecoder().decode(await sourceChannel.receive()),105 ) as AdoptRequest;106 const source = grantAdoption(sourceChannel, request, {107 sourceIdentity: identity("source"),108 rootKeyHex,109 vaultId: vi.fn(),110 onConsent: vi.fn(async () => true),111 upsertPairedDevice: vi.fn(),112 });113114 await expect(receiver).resolves.toBeNull();115 await expect(source).resolves.toBeNull();116 expect(rootKeyHex).not.toHaveBeenCalled();117 expect(createVaultFromRootKey).not.toHaveBeenCalled();118 });119120 it("T-A12: consent reject aborts before ADOPT_GRANT and receiver remains vault-less", async () => {121 const [receiverChannel, sourceChannel] = memoryPair();122 const createVaultFromRootKey = vi.fn();123 const errors: string[] = [];124 const receiver = requestAdoption(receiverChannel, {125 deviceName: "Receiver",126 generateDeviceId: () => "receiver-ephemeral",127 confirmSas: vi.fn(async () => true),128 createVaultFromRootKey,129 deriveLocalIdentity: vi.fn(),130 upsertPairedDevice: vi.fn(),131 onError: (err) => errors.push(err),132 });133 const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;134 await grantAdoption(sourceChannel, request, {135 sourceIdentity: identity("source"),136 rootKeyHex: vi.fn(),137 vaultId: vi.fn(),138 onConsent: vi.fn(async () => false),139 upsertPairedDevice: vi.fn(),140 });141 await Promise.race([receiver, Promise.resolve()]);142143 expect(createVaultFromRootKey).not.toHaveBeenCalled();144 expect(errors).toEqual([]);145 });146147 it("T-A13: invalid root_key_hex aborts gracefully", async () => {148 const [receiverChannel, sourceChannel] = memoryPair();149 const onError = vi.fn();150 const createVaultFromRootKey = vi.fn();151 const receiver = requestAdoption(receiverChannel, {152 deviceName: "Receiver",153 generateDeviceId: () => "receiver-ephemeral",154 confirmSas: vi.fn(async () => true),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: "bad",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 } as never);167 await receiver;168169 expect(onError).toHaveBeenCalledWith("ADOPT_UNEXPECTED_FRAME");170 expect(createVaultFromRootKey).not.toHaveBeenCalled();171 });172173 it("T-A14: vault_id mismatch aborts before vault creation", async () => {174 const [receiverChannel, sourceChannel] = memoryPair();175 const onError = vi.fn();176 const createVaultFromRootKey = vi.fn();177 const receiver = requestAdoption(receiverChannel, {178 deviceName: "Receiver",179 generateDeviceId: () => "receiver-ephemeral",180 confirmSas: vi.fn(async () => true),181 createVaultFromRootKey,182 deriveLocalIdentity: vi.fn(),183 upsertPairedDevice: vi.fn(),184 onError,185 });186 await sourceChannel.receive();187 await sendFrame(sourceChannel, {188 type: "ADOPT_GRANT",189 root_key_hex: "22".repeat(32),190 vault_id: "00".repeat(32),191 source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },192 });193 await receiver;194195 expect(onError).toHaveBeenCalledWith("ADOPT_VAULT_ID_MISMATCH");196 expect(createVaultFromRootKey).not.toHaveBeenCalled();197 });198199 it("T-A15: source does not persist receiver until ADOPT_IDENTITY arrives", async () => {200 const [receiverChannel, sourceChannel] = memoryPair();201 const upsert = vi.fn();202 const promise = grantAdoption(sourceChannel, {203 type: "ADOPT_REQUEST",204 device_id: "receiver-ephemeral",205 device_name: "Receiver",206 device_type: "extension",207 }, {208 sourceIdentity: identity("source"),209 rootKeyHex: vi.fn(async () => "33".repeat(32)),210 vaultId: vi.fn(async () => await computeVaultId(Uint8Array.from(Buffer.from("33".repeat(32), "hex")))),211 onConsent: vi.fn(async () => true),212 upsertPairedDevice: upsert,213 });214 await sendFrame(receiverChannel, {215 type: "ADOPT_CONFIRM",216 confirmed: true,217 });218219 await Promise.race([promise, Promise.resolve()]);220 expect(upsert).not.toHaveBeenCalled();221 });222223 it("T-A31: does not apply a license if vault creation fails", async () => {224 const [receiverChannel, sourceChannel] = memoryPair();225 const rootKeyHex = "44".repeat(32);226 const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));227 const applyLicense = vi.fn();228 const receiver = requestAdoption(receiverChannel, {229 deviceName: "Receiver",230 generateDeviceId: () => "receiver-ephemeral",231 confirmSas: vi.fn(async () => true),232 createVaultFromRootKey: vi.fn(async () => {233 throw new Error("commit_failed");234 }),235 deriveLocalIdentity: vi.fn(),236 upsertPairedDevice: vi.fn(),237 applyLicense,238 onError: vi.fn(),239 });240 await sourceChannel.receive();241 await sendFrame(sourceChannel, {242 type: "ADOPT_GRANT",243 root_key_hex: rootKeyHex,244 vault_id: vaultId,245 license: "PPK-test",246 source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },247 });248249 await expect(receiver).rejects.toThrow("commit_failed");250 expect(applyLicense).not.toHaveBeenCalled();251 });252253 it("T-A32: never creates a vault when the grant vault_id mismatches the root key", async () => {254 const [receiverChannel, sourceChannel] = memoryPair();255 const createVaultFromRootKey = vi.fn();256 const receiver = requestAdoption(receiverChannel, {257 deviceName: "Receiver",258 generateDeviceId: () => "receiver-ephemeral",259 confirmSas: vi.fn(async () => true),260 createVaultFromRootKey,261 deriveLocalIdentity: vi.fn(),262 upsertPairedDevice: vi.fn(),263 applyLicense: vi.fn(),264 onError: vi.fn(),265 });266 await sourceChannel.receive();267 await sendFrame(sourceChannel, {268 type: "ADOPT_GRANT",269 root_key_hex: "55".repeat(32),270 vault_id: "99".repeat(32),271 license: "PPK-test",272 source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },273 });274275 await receiver;276 expect(createVaultFromRootKey).not.toHaveBeenCalled();277 });278});