diff --git a/docs/superpowers/plans/2026-04-12-idfoto-firefox-extension.md b/docs/superpowers/plans/2026-04-12-idfoto-firefox-extension.md new file mode 100644 index 0000000..c6a93ff --- /dev/null +++ b/docs/superpowers/plans/2026-04-12-idfoto-firefox-extension.md @@ -0,0 +1,323 @@ +# idfoto Firefox Extension Port 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:** Port the Chrome extension to Firefox with shared TypeScript source, a Firefox-specific manifest, and a separate webpack build target. + +**Architecture:** All TypeScript source is shared. The only code change is an environment check in `index.ts` for WASM loading (service worker vs background script). A second webpack config produces `dist-firefox/` with the Firefox manifest. + +**Tech Stack:** TypeScript, webpack, Firefox WebExtensions MV3 + +**Spec:** `docs/superpowers/specs/2026-04-12-idfoto-firefox-extension-design.md` + +--- + +## File Structure + +### New files + +``` +extension/manifest.firefox.json # Firefox-specific manifest +extension/webpack.firefox.config.js # Webpack config for Firefox build +``` + +### Modified files + +``` +extension/src/service-worker/index.ts # Environment-aware WASM loading +extension/package.json # Add Firefox build scripts +.gitignore # Add extension/dist-firefox/ +``` + +--- + +## Task 1: Firefox Manifest and Webpack Config + +**Files:** +- Create: `extension/manifest.firefox.json` +- Create: `extension/webpack.firefox.config.js` +- Modify: `extension/package.json` +- Modify: `.gitignore` + +- [ ] **Step 1: Create Firefox manifest** + +Create `extension/manifest.firefox.json`: + +```json +{ + "manifest_version": 3, + "name": "idfoto", + "version": "0.1.0", + "description": "Two-factor encrypted password manager", + "browser_specific_settings": { + "gecko": { + "id": "idfoto@adlee.work", + "strict_min_version": "128.0" + } + }, + "permissions": ["storage", "activeTab", "clipboardWrite"], + "host_permissions": [""], + "background": { + "scripts": ["service-worker.js"] + }, + "action": { + "default_popup": "popup.html", + "default_icon": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content.js"], + "run_at": "document_idle" + } + ], + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" + }, + "web_accessible_resources": [ + { + "resources": [ + "setup.html", + "setup.js", + "styles.css", + "idfoto_wasm_bg.wasm", + "idfoto_wasm.js" + ] + } + ] +} +``` + +- [ ] **Step 2: Create Firefox webpack config** + +Create `extension/webpack.firefox.config.js`: + +```javascript +const path = require('path'); +const CopyPlugin = require('copy-webpack-plugin'); + +module.exports = { + entry: { + 'service-worker': './src/service-worker/index.ts', + popup: './src/popup/popup.ts', + content: './src/content/detector.ts', + setup: './src/setup/setup.ts', + }, + output: { + path: path.resolve(__dirname, 'dist-firefox'), + filename: '[name].js', + clean: true, + }, + resolve: { + extensions: ['.ts', '.js'], + }, + module: { + rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }], + }, + plugins: [ + new CopyPlugin({ + patterns: [ + { from: 'manifest.firefox.json', to: 'manifest.json' }, + { from: 'src/popup/index.html', to: 'popup.html' }, + { from: 'src/popup/styles.css', to: 'styles.css' }, + { from: 'setup.html', to: '.' }, + { from: 'icons', to: 'icons' }, + { from: 'wasm/idfoto_wasm_bg.wasm', to: '.' }, + { from: 'wasm/idfoto_wasm.js', to: '.' }, + ], + }), + ], + experiments: { asyncWebAssembly: true }, +}; +``` + +- [ ] **Step 3: Add Firefox build scripts to package.json** + +In `extension/package.json`, update the `scripts` section: + +```json +{ + "scripts": { + "build": "webpack --mode production", + "build:firefox": "webpack --config webpack.firefox.config.js --mode production", + "build:all": "npm run build:wasm && npm run build && npm run build:firefox", + "dev": "webpack --mode development --watch", + "dev:firefox": "webpack --config webpack.firefox.config.js --mode development --watch", + "build:wasm": "wasm-pack build ../crates/idfoto-wasm --target web --out-dir ../../extension/wasm" + } +} +``` + +- [ ] **Step 4: Add `dist-firefox/` to `.gitignore`** + +Append to the root `.gitignore`: + +``` +extension/dist-firefox/ +``` + +- [ ] **Step 5: Commit** + +```bash +git add extension/manifest.firefox.json extension/webpack.firefox.config.js extension/package.json .gitignore +git commit -m "feat: add Firefox manifest and webpack config" +``` + +--- + +## Task 2: Environment-Aware WASM Loading + +**Files:** +- Modify: `extension/src/service-worker/index.ts` + +- [ ] **Step 1: Update the WASM init function** + +In `extension/src/service-worker/index.ts`, replace the current `initWasm` function and its surrounding comments (lines 23-49) with: + +```typescript +// --- WASM initialization --- + +// Chrome MV3 uses service workers which do NOT support dynamic import(). +// Firefox MV3 uses background scripts which DO support dynamic import(). +// We detect the environment at runtime and use the appropriate loading strategy. +// +// The JS glue is imported statically so webpack bundles it. Both initSync +// (Chrome) and the default export (Firefox) are available. + +// @ts-ignore TS2307 — resolved by webpack alias / copy +import initDefault, { initSync } from '../../wasm/idfoto_wasm.js'; +// @ts-ignore TS2307 +import * as wasmBindings from '../../wasm/idfoto_wasm.js'; + +type WasmModule = typeof wasmBindings; +let wasm: WasmModule | null = null; + +async function initWasm(): Promise { + if (wasm) return wasm; + + const isServiceWorker = typeof ServiceWorkerGlobalScope !== 'undefined' + && self instanceof ServiceWorkerGlobalScope; + + if (isServiceWorker) { + // Chrome: fetch WASM binary and instantiate synchronously + const wasmResponse = await fetch(chrome.runtime.getURL('idfoto_wasm_bg.wasm')); + const wasmBytes = await wasmResponse.arrayBuffer(); + initSync({ module: new WebAssembly.Module(wasmBytes) }); + } else { + // Firefox: background script — dynamic init works + const wasmUrl = chrome.runtime.getURL('idfoto_wasm_bg.wasm'); + await initDefault(wasmUrl); + } + + vault.setWasm(wasmBindings); + wasm = wasmBindings; + wasmReady = true; + return wasm; +} +``` + +- [ ] **Step 2: Update the module doc comment** + +Change the doc comment at the top of the file (line 1) from: + +```typescript +/// Service worker entry point for the idfoto Chrome extension. +``` + +To: + +```typescript +/// Background script entry point for the idfoto browser extension. +/// +/// In Chrome this runs as a service worker (MV3). In Firefox this runs +/// as a persistent background script. WASM loading adapts automatically. +``` + +- [ ] **Step 3: Build both targets** + +```bash +cd extension && bun run build && bun run build:firefox +``` + +Expected: Both builds succeed with 0 errors. + +- [ ] **Step 4: Commit** + +```bash +git add extension/src/service-worker/index.ts +git commit -m "feat: add environment-aware WASM loading for Chrome/Firefox" +``` + +--- + +## Task 3: Build and Manual Test + +**Files:** None (integration testing) + +- [ ] **Step 1: Verify Chrome build still works** + +```bash +cd extension && bun run build +``` + +Expected: `dist/` output, 0 errors. Reload in Chrome — unlock, list, autofill all work. + +- [ ] **Step 2: Build Firefox** + +```bash +bun run build:firefox +``` + +Expected: `dist-firefox/` output with `manifest.json` (Firefox version), all JS bundles, WASM files, icons. + +- [ ] **Step 3: Verify Firefox manifest** + +```bash +cat dist-firefox/manifest.json | grep -E "gecko|background" +``` + +Expected: `browser_specific_settings.gecko.id` present, `background.scripts` (not `service_worker`). + +- [ ] **Step 4: Load in Firefox** + +1. Open Firefox +2. Navigate to `about:debugging#/runtime/this-firefox` +3. Click "Load Temporary Add-on..." +4. Select `extension/dist-firefox/manifest.json` +5. Extension icon appears in toolbar + +- [ ] **Step 5: Test basic flow** + +1. Click extension icon — popup opens, shows setup prompt (or unlock if already configured) +2. Open `setup.html` via the setup button — full-page wizard loads +3. Configure vault (or verify it's already configured from Chrome) +4. Unlock with passphrase — entry list appears +5. Navigate entries, check TOTP countdown works +6. Visit a login page — field icon appears +7. Test autofill +8. If credential capture is enabled, test save prompt appears on form submit + +- [ ] **Step 6: Fix any Firefox-specific issues** + +- [ ] **Step 7: Final commit** + +```bash +git add -A +git commit -m "feat: complete Firefox extension port" +``` + +--- + +## Task Summary + +| Task | Description | Dependencies | +|------|-------------|--------------| +| 1 | Firefox manifest + webpack config + scripts | None | +| 2 | Environment-aware WASM loading | None | +| 3 | Build and manual test | Tasks 1, 2 | + +Tasks 1 and 2 are independent and can run in parallel.