Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/branch-memory/feature-firefox-signed-xpi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# feature/firefox-signed-xpi
*Started: 2026-10-14 Β· Updated: 2026-10-15*

**What this branch did**
Resolved issue #100: the Firefox extension now ships as a signed, unlisted, self-hosted XPI. Merged as PR #105. Follow-ups: #108 set the version to 0.1.0, and #109 (generated by the workflow) published it. 0.1.0 went live at `savr.link/extension/firefox.xpi`. Installs and the auto-update to 0.1.1 were verified on desktop and Android, and #100 is closed. 0.1.1 (#110) narrowed the popup and added an "Extension settings" button. 0.1.2 (#111) fit the popup and options to phone screens. Primary-source research is in `docs/extension-distribution-research.md`. The decision and release process are in `browser-extension/README.md` (Distribution / Versioning / Releasing).

**Decisions**
- Signed as **unlisted on AMO, hosted on GitHub Releases** rather than an AMO listing. The user wanted less friction (no listing metadata or human review queue) and was open to listing later. Migrating is documented: ship one final self-hosted version *without* `update_url`, and Firefox moves those installs to AMO.
- `update_url` and the install link live on **savr.link**, while the files live on GitHub. Installed copies keep the `update_url` they were installed with forever, so the domain gives us an indirection we control.
- The publish step is a **PR that updates `public/extension/updates.json` and `public/_redirects`**, not a direct push to main. Merging that PR (plus the Netlify deploy) is the deliberate "go live" gate. The release and XPI exist before it.
- Extension releases are created with `--latest=false`, because the repo's single "latest release" belongs to the app (`v0.2.0`). Install and update links therefore use per-version URLs, never `releases/latest/download`.
- Extension versions are **independent semver with `ext-v*` tags** in the same repo. The app/extension contract is versioned separately by `protocol: 1` on bridge payloads.
- Started at **0.1.0, not 1.0.0**. Firefox only updates upward and AMO burns every submitted number, so 0.x keeps 1.0 for after Android and auto-update are verified.
- `data_collection_permissions` declares `websiteContent` + `browsingActivity` rather than `none`. Mozilla's definition doesn't cover "handed to a client-side app that syncs to the user's own storage", and declaring is the safe reading. That forced `strict_min_version` 142 (the key needs desktop 140 / Android 142; the user chose 142 for both).
- The extension stays **in this repo**. `youtube-transcript.js` is shared with the app's bookmarklet build, and the e2e tests pin both sides of the bridge.

**Rejected**
- Hosting the XPI on savr.link/Netlify with a `_headers` Content-Type rule. GitHub assets keep their upload Content-Type and avoid coupling releases to app deploys.
- Signing through the AMO web UI by hand. It works (it's the recovery path in the README), but the API key lets CI do it.
- Tying the extension version to the app version. Every app release would force a re-sign and push an update to every user.
- Chrome self-hosted `.crx`. Google Chrome on Windows/macOS disables off-store installs for consumers. Chrome is tracked as an unlisted Chrome Web Store item in #106.

**Gotchas**
- `updates.json` entries must use **`applications.gecko`**. Firefox silently ignores `browser_specific_settings` there (uBlock's file gets this wrong). A test in `scripts/extension-release.test.mjs` guards it.
- `gh release upload` stores `.xpi` as `application/octet-stream`, which makes Firefox download instead of install. The workflow uploads through the API with `application/x-xpinstall` and asserts the stored type.
- The vite-plugin-pwa service worker's `navigateFallback` would serve the app shell for `/extension/firefox.xpi`, so `vite.config.ts` has `navigateFallbackDenylist: [/^\/extension\//]`.
- `web-ext lint` errors on `update_url` unless run with `--self-hosted`, which is what AMO applies to unlisted uploads.
- Firefox **Android does not install from a web link** outside AMO. Users download the file, then Settings β†’ About Firefox β†’ tap the logo 5Γ— β†’ Install Extension from File.
- Firefox Android has no `windows` API, so `visitSavr` guards `api.windows`.
- In agent shells on this machine, Playwright's Chromium fails with GLIBC errors from an inherited `LD_LIBRARY_PATH` (the paseo alsa-lib). Run e2e as `env -u LD_LIBRARY_PATH -u PLAYWRIGHT_BROWSERS_PATH flox activate -c "node scripts/run-e2e.js …"`.
- AMO signing for 0.1.0 took about 3 minutes, against the documented "up to 24 hours".
- Firefox Android opens the popup and options as full pages. Without `<meta name="viewport">` they are laid out ~980px wide and scaled down to a tiny box. The `@media (pointer: coarse)` rules widen them on phones only, because desktop popups size to content and a width-based media query there would be circular.
- Forcing an extension update on Firefox Android: Settings β†’ About Firefox (tap the logo 5Γ—) β†’ Secret Settings β†’ Enable Debug Drawer β†’ floating button β†’ Add-ons Tools β†’ Check for updates. Otherwise Fenix checks every 12 h (`DefaultAddonUpdater` frequency in `Components.kt`).
- Publish PRs opened by the workflow (via GITHUB_TOKEN) trigger **no CI**. That's expected, since they only touch `updates.json`/`_redirects` and the release job already ran the script tests.
- CI `e2e-smoke` can flake when `npx http-server` downloads slowly (the content server misses its 10 s start window). Rerunning fixes it.

**Open threads**
- Confirm the 0.1.2 phone layout on a real device (it was only checked under Playwright Pixel 7 emulation).
- #106: Chrome Web Store, unlisted. Since #104, `*://*/*` is a *required* host permission, which may lengthen review.
- "Allow GitHub Actions to create and approve pull requests" is now enabled, so the workflow's fallback warning path won't be hit.
- The app has no version-gating on `protocol`. Add handling only when a breaking payload change happens.
Loading