diff --git a/extension/src/setup/__tests__/setup-steps.test.ts b/extension/src/setup/__tests__/setup-steps.test.ts new file mode 100644 index 0000000..e8212ad --- /dev/null +++ b/extension/src/setup/__tests__/setup-steps.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { state, clearWizardState, renderVaultNew } from '../setup-steps'; + +describe('renderVaultNew — second-factor container choice', () => { + afterEach(() => clearWizardState()); + + it('default (image) mode shows the stego note', () => { + const html = renderVaultNew(); + expect(html).toContain('A 256-bit secret will be steganographically embedded'); + }); + + it('keyfile mode shows "Key File" and hides the carrier drop / stego note', () => { + state.secondFactor = 'keyfile'; + const html = renderVaultNew(); + expect(html).toContain('Key File'); + expect(html).not.toContain('A 256-bit secret will be steganographically embedded'); + }); + + it('keyfile mode shows the key-file note copy', () => { + state.secondFactor = 'keyfile'; + const html = renderVaultNew(); + expect(html).toContain('A 32-byte key file will be generated'); + }); +}); diff --git a/extension/src/setup/setup-steps.ts b/extension/src/setup/setup-steps.ts index 8382072..db8984b 100644 --- a/extension/src/setup/setup-steps.ts +++ b/extension/src/setup/setup-steps.ts @@ -45,6 +45,7 @@ export interface WizardState { connectionTested: boolean; vaultProbe: VaultProbe | null; carrierImageBytes: Uint8Array | null; + secondFactor: 'image' | 'keyfile'; referenceImageBytesAttach: Uint8Array | null; passphrase: string; passphraseConfirm: string; @@ -61,7 +62,7 @@ export interface WizardState { export const state: WizardState = { stepId: 'mode', mode: null, hostType: 'gitea', hostUrl: '', repoPath: '', apiToken: '', - connectionTested: false, vaultProbe: null, carrierImageBytes: null, referenceImageBytesAttach: null, + connectionTested: false, vaultProbe: null, carrierImageBytes: null, secondFactor: 'image', referenceImageBytesAttach: null, passphrase: '', passphraseConfirm: '', passphraseScore: -1, passphraseGuessesLog10: -1, passphraseVisible: false, confirmVisible: false, referenceImageBytes: null, creating: false, attaching: false, error: null, deviceName: '', @@ -375,7 +376,7 @@ function renderVaultAttach(): string { `; } -function renderVaultNew(): string { +export function renderVaultNew(): string { const score = state.passphraseScore; const hasScore = score >= 0; const meterClass = hasScore ? `s${score}` : ''; @@ -397,6 +398,14 @@ function renderVaultNew(): string { return `

create vault

+
+ +
+ + +
+
+ ${state.secondFactor === 'image' ? `
@@ -404,7 +413,10 @@ function renderVaultNew(): string { ${state.carrierImageBytes ? '

image loaded

' : '

click to select a JPEG photo

'}

A 256-bit secret will be steganographically embedded in this image.

-
+
` : ` +
+

A 32-byte key file will be generated for you to save — it is your second factor.

+
`}
A long phrase of unrelated words is stronger than a short complex password. Your vault needs good (score ≥ 3) to continue.
@@ -492,20 +504,28 @@ function attachVaultAttach(ctx: StepContext): () => void { } function attachVaultNew(ctx: StepContext): () => void { - const fileDrop = document.getElementById('file-drop')!; - const fileInput = document.getElementById('file-input') as HTMLInputElement; - fileDrop.addEventListener('click', () => fileInput.click()); - fileInput.addEventListener('change', () => { - const file = fileInput.files?.[0]; - if (!file) return; - const reader = new FileReader(); - reader.onload = () => { - state.carrierImageBytes = new Uint8Array(reader.result as ArrayBuffer); - state.error = null; + document.querySelectorAll('[data-factor]').forEach((btn) => { + btn.addEventListener('click', () => { + state.secondFactor = (btn as HTMLElement).dataset.factor as 'image' | 'keyfile'; ctx.rerender(); - }; - reader.readAsArrayBuffer(file); + }); }); + if (state.secondFactor === 'image') { + const fileDrop = document.getElementById('file-drop')!; + const fileInput = document.getElementById('file-input') as HTMLInputElement; + fileDrop.addEventListener('click', () => fileInput.click()); + fileInput.addEventListener('change', () => { + const file = fileInput.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + state.carrierImageBytes = new Uint8Array(reader.result as ArrayBuffer); + state.error = null; + ctx.rerender(); + }; + reader.readAsArrayBuffer(file); + }); + } // Track passphrase changes inline (no full re-render) so the input keeps focus. // zxcvbn score is computed via the SW on a 150ms debounce — see scheduleRate. const passInput = document.getElementById('passphrase') as HTMLInputElement | null; @@ -778,6 +798,7 @@ export function clearWizardState(): void { state.connectionTested = false; state.vaultProbe = null; state.carrierImageBytes = null; + state.secondFactor = 'image'; state.referenceImageBytesAttach = null; state.passphrase = ''; state.passphraseConfirm = '';