Files
relicario/docs/superpowers/plans/2026-04-12-relicario-init-wizard.md
adlee-was-taken 39ae2ecbf3 style: capitalize "Relicario" in prose / UI / CLI help
Brand name uses capital R in user-facing text — extension UI strings,
CLI clap help / descriptions / error prose, markdown docs. Lowercase
preserved for the binary command, crate names, npm package, file
paths, env vars, and code identifiers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 17:29:10 -04:00

956 lines
30 KiB
Markdown

# Relicario Vault Initialization Wizard Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a browser-based wizard that creates a new Relicario vault, pushes it to Gitea/GitHub via API, downloads the reference image, and optionally configures the Chrome extension.
**Architecture:** Single HTML page (`extension/setup.html`) bundled by webpack as a new entry point. Reuses the existing git API layer and WASM module. New `embed_image_secret` function added to the WASM crate. The wizard runs entirely client-side — all crypto happens in the browser via WASM.
**Tech Stack:** TypeScript, wasm-bindgen (existing WASM crate), webpack, Chrome extension APIs
**Spec:** `docs/superpowers/specs/2026-04-12-relicario-init-wizard-design.md`
---
## File Structure
### Rust (modified)
```
crates/relicario-wasm/src/lib.rs # Add embed_image_secret function
```
### Extension (new)
```
extension/
├── setup.html # Standalone wizard page
└── src/
└── setup/
└── setup.ts # 4-step wizard logic
```
### Extension (modified)
```
extension/webpack.config.js # Add 'setup' entry point + copy setup.html
extension/manifest.json # Add web_accessible_resources for setup.html
```
---
## Task 1: Add `embed_image_secret` to WASM Crate
**Files:**
- Modify: `crates/relicario-wasm/src/lib.rs`
- [ ] **Step 1: Write the test**
Add to the `#[cfg(test)] mod tests` block in `crates/relicario-wasm/src/lib.rs`:
```rust
#[test]
fn embed_then_extract_round_trip() {
// Create a synthetic test JPEG (same approach as relicario-core tests)
use image::codecs::jpeg::JpegEncoder;
use image::{ImageBuffer, ImageEncoder, Rgb};
let img = ImageBuffer::from_fn(400, 300, |x, y| {
Rgb([
((x * 7 + y * 13) % 256) as u8,
((x * 11 + y * 3) % 256) as u8,
((x * 5 + y * 17) % 256) as u8,
])
});
let mut jpeg_buf = Vec::new();
let encoder = JpegEncoder::new_with_quality(&mut jpeg_buf, 92);
encoder
.write_image(img.as_raw(), 400, 300, image::ExtendedColorType::Rgb8)
.unwrap();
let secret = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1Cu8];
let stego = embed_image_secret(&jpeg_buf, &secret).unwrap();
let extracted = extract_image_secret(&stego).unwrap();
assert_eq!(extracted, secret);
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `cargo test -p relicario-wasm embed_then_extract`
Expected: FAIL — `embed_image_secret` not defined.
- [ ] **Step 3: Add `image` dev-dependency to Cargo.toml**
Add to `crates/relicario-wasm/Cargo.toml` under `[dev-dependencies]`:
```toml
[dev-dependencies]
wasm-bindgen-test = "0.3"
image = { version = "0.25", default-features = false, features = ["jpeg"] }
```
- [ ] **Step 4: Implement the function**
Add to `crates/relicario-wasm/src/lib.rs`, after the `extract_image_secret` function:
```rust
/// Embed a 256-bit secret into a carrier JPEG image.
///
/// Takes the raw bytes of a JPEG image and a 32-byte secret, returns the
/// modified JPEG with the secret embedded via DCT steganography.
///
/// The returned JPEG should be saved as the "reference image" — the user
/// needs it alongside their passphrase to unlock the vault.
#[wasm_bindgen]
pub fn embed_image_secret(carrier_jpeg: &[u8], secret: &[u8]) -> Result<Vec<u8>, JsValue> {
let secret: [u8; 32] = secret
.try_into()
.map_err(|_| JsValue::from_str("secret must be exactly 32 bytes"))?;
relicario_core::imgsecret::embed(carrier_jpeg, &secret)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
```
- [ ] **Step 5: Run test to verify it passes**
Run: `cargo test -p relicario-wasm embed_then_extract`
Expected: PASS
- [ ] **Step 6: Rebuild WASM**
Run: `wasm-pack build crates/relicario-wasm --target web --out-dir ../../extension/wasm`
Expected: Builds successfully.
- [ ] **Step 7: Commit**
```bash
git add crates/relicario-wasm/src/lib.rs crates/relicario-wasm/Cargo.toml
git commit -m "feat: add embed_image_secret to WASM crate"
```
---
## Task 2: Add WASM Type Declaration for New Function
**Files:**
- Modify: `extension/src/wasm.d.ts`
- [ ] **Step 1: Add the type declaration**
Add to `extension/src/wasm.d.ts` alongside the existing declarations:
```typescript
export function embed_image_secret(carrier_jpeg: Uint8Array, secret: Uint8Array): Uint8Array;
```
- [ ] **Step 2: Commit**
```bash
git add extension/src/wasm.d.ts
git commit -m "feat: add embed_image_secret type declaration"
```
---
## Task 3: Create Setup Page HTML
**Files:**
- Create: `extension/setup.html`
- [ ] **Step 1: Create the HTML file**
Create `extension/setup.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>relicario — vault setup</title>
<link rel="stylesheet" href="styles.css">
<style>
/* Override popup constraints for full-page layout */
body {
width: auto;
min-height: 100vh;
max-height: none;
overflow-y: auto;
display: flex;
justify-content: center;
padding: 40px 20px;
}
#app {
max-width: 560px;
width: 100%;
}
.step-instructions {
background: #161b22;
border: 1px solid #30363d;
border-radius: 4px;
padding: 16px;
margin: 12px 0;
font-size: 12px;
line-height: 1.8;
}
.step-instructions ol {
padding-left: 20px;
}
.step-instructions li {
margin-bottom: 4px;
}
.step-instructions code {
background: #0d1117;
padding: 1px 4px;
border-radius: 2px;
color: #58a6ff;
}
.image-preview {
max-width: 200px;
max-height: 150px;
border: 1px solid #30363d;
border-radius: 2px;
margin-top: 8px;
}
.strength-bar {
height: 3px;
background: #21262d;
margin-top: 4px;
border-radius: 2px;
}
.strength-bar-fill {
height: 3px;
border-radius: 2px;
transition: width 0.3s, background 0.3s;
}
.success-box {
background: #0d1117;
border: 1px solid #3fb950;
border-radius: 4px;
padding: 16px;
margin: 12px 0;
}
.config-blob {
background: #161b22;
border: 1px solid #30363d;
border-radius: 4px;
padding: 12px;
font-size: 11px;
word-break: break-all;
margin-top: 8px;
cursor: pointer;
}
.config-blob:hover {
border-color: #58a6ff;
}
.test-result {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 6px;
font-size: 11px;
}
.test-ok { color: #3fb950; }
.test-fail { color: #f85149; }
</style>
</head>
<body>
<div id="app"></div>
<script src="setup.js"></script>
</body>
</html>
```
- [ ] **Step 2: Commit**
```bash
git add extension/setup.html
git commit -m "feat: add setup wizard HTML page"
```
---
## Task 4: Create Setup Wizard TypeScript
**Files:**
- Create: `extension/src/setup/setup.ts`
This is the main task. The wizard is a 4-step state machine that reuses the existing git API layer and WASM module.
- [ ] **Step 1: Create the setup wizard**
Create `extension/src/setup/setup.ts`:
```typescript
/// Standalone vault initialization wizard.
///
/// 4-step flow:
/// 1. Choose git host (Gitea/GitHub) with setup instructions
/// 2. Configure connection (URL, repo, token) with test button
/// 3. Create vault (carrier image, passphrase, generate + push)
/// 4. Finish (download reference image, push config to extension)
import { createGitHost, uint8ArrayToBase64, base64ToUint8Array } from '../service-worker/git-host';
import type { GitHost } from '../service-worker/git-host';
import type { VaultConfig } from '../shared/types';
// --- State ---
interface WizardState {
step: number;
hostType: 'gitea' | 'github';
hostUrl: string;
repoPath: string;
apiToken: string;
connectionTested: boolean;
carrierImageBytes: Uint8Array | null;
carrierImageName: string;
passphrase: string;
passphraseConfirm: string;
referenceImageBytes: Uint8Array | null;
creating: boolean;
error: string;
extensionDetected: boolean;
configPushed: boolean;
}
let state: WizardState = {
step: 1,
hostType: 'gitea',
hostUrl: '',
repoPath: '',
apiToken: '',
connectionTested: false,
carrierImageBytes: null,
carrierImageName: '',
passphrase: '',
passphraseConfirm: '',
referenceImageBytes: null,
creating: false,
error: '',
extensionDetected: false,
configPushed: false,
};
// --- WASM ---
type WasmModule = typeof import('relicario-wasm');
let wasm: WasmModule | null = null;
async function initWasm(): Promise<WasmModule> {
if (wasm) return wasm;
const mod = await import(/* webpackIgnore: true */ '../relicario_wasm.js');
await mod.default();
wasm = mod;
return mod;
}
// --- Helpers ---
function escapeHtml(s: string): string {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function passwordStrength(pw: string): { label: string; color: string; pct: number } {
if (pw.length < 8) return { label: 'too short', color: '#f85149', pct: 10 };
let score = 0;
if (pw.length >= 12) score++;
if (pw.length >= 16) score++;
if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score++;
if (/[0-9]/.test(pw)) score++;
if (/[^a-zA-Z0-9]/.test(pw)) score++;
if (score <= 1) return { label: 'weak', color: '#f85149', pct: 30 };
if (score <= 3) return { label: 'ok', color: '#d29922', pct: 60 };
return { label: 'strong', color: '#3fb950', pct: 100 };
}
// --- Render ---
function render(): void {
const app = document.getElementById('app')!;
const stepNames = ['git host', 'connection', 'create vault', 'done'];
let html = `
<div class="brand" style="font-size:18px;margin-bottom:4px">relicario setup</div>
<div class="wizard-step">step ${state.step} of 4 — ${stepNames[state.step - 1]}</div>
<div class="progress-bar"><div class="progress-bar-fill" style="width:${(state.step / 4) * 100}%"></div></div>
`;
if (state.error) {
html += `<div class="error" style="margin-bottom:12px">${escapeHtml(state.error)}</div>`;
}
switch (state.step) {
case 1: html += renderStep1(); break;
case 2: html += renderStep2(); break;
case 3: html += renderStep3(); break;
case 4: html += renderStep4(); break;
}
app.innerHTML = html;
attachListeners();
}
// --- Step 1: Choose Git Host ---
function renderStep1(): string {
return `
<div class="form-group" style="margin-top:16px">
<div class="label">HOST TYPE</div>
<div class="host-toggle" style="display:flex;gap:8px;margin-top:4px">
<button class="group-tab ${state.hostType === 'gitea' ? 'active' : ''}" data-action="host" data-host="gitea">gitea</button>
<button class="group-tab ${state.hostType === 'github' ? 'active' : ''}" data-action="host" data-host="github">github</button>
</div>
</div>
<div class="step-instructions">
${state.hostType === 'gitea' ? `
<div class="label" style="margin-bottom:8px">GITEA SETUP</div>
<ol>
<li>Log in to your Gitea instance</li>
<li>Click <code>+</code> → <code>New Repository</code></li>
<li>Name it (e.g. <code>relicario-vault</code>), leave it <strong>empty</strong> — no README, no .gitignore</li>
<li>Go to <code>Settings</code> → <code>Applications</code> → <code>Manage Access Tokens</code></li>
<li>Generate a new token with <code>repo</code> scope (read/write)</li>
<li>Copy the token — you'll need it in the next step</li>
</ol>
` : `
<div class="label" style="margin-bottom:8px">GITHUB SETUP</div>
<ol>
<li>Go to <strong>github.com</strong> → <code>New Repository</code></li>
<li>Name it (e.g. <code>relicario-vault</code>), set to <strong>Private</strong>, leave it <strong>empty</strong> — no README, no .gitignore, no license</li>
<li>Go to <code>Settings</code> → <code>Developer Settings</code> → <code>Personal Access Tokens</code> → <code>Fine-grained tokens</code></li>
<li>Click <code>Generate new token</code></li>
<li>Select <strong>only</strong> the vault repository under "Repository access"</li>
<li>Under Permissions → Repository → <code>Contents</code>: set to <strong>Read and write</strong></li>
<li>Generate and copy the token</li>
</ol>
`}
</div>
<div class="actions">
<button class="btn btn-primary" data-action="next">Next →</button>
</div>
`;
}
// --- Step 2: Configure Connection ---
function renderStep2(): string {
const defaultUrl = state.hostType === 'github' ? 'https://github.com' : '';
return `
<div class="form-group" style="margin-top:16px">
<div class="label">HOST URL</div>
<input type="text" id="host-url" value="${escapeHtml(state.hostUrl || defaultUrl)}" placeholder="${state.hostType === 'gitea' ? 'https://git.example.com' : 'https://github.com'}">
</div>
<div class="form-group">
<div class="label">REPO PATH</div>
<input type="text" id="repo-path" value="${escapeHtml(state.repoPath)}" placeholder="owner/repo-name">
</div>
<div class="form-group">
<div class="label">API TOKEN</div>
<input type="password" id="api-token" value="${escapeHtml(state.apiToken)}" placeholder="Paste your token">
</div>
<div id="test-result"></div>
<div class="actions">
<button class="btn" data-action="back">← Back</button>
<button class="btn" data-action="test-connection">Test Connection</button>
<button class="btn btn-primary" data-action="next" ${!state.connectionTested ? 'disabled' : ''}>Next →</button>
</div>
`;
}
// --- Step 3: Create Vault ---
function renderStep3(): string {
const strength = state.passphrase ? passwordStrength(state.passphrase) : null;
const mismatch = state.passphraseConfirm && state.passphrase !== state.passphraseConfirm;
return `
<div class="form-group" style="margin-top:16px">
<div class="label">CARRIER IMAGE</div>
<p class="secondary" style="font-size:11px;margin-bottom:8px">
Pick any JPEG photo. A phone photo works great — at least 400x300 pixels.
</p>
<input type="file" id="carrier-image" accept="image/jpeg" style="font-size:11px">
${state.carrierImageName ? `<div class="secondary" style="margin-top:4px;font-size:11px">✓ ${escapeHtml(state.carrierImageName)}</div>` : ''}
</div>
<div class="form-group">
<div class="label">PASSPHRASE</div>
<input type="password" id="passphrase" value="${escapeHtml(state.passphrase)}" placeholder="Min 8 characters">
${strength ? `
<div class="strength-bar"><div class="strength-bar-fill" style="width:${strength.pct}%;background:${strength.color}"></div></div>
<div style="font-size:10px;color:${strength.color};margin-top:2px">${strength.label}</div>
` : ''}
</div>
<div class="form-group">
<div class="label">CONFIRM PASSPHRASE</div>
<input type="password" id="passphrase-confirm" value="${escapeHtml(state.passphraseConfirm)}" placeholder="Type it again">
${mismatch ? '<div class="error" style="margin-top:2px">Passphrases do not match</div>' : ''}
</div>
<div class="actions">
<button class="btn" data-action="back">← Back</button>
<button class="btn btn-primary" data-action="create-vault" ${state.creating ? 'disabled' : ''}>
${state.creating ? '<span class="spinner"></span> Creating...' : 'Create Vault'}
</button>
</div>
`;
}
// --- Step 4: Finish ---
function renderStep4(): string {
return `
<div class="success-box" style="margin-top:16px">
<div style="color:#3fb950;font-size:14px;margin-bottom:8px">✓ Vault created</div>
<p class="secondary" style="font-size:12px">
Your vault has been pushed to <strong>${escapeHtml(state.repoPath)}</strong>.
</p>
</div>
<div class="form-group">
<div class="label">REFERENCE IMAGE</div>
<p class="secondary" style="font-size:11px;margin-bottom:8px">
Download this image and keep it safe. You need it alongside your passphrase to unlock the vault.
Store it somewhere you won't lose it — a USB drive, a safe, your phone's photo library.
</p>
<button class="btn btn-primary" data-action="download-image">Download reference.jpg</button>
</div>
${state.extensionDetected ? `
<div class="form-group" style="margin-top:16px">
<div class="label">EXTENSION</div>
${state.configPushed ? `
<p class="secondary" style="font-size:11px;color:#3fb950">
✓ Extension configured. Open the extension popup and enter your passphrase to unlock.
</p>
` : `
<p class="secondary" style="font-size:11px;margin-bottom:8px">
relicario extension detected. Push your vault config to it?
</p>
<button class="btn" data-action="push-to-extension">Configure Extension</button>
`}
</div>
` : `
<div class="form-group" style="margin-top:16px">
<div class="label">EXTENSION SETUP</div>
<p class="secondary" style="font-size:11px;margin-bottom:8px">
Install the relicario extension, then enter these details in the setup wizard:
</p>
<div class="config-blob" data-action="copy-config" title="Click to copy">
${escapeHtml(JSON.stringify({
hostType: state.hostType,
hostUrl: state.hostUrl,
repoPath: state.repoPath,
apiToken: state.apiToken,
}, null, 2))}
</div>
<div class="secondary" style="font-size:10px;margin-top:4px">Click to copy</div>
</div>
`}
`;
}
// --- Event Listeners ---
function attachListeners(): void {
// Host type toggle
document.querySelectorAll('[data-action="host"]').forEach(btn => {
btn.addEventListener('click', () => {
state.hostType = (btn as HTMLElement).dataset.host as 'gitea' | 'github';
state.connectionTested = false;
render();
});
});
// Navigation
document.querySelectorAll('[data-action="next"]').forEach(btn => {
btn.addEventListener('click', () => {
readInputs();
state.error = '';
state.step++;
render();
});
});
document.querySelectorAll('[data-action="back"]').forEach(btn => {
btn.addEventListener('click', () => {
readInputs();
state.error = '';
state.step--;
render();
});
});
// Test connection
document.querySelector('[data-action="test-connection"]')?.addEventListener('click', async () => {
readInputs();
state.error = '';
if (!state.hostUrl || !state.repoPath || !state.apiToken) {
state.error = 'All fields are required';
render();
return;
}
const resultEl = document.getElementById('test-result')!;
resultEl.innerHTML = '<div class="test-result"><span class="spinner"></span> Testing...</div>';
try {
const git = createGitHost(state.hostType, state.hostUrl, state.repoPath, state.apiToken);
// Try to list the root directory — if the repo exists and token works, this succeeds
await git.listDir('');
state.connectionTested = true;
resultEl.innerHTML = '<div class="test-result test-ok">✓ Connected</div>';
// Re-render to enable Next button
const nextBtn = document.querySelector('[data-action="next"]') as HTMLButtonElement;
if (nextBtn) nextBtn.disabled = false;
} catch (err) {
state.connectionTested = false;
resultEl.innerHTML = `<div class="test-result test-fail">✗ ${escapeHtml(String(err))}</div>`;
}
});
// Carrier image file picker
document.getElementById('carrier-image')?.addEventListener('change', (e) => {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const arrayBuf = reader.result as ArrayBuffer;
state.carrierImageBytes = new Uint8Array(arrayBuf);
state.carrierImageName = file.name;
render();
};
reader.readAsArrayBuffer(file);
});
// Passphrase inputs (read on change, re-render for strength indicator)
document.getElementById('passphrase')?.addEventListener('input', (e) => {
state.passphrase = (e.target as HTMLInputElement).value;
// Only re-render the strength indicator, not the whole page (avoids losing focus)
const strength = passwordStrength(state.passphrase);
const bar = document.querySelector('.strength-bar-fill') as HTMLElement;
if (bar) {
bar.style.width = `${strength.pct}%`;
bar.style.background = strength.color;
}
const label = bar?.parentElement?.nextElementSibling as HTMLElement;
if (label) {
label.textContent = strength.label;
label.style.color = strength.color;
}
});
document.getElementById('passphrase-confirm')?.addEventListener('input', (e) => {
state.passphraseConfirm = (e.target as HTMLInputElement).value;
});
// Create vault
document.querySelector('[data-action="create-vault"]')?.addEventListener('click', async () => {
readInputs();
state.error = '';
// Validation
if (!state.carrierImageBytes) {
state.error = 'Select a carrier image';
render();
return;
}
if (state.passphrase.length < 8) {
state.error = 'Passphrase must be at least 8 characters';
render();
return;
}
if (state.passphrase !== state.passphraseConfirm) {
state.error = 'Passphrases do not match';
render();
return;
}
state.creating = true;
render();
try {
await createVault();
state.creating = false;
// Detect extension
state.extensionDetected = await detectExtension();
state.step = 4;
render();
} catch (err) {
state.creating = false;
state.error = `Vault creation failed: ${String(err)}`;
render();
}
});
// Download reference image
document.querySelector('[data-action="download-image"]')?.addEventListener('click', () => {
if (!state.referenceImageBytes) return;
const blob = new Blob([state.referenceImageBytes], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'reference.jpg';
a.click();
URL.revokeObjectURL(url);
});
// Push config to extension
document.querySelector('[data-action="push-to-extension"]')?.addEventListener('click', async () => {
try {
const config: VaultConfig = {
hostType: state.hostType,
hostUrl: state.hostUrl,
repoPath: state.repoPath,
apiToken: state.apiToken,
};
const imageBase64 = uint8ArrayToBase64(state.referenceImageBytes!);
chrome.runtime.sendMessage(
{ type: 'save_setup', config, imageBase64 },
(response) => {
if (response?.ok) {
state.configPushed = true;
render();
}
}
);
} catch {
state.error = 'Failed to push config to extension';
render();
}
});
// Copy config blob
document.querySelector('[data-action="copy-config"]')?.addEventListener('click', async () => {
const config = JSON.stringify({
hostType: state.hostType,
hostUrl: state.hostUrl,
repoPath: state.repoPath,
apiToken: state.apiToken,
}, null, 2);
await navigator.clipboard.writeText(config);
const el = document.querySelector('[data-action="copy-config"]')!;
el.classList.add('test-ok');
setTimeout(() => el.classList.remove('test-ok'), 1500);
});
}
// --- Read form inputs into state ---
function readInputs(): void {
const hostUrl = document.getElementById('host-url') as HTMLInputElement;
const repoPath = document.getElementById('repo-path') as HTMLInputElement;
const apiToken = document.getElementById('api-token') as HTMLInputElement;
const passphrase = document.getElementById('passphrase') as HTMLInputElement;
const passphraseConfirm = document.getElementById('passphrase-confirm') as HTMLInputElement;
if (hostUrl) state.hostUrl = hostUrl.value.trim();
if (repoPath) state.repoPath = repoPath.value.trim();
if (apiToken) state.apiToken = apiToken.value.trim();
if (passphrase) state.passphrase = passphrase.value;
if (passphraseConfirm) state.passphraseConfirm = passphraseConfirm.value;
}
// --- Vault Creation ---
async function createVault(): Promise<void> {
const w = await initWasm();
const git = createGitHost(state.hostType, state.hostUrl, state.repoPath, state.apiToken);
// 1. Generate random 32-byte image_secret
const imageSecret = new Uint8Array(32);
crypto.getRandomValues(imageSecret);
// 2. Embed secret into carrier JPEG
const referenceJpeg = w.embed_image_secret(state.carrierImageBytes!, imageSecret);
state.referenceImageBytes = new Uint8Array(referenceJpeg);
// 3. Generate random 32-byte salt
const salt = new Uint8Array(32);
crypto.getRandomValues(salt);
// 4. Create KDF params
const params = { argon2_m: 65536, argon2_t: 3, argon2_p: 4 };
const paramsJson = JSON.stringify(params);
// 5. Derive master_key
const masterKey = w.derive_master_key(state.passphrase, imageSecret, salt, paramsJson);
// 6. Encrypt empty manifest
const emptyManifest = JSON.stringify({ entries: {}, version: 1 });
const manifestEnc = w.encrypt_manifest(emptyManifest, masterKey);
// 7. Push vault files to repo
await git.writeFile('.relicario/salt', salt, 'feat: initialize relicario vault');
await git.writeFile('.relicario/params.json', new TextEncoder().encode(paramsJson), 'chore: add KDF params');
await git.writeFile('.relicario/devices.json', new TextEncoder().encode('[]'), 'chore: add empty devices list');
await git.writeFile('manifest.enc', new Uint8Array(manifestEnc), 'feat: add encrypted manifest');
}
// --- Extension Detection ---
function detectExtension(): Promise<boolean> {
return new Promise((resolve) => {
try {
if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
resolve(false);
return;
}
chrome.runtime.sendMessage(
{ type: 'get_setup_state' },
(response) => {
if (chrome.runtime.lastError || !response) {
resolve(false);
} else {
resolve(true);
}
}
);
} catch {
resolve(false);
}
});
}
// --- Init ---
document.addEventListener('DOMContentLoaded', render);
```
- [ ] **Step 2: Commit**
```bash
git add extension/src/setup/setup.ts
git commit -m "feat: add vault initialization wizard"
```
---
## Task 5: Update Webpack and Manifest
**Files:**
- Modify: `extension/webpack.config.js`
- Modify: `extension/manifest.json`
- [ ] **Step 1: Add setup entry point to webpack**
In `extension/webpack.config.js`, add `setup` to the `entry` object:
```javascript
entry: {
'service-worker': './src/service-worker/index.ts',
popup: './src/popup/popup.ts',
content: './src/content/detector.ts',
setup: './src/setup/setup.ts',
},
```
Add `setup.html` to the CopyPlugin patterns array:
```javascript
{ from: 'setup.html', to: '.' },
```
- [ ] **Step 2: Add web_accessible_resources to manifest.json**
Add to `extension/manifest.json`, after the `content_security_policy` block:
```json
"web_accessible_resources": [{
"resources": ["setup.html", "setup.js", "styles.css", "relicario_wasm_bg.wasm", "relicario_wasm.js"],
"matches": ["<all_urls>"]
}]
```
- [ ] **Step 3: Build and verify**
```bash
cd extension && bun run build
```
Expected: Builds successfully with `dist/setup.js` and `dist/setup.html` in the output.
- [ ] **Step 4: Commit**
```bash
git add extension/webpack.config.js extension/manifest.json
git commit -m "feat: add setup wizard to webpack build and extension manifest"
```
---
## Task 6: Build and Manual Test
**Files:** None (integration testing)
- [ ] **Step 1: Rebuild WASM**
```bash
wasm-pack build crates/relicario-wasm --target web --out-dir ../../extension/wasm
```
- [ ] **Step 2: Rebuild extension**
```bash
cd extension && bun run build
```
- [ ] **Step 3: Run Rust tests**
```bash
cargo test
```
Expected: All tests pass (including the new `embed_then_extract_round_trip`).
- [ ] **Step 4: Load in Chrome and test**
1. Open `chrome://extensions/`, reload the unpacked extension from `extension/dist/`
2. Open `chrome-extension://<extension-id>/setup.html`
3. Verify:
- Step 1: host toggle switches between Gitea/GitHub instructions
- Step 2: enter real host/token/repo, test connection works
- Step 3: pick a JPEG, enter passphrase, create vault pushes files
- Step 4: download reference image works, extension detection works
4. Verify the vault repo now has `.relicario/salt`, `.relicario/params.json`, `.relicario/devices.json`, `manifest.enc`
5. Open extension popup, unlock with passphrase — should work with the just-created vault
- [ ] **Step 5: Fix any issues found**
- [ ] **Step 6: Final commit**
```bash
git add -A
git commit -m "feat: complete vault initialization wizard"
```
---
## Task Summary
| Task | Description | Dependencies |
|------|-------------|--------------|
| 1 | Add `embed_image_secret` to WASM crate | None |
| 2 | Add WASM type declaration | Task 1 |
| 3 | Create setup page HTML | None |
| 4 | Create setup wizard TypeScript | Task 2, 3 |
| 5 | Update webpack and manifest | Task 3, 4 |
| 6 | Build and manual test | All |
Tasks 1 and 3 can run in parallel. Tasks 2 and 4 are sequential after 1. Task 5 depends on 3+4. Task 6 is final integration.