6fbc284fc874

fix: preserve purge tombstones across replace restore (PB-26-101, v5.0.32)

time
2026-09-01 20:38 UTC · 1 month ago
machine
koh2
agent
gpt-5
session
2026-09-01 security assessment remediation
parent
04e8b01a0563
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/vault.tombstone-ledger.test.ts+23 −0src/background/vault.ts+6 −2
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach Password Manager",4-  "version": "5.0.31",4+  "version": "5.0.32",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -2,7 +2,7 @@22   "manifest_version": 3,33   "minimum_chrome_version": "116",44   "name": "Peach Password Manager",5-  "version": "5.0.31",5+  "version": "5.0.32",66   "description": "Zero-knowledge password manager",77   "action": {88     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "5.0.31",3+  "version": "5.0.32",44   "description": "Zero-knowledge password manager",55   "license": "GPL-3.0-only",66   "type": "module",

view file

src/background/vault.tombstone-ledger.test.ts+23 −0
@@ -95,6 +95,29 @@9595     expect(vault.getEntries().some((item) => item.id === entry.id)).toBe(false);9696   });9797 98+  it("retains purged tombstones across replace restore and rejects stale remote entries", async () => {99+    const purged = await vault.addEntry({ type: "login", title: "Purged", password: "pw" });100+    const stale = { ...purged };101+    await vault.deleteEntry(purged.id);102+    const deletedAt = vault.getDeletedEntries().find((item) => item.id === purged.id)!.deletedAt!;103+    await vault.purgeEntry(purged.id);104+105+    await vault.restoreEntries([106+      {107+        id: crypto.randomUUID(),108+        type: "login",109+        title: "Replacement",110+        password: "new",111+        createdAt: deletedAt + 1,112+        modifiedAt: deletedAt + 1,113+      },114+    ], "replace");115+116+    expect(vault.getTombstoneManifest()).toEqual({ [purged.id]: deletedAt });117+    expect(await vault.applyMergedRemoteEntries([stale], {})).toMatchObject({ applied: 0 });118+    expect(vault.getEntryById(purged.id)).toBeUndefined();119+  });120+98121   it("records unknown remote tombstones and lets an explicit restore supersede one", async () => {99122     const id = crypto.randomUUID();100123     const deletedAt = Date.now();

view file

src/background/vault.ts+6 −2
@@ -1821,7 +1821,6 @@18211821 18221822   if (mode === "replace") {18231823     entries = [];1824-    tombstones = new Map();18251824     for (const entry of incoming) {18261825       const normalized = naturalizeRestoredId(sanitizeRestoredEntry(entry));18271826       if (!normalized) {@@ -1830,7 +1829,12 @@18301829       }18311830       entries.push(normalized);18321831       if (normalized.deletedAt) {1833-        tombstones.set(normalized.id, normalized.deletedAt);1832+        tombstones.set(1833+          normalized.id,1834+          Math.max(tombstones.get(normalized.id) ?? 0, normalized.deletedAt),1835+        );1836+      } else {1837+        tombstones.delete(normalized.id);18341838       }18351839       added += 1;18361840     }

view file