refactor(ext/setup): extract pure helpers to setup-helpers.ts
The setup wizard was 1205 lines in a single file. Extract the state-independent helpers (escapeHtml, ratePassphrase, scheduleRate, entropyText, STRENGTH_LABELS, the Strength interface) into a sibling setup-helpers.ts. updateStrengthUi stays in setup.ts since it walks the live wizard state object and would force every caller to thread that state through. setup.ts: 1205 → 1137 lines. Pure mechanical extraction; no behavior change. Existing tests are the safety net (24 vitest files, all pass). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,13 @@
|
||||
import { createGitHost, uint8ArrayToBase64 } from '../service-worker/git-host';
|
||||
import { addDevice } from '../service-worker/devices';
|
||||
import { probeVault } from './probe';
|
||||
import {
|
||||
escapeHtml,
|
||||
ratePassphrase,
|
||||
scheduleRate,
|
||||
STRENGTH_LABELS,
|
||||
entropyText,
|
||||
} from './setup-helpers';
|
||||
import type { VaultConfig } from '../shared/types';
|
||||
import type { SessionHandle } from 'relicario-wasm';
|
||||
|
||||
@@ -85,82 +92,7 @@ const state: WizardState = {
|
||||
deviceName: '',
|
||||
};
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
interface Strength { score: number; guessesLog10: number }
|
||||
|
||||
/// Call the SW to score a passphrase with zxcvbn. Returns score in [0, 4]
|
||||
/// and guesses_log10, or -1 on both if the round-trip failed.
|
||||
function ratePassphrase(passphrase: string): Promise<Strength> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: 'rate_passphrase', passphrase },
|
||||
(response: { ok: boolean; data?: { score: number; guesses_log10: number }; error?: string }) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[relicario setup] rate_passphrase lastError:', chrome.runtime.lastError);
|
||||
resolve({ score: -1, guessesLog10: -1 }); return;
|
||||
}
|
||||
if (!response?.ok) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[relicario setup] rate_passphrase rejected by SW:', response);
|
||||
resolve({ score: -1, guessesLog10: -1 }); return;
|
||||
}
|
||||
resolve({
|
||||
score: response.data?.score ?? -1,
|
||||
guessesLog10: response.data?.guesses_log10 ?? -1,
|
||||
});
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[relicario setup] rate_passphrase threw:', err);
|
||||
resolve({ score: -1, guessesLog10: -1 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 150ms debounce around the rate_passphrase call so we don't hammer the SW
|
||||
/// on every keystroke. The last invocation wins.
|
||||
let rateDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
function scheduleRate(passphrase: string, onResult: (s: Strength) => void): void {
|
||||
if (rateDebounceTimer !== null) clearTimeout(rateDebounceTimer);
|
||||
rateDebounceTimer = setTimeout(async () => {
|
||||
rateDebounceTimer = null;
|
||||
if (!passphrase) { onResult({ score: -1, guessesLog10: -1 }); return; }
|
||||
onResult(await ratePassphrase(passphrase));
|
||||
}, 150);
|
||||
}
|
||||
|
||||
const STRENGTH_LABELS: Record<number, { text: string; cls: string }> = {
|
||||
0: { text: 'very weak', cls: 's-very-weak' },
|
||||
1: { text: 'weak', cls: 's-weak' },
|
||||
2: { text: 'fair', cls: 's-fair' },
|
||||
3: { text: 'good', cls: 's-good' },
|
||||
4: { text: 'strong', cls: 's-strong' },
|
||||
};
|
||||
|
||||
/// Render the entropy readout as "~10^N guesses to crack" or a friendlier
|
||||
/// shorthand for large values. Returns empty string when no data.
|
||||
function entropyText(guessesLog10: number): string {
|
||||
if (guessesLog10 < 0) return '';
|
||||
const rounded = Math.round(guessesLog10);
|
||||
if (rounded < 6) return `~10^${rounded} guesses — trivially crackable`;
|
||||
if (rounded < 9) return `~10^${rounded} guesses — minutes on a single GPU`;
|
||||
if (rounded < 12) return `~10^${rounded} guesses — hours to days on a GPU`;
|
||||
if (rounded < 15) return `~10^${rounded} guesses — years on consumer hardware`;
|
||||
if (rounded < 20) return `~10^${rounded} guesses — beyond consumer-hardware reach`;
|
||||
return `~10^${rounded} guesses — effectively uncrackable`;
|
||||
}
|
||||
// --- State-coupled helpers (pure helpers live in ./setup-helpers.ts) ---
|
||||
|
||||
/// Update just the meter DOM without a full re-render (so the input keeps
|
||||
/// focus and the user's cursor position is preserved). Also updates the
|
||||
|
||||
Reference in New Issue
Block a user