From 648dcf386efc47e4281887de67225baddcae1b98 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 2 May 2026 16:26:01 -0400 Subject: [PATCH] feat(ext/shared): centralize error-message copy in ERROR_COPY map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the popup's regex-chain humanizeError with a total lookup over every error code returned by extension/src/service-worker/router/. A generated test discovers codes via grep so the registry can't drift. The popup keeps its small set of regex translators for Rust/serde error phrasing that doesn't go through the router's error vocabulary. Subsumes B2 — fullscreen consumer lands in the next commit. --- extension/src/popup/popup.ts | 16 +-- .../src/shared/__tests__/error-copy.test.ts | 44 ++++++++ extension/src/shared/error-copy.ts | 102 ++++++++++++++++++ 3 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 extension/src/shared/__tests__/error-copy.test.ts create mode 100644 extension/src/shared/error-copy.ts diff --git a/extension/src/popup/popup.ts b/extension/src/popup/popup.ts index fe85981..6d5f7cf 100644 --- a/extension/src/popup/popup.ts +++ b/extension/src/popup/popup.ts @@ -4,6 +4,7 @@ /// Navigation works by updating `currentState` and calling `render()`. import type { Request, Response } from '../shared/messages'; +import { lookupErrorCopy } from '../shared/error-copy'; import type { ItemId, ManifestEntry, Item } from '../shared/types'; import { registerHost } from '../shared/state'; import { renderUnlock } from './components/unlock'; @@ -144,19 +145,8 @@ export function humanizeError(err: string): string { if (/settings json:/i.test(err)) { return 'Settings are in an invalid format — try reloading the extension.'; } - if (/vault_locked/i.test(err)) { - return 'Vault is locked. Unlock and try again.'; - } - if (/origin_mismatch/i.test(err)) { - return 'This login belongs to a different site — refusing to leak credentials cross-origin.'; - } - if (/unauthorized_sender/i.test(err)) { - return 'This action is not allowed from here.'; - } - if (/tab_navigated|captured_tab_gone/i.test(err)) { - return 'The browser tab changed before the fill could complete — try again.'; - } - return err; + const copy = lookupErrorCopy(err); + return copy.body; } // --- Navigation --- diff --git a/extension/src/shared/__tests__/error-copy.test.ts b/extension/src/shared/__tests__/error-copy.test.ts new file mode 100644 index 0000000..dab56b1 --- /dev/null +++ b/extension/src/shared/__tests__/error-copy.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { execSync } from 'node:child_process'; +import { resolve } from 'node:path'; +import { ERROR_COPY, lookupErrorCopy } from '../error-copy'; + +const repoRoot = resolve(__dirname, '../../../..'); + +function discoverCodes(): Set { + const out = execSync( + `grep -rohE "ok: false, error: '[^']+'" extension/src/service-worker/ \ + --include="*.ts" --exclude-dir=__tests__`, + { cwd: repoRoot, encoding: 'utf-8' }, + ); + const codes = new Set(); + for (const line of out.split('\n')) { + const m = line.match(/error: '([^']+)'/); + if (m) codes.add(m[1]); + } + return codes; +} + +describe('ERROR_COPY', () => { + it('contains an entry for every error code returned by the service worker', () => { + const discovered = discoverCodes(); + expect(discovered.size).toBeGreaterThan(0); + const missing: string[] = []; + for (const code of discovered) { + if (!ERROR_COPY[code]) missing.push(code); + } + expect(missing).toEqual([]); + }); + + it('lookupErrorCopy returns the mapped entry for known codes', () => { + const copy = lookupErrorCopy('vault_locked'); + expect(copy.title).toBe('Vault locked'); + expect(copy.body).toMatch(/unlock/i); + }); + + it('lookupErrorCopy falls back to a generic shape for unknown codes', () => { + const copy = lookupErrorCopy('made_up_code_xyz'); + expect(copy.title).toBeTruthy(); + expect(copy.body).toContain('made_up_code_xyz'); + }); +}); diff --git a/extension/src/shared/error-copy.ts b/extension/src/shared/error-copy.ts new file mode 100644 index 0000000..71998c8 --- /dev/null +++ b/extension/src/shared/error-copy.ts @@ -0,0 +1,102 @@ +export interface ErrorCta { + label: string; + action?: 'unlock' | 'reload_extension' | 'open_setup'; +} + +export interface ErrorCopy { + title: string; + body: string; + cta?: ErrorCta; +} + +const UNLOCK_CTA: ErrorCta = { label: 'Unlock vault', action: 'unlock' }; + +export const ERROR_COPY: Record = { + vault_locked: { + title: 'Vault locked', + body: 'Unlock your vault to continue.', + cta: UNLOCK_CTA, + }, + unauthorized_sender: { + title: 'Action not allowed', + body: 'This action is not allowed from here.', + }, + unknown_message_type: { + title: 'Internal error', + body: 'The extension received an unknown request — try reloading.', + cta: { label: 'Reload extension', action: 'reload_extension' }, + }, + origin_mismatch: { + title: 'Wrong site', + body: 'This login belongs to a different site — refusing to leak credentials cross-origin.', + }, + not_a_login: { + title: 'Not a login', + body: 'That item does not have a username and password to fill.', + }, + no_totp: { + title: 'No 2FA on this item', + body: 'This item does not have a TOTP secret configured.', + }, + invalid_sender_url: { + title: 'Cannot read tab URL', + body: 'The current tab has no recognizable URL — try reloading the page.', + }, + tab_navigated: { + title: 'Tab changed', + body: 'The browser tab changed before the action could complete — try again.', + }, + captured_tab_gone: { + title: 'Tab is gone', + body: 'The browser tab closed before the action could complete — try again.', + }, + item_not_found: { + title: 'Item not found', + body: 'That item is no longer in the vault — it may have been deleted from another device.', + }, + attachment_not_found: { + title: 'Attachment missing', + body: 'The attachment is referenced in the item but is not present in the vault.', + }, + upload_failed: { + title: 'Upload failed', + body: 'Could not upload the attachment — check your connection and try again.', + }, + download_failed: { + title: 'Download failed', + body: 'Could not download the attachment — check your connection and try again.', + }, + 'invalid base32 secret': { + title: 'Invalid secret', + body: 'The TOTP secret must be valid Base32 (letters A-Z and digits 2-7 only).', + }, + 'no items to import': { + title: 'Nothing to import', + body: 'The CSV did not contain any importable items.', + }, + 'no reference image stored locally': { + title: 'No reference image', + body: 'This device has no reference image saved locally — re-attach the device or restore from backup.', + }, + 'remote already contains a Relicario vault': { + title: 'Vault already exists', + body: 'The selected repository already contains a vault — use Attach existing instead of Create new.', + }, + 'Extension not configured. Run setup first.': { + title: 'Extension not configured', + body: 'Run setup before using this action.', + cta: { label: 'Open setup', action: 'open_setup' }, + }, + 'Reference image not set. Run setup first.': { + title: 'Reference image missing', + body: 'Run setup to save your reference image.', + cta: { label: 'Open setup', action: 'open_setup' }, + }, +}; + +export function lookupErrorCopy(code: string): ErrorCopy { + return ERROR_COPY[code] ?? { + title: 'Something went wrong', + body: code, + }; +}