# 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, 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": "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 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('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 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/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

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)
