docs: add Firefox extension port design spec
Shared TypeScript source with separate manifests and webpack configs. Firefox uses background scripts (not service workers) so WASM loading uses dynamic import instead of initSync. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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": ["<all_urls>"],
|
||||||
|
"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": ["<all_urls>"],
|
||||||
|
"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<WasmModule> {
|
||||||
|
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)
|
||||||
Reference in New Issue
Block a user