fix(extension-chrome): build content scripts as classic IIFE so Chrome injects them - #175
Conversation
…e injects them MV3 injects content scripts as *classic* scripts. A built content-script file that carries a top-level `import`/`export` statement is not a valid classic script, so Chrome silently refuses to inject it — its `chrome.runtime.onMessage` listener never registers and every service-worker→content-script `sendMessage` fails with "Could not establish connection. Receiving end does not exist" (surfaced by the bridge as "N URL match(es), none responded"). `build.ts` compiled every entry with `format: 'esm'`. That was fine until commit 0ce0949 (#148) added `export function readDomValues` to `content.ts`: the bundle became a real ES module ending in `export { readDomValues };`, and `content.js` stopped injecting. Because `capture-logger.js` has no exports it kept injecting, so storage/CSRF capture still worked while every fetch() through the bridge died — exactly the "bridge down / Receiving end does not exist" outage seen in the field for the fetchproxy-backed cohort MCPs (opentable, tock). Split the build: background + popup remain `esm` (genuine modules — manifest `"type":"module"` and `<script type="module">`), while content.ts + capture-logger.ts build as `iife`, which emits no top-level import/export regardless of what the source exports. Adds tests/content-scripts-classic.test.ts to enforce the invariant (fails on the old esm config, passes on iife). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @chrischall's task in 4m 35s —— View job I'll analyze this and get back to you. |
| it('background is loaded as a module service worker', async () => { | ||
| // Not asserting on export/import here — background is declared | ||
| // `"type": "module"` in the manifest and popup via | ||
| // `<script type="module">`, so ESM output is correct for them. | ||
| expect(moduleEntryOptions.format).toBe('esm'); | ||
| expect(contentScriptEntryOptions.format).toBe('iife'); |
There was a problem hiding this comment.
🟡 Nit: this test's name ("background is loaded as a module service worker") doesn't match what it checks — it only re-asserts the static .format string on the same option objects under test, without ever building/inspecting a background.js bundle. It's tautological (true by construction from the object literals in build.ts) and doesn't add regression coverage beyond what the first describe block already exercises for content scripts. Consider either dropping this test or replacing it with an actual bundle build + assertion that the background output contains a real import/export (mirroring the classic-script checks above, inverted).
Review: build content scripts as classic IIFERoot cause and fix are correct. MV3 content scripts must be classic scripts (no top-level Other observations, all verified against the code:
One nit (left inline): the third test ("background is loaded as a module service worker") doesn't actually build or inspect a No correctness, security, or convention issues found. Verdict: pass |
|
🟡 Auto-review verdict: warn — The classic-IIFE build fix correctly resolves the MV3 content-script injection bug and is backed by a targeted regression test; only a minor test-quality nit was found, no correctness/security/convention issues. |
…o client (#178) ## Summary Adds a new opt-in `graphql` capability so an MCP can invoke a page-declared GraphQL operation through the matched tab's own `window.__APOLLO_CLIENT__` in the MAIN world, instead of the isolated-world `fetch()` path. **Why:** some endpoints (OpenTable's `RestaurantsAvailability`) reject the isolated-world `fetch` at the edge — the bot-detection telemetry (Akamai) lives inside the page's own Apollo link chain, not on `window.fetch`. Routing through the real client clears it. Verified live against a signed-in opentable.com tab (La Belle Hélène, restaurant_id 1175428): the isolated-world path 403/409s; `client.query(...)` through the page's own Apollo client returns 200 with real slots, using the MCP's existing (unchanged) variable shape. **Design:** the extension carries no hardcoded query text or persisted-query hash. It captures the live `DocumentNode` the page's Apollo client already observed for a declared `operationName` and reuses it — so it auto-adapts when a site revises its query. Gated by capability + a declared `graphqlOps` allowlist (approved and diffed at pair time, like every other capability) + the existing domain allowlist and host-or-subdomain tab match. Full design + the live PoC findings: `docs/superpowers/specs/2026-07-29-graphql-page-apollo-capability.md`. **Caught in review:** the extension-bridge task initially shipped the MAIN-world bridge with a top-level ES `export`, which MV3 silently refuses to inject as a classic content script (this branch was missing the classic-IIFE build split from #175). Fixed by rebuilding `extension-chrome/build.ts` with the `content`/`capture-logger` → `format:'iife'` split, with a regression test (`content-scripts-classic.test.ts`) pinning no top-level export in either bundled script. Also added size guards on `graphql_query` request `variables` / response `data`, mirroring the existing `fetch` op's `MAX_REQUEST_BODY_BYTES`/`MAX_RESPONSE_BODY_BYTES` convention. No version bumps (release-please owns that). ## Test plan - [x] `npm test` — 1042/1042 passing across 75 files - [x] `npm run build` — clean across all 7 workspaces - [x] `npm run typecheck` — clean - [x] Confirmed no top-level `export` in bundled `content.js`/`capture-logger.js` - [x] Live-verified the Apollo-client routing approach against opentable.com in a real signed-in tab (see spec doc) - [ ] Follow-up (separate PRs, gated on this releasing): bump `@fetchproxy/server` in `mcp-utils`, then switch `opentable-mcp`'s `find_slots` to the new capability and re-verify live 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
🤖 I have created a release *beep* *boop* --- ## [1.7.0](v1.6.2...v1.7.0) (2026-07-29) ### Features * **graphql:** route declared GraphQL ops through the tab's own Apollo client ([#178](#178)) ([0c3fdf4](0c3fdf4)) ### Bug Fixes * **extension-chrome:** build content scripts as classic IIFE so Chrome injects them ([#175](#175)) ([f4a3728](f4a3728)) * **graphql:** address all four tracked nits from PR [#178](#178 auto-review ([#180](#180)) ([9d88ac9](9d88ac9)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Root cause
MV3 injects content scripts as classic scripts. A built content-script file that carries a top-level
import/exportis not a valid classic script, so Chrome silently refuses to inject it —content.ts'schrome.runtime.onMessagelistener never registers, and every service-worker→content-scriptsendMessagefails with "Could not establish connection. Receiving end does not exist" (surfaced by the bridge as "N URL match(es), none responded").build.tscompiled every entry point withformat: 'esm'. That was harmless until0ce0949(#148) addedexport function readDomValuestocontent.ts: the bundle became a real ES module ending inexport { readDomValues };, andcontent.jsstopped injecting.capture-logger.js(no exports) kept injecting — so CSRF/storage capture still worked while everyfetch()through the bridge died. That is the "bridge down / Receiving end does not exist" outage seen in the field for the fetchproxy-backed cohort MCPs (opentable, tock); Resy was unaffected because it doesn't use the bridge.Diagnosed live: on a fresh, signed-in, fully-loaded OpenTable tab,
data-fetchproxy-csrfwas present (capture-logger alive) yet the fetch still returned "2 URL matches, none responded" — proving the split wascontent.js-specific, and the built file ended inexport { readDomValues };.Fix
Split the esbuild build:
esm(genuine modules — manifest"type": "module"and<script type="module">).iife, which emits no top-levelimport/exportregardless of what the source exports.Test
tests/content-scripts-classic.test.tsasserts the content-script bundles contain no top-levelimport/export(and thatcontent.jsstill registers itsonMessage/fetchproxy-fetchlistener). It fails on the oldesmconfig and passes oniife.Full suite green (985 tests), typecheck clean.
🤖 Generated with Claude Code