123import { createMemo, createSignal, Show } from "solid-js";45import { sendMessage } from "../App";6import { PeachWordmark } from "../components/PeachMark";7import {8 parseImportSource,9 type ImportParseResult,10} from "../../shared/import";11import {12 decodePeachScript,13 isPeachScript,14 normalisePeachScript,15} from "../../crypto/peachScript";16import { parsePayload } from "../../crypto/peachPayload";17import type { VaultEntry } from "../../shared/types";1819type ImportSubview = "menu" | "peachscript" | "file";20type ImportMergeMode = "add" | "replace";2122interface ImportProps {23 mode?: "existing" | "onboarding";24 replacesExistingVault?: boolean;25 26 initialView?: ImportSubview;27 onBack: () => void;28 onImport: () => void | Promise<void>;29}3031export function ImportView(props: ImportProps) {32 const [view, setView] = createSignal<ImportSubview>(props.initialView ?? "menu");33 const mode = () => props.mode ?? "existing";34 const isOnboarding = () => mode() === "onboarding";35 36 37 const subviewBack = () => (props.initialView ? props.onBack() : setView("menu"));3839 return (40 <div class={isOnboarding() ? "peach-lock-view" : "peach-overlay-view ov-enter-right"} style="display:flex">41 <Show when={view() === "menu"}>42 <ImportMenu43 mode={mode()}44 replacesExistingVault={props.replacesExistingVault}45 onBack={props.onBack}46 onPick={(v) => setView(v)}47 />48 </Show>49 <Show when={view() === "peachscript"}>50 <PeachScriptImport51 mode={mode()}52 replacesExistingVault={props.replacesExistingVault}53 onBack={subviewBack}54 onImport={props.onImport}55 />56 </Show>57 <Show when={view() === "file"}>58 <FileImport59 mode={mode()}60 replacesExistingVault={props.replacesExistingVault}61 onBack={subviewBack}62 onImport={props.onImport}63 />64 </Show>65 </div>66 );67}686970717273function ImportMenu(props: {74 mode: "existing" | "onboarding";75 replacesExistingVault?: boolean;76 onBack: () => void;77 onPick: (view: ImportSubview) => void;78}) {79 const isOnboarding = props.mode === "onboarding";80 return (81 <>82 <Show83 when={isOnboarding}84 fallback={<OverlayHeader title="import" onBack={props.onBack} />}85 >86 <div class="peach-lock-view-header">87 <PeachWordmark class="peach-wordmark-wrap" />88 <button class="peach-lock-restore-link" onClick={props.onBack}>89 ‹ back90 </button>91 </div>92 </Show>93 <div class="peach-settings-body peach-export-menu">94 <Show when={isOnboarding && props.replacesExistingVault}>95 <div class="peach-settings-group">96 <div class="peach-restore-warning">97 Restoring now replaces the locked local vault on this device with98 a fresh one from your backup or export file.99 </div>100 </div>101 </Show>102103 <div class="peach-settings-group peach-export-card peach-export-card--primary">104 <p class="peach-settings-label">peachscript</p>105 <p class="peach-export-card-copy">106 Paste a PeachScript backup from another Peach device.107 {props.mode === "existing" ? " We'll merge it into your vault." : ""}108 </p>109 <button110 class="peach-btn peach-btn--subtle peach-btn--sm"111 onClick={() => props.onPick("peachscript")}112 >113 Paste PeachScript →114 </button>115 </div>116117 <div class="peach-settings-group peach-export-card">118 <p class="peach-settings-label">backup file</p>119 <p class="peach-export-card-copy">120 A Peach JSON export, or a file from Bitwarden, Proton Pass,121 1Password, or a browser CSV (Chrome, Safari, Edge).122 </p>123 <button124 class="peach-btn peach-btn--subtle peach-btn--sm"125 onClick={() => props.onPick("file")}126 >127 Choose file →128 </button>129 </div>130 </div>131 </>132 );133}134135136137138139function PeachScriptImport(props: {140 mode: "existing" | "onboarding";141 replacesExistingVault?: boolean;142 onBack: () => void;143 onImport: () => void | Promise<void>;144}) {145 const [scriptInput, setScriptInput] = createSignal("");146 const [scriptPass, setScriptPass] = createSignal("");147 const [masterPassword, setMasterPassword] = createSignal("");148 const [masterConfirm, setMasterConfirm] = createSignal("");149 const [working, setWorking] = createSignal(false);150 const [error, setError] = createSignal<string | null>(null);151 const [result, setResult] = createSignal<{ entryCount: number } | null>(null);152153 const scriptLooksValid = createMemo(() => isPeachScript(scriptInput()));154 const isOnboarding = props.mode === "onboarding";155 const masterPasswordReady = createMemo(156 () => masterPassword().length >= 8 && masterPassword() === masterConfirm()157 );158159 async function handleImport() {160 setError(null);161 setResult(null);162 if (!scriptLooksValid()) {163 setError("Paste your PeachScript first.");164 return;165 }166 if (!scriptPass().trim()) {167 setError("Enter the backup password.");168 return;169 }170 setWorking(true);171 try {172 const bytes = decodePeachScript(normalisePeachScript(scriptInput()));173 const payload = await parsePayload(bytes, scriptPass());174175 if (isOnboarding) {176 if (!masterPasswordReady()) {177 throw new Error("MASTER_PASSWORD_REQUIRED");178 }179 const createRes = await sendMessage({180 type: "CREATE_VAULT",181 payload: { password: masterPassword() },182 });183 if (!(createRes as { ok: boolean }).ok) throw new Error("CREATE_VAULT_FAILED");184 }185186 const restoreRes = await sendMessage({187 type: "RESTORE_ENTRIES",188 payload: {189 entries: payload.entries,190 mode: "merge",191 settings: payload.settings,192 },193 });194 const restoreEnvelope = restoreRes as { ok: boolean; error?: string };195 if (!restoreEnvelope.ok) {196 throw new Error(restoreEnvelope.error ?? "RESTORE_FAILED");197 }198199 if (payload.license?.key) {200 await sendMessage({201 type: "ACTIVATE_LICENSE",202 payload: { key: payload.license.key },203 }).catch(() => {});204 }205206 setResult({ entryCount: payload.entries.length });207 await props.onImport();208 } catch (err) {209 const code = err instanceof Error ? err.message : String(err);210 setError(getImportErrorCopy(code));211 } finally {212 setWorking(false);213 }214 }215216 return (217 <>218 <SubviewHeader title="peachscript" onBack={props.onBack} mode={props.mode} />219 <div class="peach-settings-body">220 <Show when={isOnboarding && props.replacesExistingVault}>221 <div class="peach-settings-group">222 <div class="peach-restore-warning">223 Restoring now replaces the locked local vault on this device with224 a fresh one from your backup.225 </div>226 </div>227 </Show>228 <div class="peach-settings-group peach-export-card peach-export-card--primary peach-export-pass-group">229 <p class="peach-settings-label">paste peachscript</p>230 <p class="peach-export-card-copy">231 {isOnboarding232 ? "Paste a PeachScript backup from another Peach device to restore your vault."233 : "We'll merge the entries into your existing vault."}234 </p>235 <textarea236 class="peach-script-input"237 placeholder="paste PeachScript here…"238 value={scriptInput()}239 onInput={(e) => setScriptInput((e.target as HTMLTextAreaElement).value)}240 />241 <div class="peach-settings-stack peach-export-password-stack">242 <input243 class="peach-settings-input"244 type="password"245 placeholder="PeachScript password"246 value={scriptPass()}247 onInput={(e) => setScriptPass((e.target as HTMLInputElement).value)}248 />249 </div>250 <Show when={isOnboarding}>251 <p class="peach-settings-label" style="margin-top:4px">new master password</p>252 <p class="peach-export-card-copy">253 This password unlocks Peach on this device after the restore.254 </p>255 <div class="peach-settings-stack peach-export-password-stack">256 <input257 class="peach-settings-input"258 type="password"259 placeholder={`master password (min 8 chars)`}260 value={masterPassword()}261 onInput={(e) => setMasterPassword((e.target as HTMLInputElement).value)}262 />263 <input264 class="peach-settings-input"265 type="password"266 placeholder="confirm master password"267 value={masterConfirm()}268 onInput={(e) => setMasterConfirm((e.target as HTMLInputElement).value)}269 />270 <Show when={masterConfirm().length > 0 && masterPassword() !== masterConfirm()}>271 <p class="peach-export-note" style="color:var(--peach-t2)">272 Passwords don't match273 </p>274 </Show>275 </div>276 </Show>277 <button278 class="peach-btn peach-btn--subtle peach-btn--sm"279 onClick={() => void handleImport()}280 disabled={281 working() ||282 !scriptLooksValid() ||283 !scriptPass().trim() ||284 (isOnboarding && !masterPasswordReady())285 }286 >287 {working() ? "decoding…" : "Restore from PeachScript"}288 </button>289 <Show when={error()}>290 <div class="peach-restore-error">{error()}</div>291 </Show>292 <Show when={result()}>293 <div class="peach-export-success">294 <span class="peach-export-success-check">✓</span>295 <span>296 Restored {result()!.entryCount} entr297 {result()!.entryCount === 1 ? "y" : "ies"}.298 </span>299 </div>300 </Show>301 </div>302 </div>303 </>304 );305}306307308309310311function FileImport(props: {312 mode: "existing" | "onboarding";313 replacesExistingVault?: boolean;314 onBack: () => void;315 onImport: () => void | Promise<void>;316}) {317 const isOnboarding = props.mode === "onboarding";318 const needsMasterPassword = () => isOnboarding;319320 const [preview, setPreview] = createSignal<ImportParseResult | null>(null);321 const [rawImport, setRawImport] = createSignal<string | null>(null);322 const [backupPassword, setBackupPassword] = createSignal("");323 const [masterPassword, setMasterPassword] = createSignal("");324 const [masterConfirm, setMasterConfirm] = createSignal("");325 const [importing, setImporting] = createSignal(false);326 const [error, setError] = createSignal<string | null>(null);327 const [mergeMode, setMergeMode] = createSignal<ImportMergeMode>("add");328 const [awaitingReplaceConfirm, setAwaitingReplaceConfirm] = createSignal(false);329 const [importProgress, setImportProgress] = createSignal<{ current: number; total: number } | null>(null);330 const [duplicateWarning, setDuplicateWarning] = createSignal<number>(0);331 const [existingCount, setExistingCount] = createSignal<number>(0);332333 const needsBackupPassword = createMemo(() => preview()?.requiresPassword ?? false);334 const masterPasswordReady = createMemo(335 () => masterPassword().length >= 8 && masterPassword() === masterConfirm()336 );337338 async function refreshExistingCount() {339 if (isOnboarding) return;340 const res = await sendMessage({ type: "GET_ENTRIES" });341 const list = (res as { ok: boolean; data?: VaultEntry[] }).data ?? [];342 setExistingCount(list.length);343 }344 void refreshExistingCount();345346 async function parseSource(text: string, password?: string) {347 try {348 const result = await parseImportSource(text, password);349 setPreview(result);350 setError(null);351 if (result.format === "unknown") {352 setError(353 "We couldn't recognise this file format. Peach supports: Peach JSON backups, Bitwarden JSON/CSV, Proton Pass JSON/CSV, 1Password JSON, and generic browser CSV exports."354 );355 }356 return result;357 } catch (err) {358 const message = err instanceof Error ? err.message : String(err);359 setPreview(null);360 setError(getImportErrorCopy(message));361 return null;362 }363 }364365 async function handleFileSelect(event: Event) {366 const file = (event.target as HTMLInputElement).files?.[0];367 if (!file) return;368369 setPreview(null);370 setRawImport(null);371 setBackupPassword("");372 setError(null);373 setAwaitingReplaceConfirm(false);374375 const text = await file.text();376 setRawImport(text);377 await parseSource(text);378 }379380 async function handleUnlockBackup() {381 const raw = rawImport();382 if (!raw || !backupPassword().trim()) return;383 await parseSource(raw, backupPassword());384 }385386 async function countDuplicates(incoming: ImportParseResult["entries"]): Promise<number> {387 if (incoming.length === 0) return 0;388 const res = await sendMessage({ type: "GET_ENTRIES" });389 const existing = ((res as { ok: boolean; data?: VaultEntry[] }).data ?? []).filter((e) => !e.deletedAt);390 const keyOf = (title?: string, url?: string, username?: string) =>391 `${(title ?? "").toLowerCase()}|${(url ?? "").toLowerCase()}|${(username ?? "").toLowerCase()}`;392 const existingKeys = new Set(existing.map((e) => keyOf(e.title, e.url, e.username)));393 let dupes = 0;394 for (const entry of incoming) {395 const u = "username" in entry ? entry.username : undefined;396 const url = "url" in entry ? entry.url : undefined;397 if (existingKeys.has(keyOf(entry.title, url, u))) dupes += 1;398 }399 return dupes;400 }401402 async function handleImport() {403 const currentPreview = preview();404 if (!currentPreview) return;405406 if (needsBackupPassword() && !backupPassword().trim()) {407 setError("Enter the backup password used for this export.");408 return;409 }410411 if (needsMasterPassword()) {412 if (masterPassword().length < 8) {413 setError("Master password must be at least 8 characters.");414 return;415 }416 if (!masterPasswordReady()) {417 setError("Master passwords do not match.");418 return;419 }420 }421422 if (!isOnboarding && mergeMode() === "replace" && !awaitingReplaceConfirm()) {423 setAwaitingReplaceConfirm(true);424 return;425 }426427 setImporting(true);428 setError(null);429 setDuplicateWarning(0);430 setImportProgress(null);431432 try {433 if (currentPreview.entries.length === 0) {434 throw new Error("NO_ENTRIES_IMPORTED");435 }436437 if (needsMasterPassword()) {438 const createResponse = await sendMessage({439 type: "CREATE_VAULT",440 payload: { password: masterPassword() },441 });442 if (!(createResponse as { ok: boolean }).ok) {443 throw new Error("CREATE_VAULT_FAILED");444 }445 }446447 if (!isOnboarding && mergeMode() === "replace") {448 const clearRes = await sendMessage({ type: "CLEAR_VAULT_ENTRIES" });449 if (!(clearRes as { ok: boolean }).ok) {450 throw new Error("CLEAR_VAULT_FAILED");451 }452 }453454 let dupes = 0;455 if (!isOnboarding && mergeMode() === "add") {456 dupes = await countDuplicates(currentPreview.entries);457 }458459 let imported = 0;460 setImportProgress({ current: 0, total: currentPreview.entries.length });461462 const batchRes = await sendMessage({463 type: "BATCH_ADD_ENTRIES",464 payload: { entries: currentPreview.entries },465 });466467 if ((batchRes as { ok: boolean }).ok) {468 imported = ((batchRes as { ok: boolean; data?: VaultEntry[] }).data ?? []).length;469 setImportProgress({ current: imported, total: currentPreview.entries.length });470 } else {471 for (const entry of currentPreview.entries) {472 const response = await sendMessage({473 type: "ADD_ENTRY",474 payload: entry,475 });476 if ((response as { ok: boolean }).ok) imported += 1;477 setImportProgress({ current: imported, total: currentPreview.entries.length });478 }479 }480481 if (imported === 0) {482 throw new Error("NO_ENTRIES_IMPORTED");483 }484485 if (currentPreview.licenseKey) {486 await sendMessage({ type: "ACTIVATE_LICENSE", payload: { key: currentPreview.licenseKey } }).catch(() => {});487 }488489 setDuplicateWarning(dupes);490 await props.onImport();491 } catch (err) {492 const message = err instanceof Error ? err.message : String(err);493 setError(getImportErrorCopy(message));494 setImporting(false);495 setImportProgress(null);496 return;497 }498499 setImporting(false);500 setImportProgress(null);501 }502503 return (504 <>505 <SubviewHeader title="backup file" onBack={props.onBack} mode={props.mode} />506 <div class="peach-settings-body">507 <Show when={isOnboarding && props.replacesExistingVault}>508 <div class="peach-settings-group">509 <div class="peach-restore-warning">510 Restoring now replaces the locked local vault on this device with511 a fresh one from your backup.512 </div>513 </div>514 </Show>515 <div class="peach-settings-group peach-export-card peach-export-card--primary">516 <p class="peach-settings-label">choose a file</p>517 <p class="peach-export-card-copy">518 Bitwarden, 1Password, Proton Pass, Chrome / Edge / Safari CSV, FIDO519 credential-exchange passkeys, or a Peach JSON backup.520 </p>521522 <div class="peach-provider-chips">523 <span class="peach-provider-chip">Peach JSON</span>524 <span class="peach-provider-chip">FIDO passkeys</span>525 <span class="peach-provider-chip">Proton Pass</span>526 <span class="peach-provider-chip">Bitwarden</span>527 <span class="peach-provider-chip">1Password</span>528 <span class="peach-provider-chip">Browser CSV</span>529 </div>530531 <input532 class="peach-restore-file"533 type="file"534 accept=".csv,.json"535 onChange={(event) => void handleFileSelect(event)}536 />537 </div>538539 <Show when={needsBackupPassword()}>540 <div class="peach-settings-group">541 <p class="peach-settings-label">unlock backup</p>542 <p class="peach-export-card-copy">543 Enter the password used when you created this encrypted Peach JSON544 backup.545 </p>546 <input547 class="peach-restore-input"548 type="password"549 placeholder="backup password"550 value={backupPassword()}551 onInput={(event) =>552 setBackupPassword((event.target as HTMLInputElement).value)553 }554 onBlur={() => { if (backupPassword().trim()) void handleUnlockBackup(); }}555 onKeyDown={(event) => {556 if (event.key === "Enter") {557 event.preventDefault();558 void handleUnlockBackup();559 }560 }}561 />562 <div class="peach-settings-actions">563 <button564 class="peach-btn peach-btn--subtle peach-btn--sm"565 onClick={() => void handleUnlockBackup()}566 disabled={!backupPassword().trim()}567 >568 Decrypt →569 </button>570 </div>571 </div>572 </Show>573574 <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>575 <div class="peach-settings-group">576 <p class="peach-settings-label">{preview()!.label}</p>577 <div class="peach-import-summary">578 Found <strong>{preview()!.entries.length}</strong>{" "}579 entr{preview()!.entries.length === 1 ? "y" : "ies"}580 {preview()!.skippedItems > 0581 ? `. ${preview()!.skippedItems} skipped because no password was present.`582 : "."}583 </div>584585 <div class="peach-import-preview">586 {preview()!.entries.slice(0, 8).map((entry) => (587 <div class="peach-import-preview-row">588 <span class="peach-import-preview-title">{entry.title}</span>589 <span class="peach-import-preview-meta">590 {entry.type === "passkey"591 ? `${entry.userName || "passkey"} • ${entry.rpId || "site"}`592 : entry.type === "note"593 ? "secure note"594 : entry.username || entry.url || "password item"}595 </span>596 </div>597 ))}598 <Show when={preview()!.entries.length > 8}>599 <div class="peach-import-preview-more">600 ...and {preview()!.entries.length - 8} more601 </div>602 </Show>603 </div>604 </div>605 </Show>606607 <Show when={!isOnboarding && preview() && !needsBackupPassword() && preview()!.format !== "unknown" && preview()!.entries.length > 0}>608 <div class="peach-settings-group">609 <p class="peach-settings-label">merge or replace</p>610 <label class="peach-import-choice">611 <input612 type="radio"613 name="merge-mode"614 value="add"615 checked={mergeMode() === "add"}616 onChange={() => { setMergeMode("add"); setAwaitingReplaceConfirm(false); }}617 />618 <span class="peach-import-choice-main">619 <span class="peach-import-choice-label">Add to existing vault</span>620 <span class="peach-import-choice-sub">621 This will add {preview()!.entries.length} entries to your existing {existingCount()}.622 </span>623 </span>624 </label>625 <label class="peach-import-choice">626 <input627 type="radio"628 name="merge-mode"629 value="replace"630 checked={mergeMode() === "replace"}631 onChange={() => setMergeMode("replace")}632 />633 <span class="peach-import-choice-main">634 <span class="peach-import-choice-label">Replace vault</span>635 <span class="peach-import-choice-sub">636 This will replace your {existingCount()} entries with {preview()!.entries.length} imported ones.637 </span>638 </span>639 </label>640 <Show when={awaitingReplaceConfirm()}>641 <div class="peach-restore-warning" style="margin-top:6px">642 Replace forever? This cannot be undone.643 </div>644 </Show>645 </div>646 </Show>647648 <Show when={needsMasterPassword() && preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>649 <div class="peach-settings-group">650 <p class="peach-settings-label">new master password</p>651 <p class="peach-export-card-copy">652 This password unlocks the Peach vault created on this device after653 import.654 </p>655 <div class="peach-auth-grid">656 <input657 class="peach-restore-input"658 type="password"659 placeholder="new master password"660 value={masterPassword()}661 onInput={(event) =>662 setMasterPassword((event.target as HTMLInputElement).value)663 }664 />665 <input666 class="peach-restore-input"667 type="password"668 placeholder="confirm master password"669 value={masterConfirm()}670 onInput={(event) =>671 setMasterConfirm((event.target as HTMLInputElement).value)672 }673 />674 </div>675 </div>676 </Show>677678 <Show when={error()}>679 <div class="peach-settings-group">680 <div class="peach-restore-error">{error()}</div>681 </div>682 </Show>683684 <Show when={importing() && importProgress()}>685 <div class="peach-settings-group">686 <div class="peach-import-summary">687 Importing... [{importProgress()!.current} / {importProgress()!.total}]688 </div>689 </div>690 </Show>691692 <Show when={duplicateWarning() > 0 && !importing()}>693 <div class="peach-settings-group">694 <div class="peach-export-card-copy">695 {duplicateWarning()} {duplicateWarning() === 1 ? "entry appears" : "entries appear"} to be duplicates of existing ones. They were still imported — you can review and delete them in your vault.696 </div>697 </div>698 </Show>699700 <Show when={preview() && !needsBackupPassword() && preview()!.format !== "unknown"}>701 <div class="peach-settings-group">702 <div class="peach-settings-actions">703 <button704 class="peach-btn peach-btn--subtle peach-btn--sm"705 onClick={() => void handleImport()}706 disabled={707 importing() ||708 preview()!.entries.length === 0 ||709 (needsMasterPassword() && !masterPasswordReady())710 }711 >712 {importing()713 ? "importing..."714 : isOnboarding715 ? `Create vault + import ${preview()!.entries.length}`716 : mergeMode() === "replace"717 ? awaitingReplaceConfirm()718 ? "Yes, replace forever"719 : `Replace vault with ${preview()!.entries.length}`720 : `Import ${preview()!.entries.length}`}721 </button>722 </div>723 </div>724 </Show>725 </div>726 </>727 );728}729730731732733734function OverlayHeader(props: { title: string; onBack: () => void }) {735 return (736 <div class="peach-overlay-header">737 <button738 class="peach-btn peach-btn--ghost peach-btn--sm peach-icon-btn"739 onClick={props.onBack}740 aria-label="Back"741 >742 <svg width="7" height="12" viewBox="0 0 7 12" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">743 <path d="M6 1L1 6L6 11" />744 </svg>745 </button>746 <span class="peach-overlay-title">{props.title}</span>747 <div class="peach-overlay-spacer"></div>748 </div>749 );750}751752function SubviewHeader(props: {753 title: string;754 onBack: () => void;755 mode: "existing" | "onboarding";756}) {757 758 759 return (760 <Show761 when={props.mode === "onboarding"}762 fallback={<OverlayHeader title={props.title} onBack={props.onBack} />}763 >764 <div class="peach-lock-view-header">765 <PeachWordmark class="peach-wordmark-wrap" />766 <button class="peach-lock-restore-link" onClick={props.onBack}>767 ‹ back768 </button>769 </div>770 </Show>771 );772}773774function getImportErrorCopy(error: string): string {775 switch (error) {776 case "UNSUPPORTED_IMPORT_FORMAT":777 return "Unrecognised file format. Use CSV, Bitwarden JSON, Proton Pass JSON/CSV, or a Peach encrypted backup.";778 case "INVALID_EXPORT_PASSWORD":779 case "INVALID_PASSPHRASE":780 return "Wrong backup password. This is the password you chose when you created the backup.";781 case "INVALID_PEACH_EXPORT":782 return "This Peach backup file looks corrupted or incomplete.";783 case "INVALID_PEACHSCRIPT_CHARACTER":784 return "This doesn't look like PeachScript. Make sure you copied the entire backup string.";785 case "PAYLOAD_TOO_SHORT":786 return "PeachScript looks truncated. Copy the full backup string and try again.";787 case "BAD_MAGIC":788 case "UNSUPPORTED_VERSION":789 return "This PeachScript was created by a different version of Peach.";790 case "CHECKSUM_FAILED":791 return "PeachScript appears corrupted. Re-copy from the source device.";792 case "CREATE_VAULT_FAILED":793 return "Peach couldn't create a new local vault before importing.";794 case "CLEAR_VAULT_FAILED":795 return "Peach couldn't clear the existing vault before replacing it.";796 case "MASTER_PASSWORD_REQUIRED":797 return "Choose a master password for your new vault first.";798 case "RESTORE_FAILED":799 return "Restore failed. Your existing vault is unchanged.";800 case "vault locked":801 return "Peach is locked. Open Peach with your master password and try again.";802 case "refuse to replace with empty entry set":803 return "This PeachScript decoded to zero entries — refusing to wipe the vault.";804 case "NO_ENTRIES_IMPORTED":805 return "No importable password entries were found in that file.";806 default:807 return error;808 }809}