c520b474a1f3

spec-align .peach: filename peach-backup-YYYY-MM-DD.peach (date-only), CSV/file picker accepts .peach + isPeachScript-content sniff, FileImport routes .peach to PeachScript subview via prefill, bump v2.11.5

time
2026-04-29 00:51 UTC · 6 months ago
machine
koh2
parent
f6e79034f2cd
5 files manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/views/Export.tsx+4 −5src/popup/views/Import.tsx+40 −4
manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.11.4",4+  "version": "2.11.5",55   "description": "Zero-knowledge password manager",66   "browser_specific_settings": {77     "gecko": {

view file

manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22   "manifest_version": 3,33   "name": "Peach",4-  "version": "2.11.4",4+  "version": "2.11.5",55   "description": "Zero-knowledge password manager",66   "action": {77     "default_popup": "src/popup/index.html",

view file

package.json+1 −1
@@ -1,6 +1,6 @@11 {22   "name": "peach",3-  "version": "2.11.4",3+  "version": "2.11.5",44   "description": "Zero-knowledge password manager",55   "type": "module",66   "scripts": {

view file

src/popup/views/Export.tsx+4 −5
@@ -160,15 +160,14 @@160160     // .peach file = the same PeachScript text, in a single file. The PUA161161     // codepoints render as runes only when paired with PeachScript-Regular.otf;162162     // outside Peach the file looks like opaque bytes, which is the point.163+    // Filename per the verification spec: peach-backup-YYYY-MM-DD.peach,164+    // hyphens only, lowercase extension, date-only (not time-of-day).163165     const blob = new Blob([s], { type: "application/x-peach;charset=utf-8" });164166     const url = URL.createObjectURL(blob);165-    const stamp = new Date()166-      .toISOString()167-      .replace(/[:T]/g, "-")168-      .replace(/\..*$/, "");167+    const date = new Date().toISOString().slice(0, 10);169168     const a = document.createElement("a");170169     a.href = url;171-    a.download = `peach-vault-${stamp}.peach`;170+    a.download = `peach-backup-${date}.peach`;172171     document.body.appendChild(a);173172     a.click();174173     document.body.removeChild(a);

view file

src/popup/views/Import.tsx+40 −4
@@ -30,6 +30,9 @@3030 3131 export function ImportView(props: ImportProps) {3232   const [view, setView] = createSignal<ImportSubview>(props.initialView ?? "menu");33+  // When FileImport detects a .peach file, it stashes the contents here and34+  // routes the view to "peachscript" so the existing decode flow handles it.35+  const [peachScriptPrefill, setPeachScriptPrefill] = createSignal<string>("");3336   const mode = () => props.mode ?? "existing";3437   const isOnboarding = () => mode() === "onboarding";3538   // When the caller deep-links into a subview, the back arrow exits the@@ -50,7 +53,11 @@5053         <PeachScriptImport5154           mode={mode()}5255           replacesExistingVault={props.replacesExistingVault}53-          onBack={subviewBack}56+          initialScript={peachScriptPrefill()}57+          onBack={() => {58+            setPeachScriptPrefill("");59+            subviewBack();60+          }}5461           onImport={props.onImport}5562         />5663       </Show>@@ -60,6 +67,10 @@6067           replacesExistingVault={props.replacesExistingVault}6168           onBack={subviewBack}6269           onImport={props.onImport}70+          onLoadPeachFile={(content) => {71+            setPeachScriptPrefill(content);72+            setView("peachscript");73+          }}6374         />6475       </Show>6576     </div>@@ -139,10 +150,13 @@139150 function PeachScriptImport(props: {140151   mode: "existing" | "onboarding";141152   replacesExistingVault?: boolean;153+  /** Optional initial PeachScript content — set when the user loaded a154+   *  .peach file from the CSV/file card and was routed here. */155+  initialScript?: string;142156   onBack: () => void;143157   onImport: () => void | Promise<void>;144158 }) {145-  const [scriptInput, setScriptInput] = createSignal("");159+  const [scriptInput, setScriptInput] = createSignal(props.initialScript ?? "");146160   const [scriptPass, setScriptPass] = createSignal("");147161   const [masterPassword, setMasterPassword] = createSignal("");148162   const [masterConfirm, setMasterConfirm] = createSignal("");@@ -337,6 +351,9 @@337351   replacesExistingVault?: boolean;338352   onBack: () => void;339353   onImport: () => void | Promise<void>;354+  /** Called when the user picks a .peach file from this picker — the parent355+   *  routes them to the PeachScript subview with the contents prefilled. */356+  onLoadPeachFile?: (content: string) => void;340357 }) {341358   const isOnboarding = props.mode === "onboarding";342359   const needsMasterPassword = () => isOnboarding;@@ -387,7 +404,8 @@387404   }388405 389406   async function handleFileSelect(event: Event) {390-    const file = (event.target as HTMLInputElement).files?.[0];407+    const input = event.target as HTMLInputElement;408+    const file = input.files?.[0];391409     if (!file) return;392410 393411     setPreview(null);@@ -397,6 +415,24 @@397415     setAwaitingReplaceConfirm(false);398416 399417     const text = await file.text();418+419+    // Route .peach files to the PeachScript subview. Detection is two-pronged:420+    // either the filename ends in .peach, or the body is entirely PUA chars421+    // (the canonical PeachScript shape). The two-pronged check protects422+    // against renamed-extension imports and against text editors that423+    // accidentally save with a different extension.424+    const looksLikePeach =425+      file.name.toLowerCase().endsWith(".peach") || isPeachScript(text);426+    if (looksLikePeach) {427+      if (props.onLoadPeachFile) {428+        props.onLoadPeachFile(text);429+        input.value = "";430+        return;431+      }432+      setError("This is a .peach file. Open the PeachScript card to import it.");433+      return;434+    }435+400436     setRawImport(text);401437     await parseSource(text);402438   }@@ -555,7 +591,7 @@555591           <input556592             class="peach-restore-file"557593             type="file"558-            accept=".csv,.json"594+            accept=".csv,.json,.peach,application/x-peach"559595             onChange={(event) => void handleFileSelect(event)}560596           />561597         </div>

view file