diff --git a/extension/src/setup/setup.ts b/extension/src/setup/setup.ts
index 20b38fe..8c0fa6c 100644
--- a/extension/src/setup/setup.ts
+++ b/extension/src/setup/setup.ts
@@ -9,6 +9,7 @@
import { createGitHost, uint8ArrayToBase64 } from '../service-worker/git-host';
import { probeVault } from './probe';
import type { VaultConfig } from '../shared/types';
+import type { SessionHandle } from 'relicario-wasm';
// --- WASM module (loaded dynamically) ---
@@ -47,7 +48,7 @@ interface WizardState {
passphraseVisible: boolean;
confirmVisible: boolean;
referenceImageBytes: Uint8Array | null;
- verifiedHandle: number | null;
+ verifiedHandle: SessionHandle | null;
creating: boolean;
attaching: boolean;
error: string | null;
@@ -323,14 +324,151 @@ function attachStep0(): void {
});
}
-// --- Step 3 (attach variant) — placeholder; filled in Task 8 ---
+// --- Step 3 (attach variant) ---
function renderStep3Attach(): string {
- return '
';
+ const p = state.passphrase;
+ const pType = state.passphraseVisible ? 'text' : 'password';
+ const pToggle = state.passphraseVisible ? 'hide' : 'show';
+ const hasImage = !!state.referenceImageBytesAttach;
+ const gateDisabled = state.attaching || !p || !hasImage;
+
+ return `
+
+
attach this device
+
+ Use your existing passphrase and reference image to attach this browser
+ to your vault. We'll verify both before registering this device.
+
+
+
+
+
+
+
+
+
+
+
+ `;
}
function attachStep3Attach(): void {
- /* filled in Task 8 */
+ const refDrop = document.getElementById('ref-drop')!;
+ const refInput = document.getElementById('ref-input') as HTMLInputElement;
+
+ refDrop.addEventListener('click', () => refInput.click());
+ refInput.addEventListener('change', () => {
+ const file = refInput.files?.[0];
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = () => {
+ state.referenceImageBytesAttach = new Uint8Array(reader.result as ArrayBuffer);
+ state.error = null;
+ render();
+ };
+ reader.readAsArrayBuffer(file);
+ });
+
+ const passInput = document.getElementById('passphrase') as HTMLInputElement | null;
+ passInput?.addEventListener('input', (e) => {
+ state.passphrase = (e.target as HTMLInputElement).value;
+ const btn = document.getElementById('attach-btn') as HTMLButtonElement | null;
+ if (btn) btn.disabled = state.attaching || !state.passphrase || !state.referenceImageBytesAttach;
+ });
+
+ document.getElementById('eye-btn')?.addEventListener('click', () => {
+ state.passphraseVisible = !state.passphraseVisible;
+ if (passInput) passInput.type = state.passphraseVisible ? 'text' : 'password';
+ const btn = document.getElementById('eye-btn');
+ if (btn) btn.textContent = state.passphraseVisible ? 'hide' : 'show';
+ passInput?.focus();
+ });
+
+ document.getElementById('back-btn')?.addEventListener('click', () => {
+ state.step = 2;
+ state.error = null;
+ render();
+ });
+
+ document.getElementById('attach-btn')?.addEventListener('click', async () => {
+ if (!state.referenceImageBytesAttach || !state.passphrase) return;
+ state.attaching = true;
+ state.error = null;
+ render();
+
+ const log = (stage: string, detail?: unknown) => console.log(`[relicario setup] ${stage}`, detail ?? '');
+ let stage = 'init';
+ let handle: SessionHandle | null = null;
+ try {
+ stage = 'load wasm';
+ log(stage);
+ const w = await loadWasm();
+
+ stage = 'fetch vault metadata';
+ log(stage);
+ const hostUrl = state.hostType === 'github' ? 'https://api.github.com' : state.hostUrl;
+ const host = createGitHost(state.hostType, hostUrl, state.repoPath, state.apiToken);
+ const [salt, paramsBytes, encryptedManifest] = await Promise.all([
+ host.readFile('.relicario/salt'),
+ host.readFile('.relicario/params.json'),
+ host.readFile('manifest.enc'),
+ ]);
+ const paramsJson = new TextDecoder().decode(paramsBytes);
+
+ stage = 'derive session handle';
+ log(stage);
+ handle = w.unlock(state.passphrase, state.referenceImageBytesAttach, salt, paramsJson);
+
+ stage = 'decrypt manifest';
+ log(stage);
+ // Throws if AEAD verification fails — wrong passphrase or wrong image.
+ w.manifest_decrypt(handle, encryptedManifest);
+
+ log('attach verified — advancing');
+ state.verifiedHandle = handle;
+ state.attaching = false;
+ state.step = 4;
+ state.error = null;
+ render();
+ } catch (err: unknown) {
+ console.error(`[relicario setup] attach FAILED during "${stage}":`, err);
+ state.attaching = false;
+ // Lock any partial handle to avoid leaking key material.
+ if (handle !== null) {
+ try { (await loadWasm()).lock(handle); } catch { /* best effort */ }
+ }
+ state.verifiedHandle = null;
+ const detail = err instanceof Error ? err.message : String(err);
+ // Stage-aware copy: if we got past 'fetch', this is a credential failure.
+ if (stage === 'derive session handle' || stage === 'decrypt manifest') {
+ state.error = 'Could not decrypt vault — wrong passphrase or reference image.';
+ } else {
+ state.error = `Attach failed at "${stage}": ${detail}`;
+ }
+ render();
+ }
+ });
}
// --- Step 1: Choose Host ---