b5de3f75e43a

fix(sync): sync_received writes do not advance vault version — no-op sync optimisation restored, prevents ping-pong (v3.6.112)

time
2026-05-28 20:07 UTC · 5 months ago
machine
koh2
parent
19da2c5da38d
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/vault.test.ts+37 −0src/background/vault.ts+8 −1
manifest.firefox.json+1 −1
@@ -2,7 +2,7 @@22   "_comment_audio_capture": "Firefox does not honor the Chrome 'audioCapture' optional_permission — getUserMedia in WebExtensions is gated solely by the runtime permission prompt. Microphone access for chirp pairing works from the popup under the existing '<all_urls>' host permission; no manifest entry is required or accepted here.",33   "manifest_version": 3,44   "name": "Peach Password Manager",5-  "version": "3.6.111",5+  "version": "3.6.112",66   "description": "Zero-knowledge password manager",77   "browser_specific_settings": {88     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "3.6.111",4+  "version": "3.6.112",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "3.6.111",3+  "version": "3.6.112",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/background/vault.test.ts+37 −0
@@ -932,4 +932,41 @@932932     const restored = vault.getEntries().find((e) => e.id === note.id);933933     expect(restored?.passwordChangedAt).toBeUndefined();934934   });935+936+  it("applyMergedRemoteEntries does not advance the vault version", async () => {937+    const vault = await import("./vault");938+    await vault.createVault("syncver-pass-aaaaa");939+    await vault.addEntry({ title: "Local", username: "june", password: "p", url: "https://l.test" });940+941+    const before = await vault.getVaultVersion();942+    await new Promise((r) => setTimeout(r, 2));943+    await vault.applyMergedRemoteEntries(944+      [945+        {946+          id: "remote-1",947+          type: "login",948+          title: "Remote",949+          username: "r",950+          password: "x",951+          url: "https://r.test",952+          createdAt: Date.now(),953+          modifiedAt: Date.now(),954+        },955+      ],956+      {},957+    );958+959+    expect(await vault.getVaultVersion()).toBe(before);960+  });961+962+  it("addEntry advances the vault version", async () => {963+    const vault = await import("./vault");964+    await vault.createVault("syncver-pass-bbbbb");965+966+    const before = await vault.getVaultVersion();967+    await new Promise((r) => setTimeout(r, 2));968+    await vault.addEntry({ title: "Local", username: "june", password: "p", url: "https://l.test" });969+970+    expect(await vault.getVaultVersion()).toBeGreaterThan(before);971+  });935972 });

view file

src/background/vault.ts+8 −1
@@ -1847,7 +1847,14 @@18471847   if (!duressConfigured) {18481848     await writeSlot(SLOT_B, generateNoiseBlob(ciphertext.length / 2));18491849   }1850-  await stampLocalVaultMutation();1850+  // Only advance the vault version for locally-originated writes. Entries1851+  // applied from a sync peer ("sync_received") must NOT bump the counter —1852+  // otherwise applying a remote round makes us look newer than the peer and1853+  // triggers a redundant outbound sync, defeating the no-op optimisation1854+  // and risking a ping-pong between two devices (SEV-2-C).1855+  if (reason !== "sync_received") {1856+    await stampLocalVaultMutation();1857+  }18511858 18521859   return nextSlotA;18531860 }

view file