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
↓ peach-browser-c520b474.tar.gzatlas at this save
files changed5 files
manifest.firefox.json+1 −1manifest.json+1 −1package.json+1 −1src/popup/views/Export.tsx+4 −5src/popup/views/Import.tsx+40 −4manifest.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": {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",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": {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);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>