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:
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
3
extension/src/wasm.d.ts
vendored
3
extension/src/wasm.d.ts
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user