Skip to content

fix: reflect CORS for chrome-extension origins so Side Panel fetches succeed#1550

Open
Bmathews721 wants to merge 1 commit into
garrytan:mainfrom
Bmathews721:fix/cors-sidepanel-fetches
Open

fix: reflect CORS for chrome-extension origins so Side Panel fetches succeed#1550
Bmathews721 wants to merge 1 commit into
garrytan:mainfrom
Bmathews721:fix/cors-sidepanel-fetches

Conversation

@Bmathews721
Copy link
Copy Markdown

Summary

  • Side Panel fetches to /health, /command, /refs, /activity/stream, and terminal-agent's /claude-available were failing CORS because none of those responses set Access-Control-Allow-Origin or Access-Control-Allow-Credentials. Panel stays stuck on "Browse server not ready" and the install-card shows "Claude Code not found" even when claude is on PATH.
  • Adds a withCors middleware in server.ts that wraps both makeFetchHandler call sites — preflight OPTIONS 204 + response-header reflection, only for origins starting with chrome-extension://. Arbitrary websites still get no CORS headers.
  • Same Origin + Credentials reflection on terminal-agent's /claude-available. The /ws upgrade keeps Sec-WebSocket-Protocol auth and doesn't need CORS; /internal/* stays loopback + bearer-auth.
  • 3 source-pattern tests matching the existing /refs has no wildcard CORS header style in server-auth.test.ts.

Repro before fix

$ ./browse/dist/browse connect       # launch headed
# Open Side Panel in the launched Chromium → DevTools console:
# Access to fetch at 'http://127.0.0.1:34567/health' from origin
# 'chrome-extension://fglemgoabpknaffepocnhgkikfpfjflj' has been blocked
# by CORS policy: No 'Access-Control-Allow-Origin' header is present...

After the panel is reloaded, the next blocker surfaces:

Access to fetch at 'http://127.0.0.1:53144/claude-available' ... has been
blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials'
header in the response is '' which must be 'true' when the request's
credentials mode is 'include'.

Verification curl

$ curl -H "Origin: chrome-extension://abc" -D - http://127.0.0.1:34567/health
HTTP/1.1 200 OK
Access-Control-Allow-Origin: chrome-extension://abc
Access-Control-Allow-Credentials: true

$ curl -H "Origin: https://evil.com" -D - http://127.0.0.1:34567/health
HTTP/1.1 200 OK
(no Access-Control-* headers — non-extension origins still blocked)

$ curl -X OPTIONS -H "Origin: chrome-extension://abc" -D - http://127.0.0.1:34567/refs
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: chrome-extension://abc
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true

Test plan

  • bun test browse/test/server-auth.test.ts browse/test/terminal-agent.test.ts — 3 new tests pass; pre-existing failures unrelated (markers for Sidebar agent started etc.)
  • Live curl against running headed server — both endpoints return correct CORS headers for chrome-extension origins
  • Non-extension origins receive zero CORS headers (security boundary preserved)
  • Side Panel reaches "connected" state in real browser; embedded PTY card transitions from "Claude Code not found" to terminal-ready

Security notes

  • Only reflects Origin when it starts with chrome-extension://. Never wildcards. Arbitrary websites can't read these endpoints' responses.
  • /health token exposure is unchanged (still gated on headed-mode or chrome-extension origin per the existing logic).
  • /ws upgrade in terminal-agent is unchanged — still requires Sec-WebSocket-Protocol auth + extension Origin check.
  • /internal/grant and /internal/revoke still loopback + Bearer; CORS doesn't apply.

🤖 Generated with Claude Code

…succeed

The Side Panel runs in a chrome-extension:// origin and fetches /health,
/command, /refs, /activity/stream, and the terminal-agent's /claude-available
with credentials:'include'. None of those responses set
Access-Control-Allow-Origin or Access-Control-Allow-Credentials, so every
fetch fails CORS and the panel never reaches the connected state — the
bootstrap card stays stuck on "Browse server not ready" and the install-card
shows "Claude Code not found" even when claude is on PATH.

Repro: launch `browse connect`, open the Side Panel from a fresh extension
load, watch DevTools console fire "Access to fetch at 'http://127.0.0.1:34567/health'
from origin 'chrome-extension://...' has been blocked by CORS policy".

Fix:
  * server.ts — add a `withCors` middleware (preflight OPTIONS 204 +
    response-header reflection) and wrap both makeFetchHandler call sites.
    Reflects the Origin header back ONLY when it starts with
    chrome-extension://, so arbitrary websites still get no CORS headers.
    Sets Access-Control-Allow-Credentials: true to satisfy
    credentials:'include' on the panel's fetches and EventSource(...,
    {withCredentials:true}).

  * terminal-agent.ts — add the same Origin + Credentials headers to the
    /claude-available response. WS upgrade keeps Sec-WebSocket-Protocol
    auth and doesn't need CORS; /internal/* stays loopback + bearer-auth.

  * server-auth.test.ts, terminal-agent.test.ts — source-pattern tests
    matching the existing /refs and /activity/history CORS-test style.
    Verify the middleware exists, gates on chrome-extension://, sets both
    headers, and is never wildcarded.

Verification curl (post-patch):
  $ curl -H "Origin: chrome-extension://abc" -D - http://127.0.0.1:34567/health
  HTTP/1.1 200 OK
  Access-Control-Allow-Origin: chrome-extension://abc
  Access-Control-Allow-Credentials: true
  $ curl -H "Origin: https://evil.com" -D - http://127.0.0.1:34567/health
  HTTP/1.1 200 OK
  (no Access-Control-* headers — non-extension origins still blocked)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Bmathews721 Bmathews721 force-pushed the fix/cors-sidepanel-fetches branch from c17f88d to e268718 Compare May 18, 2026 16:45
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