feat(ext/shared): centralize error-message copy in ERROR_COPY map
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.
This commit is contained in:
@@ -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 ---
|
||||
|
||||
44
extension/src/shared/__tests__/error-copy.test.ts
Normal file
44
extension/src/shared/__tests__/error-copy.test.ts
Normal file
@@ -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<string> {
|
||||
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<string>();
|
||||
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');
|
||||
});
|
||||
});
|
||||
102
extension/src/shared/error-copy.ts
Normal file
102
extension/src/shared/error-copy.ts
Normal file
@@ -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<string, ErrorCopy> = {
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user