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
7 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 −0
manifest.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",

view file

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",

view file

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": {

view file

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 () => {

view file

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 BEFORE

view file

src/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);

view file

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"

view file