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