Skip to content

Permissions and configuration

The manifest that the build makes

wxt.config.ts makes a manifest for each browser from one configuration. This is the Chrome MV3 manifest:

{
"name": "__MSG_extensionName__",
"description": "__MSG_extensionDescription__",
"default_locale": "en",
"version": "0.1.0",
"permissions": ["storage", "scripting", "tabs", "sidePanel"],
"host_permissions": ["*://*.patreon.com/*"],
"optional_host_permissions": ["*://*.patreonusercontent.com/*", "*://*.cpatreon.net/*"],
"action": { "default_title": "__MSG_actionTitle__" },
"commands": { "_execute_action": { "suggested_key": { "default": "Ctrl+Shift+Y" } } },
"content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self';" },
"side_panel": { "default_path": "sidepanel.html" },
"content_scripts": [{ "matches": ["*://*.patreon.com/*"] }]
}

Each __MSG_*__ value is a reference into public/_locales/. The browser puts the text of the user in its place when it reads the manifest. See localization.

WXT changes some keys for the Firefox MV2 build. It writes sidebar_action in place of side_panel, and it moves the host permissions into permissions. WXT does not change two other keys, thus wxt.config.ts writes them: optional_permissions in place of optional_host_permissions, and _execute_sidebar_action in place of _execute_action. _execute_sidebar_action is the reserved command that opens the sidebar. The earlier _execute_browser_action fired the toolbar button, which is a different surface, and no code listened to it.

Two keys need more than the manifest factory:

  • sidebar_action.default_title. WXT writes the whole sidebar_action key after the factory gives its result, and it takes the title from the <title> text of entrypoints/sidepanel/index.html. A __MSG_*__ reference in the factory has no effect. The build:manifestGenerated hook in wxt.config.ts writes the reference again after the build makes the manifest, thus the German sidebar header has German text.
  • background.persistent. WXT writes background from the options of the background entry point, not from the factory. defineBackground({ persistent: false, main() {...} }) in entrypoints/background.ts gives Firefox an event page. MV3 does not read the value. Without it, MV2 makes a full background page that stays in memory for the whole session. The design of the service worker expects the opposite.

Configuration decisions

  • Host permission: variant A (decided 2026-08-31). The manifest declares host_permissions for patreon.com, and the content script is static. Gopher is a viewer for patreon.com, thus the permission message names the origin that the user expects. The media CDNs keep optional host permissions (see media, CDN, and CSP).
  • A side panel and no popup. The manifest does not declare default_popup. If it declares a popup, a click on the icon opens the popup and not the side panel. The onInstalled handler calls sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).
  • CSP. The manifest keeps the default script-src 'self' of MV3. There is no frame-src, because Gopher frames no content. A third-party player opens in the tab of the provider (see the panel).
  • Keyboard. Ctrl+Shift+Y opens the panel. On macOS the keys are Cmd+Shift+Y.
  • Firefox. The value of browser_specific_settings.gecko.id is gopher@michi.onl. This ID must not change. The ID is the origin of the extension, and the origin is the key of the IndexedDB database. If the ID changes, the browser deletes the cache. The manifest also declares data_collection_permissions: { required: ['none'] } for AMO.
  • modulePreload is off in the Vite configuration. A <link rel="modulepreload"> tag does not operate in an extension origin. Chrome writes the message “cross-world extension resource mismatch” and shows an error mark on the extensions page.
  • No cookie code. There is no cookies permission, no call to chrome.cookies, and no OAuth. The runner uses the session of the page because it has the same origin (ADR-002).
  • No analytics software. Such software makes the statement “collects no data” false. Note that setUninstallURL sends a request when the user removes the extension, also when it has no parameters. Do not use it.
  • Language. default_locale is en, and the manifest gives five strings as __MSG_*__ references. Without default_locale the browser refuses to load an extension that uses such a reference. Read localization.
  • homepage_url gives https://gopher.michi.onl, the address of this site’s privacy policy. It also replaces the URL for removal that Gopher does not set. setUninstallURL() sends a request when a user removes the extension, and that would make the non-goal “no server” false (see principles).
  • The toolbar button has a listener. setPanelBehavior() opens the panel on Chrome. Firefox has no sidePanel, thus the worker also listens to action.onClicked and browserAction.onClicked and calls sidebarAction.toggle(). The listener is also the path for Chrome if setPanelBehavior() does not succeed. The manifest declares no default_popup, because a popup wins over the side panel.
  • The content security policy names img-src and media-src. Only the CDN hosts of Patreon, 'self', blob:, and data: can give a picture or a media file to a panel page. This makes the host list in lib/safe-url.ts structural (see media, CDN, and CSP). default-src stays unset: hls.js reads video on a web worker that no rule names, and a limit there needs an examination with a real stream.