b56583e1a012
test: cover relay adoption receiver mesh manifest path (v4.6.6)
- time
- 2026-07-31 15:58 UTC · 2 months ago
- machine
- koh2
- parent
- 84225691c252
↓ peach-browser-b56583e1.tar.gzatlas at this save
files changed4 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/relay-adoption-bg.test.ts+146 −9manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "4.6.5",4+ "version": "4.6.6",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -2,7 +2,7 @@22 "manifest_version": 3,33 "minimum_chrome_version": "116",44 "name": "Peach Password Manager",5- "version": "4.6.5",5+ "version": "4.6.6",66 "description": "Zero-knowledge password manager",77 "action": {88 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "4.6.5",3+ "version": "4.6.6",44 "description": "Zero-knowledge password manager",55 "license": "GPL-3.0-only",66 "type": "module",src/background/relay-adoption-bg.test.ts+146 −9
@@ -14,8 +14,9 @@1414 getLicenseKey: vi.fn(),1515 localIdentity: vi.fn(),1616 rollbackVault: vi.fn(),17- storageSet: vi.fn(),1817 createSyncAdapter: vi.fn(),18+ __applyIncomingDeviceManifest: vi.fn(),19+ storageSet: vi.fn(),1920 sasDecisions: [] as Array<{ id: string; ok: boolean }>,2021 }));2122 @@ -38,6 +39,139 @@3839 ...actual,3940 generatePairingRendezvousToken: () => "AAECAwQFBgcICQoLDA0ODw",4041 };42+ it("applies the source's device manifest after the snapshot, then acks", async () => {43+ // Mesh propagation: a 3rd browser that completes an adoption with a44+ // snapshot-capable source must also receive the source's signed device45+ // manifest and forward it to the sync adapter before the receiver46+ // resolves. The relay stays an opaque forwarder, so the whole path47+ // lives in `relay-adoption-bg.ts` and `sync-vault-adapter.ts`.48+ const rootKeyHex = "f0".repeat(32);49+ const vaultId = "b".repeat(64);50+ const longTermPubkeyHex = "99".repeat(32);51+ const { buildSignedDeviceManifest } = await import("../shared/device-manifest");52+ const { deriveSyncSigningKeypair, pubkeyToHex } = await import(53+ "../shared/sync-identity"54+ );55+ const issuerKp = await deriveSyncSigningKeypair(56+ new Uint8Array(32).fill(0x55),57+ "phone-source-id",58+ );59+ const manifest = await buildSignedDeviceManifest(60+ "phone-source-id",61+ [62+ {63+ deviceId: "peer-b",64+ deviceName: "Pool B",65+ deviceType: "extension",66+ longTermPublicKeyHex: "11".repeat(32),67+ signingPublicKeyHex: "22".repeat(32),68+ wsPort: 49152,69+ relayUrl: "wss://relay.peachpasswords.com",70+ pairedAt: 1,71+ },72+ ],73+ issuerKp.privateKey,74+ );75+ const socket = socketWithFrames([76+ { type: "PEACH_HELLO", token: "AAECAwQFBgcICQoLDA0ODw" },77+ {78+ type: "ADOPT_GRANT",79+ root_key_hex: rootKeyHex,80+ vault_id: vaultId,81+ license: "PPK-test",82+ snapshot_capable: true,83+ source: {84+ device_id: "phone-source-id",85+ device_name: "June's phone",86+ device_type: "android",87+ long_term_public_key_hex: longTermPubkeyHex,88+ signing_public_key_hex: pubkeyToHex(issuerKp.publicKeyRaw),89+ relay_url: "wss://relay.peachpasswords.com",90+ },91+ },92+ // Inline snapshot ack loop ends with ADOPT_COMPLETE; the manifest is93+ // an optional follow-up frame on the same channel.94+ { type: "ADOPT_COMPLETE" },95+ { type: "ADOPT_DEVICE_MANIFEST", manifest },96+ ], { closeOnEmpty: true });97+ mocks.createVault.mockResolvedValue({ vaultId });98+ mocks.openSocket.mockResolvedValue(socket);99+ mocks.__applyIncomingDeviceManifest.mockClear();100+101+ const { startRelayAdoptionReceiver } = await import("./relay-adoption-bg");102+ await startRelayAdoptionReceiver("receiver-password");103+ await waitForPush("ADOPTION_SOURCE_RESULT");104+105+ expect(mocks.__applyIncomingDeviceManifest).toHaveBeenCalledTimes(1);106+ const [forwarded, pubkey] = mocks.__applyIncomingDeviceManifest.mock.calls[0];107+ expect(forwarded).toEqual(manifest);108+ expect(pubkey).toBeInstanceOf(Uint8Array);109+ expect(pubkey.length).toBe(32);110+ // The receiver must NOT have sent ADOPT_DEVICE_MANIFEST_APPLIED if the111+ // adapter path was skipped (the ack only follows a real apply).112+ const sentTypes = (socket.send as unknown as { mock: { calls: unknown[][] } })113+ .mock.calls114+ .map(([bytes]) => {115+ try {116+ return JSON.parse(new TextDecoder().decode(bytes as Uint8Array)).type;117+ } catch {118+ return undefined;119+ }120+ });121+ expect(sentTypes).toContain("ADOPT_DEVICE_MANIFEST_APPLIED");122+ // And the source's ADOPT_DEVICE_MANIFEST frame must have been consumed.123+ expect(sentTypes).not.toContain("ADOPT_DEVICE_MANIFEST");124+ });125+126+ it("does not call applyIncomingDeviceManifest when the source omits the manifest frame", async () => {127+ // The source is snapshot-capable but does not send an ADOPT_DEVICE_MANIFEST128+ // frame. The receiver should commit the snapshot, send ADOPT_SNAPSHOT_APPLIED,129+ // and quietly time out the optional 5s wait without acking a manifest130+ // or invoking the adapter path.131+ const rootKeyHex = "f0".repeat(32);132+ const vaultId = "b".repeat(64);133+ const longTermPubkeyHex = "99".repeat(32);134+ const socket = socketWithFrames([135+ { type: "PEACH_HELLO", token: "AAECAwQFBgcICQoLDA0ODw" },136+ {137+ type: "ADOPT_GRANT",138+ root_key_hex: rootKeyHex,139+ vault_id: vaultId,140+ license: "PPK-test",141+ snapshot_capable: true,142+ source: {143+ device_id: "phone-source-id",144+ device_name: "June's phone",145+ device_type: "android",146+ long_term_public_key_hex: longTermPubkeyHex,147+ signing_public_key_hex: "aa".repeat(32),148+ relay_url: "wss://relay.peachpasswords.com",149+ },150+ },151+ { type: "ADOPT_COMPLETE" },152+ // No ADOPT_DEVICE_MANIFEST follow-up.153+ ], { closeOnEmpty: true });154+ mocks.createVault.mockResolvedValue({ vaultId });155+ mocks.openSocket.mockResolvedValue(socket);156+ mocks.__applyIncomingDeviceManifest.mockClear();157+158+ const { startRelayAdoptionReceiver } = await import("./relay-adoption-bg");159+ await startRelayAdoptionReceiver("receiver-password");160+ await waitForPush("ADOPTION_SOURCE_RESULT");161+162+ expect(mocks.__applyIncomingDeviceManifest).not.toHaveBeenCalled();163+ const sentTypes = (socket.send as unknown as { mock: { calls: unknown[][] } })164+ .mock.calls165+ .map(([bytes]) => {166+ try {167+ return JSON.parse(new TextDecoder().decode(bytes as Uint8Array)).type;168+ } catch {169+ return undefined;170+ }171+ });172+ expect(sentTypes).toContain("ADOPT_SNAPSHOT_APPLIED");173+ expect(sentTypes).not.toContain("ADOPT_DEVICE_MANIFEST_APPLIED");174+ });41175 });42176 43177 @@ -163,14 +297,17 @@163297 });164298 mocks.getRootKeyHex.mockReturnValue("c".repeat(64));165299 mocks.upsert.mockResolvedValue(undefined);166- mocks.createSyncAdapter.mockReturnValue({167- buildEntryManifest: vi.fn(async () => ({})),168- getEncryptedEntries: vi.fn(async () => ({})),169- buildTombstoneManifest: vi.fn(async () => ({})),170- applyEntries: vi.fn(async () => undefined),171- applyTombstones: vi.fn(async () => undefined),172- buildOutgoingDeviceManifest: vi.fn(async () => null),173- });300+ const applyIncomingDeviceManifest = vi.fn(async () => undefined);301+ mocks.__applyIncomingDeviceManifest = applyIncomingDeviceManifest;302+ mocks.createSyncAdapter.mockReturnValue({303+ buildEntryManifest: vi.fn(async () => ({})),304+ getEncryptedEntries: vi.fn(async () => ({})),305+ buildTombstoneManifest: vi.fn(async () => ({})),306+ applyEntries: vi.fn(async () => undefined),307+ applyTombstones: vi.fn(async () => undefined),308+ buildOutgoingDeviceManifest: vi.fn(async () => null),309+ applyIncomingDeviceManifest,310+ });174311 Object.defineProperty(globalThis, "chrome", {175312 configurable: true,176313 value: {