docs: add vault initialization wizard design spec
Browser-based 4-step wizard for creating idfoto vaults without the CLI. Uses WASM for crypto, pushes vault files via git API, downloads reference image, and optionally configures the Chrome extension. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
178
docs/superpowers/specs/2026-04-12-idfoto-init-wizard-design.md
Normal file
178
docs/superpowers/specs/2026-04-12-idfoto-init-wizard-design.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# idfoto — Standalone Vault Initialization Wizard Design
|
||||
|
||||
A browser-based wizard that guides new users through creating an idfoto vault from scratch. Lives at `extension/setup.html`, uses the same WASM module as the extension, same terminal dark aesthetic. No server, no Rust toolchain required.
|
||||
|
||||
## Scope
|
||||
|
||||
- Single HTML page with inline JS (bundled by webpack) at `extension/setup.html`
|
||||
- 4-step wizard: choose host → configure connection → create vault → finish
|
||||
- Pushes vault files directly to Gitea/GitHub via API
|
||||
- Downloads reference image to user's machine
|
||||
- Optionally pushes config to the Chrome extension if installed
|
||||
|
||||
## Flow
|
||||
|
||||
### Step 1: Choose Git Host
|
||||
|
||||
Toggle between Gitea and GitHub. Below the toggle, show inline setup instructions:
|
||||
|
||||
**Gitea instructions:**
|
||||
1. Log in to your Gitea instance
|
||||
2. Create a new empty repository (no README, no .gitignore)
|
||||
3. Go to Settings → Applications → Generate New Token
|
||||
4. Select scope: `repo` (read/write)
|
||||
5. Copy the token
|
||||
|
||||
**GitHub instructions:**
|
||||
1. Go to github.com → New Repository
|
||||
2. Create an empty repository (no README, no .gitignore, no license)
|
||||
3. Go to Settings → Developer Settings → Personal Access Tokens → Fine-grained tokens
|
||||
4. Generate new token, select only the target repository
|
||||
5. Permissions: Contents → Read and write
|
||||
6. Copy the token
|
||||
|
||||
Step includes a "Next" button. No validation needed at this step.
|
||||
|
||||
### Step 2: Configure Connection
|
||||
|
||||
Fields:
|
||||
- Host URL (e.g. `https://git.adlee.work` or `https://github.com`) — pre-filled based on host type selection
|
||||
- Repository path (e.g. `alee/idfoto-vault`)
|
||||
- API token (password field)
|
||||
|
||||
"Test Connection" button:
|
||||
- Hits the git API to verify the token works and the repo exists
|
||||
- Checks that the repo is empty (no files) or contains only a README
|
||||
- Shows green checkmark on success, red error on failure
|
||||
- Must pass before "Next" is enabled
|
||||
|
||||
Uses the same `GitHost` interface (GiteaHost/GitHubHost) from the extension's service worker code.
|
||||
|
||||
### Step 3: Create Vault
|
||||
|
||||
Two inputs:
|
||||
- **Carrier image:** File picker for a JPEG. Shows preview thumbnail after selection. Minimum size guidance ("use a photo from your phone — at least 400x300").
|
||||
- **Passphrase:** Password field with confirmation. Minimum 8 characters enforced. Shows basic strength indicator (weak/ok/strong based on length + character variety).
|
||||
|
||||
"Create Vault" button triggers:
|
||||
|
||||
1. Load WASM module
|
||||
2. Generate random 32-byte `image_secret` via `crypto.getRandomValues()`
|
||||
3. Embed secret into carrier JPEG via WASM `extract_image_secret` — wait, that's extract. We need `embed`. Check: the WASM crate currently only exposes `extract_image_secret`, not `embed`. **We need to add a `embed_image_secret` function to `idfoto-wasm`.**
|
||||
4. Generate random 32-byte `salt` via `crypto.getRandomValues()`
|
||||
5. Create `params.json` with default KDF params (`{"argon2_m":65536,"argon2_t":3,"argon2_p":4}`)
|
||||
6. Derive `master_key` via WASM `derive_master_key(passphrase, image_secret, salt, params_json)`
|
||||
7. Encrypt empty manifest (`{"entries":{},"version":1}`) via WASM `encrypt_manifest`
|
||||
8. Push files to repo via git API:
|
||||
- `.idfoto/salt` (raw 32 bytes)
|
||||
- `.idfoto/params.json` (JSON string)
|
||||
- `.idfoto/devices.json` (`[]`)
|
||||
- `manifest.enc` (encrypted manifest bytes)
|
||||
9. Show progress bar during push operations
|
||||
|
||||
Spinner/progress during the Argon2id derivation (~1-2 seconds) and API calls.
|
||||
|
||||
### Step 4: Finish
|
||||
|
||||
Two things happen:
|
||||
|
||||
**Download reference image:**
|
||||
- Browser downloads the steganographic JPEG as `reference.jpg`
|
||||
- Show warning: "Keep this image safe. You need it alongside your passphrase to unlock the vault. Store it somewhere you won't lose it."
|
||||
|
||||
**Push config to extension (if available):**
|
||||
- Try to detect the idfoto extension via `chrome.runtime.sendMessage` with a `get_setup_state` message
|
||||
- If extension responds: push `save_setup` message with `{ config: { hostType, hostUrl, repoPath, apiToken }, imageBase64 }`. Show "Extension configured! You can now open the extension and unlock your vault."
|
||||
- If extension not detected: show the config as a copyable JSON blob with instructions: "Install the idfoto extension, then paste this into the setup wizard." (Or just tell them to run through the extension setup manually with the same host/token/repo.)
|
||||
|
||||
## WASM Crate Change
|
||||
|
||||
The `idfoto-wasm` crate needs one new function:
|
||||
|
||||
```rust
|
||||
#[wasm_bindgen]
|
||||
pub fn embed_image_secret(carrier_jpeg: &[u8], secret: &[u8]) -> Result<Vec<u8>, JsValue>
|
||||
```
|
||||
|
||||
This wraps `idfoto_core::imgsecret::embed`. Currently only `extract_image_secret` is exposed.
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
extension/
|
||||
├── setup.html # standalone wizard page
|
||||
├── src/
|
||||
│ └── setup/
|
||||
│ └── setup.ts # wizard logic (4-step state machine)
|
||||
├── webpack.config.js # add 'setup' entry point
|
||||
```
|
||||
|
||||
The setup page reuses:
|
||||
- `extension/wasm/` — same WASM module
|
||||
- `extension/src/service-worker/git-host.ts`, `gitea.ts`, `github.ts` — git API layer
|
||||
- `extension/src/popup/styles.css` — terminal dark theme (imported or linked)
|
||||
- `extension/src/shared/types.ts` — VaultConfig type
|
||||
|
||||
## UI Design
|
||||
|
||||
Same terminal dark aesthetic as the popup but in a full-page layout (not 360px constrained). Centered content area, max-width ~600px. Same color scheme (#0d1117 bg, #58a6ff blue, monospace font).
|
||||
|
||||
Progress bar at top showing step 1-4. Each step is a full-page view with back/next navigation.
|
||||
|
||||
## Extension Detection
|
||||
|
||||
```typescript
|
||||
// Try to send a message to the extension
|
||||
function detectExtension(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
chrome.runtime.sendMessage(
|
||||
{ type: 'get_setup_state' },
|
||||
(response) => {
|
||||
if (chrome.runtime.lastError || !response) {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Note: this only works if `setup.html` is served from the extension itself (`chrome-extension://<id>/setup.html`) or if we use `externally_connectable` in the manifest. For a local file, extension detection won't work — fall back to manual config copy.
|
||||
|
||||
If we add `setup.html` to the extension's web_accessible_resources and the user opens it via `chrome-extension://` URL, messaging works natively.
|
||||
|
||||
## manifest.json Changes
|
||||
|
||||
Add `setup.html` to the extension so it can be opened as a chrome-extension page:
|
||||
|
||||
```json
|
||||
{
|
||||
"web_accessible_resources": [{
|
||||
"resources": ["setup.html", "setup.js", "styles.css", "idfoto_wasm_bg.wasm", "idfoto_wasm.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
The setup page can then be opened at `chrome-extension://<extension-id>/setup.html`. The extension popup can link to it, or the user can navigate directly.
|
||||
|
||||
## Security
|
||||
|
||||
- Passphrase never leaves the browser
|
||||
- image_secret generated client-side, embedded client-side, never transmitted
|
||||
- master_key derived and used in-browser only, then discarded
|
||||
- API token used only for pushing vault files and optionally passed to extension storage
|
||||
- The reference image download is the only artifact the user needs to keep safe
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Creating repos via API (user creates the repo manually — API permissions for repo creation vary widely)
|
||||
- Git operations beyond file CRUD (no commits history, no branches)
|
||||
- Password strength estimation beyond basic length/variety checks
|
||||
- Mobile support (desktop Chrome only for now)
|
||||
Reference in New Issue
Block a user