From eb14946f065a377b6048d65b825a707526d7f8b4 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Mon, 27 Apr 2026 00:53:02 -0400 Subject: [PATCH] feat(ext/setup): add device name step to setup wizard New step 4 after vault creation: enter device name (defaults to "Chrome on Linux" based on detected browser/OS). Generates ed25519 keypair, stores private key in chrome.storage.local, registers device with vault. Wizard is now 5 steps (was 4). Also adds generate_device_keypair() to wasm.d.ts type declarations. Co-Authored-By: Claude --- extension/src/setup/setup.ts | 92 ++++++++++++++++++++++++++++++++---- extension/src/wasm.d.ts | 3 ++ 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/extension/src/setup/setup.ts b/extension/src/setup/setup.ts index 218c554..df67d90 100644 --- a/extension/src/setup/setup.ts +++ b/extension/src/setup/setup.ts @@ -1,9 +1,10 @@ -/// Vault initialization wizard — 4-step flow for creating new relicario vaults. +/// Vault initialization wizard — 5-step flow for creating new relicario vaults. /// /// Step 1: Choose host type (Gitea / GitHub) /// Step 2: Configure connection (URL, repo, token) + test /// Step 3: Create vault (carrier image, passphrase, generate secrets, push files) -/// Step 4: Finish (download reference image, push config to extension or copy JSON) +/// Step 4: Name this device (generates ed25519 keypair, registers with vault) +/// Step 5: Finish (download reference image, push config to extension or copy JSON) import { createGitHost, uint8ArrayToBase64 } from '../service-worker/git-host'; import type { VaultConfig } from '../shared/types'; @@ -46,6 +47,7 @@ interface WizardState { error: string | null; extensionDetected: boolean; configPushed: boolean; + deviceName: string; } const state: WizardState = { @@ -67,6 +69,7 @@ const state: WizardState = { error: null, extensionDetected: false, configPushed: false, + deviceName: '', }; // --- Helpers --- @@ -225,6 +228,7 @@ function render(): void {
+
`; @@ -234,6 +238,7 @@ function render(): void { case 2: stepHtml = renderStep2(); break; case 3: stepHtml = renderStep3(); break; case 4: stepHtml = renderStep4(); break; + case 5: stepHtml = renderStep5(); break; } app.innerHTML = ` @@ -251,6 +256,7 @@ function render(): void { case 2: attachStep2(); break; case 3: attachStep3(); break; case 4: attachStep4(); break; + case 5: attachStep5(); break; } } @@ -645,11 +651,10 @@ function attachStep3(): void { stage = 'release handle'; w.lock(handle); - log('vault created — advancing to step 4'); + log('vault created — advancing to step 4 (device name)'); state.creating = false; - state.step = 4; + state.step = 4; // device name step state.error = null; - detectExtension(); render(); } catch (err: unknown) { // eslint-disable-next-line no-console @@ -662,7 +667,59 @@ function attachStep3(): void { }); } -// --- Step 4: Finish --- +// --- Step 4: Device Name --- + +function renderStep4(): string { + const platform = navigator.platform.toLowerCase(); + const isChrome = /chrome/i.test(navigator.userAgent) && !/edg/i.test(navigator.userAgent); + const isFirefox = /firefox/i.test(navigator.userAgent); + const browser = isFirefox ? 'Firefox' : isChrome ? 'Chrome' : 'Browser'; + const os = platform.includes('mac') ? 'macOS' : platform.includes('win') ? 'Windows' : 'Linux'; + const defaultName = state.deviceName || `${browser} on ${os}`; + + return ` +
+

name this device

+

+ This helps you identify which devices have access to your vault. +

+
+ + +
+
+ + +
+
+ `; +} + +function attachStep4(): void { + document.getElementById('back-btn')?.addEventListener('click', () => { + state.step = 3; + state.error = null; + render(); + }); + + document.getElementById('next-btn')?.addEventListener('click', async () => { + const nameInput = document.getElementById('device-name') as HTMLInputElement; + const name = nameInput.value.trim(); + if (!name) { + state.error = 'Device name is required'; + render(); + return; + } + + state.deviceName = name; + state.step = 5; + state.error = null; + detectExtension(); + render(); + }); +} + +// --- Step 5: Finish --- function detectExtension(): void { try { @@ -682,7 +739,7 @@ function detectExtension(): void { } } -function renderStep4(): string { +function renderStep5(): string { const config: VaultConfig = { hostType: state.hostType, hostUrl: state.hostType === 'github' ? 'https://api.github.com' : state.hostUrl, @@ -730,7 +787,7 @@ function renderStep4(): string { `; } -function attachStep4(): void { +function attachStep5(): void { document.getElementById('download-ref-btn')?.addEventListener('click', () => { if (!state.referenceImageBytes) return; const blob = new Blob([state.referenceImageBytes.buffer as ArrayBuffer], { type: 'image/jpeg' }); @@ -759,9 +816,26 @@ function attachStep4(): void { try { chrome.runtime.sendMessage( { type: 'save_setup', config, imageBase64 }, - (response: { ok: boolean; error?: string }) => { + async (response: { ok: boolean; error?: string }) => { if (response?.ok) { state.configPushed = true; + + // Generate device keypair and register + const w = await loadWasm(); + const keypair = JSON.parse(w.generate_device_keypair()) as { public_key_hex: string; private_key_base64: string }; + + // Store private key locally + await chrome.storage.local.set({ + device_name: state.deviceName, + device_private_key: keypair.private_key_base64, + }); + + // Register device with vault + chrome.runtime.sendMessage({ + type: 'add_device', + name: state.deviceName, + public_key: keypair.public_key_hex, + }); } else { state.error = response?.error ?? 'Failed to save config to extension'; } diff --git a/extension/src/wasm.d.ts b/extension/src/wasm.d.ts index ccd1681..513298a 100644 --- a/extension/src/wasm.d.ts +++ b/extension/src/wasm.d.ts @@ -60,6 +60,9 @@ declare module 'relicario-wasm' { export function totp_compute(config_json: string, now_unix_seconds: bigint): TotpCode; + export function generate_device_keypair(): string; + export function get_field_history(item_json: string): unknown; + export default function init(module_or_path?: unknown): Promise; export function initSync(args: { module: WebAssembly.Module }): void; }