Skip to content

feat(extension-core): serve binary fetch from the background for hosted file downloads - #122

Open
chrischall wants to merge 1 commit into
mainfrom
feat/fetch-binary-base64
Open

chrischall wants to merge 1 commit into
mainfrom
feat/fetch-binary-base64

Conversation

@chrischall

Copy link
Copy Markdown
Contributor

This is the extension half of chrischall/fetchproxy#460. The fetchproxy half is chrischall/fetchproxy#461.

A fetch with init.responseType: 'base64' now returns the response bytes, so a hosted MCP can read a PDF, an office file or an image through the user's browser. Before this, response.text() decoded every body lossily.

What changes

  • background/handlers/fetch-binary.ts (new)
    • Runs from the background service worker (an event page on Safari), not a tab. host_permissions make CORS irrelevant there, so a signed file host that sends no CORS headers is readable. It follows redirects.
    • Scope: the request URL (checked by handleFetchRequest, as for text) and the final URL after redirects must be on the MCP's declared domains. An off-domain final URL is cancelled before any of its body is read, and the error names its host only, so no token from the URL is echoed.
    • Cap: MAX_BINARY_RESPONSE_BYTES (16 MiB), applied early on content-length and again while streaming. The refusal uses code: 'response_too_large'.
    • Relays content-type, content-length and content-disposition.
    • 120 s transfer timeout.
    • Base64 is encoded in chunks, staying under the argument limit of JSC (Safari's JavaScript engine).
  • handlers/fetch.ts: routes responseType: 'base64' to the new handler after the existing url and tabUrl domain checks, and refuses inPage in that mode. Text mode is untouched.
  • socket.ts: the hello advertises features: ['fetch_base64'] on every platform. This is the one intended change to Chrome's hello bytes.
  • Docs: PRIVACY.md §4 names the new network path, with a guard test. The host_permissions justification, the extension-chrome README and CLAUDE.md are updated too.
  • No new permission.

⚠️ Depends on an unreleased protocol

@fetchproxy/protocol goes from ^3.6.3 to ^3.7.0, the release that will carry chrischall/fetchproxy#461. CI stays red until 3.7.0 is on npm. The lock entry has no integrity until then. Run npm install after the publish to record it, as was done for #25.

Release order: merge fetchproxy#461 → release fetchproxy 3.7.0 (npm view @fetchproxy/protocol version) → refresh this lockfile → merge this → release ContextMint Bridge. After that, submit to the Chrome Web Store and the App Store; Safari reaches users via apple/'s TestFlight/App Store build.

Tests

  • runBinaryFetch:
    • all 256 byte values round-trip exactly;
    • only the three headers are relayed;
    • redirect: 'follow', default credentials: 'include', and 'omit' are honoured;
    • the final URL of a redirect is reported;
    • an off-domain final URL is refused unread without echoing the URL;
    • early refusal on content-length (body never read);
    • streaming refusal with no length, and with a length that lies;
    • exact-cap accept, default cap, zero-byte body, network error, timeout, oversize request body.
  • handleFetchRequest:
    • no tab is messaged, and the answer passes the protocol's validateInnerFrame;
    • an off-domain request URL is refused before any fetch;
    • inPage with base64 is refused;
    • the typed size refusal is forwarded;
    • text mode keeps exactly its old response keys.
  • Hello: features is advertised on Chrome and Safari.

Run locally against the fetchproxy#461 protocol build: typecheck, npm test (117 files, 1642 tests) and npm run build (Chrome and Safari) all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_012aZHZ6pnrdzrECCUaQW3gY

…ed file downloads

Implements the extension half of chrischall/fetchproxy#460. A `fetch` with
`init.responseType: 'base64'` now returns the response BYTES instead of
`response.text()`'s lossy decode, so a hosted MCP can read a PDF, an office
file or an image through the user's browser.

- handlers/fetch-binary.ts: runs from the background service worker (event
  page on Safari), where host_permissions make CORS irrelevant, so a signed
  file host that sends no CORS headers is readable. Follows redirects.
  Scope: the request URL (checked by handleFetchRequest, as for text) AND the
  final URL after redirects must be on the MCP's declared domains; an
  off-domain final URL is cancelled unread and named by host only. Caps the
  body at MAX_BINARY_RESPONSE_BYTES (16 MiB) early on content-length and
  again while streaming, refusing with code 'response_too_large'. Relays
  content-type / content-length / content-disposition. 120 s transfer
  timeout. Chunked base64 encoding (JSC argument limit safe).
- handlers/fetch.ts: routes responseType 'base64' there after the existing
  url / tabUrl domain checks; refuses inPage in this mode. Text mode is
  untouched.
- socket.ts: the hello advertises `features: ['fetch_base64']` on every
  platform (unsigned, advisory; servers trust the answer's bodyEncoding
  marker).
- docs: PRIVACY.md names the new network path, the host_permissions
  justification and extension-chrome README cover it, CLAUDE.md gotcha.
- deps: @fetchproxy/protocol ^3.6.3 -> ^3.7.0 (first-party; the release
  carrying responseType, bodyEncoding, features and the size-refusal
  helpers). CI stays red until 3.7.0 is on npm. The lock entry has no
  integrity until then; `npm install` after the publish records it.

No new permission.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aZHZ6pnrdzrECCUaQW3gY
@chrischall chrischall added the enhancement Release notes: Features label Nov 7, 2026
@github-actions github-actions Bot added the auto-review Auto-review pipeline is handling this PR label Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

✅ Auto-review verdict: pass — The background binary-fetch path checks scope correctly: the request URL and tabUrl are checked first, then the final URL after redirects, before any of the body is read. The size cap applies both up front and while streaming, and the tests cover these paths well. I found no blocking issues. The only open question is whether @fetchproxy/protocol 3.7.0 has been published, which I couldn't check.

Reviewed commit 8bf3a63.


No issues found.

What I checked

  • handlers/fetch-binary.ts (new). The final URL (response.url || init.url) is checked with isUrlAllowedForAnyDomain before the body is read. An off-domain final URL cancels the body and the error names only the host (hostOf), so tokens in the query string aren't echoed. The cap is enforced up front on a numeric content-length that is over the limit, and again on the streamed total, so a missing or understated length is still caught. Status values that aren't positive integers are rejected. Only RELAYED_RESPONSE_HEADERS are copied. The timer is cleared in finally, and a timeout is reported separately from a network error. Base64 is encoded in chunks of 3×0x4000 bytes, so there's no padding mid-stream and each fromCharCode.apply call stays under ~65k arguments. The request-body guard matches the text path's guard (content.ts:841: if (init.body && init.body.length > MAX_REQUEST_BODY_BYTES)).
  • handlers/fetch.ts. The binary branch runs only after the existing inPage capability check and both domain checks (url, tabUrl). inPage: true with base64 is refused outright rather than silently dropped. The text path is unchanged, and the text-mode test pins that its response keys are the same as before.
  • socket.ts / platform.test.ts. The hello gains features: ['fetch_base64'] on Chrome and Safari. The Chrome hello-keys test was updated deliberately and annotated. This adds a field and doesn't rename or remove anything, so it isn't a contract break.
  • Docs. PRIVACY.md §4 names the new background network path, and a guard test checks for it. The new relay-visible field ("optional features it supports") is documented. The permission justifications, the chrome README and CLAUDE.md are updated. No new permission is added.
  • Tests. fetch-binary.test.ts covers byte-exact round-tripping, header filtering, credentials and redirects, off-domain redirect refusal without echoing the URL, early and streaming cap refusals, exact-cap acceptance, empty bodies, network errors, timeouts, oversize request bodies, and handler integration, including validateInnerFrame. Two small branches have no dedicated test: the non-positive-status rejection and a timeout that fires while the body is being read. That isn't enough to raise as a finding.

Open questions (not counted toward the verdict)

  • Dependency not yet verifiable. The lockfile entry for @fetchproxy/protocol@3.7.0 has no integrity field. The PR body says CI stays red until 3.7.0 is published, and that npm install should be re-run afterwards. I couldn't check whether 3.7.0 is on npm yet, because npm view needed approval in this sandbox. I also can't confirm that the symbols the PR imports (MAX_BINARY_RESPONSE_BYTES, RELAYED_RESPONSE_HEADERS, RESPONSE_TOO_LARGE_CODE, responseTooLargeMessage, extensionFeaturesHelloField, EXTENSION_FEATURE_FETCH_BASE64) exist in the published package. Before merging, confirm that 3.7.0 is out and that the lockfile has been refreshed with its integrity hash.
  • No tab needed for binary fetches. By design, a binary fetch doesn't need a matching open tab. In text mode the request only goes through if a tab on the declared domain is open. Binary mode can reach any declared domain whenever the extension is connected. That fits the per-MCP domain approval and the documentation, but it's a small widening of when a request can happen, which the owner may want to acknowledge.

Verdict: pass

@chrischall chrischall added the ready-to-merge Arms auto-merge — added by the pipeline on a pass/warn verdict, never by hand label Nov 7, 2026
@chrischall
chrischall enabled auto-merge (squash) November 7, 2026 12:45

This branch has not been deployed

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

Labels

auto-review Auto-review pipeline is handling this PR enhancement Release notes: Features ready-to-merge Arms auto-merge — added by the pipeline on a pass/warn verdict, never by hand

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant