Skip to content

fix(bridge): add CORS headers and a preflight to the discovery route - #99

Open
PerryLink wants to merge 1 commit into
omdsh-dev:mainfrom
PerryLink:fix/bridge-config-cors
Open

PerryLink wants to merge 1 commit into
omdsh-dev:mainfrom
PerryLink:fix/bridge-config-cors

Conversation

@PerryLink

Copy link
Copy Markdown

Fixes #96.

The zero-config discovery route answers with content-type only:

handler: (_req, res) => {
  res.writeHead(200, { 'content-type': 'application/json' })
  res.end(JSON.stringify({ wsUrl: \`ws://127.0.0.1:\${ctx.webServer.port}\${BRIDGE_PATH}\` }))
},

Chromium 142+ / Edge 143+ put an extension's fetch to 127.0.0.1 through the Local Network Access CORS checks, so that response is rejected before the extension can read it:

Access to fetch at 'http://127.0.0.1:3080/ext/bridge-config' from origin 'chrome-extension://…'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the
requested resource.

Discovery therefore never resolves, and the side panel sits on "reconnecting" until the bridge URL is filled in by hand or edge://flags/#local-network-access-check is disabled. The reporter of #96 patched configRoute locally with these headers, restarted dsh, and had the panel come up connected with every browser_* tool usable — so this is a fix confirmed on real hardware, not a hypothesis.

Change

The route now sends the three headers and answers OPTIONS with an empty 204:

-    handler: (_req, res) => {
-      res.writeHead(200, { 'content-type': 'application/json' })
+    handler: (req, res) => {
+      const cors = {
+        'access-control-allow-origin': '*',
+        'access-control-allow-methods': 'GET, OPTIONS',
+        'access-control-allow-private-network': 'true',
+      }
+      if (req.method === 'OPTIONS') {
+        res.writeHead(204, cors)
+        res.end()
+        return
+      }
+      res.writeHead(200, { 'content-type': 'application/json', ...cors })
       res.end(JSON.stringify({ wsUrl: … }))
     },

Why * is safe here. The response is one field — the loopback bridge URL — and it carries no secret. Callers still have to authenticate on the WebSocket handshake: server.ts keeps the loopback-plus-chrome-extension://-origin shortcut for the token, and non-loopback remotes must present the bearer token. The mounted route only exists when this plugin is in the composition, and it already answers any caller that can reach the port.

Tests

composition.spec.ts already fetched this route and asserted the payload, so the assertions go next to the existing ones rather than into a new fixture: the GET carries all three headers, and OPTIONS returns 204 with them. That is the pair of behaviours the browser actually exercises.

I could not run macOS/Edge, and I have no Chromium 142+ here to observe the LNA gate itself — what is pinned by the test is the response contract the gate reads.

@PerryLink PerryLink changed the title fix(bridge): answer the discovery route with CORS headers and a preflight fix(bridge): add CORS headers and a preflight to the discovery route Sep 20, 2026
@Lum1104

Lum1104 commented Sep 21, 2026

Copy link
Copy Markdown
Member

@greptile review this

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.

Edge 153(Chromium 142+ Local Network Access)下侧边栏无法自动发现桥:/ext/bridge-config 缺 CORS 头

2 participants