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 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 {
<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 > 4 ? 'done' : state.step === 4 ? 'current' : ''}"></div>
<div class="step ${state.step > 5 ? 'done' : state.step === 5 ? 'current' : ''}"></div>
</div>
`;
@@ -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 `
<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 {
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';
}

View File

@@ -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<void>;
export function initSync(args: { module: WebAssembly.Module }): void;
}