diff --git a/docs/superpowers/specs/2026-04-12-idfoto-firefox-extension-design.md b/docs/superpowers/specs/2026-04-12-idfoto-firefox-extension-design.md new file mode 100644 index 0000000..813a810 --- /dev/null +++ b/docs/superpowers/specs/2026-04-12-idfoto-firefox-extension-design.md @@ -0,0 +1,196 @@ +# idfoto — Firefox Extension Port Design + +Port the existing Chrome MV3 extension to Firefox. Shared TypeScript source, separate manifests, separate build outputs. No code changes to components, popup, or content script. + +## Scope + +- Firefox-specific `manifest.json` +- WASM loading compatibility (dynamic import for Firefox background script) +- Second webpack config for Firefox build target +- npm scripts for building both browsers + +## What Stays the Same + +All TypeScript source files are shared between Chrome and Firefox: +- `src/service-worker/` — all files (with one environment check for WASM loading) +- `src/popup/` — all components, styles, HTML +- `src/content/` — detector, fill, icon, capture +- `src/setup/` — setup wizard +- `src/shared/` — types, messages +- `setup.html` — init wizard + +Firefox supports the `chrome.*` namespace for WebExtension APIs, so no `browser.*` polyfill is needed. All Chrome API calls (`chrome.runtime.sendMessage`, `chrome.storage.local`, `chrome.tabs`, etc.) work as-is. + +## Manifest Differences + +### Chrome (`manifest.json` — existing) + +```json +{ + "manifest_version": 3, + "background": { + "service_worker": "service-worker.js", + "type": "module" + } +} +``` + +### Firefox (`manifest.firefox.json` — new) + +```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"] + }] +} +``` + +Key differences from Chrome manifest: +- `browser_specific_settings.gecko.id` — required for Firefox, uses email-style ID +- `browser_specific_settings.gecko.strict_min_version` — Firefox 128+ for stable MV3 support +- `background.scripts` instead of `background.service_worker` + `type: module` — Firefox MV3 background scripts are NOT service workers, they're persistent scripts +- `web_accessible_resources` — Firefox doesn't use `matches` field in the resource entries + +## WASM Loading + +The service worker `index.ts` currently uses `initSync` with `chrome.runtime.getURL` because Chrome MV3 service workers don't support dynamic `import()`. Firefox background scripts DO support `import()`. + +Add an environment check to `index.ts`: + +```typescript +async function initWasm(): Promise { + if (wasm) return wasm; + + if (typeof ServiceWorkerGlobalScope !== 'undefined') { + // Chrome MV3: service worker context — use initSync + 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 context — dynamic import works + const wasmUrl = chrome.runtime.getURL('idfoto_wasm_bg.wasm'); + await initDefault(wasmUrl); + } + + vault.setWasm(wasmBindings); + wasm = wasmBindings; + wasmReady = true; + return wasm; +} +``` + +This uses the static import of `initSync` and the default export (`initDefault`) from the WASM glue, branching on runtime environment. Both paths end with the same `wasmBindings` module reference. + +## Build Pipeline + +### New file: `extension/webpack.firefox.config.js` + +Identical to `webpack.config.js` except: +- Output directory: `dist-firefox/` instead of `dist/` +- CopyPlugin copies `manifest.firefox.json` as `manifest.json` (instead of `manifest.json`) + +### Updated `extension/package.json` scripts + +```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" + } +} +``` + +### Output structure + +``` +extension/ +├── dist/ # Chrome build (existing) +│ ├── service-worker.js +│ ├── popup.js +│ ├── content.js +│ ├── setup.js +│ ├── manifest.json # Chrome manifest +│ └── ... +├── dist-firefox/ # Firefox build (new) +│ ├── service-worker.js +│ ├── popup.js +│ ├── content.js +│ ├── setup.js +│ ├── manifest.json # Firefox manifest (copied from manifest.firefox.json) +│ └── ... +└── wasm/ # Shared WASM (same for both) +``` + +## Testing + +### Load in Firefox + +1. Open `about:debugging#/runtime/this-firefox` +2. Click "Load Temporary Add-on..." +3. Select `extension/dist-firefox/manifest.json` +4. Extension appears in toolbar + +### Test matrix + +Same as Chrome — all features should work identically: +- Setup wizard (`setup.html`) +- Unlock with passphrase +- Entry list, search, group filtering +- Entry detail with TOTP countdown +- Add/edit/delete entries +- Autofill via field icon +- Credential capture (if enabled) +- Settings view + +### Firefox-specific checks + +- WASM loads correctly (background script, not service worker) +- master_key persists longer (background script stays alive) +- Popup dimensions render correctly +- Content script injection works on all pages + +## .gitignore + +Add `extension/dist-firefox/` to `.gitignore`. + +## Non-Goals + +- Firefox for Android (different extension API surface) +- Publishing to addons.mozilla.org (manual for now) +- Automated cross-browser testing +- Shared webpack config with conditional logic (two separate configs is clearer)