Zotero Connectors target four browsers — Chrome, Firefox, Edge, and Safari — from a single shared codebase. While the WebExtension standard covers most functionality, each browser has meaningful divergences in its extension model, API surface, and runtime constraints. This page documents every place where the connector code branches per browser and explains the engineering decisions behind each workaround.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/zotero/zotero-connectors/llms.txt
Use this file to discover all available pages before exploring further.
Manifest V2 vs Manifest V3
The most significant cross-browser split is between Manifest V2 (Firefox) and Manifest V3 (Chrome and Edge). The build system produces two separate extension directories from the same sources:build/firefox/ (MV2) and build/manifestv3/ (MV3).
Background process
| MV2 — Firefox | MV3 — Chrome / Edge | |
|---|---|---|
| Background type | Event page (persistent as needed) | Service worker |
| Manifest key | background.scripts array | background.service_worker |
| Entry point | background.js (concatenated script list) | background-worker.js |
| DOM available | Yes | No |
| Lifetime | Stays alive while events are queued | Terminated after ~30 seconds of inactivity |
Keeping the MV3 service worker alive
Because Chrome terminates idle service workers,keep-mv3-alive.js is included in the MV3 background. It periodically pings chrome.runtime.getPlatformInfo() to reset the idle timer:
10*60e3 milliseconds) with no pending operations, the interval clears itself and the service worker is allowed to terminate naturally.
Offscreen documents (MV3 only)
Service workers have no access to the DOM, but the Zotero translation framework needs to parse HTML. MV3 solves this with Chrome’s offscreen document API.Zotero.OffscreenManager (in src/browserExt/background/offscreenManager.js) manages the lifecycle:
| Document | Purpose |
|---|---|
offscreen/offscreen.html + offscreen.js | Hosts the translation engine; communicates with the service worker via postMessage |
offscreen/offscreenSandbox.html + offscreenSandbox.js | Sandboxed environment for running untrusted translator code; listed in manifest-v3.json’s sandbox.pages |
offscreen permission is required in the MV3 manifest:
Zotero.isManifestV3
The build system sets this flag at compile time via scripts/replace_browser.js. Use it to branch MV3-specific code:
Inject scripts
The MV3 inject bundle includes one extra file not present in MV2:test/testInject.js for the in-extension test runner.
Browser Detection Flags
All browser-detection properties are set by the build system inzotero.js via scripts/replace_browser.js. Their values are true or false constants — there is no runtime UA sniffing for the primary flags.
isChromium, isChrome, and isEdge are derived at runtime (not set by the build script) because both Chrome and Edge use the same MV3 build and are distinguished only by user-agent string:
Extensions installed from the Chrome Web Store on Opera or other Chromium-based browsers will also resolve
isChrome = true, since the connector treats any non-Firefox, non-Edge BrowserExt context as Chrome-compatible.Chrome / Chromium-Specific Behaviour
ArrayBuffer message passing
Chrome cannot natively passArrayBuffer objects through the extension message-passing channel. The connector works around this in three different ways depending on the build target:
MAX_CONTENT_SIZE constant is 8 * 1024 * 1024 (8 MB). This cap covers the vast majority of attachment downloads; images larger than 8 MB are truncated.
chromeMessageIframe for cross-origin messaging
MV3 Chrome does not allow injected scripts to call browser.runtime.sendMessage across origins directly in some contexts. src/browserExt/chromeMessageIframe/ provides an iframe-based relay. The iframe is listed as a web-accessible resource in manifest-v3.json:
declarativeNetRequest and styleInterceptRules.json
The MV3 manifest uses declarativeNetRequest (rather than the deprecated webRequestBlocking) for intercepting file downloads. Static rules are defined in src/browserExt/styleInterceptRules.json and referenced from the manifest:
Minimum Chrome version
Firefox-Specific Behaviour
MV2 only
As of this writing, Firefox supports only Manifest V2. The Firefox build usesbuild/firefox/manifest.json with the applications.gecko block:
applications property from the Chrome manifest using jq since Chrome rejects it.
First-party isolation cookie handling
When Firefox’s First-Party Isolation (FPI) feature is enabled, calls tobrowser.cookies.getAll() fail unless firstPartyDomain is provided. The connector passes null as a fallback, which allows saves to proceed even though cookie-dependent attachment downloads may fail:
ArrayBuffer message passing
Firefox supports passingArrayBuffer objects directly through browser.runtime.sendMessage. The packArrayBuffer helper simply returns the buffer as-is for Firefox:
Safari-Specific Behaviour
Safari uses the safari-app-extension model rather than the WebExtension model and requires a separate repository and Xcode build. Thebuild/safari/ directory produced by ./build.sh -p s is consumed by that repository.
Global page instead of service worker
Safari’s equivalent of a background page is a global page (src/safari/global.html + src/safari/global.js). It runs persistently (no service worker termination concerns) and handles all background logic.
i18n via message passing
Chrome and Firefox provide synchronousbrowser.i18n.getMessage(). Safari does not expose this API to extension pages, so the Safari build ships its own i18n layer (src/safari/i18n.js) that sends a message to the global page:
_locales/ directory structure is still used for string storage; the difference is only in how strings are looked up at runtime.
ArrayBuffer encoding
Safari cannot passArrayBuffer through its message-passing API. The connector encodes binary data as Base64 before sending:
arrayBufferToBase64 is a byte-wise encoder that does not rely on btoa, making it safe for arbitrary binary data including null bytes.
JS context compatibility shim
src/safari/jscontext_shim.js patches differences in the Safari JavaScript context (including the extension’s global page environment) to bring it closer to the standard WebExtension context expected by shared code.
URL polyfill
src/safari/url-polyfill.js provides the URL and URLSearchParams globals for Safari contexts where they are unavailable.
Empty browser-polyfill.js
During the build, an empty browser-polyfill.js (containing only ;) is written to build/safari/. Safari’s messaging layer is handled by native code, so the polyfill used in BrowserExt builds is not needed — but the file must exist because some shared HTML pages unconditionally include it.
browser-polyfill.js
Chrome exposes its extension API as chrome.* rather than browser.*. All shared connector code is written against the browser.* WebExtension API. The BrowserExt builds include Mozilla’s official browser-polyfill.js, which wraps chrome.* calls to provide the Promise-based browser.* interface:
browser.* support, so the polyfill is a no-op there. Safari has its own API bridge in the native extension code.
Per-Browser Loading Instructions
- Chrome
- Firefox
- Safari
- Edge
- Build:
./build.sh -d -p b - Open
chrome://extensions/ - Enable Developer mode (top-right toggle)
- Click Load unpacked and select
build/manifestv3/ - After rebuilding, click the ↺ reload icon on the extension card or reload the extensions page
