Import.tsx

29.6 KB · raw

1// Signal-conditioned views in this file must use <Show>, never a2// signal-conditioned early return — see Projects.md §4.1.3import { 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  /** Skip the menu and open straight into a specific subview. */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  // When the caller deep-links into a subview, the back arrow exits the36  // ImportView entirely. Otherwise it returns to the import menu.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}6869// ---------------------------------------------------------------------------70// Menu71// ---------------------------------------------------------------------------7273function 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}134135// ---------------------------------------------------------------------------136// PeachScript paste flow137// ---------------------------------------------------------------------------138139function 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}306307// ---------------------------------------------------------------------------308// File flow (Bitwarden / 1Password / Peach JSON / etc.)309// ---------------------------------------------------------------------------310311function 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}729730// ---------------------------------------------------------------------------731// Shared headers732// ---------------------------------------------------------------------------733734function 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  // Onboarding sub-flows reuse the lock-screen header; existing-vault flows758  // use the standard overlay back-arrow header.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}