Sweeping rename across crates, CLI binary, WASM bindings, extension, docs,
and vault metadata paths. Git remote updated to relicario.git.
- crates/idfoto-{core,cli,wasm} -> crates/relicario-{core,cli,wasm}
- IdfotoError -> RelicarioError
- IDFOTO_IMAGE env var -> RELICARIO_IMAGE
- ~/.config/idfoto -> ~/.config/relicario
- .idfoto/ vault metadata dir -> .relicario/ (breaking; pre-release)
- Binary name idfoto -> relicario
- Extension wasm module idfoto_wasm -> relicario_wasm
- Storage key idfotoSettings -> relicarioSettings
- All doc filenames and content references updated
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.2 KiB
relicario — 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, HTMLsrc/content/— detector, fill, icon, capturesrc/setup/— setup wizardsrc/shared/— types, messagessetup.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)
{
"manifest_version": 3,
"background": {
"service_worker": "service-worker.js",
"type": "module"
}
}
Firefox (manifest.firefox.json — new)
{
"manifest_version": 3,
"name": "relicario",
"version": "0.1.0",
"description": "Two-factor encrypted password manager",
"browser_specific_settings": {
"gecko": {
"id": "relicario@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", "relicario_wasm_bg.wasm", "relicario_wasm.js"]
}]
}
Key differences from Chrome manifest:
browser_specific_settings.gecko.id— required for Firefox, uses email-style IDbrowser_specific_settings.gecko.strict_min_version— Firefox 128+ for stable MV3 supportbackground.scriptsinstead ofbackground.service_worker+type: module— Firefox MV3 background scripts are NOT service workers, they're persistent scriptsweb_accessible_resources— Firefox doesn't usematchesfield 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:
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('relicario_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('relicario_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 ofdist/ - CopyPlugin copies
manifest.firefox.jsonasmanifest.json(instead ofmanifest.json)
Updated extension/package.json scripts
{
"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/relicario-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
- Open
about:debugging#/runtime/this-firefox - Click "Load Temporary Add-on..."
- Select
extension/dist-firefox/manifest.json - 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)