--- name: firefox-extension description: Comprehensive guide for developing WebExtensions (browser extensions) for Mozilla Firefox, including Manifest V2/V3 configuration, all WebExtension APIs, security practices, web-ext CLI, and AMO submission. metadata: author: mte90 version: 1.0.0 tags: - firefox - webextension - browser-extension - mozilla - amo - manifest-v3 --- # Firefox WebExtension Development Complete reference for building, testing, and publishing browser extensions for Mozilla Firefox. ## Overview Firefox extensions use the WebExtensions API with the `browser.*` namespace (Promise-based natively). Firefox supports both Manifest V2 and V3, with MV2 NOT deprecated (unlike Chrome). **Key Characteristics:** - Global namespace: `browser` (Promise-based) - Both MV2 and MV3 supported - Firefox-specific APIs: `sidebarAction`, `userScripts`, `contextualIdentities`, `protocol_handlers` - Submission via AMO (addons.mozilla.org) ## Manifest Structure ### Manifest V3 (Recommended, Firefox 109+) ```json { "manifest_version": 3, "name": "Extension Name", "version": "1.0.0", "description": "Brief description", "browser_specific_settings": { "gecko": { "id": "extension@example.com", "strict_min_version": "109.0" }, "gecko_android": { "strict_min_version": "109.0" } }, "background": { "service_worker": "background.js", "type": "module" }, "action": { "default_popup": "popup.html", "default_icon": { "16": "icons/icon-16.png", "48": "icons/icon-48.png", "96": "icons/icon-96.png" }, "default_title": "Extension Title" }, "content_scripts": [ { "matches": ["https://*/*"], "js": ["content.js"], "css": ["styles.css"], "run_at": "document_idle" } ], "permissions": ["storage", "activeTab"], "host_permissions": ["https://api.example.com/*"], "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self';" } } ``` ### Manifest V2 (Still Supported) ```json { "manifest_version": 2, "name": "Extension Name", "version": "1.0.0", "browser_specific_settings": { "gecko": { "id": "extension@example.com", "strict_min_version": "78.0" } }, "background": { "scripts": ["background.js"], "persistent": false }, "browser_action": { "default_popup": "popup.html", "default_icon": "icons/icon-48.png" }, "permissions": ["storage", "activeTab", "https://*/*"] } ``` ### MV2 vs MV3 Key Differences | Feature | MV2 | MV3 | |---------|-----|-----| | Toolbar button | `browser_action` | `action` | | Background | `background.scripts` / `page` | `background.service_worker` | | Host permissions | In `permissions` | Separate `host_permissions` | | Default CSP | `script-src 'self'; object-src 'self';` | `script-src 'self'; upgrade-insecure-requests;` | | Request blocking | `webRequest.onBeforeRequest` | `declarativeNetRequest` | ### All Manifest Keys Reference **Metadata:** - `name` (required) - Extension name - `version` (required) - Version string (e.g., "1.0.0") - `description` - Short description - `author` - Author name - `homepage_url` - Extension homepage - `icons` - Extension icons object **Firefox-Specific:** - `browser_specific_settings.gecko.id` - **Required for AMO** - `browser_specific_settings.gecko.strict_min_version` - Minimum Firefox version - `browser_specific_settings.gecko.strict_max_version` - Maximum Firefox version - `browser_specific_settings.gecko_android` - Android-specific settings **Background & Scripts:** - `background` - Service worker (MV3) or scripts/page (MV2) - `content_scripts` - Scripts injected into pages - `userScripts` - User script registration (Firefox-only) - `declarative_net_request` - Rule-based request modification **UI Components:** - `action` (MV3) / `browser_action` (MV2) - Toolbar button - `page_action` - Address bar button - `sidebar_action` - Sidebar panel (Firefox-only) - `options_ui` - Options page configuration - `devtools_page` - DevTools extension page **Permissions:** - `permissions` - API permissions - `host_permissions` (MV3) - Host access - `optional_permissions` - Optional API permissions - `optional_host_permissions` - Optional host access **Other:** - `commands` - Keyboard shortcuts - `omnibox` - Address bar integration - `web_accessible_resources` - Resources accessible from pages - `protocol_handlers` - Custom protocol handlers - `chrome_settings_overrides` - Override homepage/search - `chrome_url_overrides` - Override new tab/bookmarks ## WebExtension APIs ### Complete API Namespace List (51 APIs) | API | Permission | Description | |-----|------------|-------------| | `action` | - | Toolbar button (MV3) | | `alarms` | `alarms` | Schedule code execution | | `bookmarks` | `bookmarks` | Bookmark management | | `browserAction` | - | Toolbar button (MV2) | | `browserSettings` | - | Browser settings | | `browsingData` | `browsingData` | Clear browsing data | | `clipboard` | `clipboardWrite` | Clipboard access | | `commands` | - | Keyboard shortcuts | | `contentScripts` | - | Register content scripts | | `contextualIdentities` | `contextualIdentities` | Container tabs (Firefox-only) | | `cookies` | `cookies` | Cookie management | | `declarativeNetRequest` | `declarativeNetRequest` | Rule-based request blocking | | `devtools` | `devtools` | DevTools integration | | `dns` | `dns` | DNS resolution | | `downloads` | `downloads` | Download management | | `events` | - | Common event types | | `extension` | - | Extension utilities | | `find` | `find` | Find text in pages | | `history` | `history` | Browser history | | `i18n` | - | Internationalization | | `identity` | `identity` | OAuth2 authentication | | `idle` | `idle` | Idle state detection | | `management` | `management` | Installed add-ons info | | `menus` | `menus` | Context menu items | | `notifications` | `notifications` | System notifications | | `omnibox` | - | Address bar suggestions | | `pageAction` | - | Address bar button (MV2) | | `permissions` | - | Runtime permissions | | `pkcs11` | `pkcs11` | PKCS#11 modules | | `privacy` | `privacy` | Privacy settings | | `proxy` | `proxy` | Request proxying | | `runtime` | - | Extension runtime | | `scripting` | `scripting` | Inject scripts/CSS (MV3) | | `search` | `search` | Search engines | | `sessions` | `sessions` | Closed tabs/windows | | `sidebarAction` | - | Sidebar (Firefox-only) | | `storage` | `storage` | Local/managed storage | | `tabGroups` | `tabGroups` | Tab groups | | `tabs` | `tabs` | Tab management | | `theme` | `theme` | Theme API | | `topSites` | `topSites` | Frequently visited | | `userScripts` | `userScripts` | User scripts (Firefox-only) | | `webNavigation` | `webNavigation` | Navigation events | | `webRequest` | `webRequest` | Request interception | | `windows` | - | Window management | ### Core API Usage Patterns **Tabs API:** ```javascript // Query tabs const tabs = await browser.tabs.query({ active: true, currentWindow: true }); // Create tab const tab = await browser.tabs.create({ url: 'https://example.com' }); // Update tab await browser.tabs.update(tabId, { active: true }); // Send message to tab await browser.tabs.sendMessage(tabId, { action: 'update' }); // Execute script (MV3) await browser.scripting.executeScript({ target: { tabId }, files: ['content.js'] }); ``` **Storage API:** ```javascript // Save data await browser.storage.local.set({ key: 'value', settings: config }); // Get data const { key, settings } = await browser.storage.local.get(['key', 'settings']); // Remove data await browser.storage.local.remove('key'); // Clear all await browser.storage.local.clear(); // Listen for changes browser.storage.onChanged.addListener((changes, area) => { if (changes.key) { console.log('Old:', changes.key.oldValue, 'New:', changes.key.newValue); } }); ``` **Runtime Messaging:** ```javascript // Content script → Background const response = await browser.runtime.sendMessage({ action: 'getData' }); // Background listener browser.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'getData') { return Promise.resolve({ data: 'response' }); } }); // Background → Content script browser.tabs.sendMessage(tabId, { action: 'update' }); // External messaging (from web pages) browser.runtime.onMessageExternal.addListener((message, sender) => { if (sender.id === 'allowed-extension-id') { // Handle message } }); ``` **Context Menus:** ```javascript // Create context menu browser.menus.create({ id: 'my-menu', title: 'My Menu Item', contexts: ['selection'] }); // Handle click browser.menus.onClicked.addListener((info, tab) => { if (info.menuItemId === 'my-menu') { console.log('Selected:', info.selectionText); } }); ``` **Alarms:** ```javascript // Create alarm browser.alarms.create('my-alarm', { delayInMinutes: 1, periodInMinutes: 5 }); // Handle alarm browser.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'my-alarm') { // Do work } }); ``` **Notifications:** ```javascript // Show notification browser.notifications.create({ type: 'basic', iconUrl: 'icon.png', title: 'Title', message: 'Message' }); ``` ### Firefox-Specific APIs **Sidebar Action:** ```javascript // Toggle sidebar browser.sidebarAction.open(); browser.sidebarAction.close(); // Set panel await browser.sidebarAction.setPanel({ panel: 'sidebar.html' }); ``` **Container Tabs (Contextual Identities):** ```javascript // List containers const containers = await browser.contextualIdentities.query({}); // Create container const container = await browser.contextualIdentities.create({ name: 'Work', color: 'blue', icon: 'briefcase' }); // Create tab in container await browser.tabs.create({ url: 'https://work.example.com', cookieStoreId: container.cookieStoreId }); ``` **User Scripts:** ```javascript // Register user script await browser.userScripts.register([{ js: [{ file: 'script.js' }], matches: ['*://example.com/*'], runAt: 'document_start' }]); ``` ## Security & Content Security Policy ### Default CSP **MV3:** `script-src 'self'; upgrade-insecure-requests;` **MV2:** `script-src 'self'; object-src 'self';` ### CSP Restrictions **Forbidden (causes AMO rejection):** - Remote script sources - `'unsafe-inline'` - `'unsafe-eval'` (except `'wasm-unsafe-eval'` for WebAssembly) - Data URLs for scripts - `eval()`, `new Function()`, string-based code execution **Allowed:** - `'self'` - Scripts from extension package - `'wasm-unsafe-eval'` - WebAssembly support - `http://localhost:` - Development only (remove before submission) ### Custom CSP Example ```json { "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';", "sandbox": "sandbox allow-scripts allow-forms allow-popups; script-src 'self';" } } ``` ### Security Best Practices 1. **Package all dependencies locally** - No CDN scripts 2. **Request minimal permissions** - Use `activeTab` instead of `` when possible 3. **Use optional_permissions** - Request permissions at runtime for non-critical features 4. **Validate user input** - Sanitize HTML before `innerHTML`, validate URLs 5. **Use HTTPS** - All external requests should use HTTPS 6. **No obfuscated code** - Must be reviewable for AMO ## web-ext CLI Reference ### Installation ```bash npm install -g web-ext ``` ### Commands **Run extension (development):** ```bash web-ext run # Default Firefox web-ext run --firefox-path /path/to/firefox # Specific Firefox web-ext run --target firefox-android # Android web-ext run --profile-create-new # Clean profile web-ext run --start-url https://example.com # Start URL web-ext run --verbose # Verbose output web-ext run --no-reload # Disable auto-reload ``` **Lint extension:** ```bash web-ext lint # Validate manifest and code web-ext lint --warnings-as-errors web-ext lint --self-hosted # Skip AMO-specific checks ``` **Build extension:** ```bash web-ext build # Create .zip in web-ext-artifacts/ web-ext build --source-dir ./src web-ext build --artifacts-dir ./dist ``` **Sign extension:** ```bash web-ext sign --api-key $KEY --api-secret $SECRET web-ext sign --channel listed # Public on AMO web-ext sign --channel unlisted # Direct download ``` **Other commands:** ```bash web-ext docs # Open documentation web-ext dump-config # Show configuration ``` ### Configuration File (.web-ext-config.js) ```javascript module.exports = { sourceDir: './src', artifactsDir: './dist', run: { firefox: '/Applications/Firefox.app/Contents/MacOS/firefox', startUrl: 'https://example.com', pref: ['extensions.webextensions.debug=true'] }, build: { overwriteDest: true }, sign: { apiKey: process.env.AMO_API_KEY, apiSecret: process.env.AMO_API_SECRET, channel: 'listed' } }; ``` ### Environment Variables ```bash WEB_EXT_API_KEY=your_key WEB_EXT_API_SECRET=your_secret WEB_EXT_SOURCE_DIR=./src WEB_EXT_ARTIFACTS_DIR=./dist WEB_EXT_FIREFOX=/path/to/firefox ``` ## AMO Submission Process ### Pre-Submission Checklist - [ ] Extension ID in `browser_specific_settings.gecko.id` - [ ] `web-ext lint` passes with no errors - [ ] All permissions are necessary and documented - [ ] Privacy policy included (if collecting data) - [ ] No obfuscated code - [ ] Source code available (if using build tools) - [ ] Icons: 48x48 and 96x96 minimum - [ ] Screenshots: minimum 464x200px - [ ] Description is clear and accurate ### Submission Steps 1. **Build extension:** ```bash web-ext build ``` 2. **Create developer account:** - Visit https://addons.mozilla.org/developers/ - Sign up and complete profile 3. **Submit:** - Go to Developer Hub → "Submit a New Add-on" - Upload `.zip` from `web-ext build` - Choose distribution: Listed (public) or Unlisted (direct) 4. **Fill listing:** - Name, description, categories - Screenshots, icons - Privacy policy URL (if collecting data) - Support email/URL 5. **Review process:** - Automated validation: 5-15 minutes - Human review: 1-7 days for listed extensions - Respond to reviewer comments promptly ### Common Rejection Reasons | Reason | Solution | |--------|----------| | Remote code execution | Package all scripts locally | | Unnecessary permissions | Remove unused permissions | | Obfuscated code | Provide source code | | Missing privacy policy | Add policy if collecting data | | Unclear functionality | Improve description | | CSP violations | Fix CSP to not allow remote scripts | | Hidden functionality | Disclose all features | ### Data Collection Requirements If extension collects/transmits user data, privacy policy must disclose: - What data is collected - How it's transmitted - Purpose of collection - User consent mechanism - Data retention policy ## Testing & Debugging ### Development Workflow ```bash # Start development web-ext run --verbose # Auto-reloads on file changes # Access via about:debugging ``` ### Debugging Tools **Access debugging:** 1. Open `about:debugging#/runtime/this-firefox` 2. Click "Inspect" next to extension **Debug components:** - **Background:** Console in debugging page - **Popup:** Right-click popup → "Inspect" - **Content scripts:** Regular page DevTools Console - **Options page:** Right-click options → "Inspect" ### Debugging Commands ```javascript // Check manifest browser.runtime.getManifest(); // Check permissions browser.permissions.contains({ permissions: ['tabs'] }); // Get extension URL browser.runtime.getURL('/path/to/resource'); // Check last error if (browser.runtime.lastError) { console.error(browser.runtime.lastError); } // Reload extension programmatically browser.runtime.reload(); ``` ### Testing Strategies **Unit Testing (Jest):** ```javascript import { mockBrowser } from 'webextension-pockito'; describe('Extension', () => { beforeEach(() => mockBrowser.reset()); test('storage', async () => { await browser.storage.local.set({ key: 'value' }); const result = await browser.storage.local.get('key'); expect(result.key).toBe('value'); }); }); ``` **Integration Testing (Selenium):** ```javascript const { Builder } = require('selenium-webdriver'); const firefox = require('selenium-webdriver/firefox'); const driver = await new Builder() .forBrowser('firefox') .setFirefoxOptions(new firefox.Options() .setPreference('extensions.autoDisableScopes', 0)) .build(); ``` ## Best Practices ### Background Service Worker (MV3) ```javascript // Keep service worker alive briefly let keepAlive; browser.runtime.onMessage.addListener((msg) => { clearTimeout(keepAlive); keepAlive = setTimeout(() => {}, 25000); return handleMessage(msg); }); ``` ### Error Handling ```javascript async function safeAsync(fn) { try { return await fn(); } catch (error) { console.error('Error:', error); return { error: error.message }; } } ``` ### Cross-Browser Compatibility ```javascript import browser from 'webextension-polyfill'; // Feature detection if (browser.sidebarAction) { // Firefox-specific browser.sidebarAction.open(); } // Fallback pattern const action = browser.action || browser.browserAction; action.setPopup({ popup: 'popup.html' }); ``` ### Performance - Use `browser.tabs.query()` with specific filters - Debounce frequent operations - Cache storage API results - Use `alarms` API instead of `setInterval` in background ## Troubleshooting ### Common Issues | Error | Cause | Solution | |-------|-------|----------| | `browser is not defined` | Script not in extension context | Check manifest script paths | | Permission denied | Missing permission | Add to manifest permissions | | CSP violation | Remote script | Move script to extension package | | Service worker terminated | Long operation | Use alarms API, avoid blocking | | `webRequest` not blocking | MV3 limitation | Use `declarativeNetRequest` | | ID mismatch | Different IDs | Set consistent ID in manifest | ### Debug Commands ```bash # Validate manifest web-ext lint --verbose # Run with debug prefs web-ext run --pref extensions.webextensions.debug=true # Test in clean profile web-ext run --profile-create-new # Check specific Firefox web-ext run --firefox /path/to/firefox-beta ``` ## Cross-Browser with webextension-polyfill ### Installation ```bash npm install webextension-polyfill ``` ### Usage ```javascript // ES modules import browser from 'webextension-polyfill'; // CommonJS const browser = require('webextension-polyfill'); // Now browser.* works in Chrome with promises const tabs = await browser.tabs.query({ active: true }); ``` ### Manifest Setup ```json { "background": { "scripts": ["browser-polyfill.js", "background.js"] }, "content_scripts": [{ "matches": [""], "js": ["browser-polyfill.js", "content.js"] }] } ``` ### TypeScript Support ```bash npm install @types/webextension-polyfill ``` ```typescript import browser from 'webextension-polyfill'; async function getActiveTab(): Promise { const [tab] = await browser.tabs.query({ active: true }); return tab; } ``` ## File Structure Template ``` my-extension/ ├── manifest.json ├── background.js ├── content.js ├── popup.html ├── popup.js ├── options.html ├── options.js ├── browser-polyfill.js ├── styles/ │ ├── popup.css │ └── content.css ├── icons/ │ ├── icon-16.png │ ├── icon-32.png │ ├── icon-48.png │ └── icon-96.png ├── _locales/ │ ├── en/ │ │ └── messages.json │ └── it/ │ └── messages.json ├── .web-ext-config.js ├── package.json └── README.md ``` ## References - [MDN WebExtensions](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions) - [Extension Workshop](https://extensionworkshop.com/) - [Manifest V3 Migration](https://extensionworkshop.com/documentation/develop/manifest-v3-migration-guide/) - [web-ext CLI Reference](https://extensionworkshop.com/documentation/develop/web-ext-command-reference/) - [AMO Developer Hub](https://addons.mozilla.org/developers/) - [webextension-polyfill](https://github.com/mozilla/webextension-polyfill) - [Browser Compatibility](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser_support_for_JavaScript_APIs)