vault-adopt.test.ts

44.9 KB · raw

1import { describe, expect, it, vi } from "vitest";2import { padSyncFrame, syncFrameBucket, unpadSyncFrame } from "./sync-padding";3import { computeVaultId } from "./sync-identity";4import {5  grantAdoption,6  isAdoptGrant,7  requestAdoption,8  sendFrame,9  sendVaultSnapshot,10  receiveVaultSnapshot,11  type AdoptRequest,12  type AdoptionReceiverDeps,13  type AdoptionSourceDeps,14} from "./vault-adopt";15import type { PairingTransport } from "./pairing-handshake";1617function memoryPair(): [PairingTransport, PairingTransport] {18  const a: Uint8Array[] = [];19  const b: Uint8Array[] = [];20  const ar: Array<(v: Uint8Array) => void> = [];21  const br: Array<(v: Uint8Array) => void> = [];22  function make(23    inbox: Uint8Array[],24    outbox: Uint8Array[],25    readers: Array<(v: Uint8Array) => void>,26    outReaders: Array<(v: Uint8Array) => void>,27  ): PairingTransport {28    return {29      async send(msg) {30        const reader = outReaders.shift();31        if (reader) reader(msg);32        else outbox.push(msg);33      },34      receive() {35        const msg = inbox.shift();36        if (msg) return Promise.resolve(msg);37        return new Promise((resolve) => readers.push(resolve));38      },39    };40  }41  return [make(a, b, ar, br), make(b, a, br, ar)];42}4344function identity(name: string) {45  return {46    deviceId: `${name}-id`,47    deviceName: name,48    deviceType: "extension" as const,49    longTermPublicKeyHex: "ab".repeat(32),50    signingPublicKeyHex: "cd".repeat(32),51  };52}5354/** Snapshot frames are padded on the wire (PB-26-604), so hand-built ones55 *  must go through padSyncFrame exactly as sendVaultSnapshot does. */56function sendSnapshotFrame(channel: PairingTransport, frame: unknown): Promise<void> {57  return channel.send(padSyncFrame(new TextEncoder().encode(JSON.stringify(frame))));58}5960describe("vault adoption sub-protocol", () => {61  it("T-A10: completes ADOPT_REQUEST through mutual completion acknowledgements", async () => {62    const [receiverChannel, sourceChannel] = memoryPair();63    const rootKeyHex = "11".repeat(32);64    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));65    const sourceUpsert = vi.fn();66    const receiverUpsert = vi.fn();67    const pairState = {68      rendezvousSecretHex: "42".repeat(32),69      relayUrl: "wss://relay.example.test",70    };71    const receiverDeps: AdoptionReceiverDeps = {72      deviceName: "Receiver",73      generateDeviceId: () => "receiver-ephemeral",74      confirmSas: vi.fn(async () => true),75      createVaultFromRootKey: vi.fn(async () => ({ vaultId })),76      deriveLocalIdentity: vi.fn(async () => identity("receiver")),77      upsertPairedDevice: receiverUpsert,78      applyLicense: vi.fn(),79      pairState,80      onError: vi.fn(),81    };82    const sourceDeps: AdoptionSourceDeps = {83      sourceIdentity: identity("source"),84      rootKeyHex: vi.fn(async () => rootKeyHex),85      vaultId: vi.fn(async () => vaultId),86      license: vi.fn(async () => "PPK-test"),87      onConsent: vi.fn(async () => true),88      upsertPairedDevice: sourceUpsert,89      pairState,90    };9192    await Promise.all([93      requestAdoption(receiverChannel, receiverDeps),94      (async () => {95        const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;96        await grantAdoption(sourceChannel, request, sourceDeps);97      })(),98    ]);99100    expect(receiverDeps.createVaultFromRootKey).toHaveBeenCalledWith(rootKeyHex);101    expect(receiverUpsert).toHaveBeenCalledWith(expect.objectContaining({102      deviceId: "source-id",103      ...pairState,104    }));105    expect(sourceUpsert).toHaveBeenCalledWith(expect.objectContaining({106      deviceId: "receiver-id",107      ...pairState,108    }));109    expect(receiverDeps.applyLicense).toHaveBeenCalledWith("PPK-test");110  });111112  it("T-A10c: rotates a copied browser identity when the source reports a device ID collision", async () => {113    const [receiverChannel, sourceChannel] = memoryPair();114    const rootKeyHex = "13".repeat(32);115    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));116    const copiedIdentity = identity("chrome");117    const freshIdentity = identity("firefox");118    const rotateLocalIdentity = vi.fn(async () => freshIdentity);119    const receiverDeps: AdoptionReceiverDeps = {120      deviceName: "Firefox",121      generateDeviceId: () => "receiver-ephemeral",122      confirmSas: vi.fn(async () => true),123      createVaultFromRootKey: vi.fn(async () => ({ vaultId })),124      deriveLocalIdentity: vi.fn(async () => copiedIdentity),125      rotateLocalIdentity,126      upsertPairedDevice: vi.fn(),127      onError: vi.fn(),128    };129130    const receiverPromise = requestAdoption(receiverChannel, receiverDeps);131    await sourceChannel.receive(); // ADOPT_REQUEST132    await sourceChannel.receive(); // ADOPT_CONFIRM133    await sendFrame(sourceChannel, {134      type: "ADOPT_GRANT",135      root_key_hex: rootKeyHex,136      vault_id: vaultId,137      license: "PPK-test",138      source: {139        device_id: "android-id",140        device_name: "Android",141        device_type: "android",142        long_term_public_key_hex: "ef".repeat(32),143      },144    });145146    const firstIdentity = JSON.parse(new TextDecoder().decode(await sourceChannel.receive()));147    expect(firstIdentity.device_id).toBe("chrome-id");148    await sendFrame(sourceChannel, {149      type: "ADOPT_REJECT",150      reason: "ADOPT_DEVICE_ID_COLLISION",151    });152    const secondIdentity = JSON.parse(new TextDecoder().decode(await sourceChannel.receive()));153    expect(secondIdentity.device_id).toBe("firefox-id");154    await sendFrame(sourceChannel, { type: "ADOPT_COMPLETE" });155    await sourceChannel.receive(); // ADOPT_APPLIED156157    expect(await receiverPromise).not.toBeNull();158    expect(rotateLocalIdentity).toHaveBeenCalledOnce();159    expect(receiverDeps.onError).not.toHaveBeenCalled();160  });161162  it("T-A10d: receiver rejects a revoked source identity and source rotates", async () => {163    const [receiverChannel, sourceChannel] = memoryPair();164    const rootKeyHex = "14".repeat(32);165    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));166    const revokedSourceIdentity = identity("chrome");167    const freshSourceIdentity = identity("chrome-fresh");168    const receiverUpsert = vi.fn();169    const receiverDeps: AdoptionReceiverDeps = {170      deviceName: "Android",171      generateDeviceId: () => "receiver-ephemeral",172      confirmSas: vi.fn(async () => true),173      createVaultFromRootKey: vi.fn(async () => ({ vaultId })),174      deriveLocalIdentity: vi.fn(async () => identity("receiver")),175      upsertPairedDevice: receiverUpsert,176      hasSourceIdentityCollision: vi.fn(async (source) =>177        source.device_id === revokedSourceIdentity.deviceId,178      ),179      onError: vi.fn(),180    };181182    const receiverPromise = requestAdoption(receiverChannel, receiverDeps);183    await sourceChannel.receive(); // ADOPT_REQUEST184    await sourceChannel.receive(); // ADOPT_CONFIRM185186    // First grant with revoked identity187    await sendFrame(sourceChannel, {188      type: "ADOPT_GRANT",189      root_key_hex: rootKeyHex,190      vault_id: vaultId,191      license: "PPK-test",192      source: {193        device_id: revokedSourceIdentity.deviceId,194        device_name: revokedSourceIdentity.deviceName,195        device_type: revokedSourceIdentity.deviceType,196        long_term_public_key_hex: revokedSourceIdentity.longTermPublicKeyHex,197      },198    });199200    // Receiver sends ADOPT_REJECT(COLLISION) instead of ADOPT_IDENTITY201    const firstResponse = JSON.parse(new TextDecoder().decode(await sourceChannel.receive()));202    expect(firstResponse.type).toBe("ADOPT_REJECT");203    expect(firstResponse.reason).toBe("ADOPT_DEVICE_ID_COLLISION");204205    // Source rotates and resends grant with fresh identity206    await sendFrame(sourceChannel, {207      type: "ADOPT_GRANT",208      root_key_hex: rootKeyHex,209      vault_id: vaultId,210      license: "PPK-test",211      source: {212        device_id: freshSourceIdentity.deviceId,213        device_name: freshSourceIdentity.deviceName,214        device_type: freshSourceIdentity.deviceType,215        long_term_public_key_hex: freshSourceIdentity.longTermPublicKeyHex,216      },217    });218219    // Receiver accepts the fresh identity and sends ADOPT_IDENTITY220    const identityFrame = JSON.parse(new TextDecoder().decode(await sourceChannel.receive()));221    expect(identityFrame.type).toBe("ADOPT_IDENTITY");222    await sendFrame(sourceChannel, { type: "ADOPT_COMPLETE" });223    await sourceChannel.receive(); // ADOPT_APPLIED224225    const result = await receiverPromise;226    expect(result).not.toBeNull();227    expect(result!.device.deviceId).toBe(freshSourceIdentity.deviceId);228    expect(receiverDeps.hasSourceIdentityCollision).toHaveBeenCalledTimes(2);229    expect(receiverDeps.onError).not.toHaveBeenCalled();230  });231232  it("T-A10e: source detects a revoked receiver identity and forces rotation", async () => {233    const [receiverChannel, sourceChannel] = memoryPair();234    const rootKeyHex = "15".repeat(32);235    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));236    const collidingIdentity = identity("chrome");237    const freshIdentity = identity("firefox");238    const sourceUpsert = vi.fn();239    const sourceDeps: AdoptionSourceDeps = {240      sourceIdentity: identity("android"),241      rootKeyHex: vi.fn(async () => rootKeyHex),242      vaultId: vi.fn(async () => vaultId),243      license: vi.fn(async () => "PPK-test"),244      onConsent: vi.fn(async () => true),245      upsertPairedDevice: sourceUpsert,246      hasDeviceIdentityCollision: vi.fn(async (device) =>247        device.deviceId === collidingIdentity.deviceId,248      ),249    };250251    const sourcePromise = (async () => {252      const request = JSON.parse(253        new TextDecoder().decode(await sourceChannel.receive()),254      ) as AdoptRequest;255      return grantAdoption(sourceChannel, request, sourceDeps);256    })();257258    // Receiver side: send request and confirm259    await sendFrame(receiverChannel, {260      type: "ADOPT_REQUEST",261      device_id: "receiver-ephemeral",262      device_name: "Chrome",263      device_type: "extension",264      license: "PPK-test",265      snapshot_capable: true,266    });267    await sendFrame(receiverChannel, { type: "ADOPT_CONFIRM", confirmed: true });268269    // Receive grant270    const grant = JSON.parse(new TextDecoder().decode(await receiverChannel.receive()));271    expect(grant.type).toBe("ADOPT_GRANT");272273    // Send colliding identity274    await sendFrame(receiverChannel, {275      type: "ADOPT_IDENTITY",276      device_id: collidingIdentity.deviceId,277      device_name: collidingIdentity.deviceName,278      device_type: collidingIdentity.deviceType,279      long_term_public_key_hex: collidingIdentity.longTermPublicKeyHex,280      signing_public_key_hex: collidingIdentity.signingPublicKeyHex,281    });282283    // Source sends ADOPT_REJECT(COLLISION)284    const reject = JSON.parse(new TextDecoder().decode(await receiverChannel.receive()));285    expect(reject.type).toBe("ADOPT_REJECT");286    expect(reject.reason).toBe("ADOPT_DEVICE_ID_COLLISION");287288    // Receiver sends fresh identity289    await sendFrame(receiverChannel, {290      type: "ADOPT_IDENTITY",291      device_id: freshIdentity.deviceId,292      device_name: freshIdentity.deviceName,293      device_type: freshIdentity.deviceType,294      long_term_public_key_hex: freshIdentity.longTermPublicKeyHex,295      signing_public_key_hex: freshIdentity.signingPublicKeyHex,296    });297298    // Source sends ADOPT_COMPLETE299    const complete = JSON.parse(new TextDecoder().decode(await receiverChannel.receive()));300    expect(complete.type).toBe("ADOPT_COMPLETE");301    await sendFrame(receiverChannel, { type: "ADOPT_APPLIED" });302303    const result = await sourcePromise;304    expect(result).not.toBeNull();305    expect(result!.deviceId).toBe(freshIdentity.deviceId);306    expect(sourceDeps.hasDeviceIdentityCollision).toHaveBeenCalledTimes(2);307  });308309  it("T-A10b: rejects without creating or pairing when neither peer has Pro", async () => {310    const [receiverChannel, sourceChannel] = memoryPair();311    const rootKeyHex = "12".repeat(32);312    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));313    const createVault = vi.fn();314    const receiverUpsert = vi.fn();315    const sourceUpsert = vi.fn();316    const onError = vi.fn();317318    const receiver = requestAdoption(receiverChannel, {319      deviceName: "Free receiver",320      generateDeviceId: () => "free-receiver",321      confirmSas: vi.fn(async () => true),322      license: vi.fn(async () => undefined),323      createVaultFromRootKey: createVault,324      deriveLocalIdentity: vi.fn(),325      upsertPairedDevice: receiverUpsert,326      onError,327    });328    const request = JSON.parse(329      new TextDecoder().decode(await sourceChannel.receive()),330    ) as AdoptRequest;331    const source = grantAdoption(sourceChannel, request, {332      sourceIdentity: identity("free-source"),333      rootKeyHex: vi.fn(async () => rootKeyHex),334      vaultId: vi.fn(async () => vaultId),335      license: vi.fn(async () => undefined),336      onConsent: vi.fn(async () => true),337      upsertPairedDevice: sourceUpsert,338    });339340    await expect(receiver).resolves.toBeNull();341    await expect(source).rejects.toThrow("ADOPT_PRO_REQUIRED");342    expect(onError).toHaveBeenCalledWith("ADOPT_PRO_REQUIRED");343    expect(createVault).not.toHaveBeenCalled();344    expect(receiverUpsert).not.toHaveBeenCalled();345    expect(sourceUpsert).not.toHaveBeenCalled();346  });347348  it("T-A10c: accepts and transfers a Pro license offered by the receiver", async () => {349    const [receiverChannel, sourceChannel] = memoryPair();350    const rootKeyHex = "13".repeat(32);351    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));352    let sourceLicense: string | undefined;353    const sourceApply = vi.fn(async (license: string) => {354      sourceLicense = license;355    });356357    const receiver = requestAdoption(receiverChannel, {358      deviceName: "Pro receiver",359      generateDeviceId: () => "pro-receiver",360      confirmSas: vi.fn(async () => true),361      license: vi.fn(async () => "PPK-receiver"),362      createVaultFromRootKey: vi.fn(async () => ({ vaultId })),363      deriveLocalIdentity: vi.fn(async () => identity("receiver")),364      upsertPairedDevice: vi.fn(),365      applyLicense: vi.fn(),366      onError: vi.fn(),367    });368    const request = JSON.parse(369      new TextDecoder().decode(await sourceChannel.receive()),370    ) as AdoptRequest;371    const source = grantAdoption(sourceChannel, request, {372      sourceIdentity: identity("free-source"),373      rootKeyHex: vi.fn(async () => rootKeyHex),374      vaultId: vi.fn(async () => vaultId),375      license: vi.fn(async () => sourceLicense),376      applyLicense: sourceApply,377      onConsent: vi.fn(async () => true),378      upsertPairedDevice: vi.fn(),379    });380381    await expect(Promise.all([receiver, source])).resolves.toEqual([382      { device: expect.objectContaining({ deviceId: "free-source-id" }), snapshotCapable: true },383      expect.objectContaining({ deviceId: "receiver-id" }),384    ]);385    expect(sourceApply).toHaveBeenCalledWith("PPK-receiver");386  });387388  it("T-A11: SAS reject aborts before ADOPT_GRANT", async () => {389    const [receiverChannel, sourceChannel] = memoryPair();390    const rootKeyHex = vi.fn(async () => "11".repeat(32));391    const createVaultFromRootKey = vi.fn();392    const receiver = requestAdoption(receiverChannel, {393      deviceName: "Receiver",394      generateDeviceId: () => "receiver-ephemeral",395      confirmSas: vi.fn(async () => false),396      createVaultFromRootKey,397      deriveLocalIdentity: vi.fn(),398      upsertPairedDevice: vi.fn(),399      onError: vi.fn(),400    });401    const request = JSON.parse(402      new TextDecoder().decode(await sourceChannel.receive()),403    ) as AdoptRequest;404    const source = grantAdoption(sourceChannel, request, {405      sourceIdentity: identity("source"),406      rootKeyHex,407      vaultId: vi.fn(),408      onConsent: vi.fn(async () => true),409      upsertPairedDevice: vi.fn(),410    });411412    await expect(receiver).resolves.toBeNull();413    await expect(source).resolves.toBeNull();414    expect(rootKeyHex).not.toHaveBeenCalled();415    expect(createVaultFromRootKey).not.toHaveBeenCalled();416  });417418  it("T-A12: consent reject aborts before ADOPT_GRANT and receiver remains vault-less", async () => {419    const [receiverChannel, sourceChannel] = memoryPair();420    const createVaultFromRootKey = vi.fn();421    const errors: string[] = [];422    const receiver = requestAdoption(receiverChannel, {423      deviceName: "Receiver",424      generateDeviceId: () => "receiver-ephemeral",425      confirmSas: vi.fn(async () => true),426      createVaultFromRootKey,427      deriveLocalIdentity: vi.fn(),428      upsertPairedDevice: vi.fn(),429      onError: (err) => errors.push(err),430    });431    const request = JSON.parse(new TextDecoder().decode(await sourceChannel.receive())) as AdoptRequest;432    await grantAdoption(sourceChannel, request, {433      sourceIdentity: identity("source"),434      rootKeyHex: vi.fn(),435      vaultId: vi.fn(),436      onConsent: vi.fn(async () => false),437      upsertPairedDevice: vi.fn(),438    });439    await Promise.race([receiver, Promise.resolve()]);440441    expect(createVaultFromRootKey).not.toHaveBeenCalled();442    expect(errors).toEqual([]);443  });444445  it("T-A13: invalid root_key_hex aborts gracefully", async () => {446    const [receiverChannel, sourceChannel] = memoryPair();447    const onError = vi.fn();448    const createVaultFromRootKey = vi.fn();449    const receiver = requestAdoption(receiverChannel, {450      deviceName: "Receiver",451      generateDeviceId: () => "receiver-ephemeral",452      confirmSas: vi.fn(async () => true),453      createVaultFromRootKey,454      deriveLocalIdentity: vi.fn(),455      upsertPairedDevice: vi.fn(),456      onError,457    });458    await sourceChannel.receive();459    await sendFrame(sourceChannel, {460      type: "ADOPT_GRANT",461      root_key_hex: "bad",462      vault_id: "00".repeat(32),463      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },464    } as never);465    await receiver;466467    expect(onError).toHaveBeenCalledWith("ADOPT_UNEXPECTED_FRAME");468    expect(createVaultFromRootKey).not.toHaveBeenCalled();469  });470471  it("T-A14: vault_id mismatch aborts before vault creation", async () => {472    const [receiverChannel, sourceChannel] = memoryPair();473    const onError = vi.fn();474    const createVaultFromRootKey = vi.fn();475    const receiver = requestAdoption(receiverChannel, {476      deviceName: "Receiver",477      generateDeviceId: () => "receiver-ephemeral",478      confirmSas: vi.fn(async () => true),479      createVaultFromRootKey,480      deriveLocalIdentity: vi.fn(),481      upsertPairedDevice: vi.fn(),482      onError,483    });484    await sourceChannel.receive();485    await sendFrame(sourceChannel, {486      type: "ADOPT_GRANT",487      root_key_hex: "22".repeat(32),488      vault_id: "00".repeat(32),489      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },490    });491    await receiver;492493    expect(onError).toHaveBeenCalledWith("ADOPT_VAULT_ID_MISMATCH");494    expect(createVaultFromRootKey).not.toHaveBeenCalled();495  });496497  it("T-A15: source does not persist receiver until ADOPT_IDENTITY arrives", async () => {498    const [receiverChannel, sourceChannel] = memoryPair();499    const upsert = vi.fn();500    const promise = grantAdoption(sourceChannel, {501      type: "ADOPT_REQUEST",502      device_id: "receiver-ephemeral",503      device_name: "Receiver",504      device_type: "extension",505    }, {506      sourceIdentity: identity("source"),507      rootKeyHex: vi.fn(async () => "33".repeat(32)),508      vaultId: vi.fn(async () => await computeVaultId(Uint8Array.from(Buffer.from("33".repeat(32), "hex")))),509      license: vi.fn(async () => "PPK-test"),510      onConsent: vi.fn(async () => true),511      upsertPairedDevice: upsert,512    });513    await sendFrame(receiverChannel, {514      type: "ADOPT_CONFIRM",515      confirmed: true,516    });517518    await Promise.race([promise, Promise.resolve()]);519    expect(upsert).not.toHaveBeenCalled();520  });521522  it("T-A16: receiver does not persist source without ADOPT_COMPLETE", async () => {523    const [receiverChannel, sourceChannel] = memoryPair();524    const rootKeyHex = "66".repeat(32);525    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));526    const receiverUpsert = vi.fn();527    const onError = vi.fn();528    const receiver = requestAdoption(receiverChannel, {529      deviceName: "Receiver",530      generateDeviceId: () => "receiver-ephemeral",531      confirmSas: vi.fn(async () => true),532      license: vi.fn(async () => "PPK-local"),533      createVaultFromRootKey: vi.fn(async () => ({ vaultId })),534      deriveLocalIdentity: vi.fn(async () => identity("receiver")),535      upsertPairedDevice: receiverUpsert,536      onError,537    });538539    await sourceChannel.receive(); // ADOPT_REQUEST540    await sourceChannel.receive(); // ADOPT_CONFIRM541    await sendFrame(sourceChannel, {542      type: "ADOPT_GRANT",543      root_key_hex: rootKeyHex,544      vault_id: vaultId,545      source: {546        device_id: "source-id",547        device_name: "Source",548        device_type: "extension",549        long_term_public_key_hex: "ab".repeat(32),550      },551    });552    await sourceChannel.receive(); // ADOPT_IDENTITY553    await sendFrame(sourceChannel, { type: "ADOPT_CONFIRM", confirmed: true });554555    await expect(receiver).resolves.toBeNull();556    expect(onError).toHaveBeenCalledWith("ADOPT_COMPLETION_MISSING");557    expect(receiverUpsert).not.toHaveBeenCalled();558  });559560  it("T-A17: receiver safety policy runs before vault creation", async () => {561    const [receiverChannel, sourceChannel] = memoryPair();562    const rootKeyHex = "77".repeat(32);563    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));564    const createVaultFromRootKey = vi.fn();565    const receiver = requestAdoption(receiverChannel, {566      deviceName: "Receiver",567      generateDeviceId: () => "receiver-ephemeral",568      confirmSas: vi.fn(async () => true),569      license: vi.fn(async () => "PPK-local"),570      validateSource: vi.fn(async () => {571        throw new Error("JOIN_POPULATED_VAULT_BLOCKED");572      }),573      createVaultFromRootKey,574      deriveLocalIdentity: vi.fn(),575      upsertPairedDevice: vi.fn(),576      onError: vi.fn(),577    });578579    await sourceChannel.receive(); // ADOPT_REQUEST580    await sourceChannel.receive(); // ADOPT_CONFIRM581    await sendFrame(sourceChannel, {582      type: "ADOPT_GRANT",583      root_key_hex: rootKeyHex,584      vault_id: vaultId,585      source: {586        device_id: "new-source",587        device_name: "Source",588        device_type: "extension",589        long_term_public_key_hex: "ab".repeat(32),590      },591    });592593    await expect(receiver).rejects.toThrow("JOIN_POPULATED_VAULT_BLOCKED");594    expect(createVaultFromRootKey).not.toHaveBeenCalled();595  });596597  it("T-A31: does not apply a license if vault creation fails", async () => {598    const [receiverChannel, sourceChannel] = memoryPair();599    const rootKeyHex = "44".repeat(32);600    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));601    const applyLicense = vi.fn();602    const receiver = requestAdoption(receiverChannel, {603      deviceName: "Receiver",604      generateDeviceId: () => "receiver-ephemeral",605      confirmSas: vi.fn(async () => true),606      createVaultFromRootKey: vi.fn(async () => {607        throw new Error("commit_failed");608      }),609      deriveLocalIdentity: vi.fn(),610      upsertPairedDevice: vi.fn(),611      applyLicense,612      onError: vi.fn(),613    });614    await sourceChannel.receive();615    await sendFrame(sourceChannel, {616      type: "ADOPT_GRANT",617      root_key_hex: rootKeyHex,618      vault_id: vaultId,619      license: "PPK-test",620      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },621    });622623    await expect(receiver).rejects.toThrow("commit_failed");624    expect(applyLicense).not.toHaveBeenCalled();625  });626627  it("T-A32: never creates a vault when the grant vault_id mismatches the root key", async () => {628    const [receiverChannel, sourceChannel] = memoryPair();629    const createVaultFromRootKey = vi.fn();630    const receiver = requestAdoption(receiverChannel, {631      deviceName: "Receiver",632      generateDeviceId: () => "receiver-ephemeral",633      confirmSas: vi.fn(async () => true),634      createVaultFromRootKey,635      deriveLocalIdentity: vi.fn(),636      upsertPairedDevice: vi.fn(),637      applyLicense: vi.fn(),638      onError: vi.fn(),639    });640    await sourceChannel.receive();641    await sendFrame(sourceChannel, {642      type: "ADOPT_GRANT",643      root_key_hex: "55".repeat(32),644      vault_id: "99".repeat(32),645      license: "PPK-test",646      source: { device_id: "source", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },647    });648649    await receiver;650    expect(createVaultFromRootKey).not.toHaveBeenCalled();651  });652});653654describe("snapshot_capable echo on ADOPT_GRANT", () => {655  function validGrant() {656    return {657      type: "ADOPT_GRANT" as const,658      root_key_hex: "11".repeat(32),659      vault_id: "22".repeat(32),660      license: "PPK-test",661      source: {662        device_id: "src-id",663        device_name: "Source",664        device_type: "extension" as const,665        long_term_public_key_hex: "ab".repeat(32),666      },667    };668  }669670  it("isAdoptGrant accepts a grant without snapshot_capable (pre-C source)", () => {671    expect(isAdoptGrant(validGrant())).toBe(true);672  });673674  it("isAdoptGrant accepts snapshot_capable: true", () => {675    expect(isAdoptGrant({ ...validGrant(), snapshot_capable: true })).toBe(true);676  });677678  it("isAdoptGrant accepts snapshot_capable: false", () => {679    expect(isAdoptGrant({ ...validGrant(), snapshot_capable: false })).toBe(true);680  });681682  it("isAdoptGrant rejects snapshot_capable with wrong type", () => {683    expect(isAdoptGrant({ ...validGrant(), snapshot_capable: "yes" })).toBe(false);684  });685686  it("grantAdoption echoes snapshot_capable: true when request had it", async () => {687    const [receiverChannel, sourceChannel] = memoryPair();688    const rootKeyHex = "33".repeat(32);689    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));690    const sourceDeps: AdoptionSourceDeps = {691      sourceIdentity: identity("source"),692      rootKeyHex: vi.fn(async () => rootKeyHex),693      vaultId: vi.fn(async () => vaultId),694      license: vi.fn(async () => "PPK-test"),695      onConsent: vi.fn(async () => true),696      upsertPairedDevice: vi.fn(),697    };698699    // Run grantAdoption concurrently with the test driver that feeds protocol frames.700    const grantPromise = grantAdoption(sourceChannel, {701      type: "ADOPT_REQUEST",702      device_id: "recv-id",703      device_name: "Receiver",704      device_type: "extension",705      snapshot_capable: true,706    }, sourceDeps);707708    // grantAdoption: consent=true → reads ADOPT_CONFIRM → sends ADOPT_GRANT → reads ADOPT_IDENTITY709    await sendFrame(receiverChannel, { type: "ADOPT_CONFIRM", confirmed: true });710    const grantBytes = await receiverChannel.receive();711    const grantFrame = JSON.parse(new TextDecoder().decode(grantBytes));712    expect(grantFrame.type).toBe("ADOPT_GRANT");713    expect(grantFrame.snapshot_capable).toBe(true);714715    // Let grantAdoption complete: send ADOPT_IDENTITY, read ADOPT_COMPLETE, send ADOPT_APPLIED.716    await sendFrame(receiverChannel, {717      type: "ADOPT_IDENTITY",718      device_id: "recv-id",719      device_name: "Receiver",720      device_type: "extension",721      long_term_public_key_hex: "ab".repeat(32),722    });723    await receiverChannel.receive(); // ADOPT_COMPLETE724    await sendFrame(receiverChannel, { type: "ADOPT_APPLIED" });725    await grantPromise;726  });727728  it("grantAdoption sets snapshot_capable: false when request lacked it", async () => {729    const [receiverChannel, sourceChannel] = memoryPair();730    const rootKeyHex = "44".repeat(32);731    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));732    const sourceDeps: AdoptionSourceDeps = {733      sourceIdentity: identity("source"),734      rootKeyHex: vi.fn(async () => rootKeyHex),735      vaultId: vi.fn(async () => vaultId),736      license: vi.fn(async () => "PPK-test"),737      onConsent: vi.fn(async () => true),738      upsertPairedDevice: vi.fn(),739    };740741    const grantPromise = grantAdoption(sourceChannel, {742      type: "ADOPT_REQUEST",743      device_id: "recv-id",744      device_name: "Receiver",745      device_type: "extension",746      // NO snapshot_capable747    }, sourceDeps);748749    await sendFrame(receiverChannel, { type: "ADOPT_CONFIRM", confirmed: true });750    const grantBytes = await receiverChannel.receive();751    const grantFrame = JSON.parse(new TextDecoder().decode(grantBytes));752    expect(grantFrame.type).toBe("ADOPT_GRANT");753    expect(grantFrame.snapshot_capable).toBe(false);754755    await sendFrame(receiverChannel, {756      type: "ADOPT_IDENTITY",757      device_id: "recv-id",758      device_name: "Receiver",759      device_type: "extension",760      long_term_public_key_hex: "ab".repeat(32),761    });762    await receiverChannel.receive(); // ADOPT_COMPLETE763    await sendFrame(receiverChannel, { type: "ADOPT_APPLIED" });764    await grantPromise;765  });766767  it("requestAdoption returns snapshotCapable: true when grant echoes it", async () => {768    const [receiverChannel, sourceChannel] = memoryPair();769    const rootKeyHex = "55".repeat(32);770    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));771    const deps: AdoptionReceiverDeps = {772      deviceName: "recv",773      generateDeviceId: () => "recv-id",774      async confirmSas() { return true; },775      async createVaultFromRootKey() { return { vaultId }; },776      async deriveLocalIdentity() {777        return { deviceId: "recv-id", deviceName: "recv", deviceType: "extension" as const, longTermPublicKeyHex: "ab".repeat(32) };778      },779      async upsertPairedDevice() {},780      onError() {},781    };782783    const receiverPromise = requestAdoption(receiverChannel, deps);784    // requestAdoption sends: ADOPT_REQUEST, ADOPT_CONFIRM (after SAS auto-accept)785    // Then reads: ADOPT_GRANT, sends: ADOPT_IDENTITY, reads: ADOPT_COMPLETE, sends: ADOPT_APPLIED786    await sourceChannel.receive(); // ADOPT_REQUEST787    await sourceChannel.receive(); // ADOPT_CONFIRM788    await sendFrame(sourceChannel, {789      type: "ADOPT_GRANT",790      root_key_hex: rootKeyHex,791      vault_id: vaultId,792      license: "PPK-test",793      source: { device_id: "src-id", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },794      snapshot_capable: true,795    });796    await sourceChannel.receive(); // ADOPT_IDENTITY797    await sendFrame(sourceChannel, { type: "ADOPT_COMPLETE" });798    await sourceChannel.receive(); // ADOPT_APPLIED799800    const result = await receiverPromise;801    expect(result).not.toBeNull();802    expect(result!.snapshotCapable).toBe(true);803  });804805  it("requestAdoption returns snapshotCapable: false when grant lacks the field", async () => {806    const [receiverChannel, sourceChannel] = memoryPair();807    const rootKeyHex = "66".repeat(32);808    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));809    const deps: AdoptionReceiverDeps = {810      deviceName: "recv",811      generateDeviceId: () => "recv-id",812      async confirmSas() { return true; },813      async createVaultFromRootKey() { return { vaultId }; },814      async deriveLocalIdentity() {815        return { deviceId: "recv-id", deviceName: "recv", deviceType: "extension" as const, longTermPublicKeyHex: "ab".repeat(32) };816      },817      async upsertPairedDevice() {},818      onError() {},819    };820821    const receiverPromise = requestAdoption(receiverChannel, deps);822    await sourceChannel.receive(); // ADOPT_REQUEST823    await sourceChannel.receive(); // ADOPT_CONFIRM824    await sendFrame(sourceChannel, {825      type: "ADOPT_GRANT",826      root_key_hex: rootKeyHex,827      vault_id: vaultId,828      license: "PPK-test",829      source: { device_id: "src-id", device_name: "Source", device_type: "extension", long_term_public_key_hex: "ab".repeat(32) },830      // NO snapshot_capable (pre-C source)831    });832    await sourceChannel.receive(); // ADOPT_IDENTITY833    await sendFrame(sourceChannel, { type: "ADOPT_COMPLETE" });834    await sourceChannel.receive(); // ADOPT_APPLIED835836    const result = await receiverPromise;837    expect(result).not.toBeNull();838    expect(result!.snapshotCapable).toBe(false);839  });840841  // T-A33: post-fix regression for the SNAPSHOT_FRAME_WIRE_CEILING (768 KiB)842  // bug. Before v4.6.4, duress rode the entries final chunk, so an entry +843  // duress whose combined raw exceeded 768 KiB threw844  // ADOPT_SNAPSHOT_CHUNK_TOO_LARGE on receive. The frame-ceiling fix moves845  // duress onto its own frame after the entries chunks; this case asserts the846  // root-cause fix rather than just the new wire path.847  it("T-A33: >768 KiB snapshot (entry + duress) succeeds because duress no longer counts against the entries frame", async () => {848    const [source, receiver] = memoryPair();849    const entries: Record<string, string> = { "id-0": "x".repeat(450 * 1024) };850    const tombstones: Record<string, number> = { "dead-0": 1 };851    // 320 KiB of ciphertext JSON ("cc".repeat(320 * 1024) is ~640 KiB of JSON852    // text — the receiver treats duress_slot as opaque and concatenates it853    // raw; ~640 KiB JSON pushes combined raw over 768 KiB once the entry and854    // envelope are added). Pre-v4.6.4 this duress rode the entries final855    // chunk and pushed that frame over the ceiling; post-fix the duress856    // frame is sent on its own and stays under the ceiling.857    const duressJson = JSON.stringify({858      version: 1,859      salt: "aa".repeat(16),860      nonce: "bb".repeat(12),861      ciphertext: "cc".repeat(320 * 1024),862      tag: "dd".repeat(16),863    });864865    const [result] = await Promise.all([866      receiveVaultSnapshot(receiver),867      sendVaultSnapshot(source, entries, tombstones, duressJson),868    ]);869870    expect(result.entries).toEqual(entries);871    expect(result.tombstones).toEqual(tombstones);872    expect(result.duressSlot).toBe(duressJson);873  });874875  // T-A34: structural proof of the duress-on-separate-frame wire change.876  // Duress must leave the entries chunk so the entries frame stays under the877  // 768 KiB ceiling, even when the combined payload would exceed it.878  it("T-A34: duress rides its own frame so intermediate entries frames stay under the wire ceiling", async () => {879    const [source] = memoryPair();880    const observed: Array<{881      size: number;882      wireSize: number;883      hasDuress: boolean;884      entriesKeys: number;885    }> = [];886    const probe: PairingTransport = {887      async send(msg) {888        const raw = unpadSyncFrame(msg);889        const f = JSON.parse(new TextDecoder().decode(raw)) as Record<string, unknown>;890        observed.push({891          size: raw.length,892          wireSize: msg.length,893          hasDuress: typeof f.duress_slot === "string",894          entriesKeys: Object.keys((f.entries as Record<string, unknown>) ?? {}).length,895        });896        return source.send(msg);897      },898      receive() { return source.receive(); },899    };900    const entries: Record<string, string> = { "id-0": "x".repeat(450 * 1024) };901    const duressJson = "y".repeat(320 * 1024);902    const CEILING = 768 * 1024;903904    await sendVaultSnapshot(probe, entries, { "dead-0": 1 }, duressJson);905906    expect(observed.length).toBe(2);907    // Entries frame: no duress, one entries key, well under 768 KiB.908    expect(observed[0].hasDuress).toBe(false);909    expect(observed[0].entriesKeys).toBe(1);910    expect(observed[0].size).toBeLessThan(CEILING);911    // Duress frame: duress present, no entries, well under 768 KiB.912    expect(observed[1].hasDuress).toBe(true);913    expect(observed[1].entriesKeys).toBe(0);914    expect(observed[1].size).toBeLessThan(CEILING);915    // Combined raw exceeds the ceiling, which is the point of the split.916    expect(observed[0].size + observed[1].size).toBeGreaterThan(CEILING);917    // PB-26-604: both frames left the wire bucket-padded, so the relay sees918    // neither exact plaintext length.919    for (const frame of observed) {920      expect(frame.wireSize).toBe(syncFrameBucket(frame.size + 4));921    }922  });923924  // T-A35: backward compat — a pre-v4.6.4 sender (duress on the entries925  // final chunk) still decodes correctly. The receiver's per-chunk926  // Object.assign (entries / tombstones) plus duress-slot capture tolerates927  // both shapes.928  it("T-A35: receiver accepts duress_slot on the entries final chunk (pre-v4.6.4 sender shape)", async () => {929    const [source, receiver] = memoryPair();930    const duressJson = JSON.stringify({931      version: 1,932      salt: "aa".repeat(16),933      nonce: "bb".repeat(12),934      ciphertext: "cc".repeat(32),935      tag: "dd".repeat(16),936    });937    // Manually construct the legacy wire shape: a single chunk with both938    // the entries, the tombstones, AND duress_slot on it.939    await sendSnapshotFrame(source, {940      type: "ADOPT_SNAPSHOT",941      chunk_index: 0,942      chunk_count: 1,943      entries: { "id-0": "ciphertext" },944      tombstones: { "dead-0": 42 },945      duress_slot: duressJson,946    });947    const result = await receiveVaultSnapshot(receiver);948    expect(result.entries).toEqual({ "id-0": "ciphertext" });949    expect(result.tombstones).toEqual({ "dead-0": 42 });950    expect(result.duressSlot).toBe(duressJson);951  });952953  // T-A36: cap boundary inclusive. With 199 entries and a duress slot, the954  // sender must fit everything within MAX_SNAPSHOT_CHUNKS = 200 (199 entries955  // + 1 duress = 200 frames). Duress MUST arrive intact — sync does not956  // carry duress, so the adopted device would permanently lack its duress957  // vault otherwise. Entries are 40 KiB so the ~8 MiB total also fits the958  // PB-26-206 10 MiB aggregate budget.959  it("T-A36: duress is reserved a frame so 199 entries + duress fits in MAX_SNAPSHOT_CHUNKS", async () => {960    const [source, receiver] = memoryPair();961    const duressJson = JSON.stringify({962      version: 1,963      salt: "aa".repeat(16),964      nonce: "bb".repeat(12),965      ciphertext: "cc".repeat(32),966      tag: "dd".repeat(16),967    });968    const big = "x".repeat(40 * 1024);969    const entries: Record<string, string> = {};970    for (let i = 0; i < 199; i++) entries[`id-${i}`] = big;971    // First sanity: 199 entries needs multiple chunks (each ~40 KiB entry972    // packs ~12 per 512 KiB chunk). Verify the chunking math is sane before973    // asserting the duress-reservation behavior.974    const [result] = await Promise.all([975      receiveVaultSnapshot(receiver),976      sendVaultSnapshot(source, entries, { "dead-0": 1 }, duressJson),977    ]);978    expect(result.entries).toEqual(entries);979    expect(result.tombstones).toEqual({ "dead-0": 1 });980    expect(result.duressSlot).toBe(duressJson);981  });982983  // T-A37: PB-26-206 supersedes the old chunk-cap truncation. An over-budget984  // snapshot is never partially sent: the sender preflights with the same985  // aggregate helper the receiver enforces and throws before the first frame,986  // so the caller falls through to the regular sync pipeline instead of987  // sending a truncated snapshot.988  it("T-A37: over-budget snapshot preflights with zero frames sent (sync fallback, never truncation)", async () => {989    const [source] = memoryPair();990    let frames = 0;991    const countingSource: PairingTransport = {992      async send(msg) {993        frames++;994        return source.send(msg);995      },996      receive() { return source.receive(); },997    };998    const duressJson = JSON.stringify({999      version: 1,1000      salt: "aa".repeat(16),1001      nonce: "bb".repeat(12),1002      ciphertext: "cc".repeat(32),1003      tag: "dd".repeat(16),1004    });1005    // 200 entries × 60 KiB ≈ 12 MiB > 10 MiB aggregate budget.1006    const big = "x".repeat(60 * 1024);1007    const entries: Record<string, string> = {};1008    for (let i = 0; i < 200; i++) entries[`id-${i}`] = big;1009    await expect(1010      sendVaultSnapshot(countingSource, entries, { "dead-0": 1 }, duressJson),1011    ).rejects.toThrow("ADOPT_SNAPSHOT_OVER_BUDGET");1012    expect(frames).toBe(0);1013  });10141015  // T-A38: the single test that proves the *root-cause fix* for the frame1016  // ceiling bug, not just the new wire path. Combines the high-level1017  // end-to-end success assertion (T-A33) with the structural proof (T-A34):1018  // a payload whose combined raw exceeds the 768 KiB wire ceiling succeeds1019  // because the duress slot rides its own frame instead of piggybacking on1020  // the entries final chunk. Pre-v4.6.4, duress counted against the entries1021  // frame, so a single frame would exceed SNAPSHOT_FRAME_WIRE_CEILING and the1022  // receiver would reject the whole snapshot with ADOPT_SNAPSHOT_CHUNK_TOO_LARGE.1023  it("T-A38: >768 KiB snapshot succeeds because duress no longer counts against the entries frame", async () => {1024    const [source, receiver] = memoryPair();1025    // Single entry that is ~450 KiB on its own; combined with a ~640 KiB1026    // duress JSON, the total payload exceeds the 768 KiB wire ceiling.1027    const entries: Record<string, string> = { "id-0": "x".repeat(450 * 1024) };1028    const tombstones: Record<string, number> = { "dead-0": 1 };1029    const duressJson = JSON.stringify({1030      version: 1,1031      salt: "aa".repeat(16),1032      nonce: "bb".repeat(12),1033      ciphertext: "cc".repeat(320 * 1024),1034      tag: "dd".repeat(16),1035    });1036    const CEILING = 768 * 1024;10371038    // Probe the wire so we can assert the structural shape alongside the1039    // end-to-end success.1040    const observed: Array<{1041      size: number;1042      wireSize: number;1043      hasDuress: boolean;1044      entriesKeys: number;1045    }> = [];1046    const probe: PairingTransport = {1047      async send(msg) {1048        const raw = unpadSyncFrame(msg);1049        const f = JSON.parse(new TextDecoder().decode(raw)) as Record<string, unknown>;1050        observed.push({1051          size: raw.length,1052          wireSize: msg.length,1053          hasDuress: typeof f.duress_slot === "string",1054          entriesKeys: Object.keys((f.entries as Record<string, unknown>) ?? {}).length,1055        });1056        return source.send(msg);1057      },1058      receive() { return source.receive(); },1059    };10601061    const [result] = await Promise.all([1062      receiveVaultSnapshot(receiver),1063      sendVaultSnapshot(probe, entries, tombstones, duressJson),1064    ]);10651066    // High-level success: the receiver accepts the >768 KiB combined payload.1067    expect(result.entries).toEqual(entries);1068    expect(result.tombstones).toEqual(tombstones);1069    expect(result.duressSlot).toBe(duressJson);10701071    // Root-cause proof: duress is on its own frame, not on the entries frame.1072    // Every frame stays under the ceiling; only the combined raw exceeds it.1073    expect(observed.length).toBe(2);1074    expect(observed[0].hasDuress).toBe(false);1075    expect(observed[0].entriesKeys).toBe(1);1076    expect(observed[0].size).toBeLessThan(CEILING);1077    expect(observed[1].hasDuress).toBe(true);1078    expect(observed[1].entriesKeys).toBe(0);1079    expect(observed[1].size).toBeLessThan(CEILING);1080    const legacyCombinedSize = new TextEncoder().encode(JSON.stringify({1081      type: "ADOPT_SNAPSHOT",1082      chunk_index: 0,1083      chunk_count: 1,1084      entries,1085      tombstones,1086      duress_slot: duressJson,1087    })).length;1088    expect(legacyCombinedSize).toBeGreaterThan(CEILING);1089  });1090});10911092/**1093 * PB-26-603 — adoption frames carry a peer-supplied `device_name` straight into1094 * `chrome.storage.local`. The manifest path caps it at 64 chars; adoption did1095 * not, so one name could be ~16Kx larger than the manifest bound.1096 */1097describe("PB-26-603 adoption device_name cap", () => {1098  it("caps a 500-char peer device_name to 64 chars on both PairedDevice records", async () => {1099    const longName = "X".repeat(500);1100    const [receiverChannel, sourceChannel] = memoryPair();1101    const rootKeyHex = "13".repeat(32);1102    const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex")));1103    const receiverUpsert = vi.fn();1104    const sourceUpsert = vi.fn();11051106    const receiverDeps: AdoptionReceiverDeps = {1107      deviceName: "receiver",1108      generateDeviceId: () => "receiver-ephemeral",1109      confirmSas: vi.fn(async () => true),1110      createVaultFromRootKey: vi.fn(async () => ({ vaultId })),1111      deriveLocalIdentity: vi.fn(async () => ({1112        deviceId: "receiver-id",1113        deviceName: longName,1114        deviceType: "extension" as const,1115        longTermPublicKeyHex: "ab".repeat(32),1116        signingPublicKeyHex: "cd".repeat(32),1117      })),1118      upsertPairedDevice: receiverUpsert,1119      applyLicense: vi.fn(),1120      onError: vi.fn(),1121    };1122    const sourceDeps: AdoptionSourceDeps = {1123      sourceIdentity: {1124        deviceId: "source-id",1125        deviceName: longName,1126        deviceType: "extension" as const,1127        longTermPublicKeyHex: "ef".repeat(32),1128        signingPublicKeyHex: "01".repeat(32),1129      },1130      rootKeyHex: vi.fn(async () => rootKeyHex),1131      vaultId: vi.fn(async () => vaultId),1132      license: vi.fn(async () => "PPK-test"),1133      onConsent: vi.fn(async () => true),1134      upsertPairedDevice: sourceUpsert,1135    };11361137    await Promise.all([1138      requestAdoption(receiverChannel, receiverDeps),1139      (async () => {1140        const request = JSON.parse(1141          new TextDecoder().decode(await sourceChannel.receive()),1142        ) as AdoptRequest;1143        await grantAdoption(sourceChannel, request, sourceDeps);1144      })(),1145    ]);11461147    const storedReceiver = receiverUpsert.mock.calls[0]?.[0] as { deviceName: string };1148    const storedSource = sourceUpsert.mock.calls[0]?.[0] as { deviceName: string };1149    expect(storedReceiver.deviceName).toBe(longName.slice(0, 64));1150    expect(storedSource.deviceName).toBe(longName.slice(0, 64));1151  });1152});