Skip to content

feat(extension): Firefox runtime — recorder window, storage fallbacks, and overlay injection - #2001

Open
ManthanNimodiya wants to merge 12 commits into
CapSoftware:mainfrom
ManthanNimodiya:feat/extension-firefox-runtime
Open

feat(extension): Firefox runtime — recorder window, storage fallbacks, and overlay injection#2001
ManthanNimodiya wants to merge 12 commits into
CapSoftware:mainfrom
ManthanNimodiya:feat/extension-firefox-runtime

Conversation

@ManthanNimodiya

@ManthanNimodiya ManthanNimodiya commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2000 #1998 , only the last commits are new here, the earlier ones will disappear once the base PR merges.

Makes the Firefox build fully functional; every fix here was found and verified in live Firefox QA (recording → instant upload → share-page playback confirmed working on Firefox ESR 140):

  • Recorder host: Firefox has no offscreen API, so the recorder document runs in a small branded popup window, centered over the browser; a one-click arm button supplies the transient activation getDisplayMedia requires, then the window minimizes and self-closes when idle. Lifecycle hardened: URL-based context lookup, unresponsive windows are closed on ready-timeout, interactive survives send retries.
  • storage.session.setAccessLevel doesn't exist on Firefox, the unconditional call killed the background on every start; now optional-chained. Background + overlay ship as classic scripts on Firefox (module event pages/import() in content scripts are unreliable/unsupported, bugzil.la/1536094); the overlay is injected by the SW via a new inject-overlay-module message.
  • storage.session is entirely absent in Firefox content scripts, shared session-state keys route through storage.local on Firefox (SHARED_STATE_AREA), cleared at browser startup to keep session semantics.
  • Capability gating (tab-capture mode and system-audio hidden on Firefox), welcome-page host-permission grant (Firefox MV3 host perms are opt-in), camera-permission copy, and a build guard that fails if the bundle is missing the background script.
  • Advisory for review: Firefox 152.0.4 on macOS 26.5 crashed reproducibly in SpiderMonkey's module loader while loading extension pages (SIGABRT in js::ModuleEnvironmentObject, no MozCrashReason) — unrelated to this code (ESR 140 is fine), but worth a check on another 152 machine before AMO submission.

Greptile Summary

This PR makes the Chrome extension work as a Firefox build. The main changes are:

  • Adds target-specific Chrome and Firefox manifests and build outputs.
  • Replaces the offscreen recorder with a Firefox popup recorder host.
  • Adds Firefox storage fallbacks for shared recording UI state.
  • Injects the overlay through the service worker for Firefox content scripts.
  • Adds Firefox host-permission, capture-mode, and system-audio gating.

Confidence Score: 4/5

This should be fixed before merging the Firefox runtime work.

  • Closing the Firefox recorder popup can leave the start flow permanently blocked.
  • Firefox shared recording UI state can survive extension update or reload and reappear in open tabs.

Files Needing Attention: apps/chrome-extension/src/background/service-worker.ts

Important Files Changed

Filename Overview
apps/chrome-extension/src/background/service-worker.ts Adds Firefox recorder-host wiring, overlay injection, startup clearing, and permission reinjection, with remaining lifecycle gaps in recorder close recovery and local shared-state clearing.
apps/chrome-extension/src/shared/storage.ts Routes shared session-like keys through target-specific storage and exposes a Firefox clear helper.
apps/chrome-extension/src/shared/storage-keys.ts Defines the target-specific shared-state storage area used by content scripts and extension pages.
apps/chrome-extension/src/welcome/main.ts Adds the Firefox host-permission grant flow and includes local-file origins.
apps/chrome-extension/src/popup/main.tsx Adds Firefox host-access status handling and hides unsupported capture controls.
apps/chrome-extension/src/background/recorder-host.ts Adds the cross-browser recorder host abstraction for offscreen documents and Firefox popup windows.
Prompt To Fix All With AI
### Issue 1
apps/chrome-extension/src/background/service-worker.ts:1855-1856
**Start Promise Stays Wedged**
When the Firefox recorder window is closed during the arm-button phase, this resets the visible status but leaves `recordingStartInFlight` pointing at the original start promise. If the start message already reached the recorder document, closing that document can leave `awaitCaptureGesture()` pending forever, so the `.finally()` that clears `recordingStartInFlight` never runs. The UI returns to idle, but later start attempts reuse the dead promise and recording cannot start again until the background is restarted.

