34691b7eed91
fix: add unlimitedStorage + surface real save errors + plain-text note paste; cap vault history 10→5 — resolves intermittent STORAGE_QUOTA_EXCEEDED save failures (v3.6.136)
- time
- 2026-05-30 06:05 UTC · 5 months ago
- machine
- koh2
- parent
- c3a13ed2e2c4
↓ peach-browser-34691b7e.tar.gzatlas at this save
files changed7 files
manifest.firefox.json+2 −1manifest.json+2 −1package.json+1 −1src/background/vault-history.test.ts+2 −2src/background/vault.ts+2 −2src/content/inject.ts+11 −3src/popup/views/NoteEditor.tsx+12 −0manifest.firefox.json+2 −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.135",5+ "version": "3.6.136",66 "description": "Zero-knowledge password manager",77 "browser_specific_settings": {88 "gecko": {@@ -92,6 +92,7 @@9292 },9393 "permissions": [9494 "storage",95+ "unlimitedStorage",9596 "tabs",9697 "activeTab",9798 "scripting",manifest.json+2 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach Password Manager",4- "version": "3.6.135",4+ "version": "3.6.136",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",@@ -54,6 +54,7 @@5454 },5555 "permissions": [5656 "storage",57+ "unlimitedStorage",5758 "tabs",5859 "activeTab",5960 "scripting",package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "3.6.135",3+ "version": "3.6.136",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {src/background/vault-history.test.ts+2 −2
@@ -89,13 +89,13 @@8989 expect(snaps[0]!.savedAt).toBeGreaterThanOrEqual(snaps[1]!.savedAt);9090 });9191 92- it("history is capped at 10 — oldest dropped", async () => {92+ it("history is capped at 5 — oldest dropped", async () => {9393 const vault = await import("./vault");9494 await vault.createVault("cap-pass-abcde");9595 for (let i = 0; i < 15; i += 1) {9696 await vault.addEntry({ title: `E${i}`, username: "", password: `p${i}`, url: "" });9797 }98- expect(readHistory().snapshots).toHaveLength(10);98+ expect(readHistory().snapshots).toHaveLength(5);9999 });100100 101101 it("entryCount reflects current in-memory active count at snapshot time", async () => {src/background/vault.ts+2 −2
@@ -2310,13 +2310,13 @@23102310 23112311 // ---------------------------------------------------------------------------23122312 // Vault history — snapshot BEFORE every vault write so the saved blob is2313-// what is about to be lost, not what was just written. Capped at 10. The2313+// what is about to be lost, not what was just written. Capped at 5. The23142314 // stored blobs are AES-256-GCM ciphertext under the master password, so23152315 // history adds zero security surface.23162316 // ---------------------------------------------------------------------------23172317 23182318 const HISTORY_KEY = "peach_vault_history";2319-const HISTORY_MAX = 10;2319+const HISTORY_MAX = 5;23202320 23212321 /**23222322 * Snapshot the CURRENT on-disk vault state into history. Call this BEFOREsrc/content/inject.ts+11 −3
@@ -2012,13 +2012,21 @@20122012 options.onFillFields?.(username || email, pw);20132013 showSuccess(title);20142014 } else {2015- const errMsg = (res as { error?: string }).error === "vault locked"2015+ const err = (res as { error?: string })?.error;2016+ const errMsg = err === "vault_locked"20162017 ? "Vault locked — open Peach to save"2017- : "Could not save entry";2018+ : err === "STORAGE_QUOTA_EXCEEDED"2019+ ? "Out of storage — open Peach → Settings to free space"2020+ : err2021+ ? `Could not save: ${err}`2022+ : "Could not save entry";20182023 showFormError(errMsg);20192024 saveBtn.disabled = false; saveBtn.textContent = "Save entry";20202025 }2021- } catch { showFormError("Could not reach Peach"); saveBtn.disabled = false; saveBtn.textContent = "Save entry"; }2026+ } catch (e) {2027+ showFormError(e instanceof Error && e.message ? `Could not reach Peach: ${e.message}` : "Could not reach Peach");2028+ saveBtn.disabled = false; saveBtn.textContent = "Save entry";2029+ }20222030 });20232031 body.appendChild(saveBtn);20242032 body.appendChild(errEl);src/popup/views/NoteEditor.tsx+12 −0
@@ -128,6 +128,17 @@128128 editorRef?.focus();129129 }130130 131+ // Paste as plain text. The live editor writes innerHTML directly and only132+ // load-time content is sanitized (toHtml); pasted HTML would otherwise carry133+ // foreign inline styles (white-space:pre kills wrapping) and disallowed tags.134+ // insertText keeps the body clean and wrapping intact, and fires `input` so135+ // onContentChange persists the change.136+ function handlePaste(e: ClipboardEvent) {137+ e.preventDefault();138+ const text = e.clipboardData?.getData("text/plain") ?? "";139+ document.execCommand("insertText", false, text);140+ }141+131142 function handleKeyDown(e: KeyboardEvent) {132143 const mod = e.metaKey || e.ctrlKey;133144 // Only intercept shortcuts for non-alphanumeric keys to avoid swallowing@@ -145,6 +156,7 @@145156 class="peach-note-editor-body peach-note-editor-body--inline"146157 contenteditable="true"147158 onKeyDown={handleKeyDown}159+ onPaste={handlePaste}148160 onInput={() => props.onContentChange?.(editorRef?.innerHTML ?? "")}149161 onBlur={() => props.onBlur?.(editorRef?.innerHTML ?? "")}150162 aria-multiline="true"