From b48ff0a05cfa43e1947bbdfec92e8069df0787d6 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sun, 12 Apr 2026 10:46:37 -0400 Subject: [PATCH] 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) --- .../2026-04-12-idfoto-init-wizard-design.md | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-12-idfoto-init-wizard-design.md diff --git a/docs/superpowers/specs/2026-04-12-idfoto-init-wizard-design.md b/docs/superpowers/specs/2026-04-12-idfoto-init-wizard-design.md new file mode 100644 index 0000000..7a37a9e --- /dev/null +++ b/docs/superpowers/specs/2026-04-12-idfoto-init-wizard-design.md @@ -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, 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 { + 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:///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": [""] + }] +} +``` + +The setup page can then be opened at `chrome-extension:///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)