Files
relicario/docs/superpowers/specs/2026-04-12-idfoto-init-wizard-design.md
adlee-was-taken b48ff0a05c 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>
2026-04-12 10:46:37 -04:00

7.3 KiB

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:

#[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

// 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:

{
  "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)