19da2c5da38d
fix(data): normalizeEntry preserves passwordChangedAt — aged-password health uses correct date across lock/unlock (v3.6.111)
- time
- 2026-05-28 20:06 UTC · 5 months ago
- machine
- koh2
- parent
- 66b318659107
↓ peach-browser-19da2c5d.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/background/vault.test.ts+32 −0src/background/vault.ts+5 −0manifest.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.110",5+ "version": "3.6.111",66 "description": "Zero-knowledge password manager",77 "browser_specific_settings": {88 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "3.6.110",4+ "version": "3.6.111",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "3.6.110",3+ "version": "3.6.111",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/vault.test.ts+32 −0
@@ -900,4 +900,36 @@900900 "https://staging.example.com",901901 ]);902902 });903+904+ it("normalizeEntry preserves passwordChangedAt through lock/unlock", async () => {905+ const vault = await import("./vault");906+ await vault.createVault("pwchanged-preserve-pass");907+ const entry = await vault.addEntry({908+ title: "Aged",909+ username: "june",910+ password: "secret",911+ url: "https://aged.test",912+ });913+ // addEntry stamps passwordChangedAt at creation; capture it and prove it914+ // survives the encode/decode round-trip unchanged (was dropped pre-fix).915+ const ts = entry.passwordChangedAt;916+ expect(typeof ts).toBe("number");917+ vault.lock();918+919+ expect(await vault.unlock("pwchanged-preserve-pass")).toBe(true);920+ const restored = vault.getEntries().find((e) => e.id === entry.id);921+ expect(restored?.passwordChangedAt).toBe(ts);922+ });923+924+ it("normalizeEntry handles entries without passwordChangedAt", async () => {925+ const vault = await import("./vault");926+ await vault.createVault("pwchanged-absent-pass");927+ // A note carries no password and therefore no passwordChangedAt stamp.928+ const note = await vault.addEntry({ type: "note", title: "Memo", notes: "x" });929+ vault.lock();930+931+ expect(await vault.unlock("pwchanged-absent-pass")).toBe(true);932+ const restored = vault.getEntries().find((e) => e.id === note.id);933+ expect(restored?.passwordChangedAt).toBeUndefined();934+ });903935 });src/background/vault.ts+5 −0
@@ -2071,6 +2071,11 @@20712071 totp: entry.totp,20722072 notes: entry.notes,20732073 breach: entry.breach,2074+ // Preserve the password-age clock across lock/unlock. Without this the2075+ // field is dropped on every decode and the advanced-health "aged password"2076+ // signal silently resets to createdAt on each unlock (SEV-2-A).2077+ passwordChangedAt:2078+ "passwordChangedAt" in entry ? entry.passwordChangedAt : undefined,20742079 canary: "canary" in entry ? entry.canary : undefined,20752080 passkey: "passkey" in entry ? entry.passkey : undefined,20762081 createdAt,