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 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-27 00:53:02 -04:00
parent abfc5aed42
commit eb14946f06
2 changed files with 86 additions and 9 deletions

View File

@@ -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 1: Choose host type (Gitea / GitHub)
/// Step 2: Configure connection (URL, repo, token) + test /// Step 2: Configure connection (URL, repo, token) + test
/// Step 3: Create vault (carrier image, passphrase, generate secrets, push files) /// 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 { createGitHost, uint8ArrayToBase64 } from '../service-worker/git-host';
import type { VaultConfig } from '../shared/types'; import type { VaultConfig } from '../shared/types';
@@ -46,6 +47,7 @@ interface WizardState {
error: string | null; error: string | null;
extensionDetected: boolean; extensionDetected: boolean;
configPushed: boolean; configPushed: boolean;
deviceName: string;
} }
const state: WizardState = { const state: WizardState = {
@@ -67,6 +69,7 @@ const state: WizardState = {
error: null, error: null,
extensionDetected: false, extensionDetected: false,
configPushed: false, configPushed: false,
deviceName: '',
}; };
// --- Helpers --- // --- Helpers ---
@@ -225,6 +228,7 @@ function render(): void {
<div class="step ${state.step > 2 ? 'done' : state.step === 2 ? 'current' : ''}"></div> <div class="step ${state.step > 2 ? 'done' : state.step === 2 ? 'current' : ''}"></div>
<div class="step ${state.step > 3 ? 'done' : state.step === 3 ? 'current' : ''}"></div> <div class="step ${state.step > 3 ? 'done' : state.step === 3 ? 'current' : ''}"></div>
<div class="step ${state.step > 4 ? 'done' : state.step === 4 ? 'current' : ''}"></div> <div class="step ${state.step > 4 ? 'done' : state.step === 4 ? 'current' : ''}"></div>
<div class="step ${state.step > 5 ? 'done' : state.step === 5 ? 'current' : ''}"></div>
</div> </div>
`; `;
@@ -234,6 +238,7 @@ function render(): void {
case 2: stepHtml = renderStep2(); break; case 2: stepHtml = renderStep2(); break;
case 3: stepHtml = renderStep3(); break; case 3: stepHtml = renderStep3(); break;
case 4: stepHtml = renderStep4(); break; case 4: stepHtml = renderStep4(); break;
case 5: stepHtml = renderStep5(); break;
} }
app.innerHTML = ` app.innerHTML = `
@@ -251,6 +256,7 @@ function render(): void {
case 2: attachStep2(); break; case 2: attachStep2(); break;
case 3: attachStep3(); break; case 3: attachStep3(); break;
case 4: attachStep4(); break; case 4: attachStep4(); break;
case 5: attachStep5(); break;
} }
} }
@@ -645,11 +651,10 @@ function attachStep3(): void {
stage = 'release handle'; stage = 'release handle';
w.lock(handle); w.lock(handle);
log('vault created — advancing to step 4'); log('vault created — advancing to step 4 (device name)');
state.creating = false; state.creating = false;
state.step = 4; state.step = 4; // device name step
state.error = null; state.error = null;
detectExtension();
render(); render();
} catch (err: unknown) { } catch (err: unknown) {
// eslint-disable-next-line no-console // 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 `
<div class="wizard-step">
<h3>name this device</h3>
<p class="muted" style="margin-bottom:12px;">
This helps you identify which devices have access to your vault.
</p>
<div class="form-group">
<label class="label" for="device-name">device name</label>
<input id="device-name" type="text" value="${escapeHtml(defaultName)}" placeholder="e.g. Chrome on Linux">
</div>
<div class="form-actions">
<button class="btn" id="back-btn">back</button>
<button class="btn btn-primary" id="next-btn">continue</button>
</div>
</div>
`;
}
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 { function detectExtension(): void {
try { try {
@@ -682,7 +739,7 @@ function detectExtension(): void {
} }
} }
function renderStep4(): string { function renderStep5(): string {
const config: VaultConfig = { const config: VaultConfig = {
hostType: state.hostType, hostType: state.hostType,
hostUrl: state.hostType === 'github' ? 'https://api.github.com' : state.hostUrl, 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', () => { document.getElementById('download-ref-btn')?.addEventListener('click', () => {
if (!state.referenceImageBytes) return; if (!state.referenceImageBytes) return;
const blob = new Blob([state.referenceImageBytes.buffer as ArrayBuffer], { type: 'image/jpeg' }); const blob = new Blob([state.referenceImageBytes.buffer as ArrayBuffer], { type: 'image/jpeg' });
@@ -759,9 +816,26 @@ function attachStep4(): void {
try { try {
chrome.runtime.sendMessage( chrome.runtime.sendMessage(
{ type: 'save_setup', config, imageBase64 }, { type: 'save_setup', config, imageBase64 },
(response: { ok: boolean; error?: string }) => { async (response: { ok: boolean; error?: string }) => {
if (response?.ok) { if (response?.ok) {
state.configPushed = true; 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 { } else {
state.error = response?.error ?? 'Failed to save config to extension'; state.error = response?.error ?? 'Failed to save config to extension';
} }

View File

@@ -60,6 +60,9 @@ declare module 'relicario-wasm' {
export function totp_compute(config_json: string, now_unix_seconds: bigint): TotpCode; 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<void>; export default function init(module_or_path?: unknown): Promise<void>;
export function initSync(args: { module: WebAssembly.Module }): void; export function initSync(args: { module: WebAssembly.Module }): void;
} }