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
↓ peach-browser-6fbc284f.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/vault.tombstone-ledger.test.ts+23 −0src/background/vault.ts+6 −2manifest.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": {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",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",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();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 }