### Issue 2
apps/chrome-extension/src/background/service-worker.ts:106-108
**Local State Survives Updates**
Firefox now stores shared recording UI state in `storage.local`, but this clears those session-like keys only on browser startup. Extension updates and reloads run `runtime.onInstalled`, which reinjects the bootstrap into open tabs without clearing stale `cap-extension-recording-state` or panel state first. If an old `creating` or `recording` value is present, the freshly injected content script reads it from `storage.local` and shows the recording UI even though no recorder host exists.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (3): Last reviewed commit: "fix(extension): include file:// in Firef..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Context used:

  • Context used - CLAUDE.md (source)
  • Context used - AGENTS.md (source)

@ManthanNimodiya ManthanNimodiya changed the title Feat/extension firefox runtime feat(extension): Firefox runtime — recorder window, storage fallbacks, and overlay injection Jul 11, 2026
Comment on lines +1925 to +1928
chrome.windows.onRemoved.addListener(() => {
if (capabilities.supportsOffscreen) return;
void syncRecordingStatus().catch(() => undefined);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Creating State Stays Stuck

When the Firefox recorder popup is closed while it is waiting on the arm button, this removal handler calls syncRecordingStatus(), but that path only resets statuses considered active. creating is not active, so the service worker keeps returning the stale starting state and the recording UI can stay stuck until another action overwrites it.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/chrome-extension/src/background/service-worker.ts
Line: 1925-1928

Comment:
**Creating State Stays Stuck**

When the Firefox recorder popup is closed while it is waiting on the arm button, this removal handler calls `syncRecordingStatus()`, but that path only resets statuses considered active. `creating` is not active, so the service worker keeps returning the stale starting state and the recording UI can stay stuck until another action overwrites it.

How can I resolve this? If you propose a fix, please make it concise.

// script (overlay, countdown, recording bar) stays inert until the user
// grants access here. The click on the button supplies the required user
// gesture for permissions.request.
const HOST_PERMISSION_ORIGINS = ["http://*/*", "https://*/*"];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 File Permission Never Granted

The Firefox manifest declares file:///* and the injection logic accepts file: tabs, but this permission request only asks for http and https. A Firefox user can complete the new grant flow and still never grant local-file access, so the content script, countdown, and recording toolbar will not run on file:// pages despite the manifest advertising support.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/chrome-extension/src/welcome/main.ts
Line: 53

Comment:
**File Permission Never Granted**

The Firefox manifest declares `file:///*` and the injection logic accepts `file:` tabs, but this permission request only asks for `http` and `https`. A Firefox user can complete the new grant flow and still never grant local-file access, so the content script, countdown, and recording toolbar will not run on `file://` pages despite the manifest advertising support.

How can I resolve this? If you propose a fix, please make it concise.

// setAccessLevel at all — calling it unconditionally throws and kills the
// whole background script — so content scripts there rely on the runtime
// message fallbacks instead of the session-storage mirror.
chrome.storage.session.setAccessLevel?.({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor hardening: this still throws if chrome.storage.session is undefined (not just setAccessLevel).

Suggested change
chrome.storage.session.setAccessLevel?.({
chrome.storage.session?.setAccessLevel?.({

Comment on lines +1925 to +1928
chrome.windows.onRemoved.addListener(() => {
if (capabilities.supportsOffscreen) return;
void syncRecordingStatus().catch(() => undefined);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fires on any window close; quick guard avoids extra getContexts calls when idle.

Suggested change
chrome.windows.onRemoved.addListener(() => {
if (capabilities.supportsOffscreen) return;
void syncRecordingStatus().catch(() => undefined);
});
chrome.windows.onRemoved.addListener(() => {
if (capabilities.supportsOffscreen) return;
if (recordingStatus.phase === "idle") return;
void syncRecordingStatus().catch(() => undefined);
});

Comment on lines +42 to +45
readFileSync(
resolve(__dirname, `../../manifests/manifest.${target}.json`),
"utf8",
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__dirname can be undefined under ESM (and this package is type: module). using import.meta.url keeps this stable in vitest.

Suggested change
readFileSync(
resolve(__dirname, `../../manifests/manifest.${target}.json`),
"utf8",
),
readFileSync(
new URL(`../../manifests/manifest.${target}.json`, import.meta.url),
"utf8",
),

@ManthanNimodiya
ManthanNimodiya force-pushed the feat/extension-firefox-runtime branch from 29f0437 to 03f6060 Compare July 21, 2026 16:13
@socket-security

socket-security Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​web-ext@​10.5.0961001009370

View full report

@socket-security

socket-security Bot commented Jul 21, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm @fregante/relaxed-json is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/web-ext@10.5.0npm/@fregante/relaxed-json@2.0.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@fregante/relaxed-json@2.0.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm @pnpm/network.ca-file is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/web-ext@10.5.0npm/@pnpm/network.ca-file@1.0.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@pnpm/network.ca-file@1.0.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@ManthanNimodiya
ManthanNimodiya force-pushed the feat/extension-firefox-runtime branch from 03f6060 to 82eb526 Compare July 21, 2026 16:22
@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@greptileai

const resolveAvailableMode = (mode: RecordingMode): RecordingMode =>
mode === "tab" && !capabilities.supportsTabCapture ? "fullscreen" : mode;

const HOST_PERMISSION_ORIGINS = ["http://*/*", "https://*/*"];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 File Access Check Missing
This popup check still treats http/https access as enough on Firefox. If a user grants those origins but leaves file access disabled, hostAccessGranted becomes true, the warning is hidden, and recording from a file:// tab still has no injected countdown or toolbar because file:///* was never granted. Keep this list in sync with the welcome-page request and the Firefox manifest.

Suggested change
const HOST_PERMISSION_ORIGINS = ["http://*/*", "https://*/*"];
const HOST_PERMISSION_ORIGINS = ["http://*/*", "https://*/*", "file:///*"];
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/chrome-extension/src/popup/main.tsx
Line: 101

Comment:
**File Access Check Missing**
This popup check still treats http/https access as enough on Firefox. If a user grants those origins but leaves file access disabled, `hostAccessGranted` becomes true, the warning is hidden, and recording from a `file://` tab still has no injected countdown or toolbar because `file:///*` was never granted. Keep this list in sync with the welcome-page request and the Firefox manifest.

```suggestion
const HOST_PERMISSION_ORIGINS = ["http://*/*", "https://*/*", "file:///*"];
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@ManthanNimodiya
ManthanNimodiya force-pushed the feat/extension-firefox-runtime branch from 056733a to 9825b54 Compare August 6, 2026 05:08
@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment on lines +1855 to +1856
if (recordingStatus.phase === "creating") {
setRecordingStatusAndBroadcast({ phase: "idle" });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Start Promise Stays Wedged
When the Firefox recorder window is closed during the arm-button phase, this resets the visible status but leaves recordingStartInFlight pointing at the original start promise. If the start message already reached the recorder document, closing that document can leave awaitCaptureGesture() pending forever, so the .finally() that clears recordingStartInFlight never runs. The UI returns to idle, but later start attempts reuse the dead promise and recording cannot start again until the background is restarted.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/chrome-extension/src/background/service-worker.ts
Line: 1855-1856

Comment:
**Start Promise Stays Wedged**
When the Firefox recorder window is closed during the arm-button phase, this resets the visible status but leaves `recordingStartInFlight` pointing at the original start promise. If the start message already reached the recorder document, closing that document can leave `awaitCaptureGesture()` pending forever, so the `.finally()` that clears `recordingStartInFlight` never runs. The UI returns to idle, but later start attempts reuse the dead promise and recording cannot start again until the background is restarted.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +106 to +108
chrome.runtime.onStartup.addListener(() => {
void clearSharedSessionState().catch(() => undefined);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Local State Survives Updates
Firefox now stores shared recording UI state in storage.local, but this clears those session-like keys only on browser startup. Extension updates and reloads run runtime.onInstalled, which reinjects the bootstrap into open tabs without clearing stale cap-extension-recording-state or panel state first. If an old creating or recording value is present, the freshly injected content script reads it from storage.local and shows the recording UI even though no recorder host exists.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/chrome-extension/src/background/service-worker.ts
Line: 106-108

Comment:
**Local State Survives Updates**
Firefox now stores shared recording UI state in `storage.local`, but this clears those session-like keys only on browser startup. Extension updates and reloads run `runtime.onInstalled`, which reinjects the bootstrap into open tabs without clearing stale `cap-extension-recording-state` or panel state first. If an old `creating` or `recording` value is present, the freshly injected content script reads it from `storage.local` and shows the recording UI even though no recorder host exists.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant