add CORS support to relay server for local extension development (#25)

* fix: add CORS middleware to allow extension connection

* fix(cors): validate extension ID to prevent other extensions from accessing endpoints

---------

Co-authored-by: Tommy D. Rossi <beats.by.morse@gmail.com>
This commit is contained in:
FiftyX
2026-01-16 23:24:06 +08:00
committed by GitHub
parent 7ebd3c190e
commit 5631fc3d19
+21 -4
View File
@@ -1,4 +1,5 @@
import { Hono } from 'hono' import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { serve } from '@hono/node-server' import { serve } from '@hono/node-server'
import { getConnInfo } from '@hono/node-server/conninfo' import { getConnInfo } from '@hono/node-server/conninfo'
import { createNodeWebSocket } from '@hono/node-ws' import { createNodeWebSocket } from '@hono/node-ws'
@@ -418,6 +419,22 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
} }
const app = new Hono() const app = new Hono()
// CORS middleware for HTTP endpoints - only allows our specific extension IDs.
// This prevents other extensions from reading responses via fetch/XHR.
// WebSocket connections have their own separate origin validation.
app.use('*', cors({
origin: (origin) => {
if (!origin.startsWith('chrome-extension://')) {
return null
}
const extensionId = origin.replace('chrome-extension://', '')
if (!OUR_EXTENSION_IDS.includes(extensionId)) {
return null
}
return origin
},
allowMethods: ['GET', 'POST', 'HEAD', 'OPTIONS'],
}))
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }) const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
const getCdpWsUrl = (c: { req: { header: (name: string) => string | undefined } }) => { const getCdpWsUrl = (c: { req: { header: (name: string) => string | undefined } }) => {
@@ -531,7 +548,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
// from connecting to the local WebSocket server. // from connecting to the local WebSocket server.
app.get('/cdp/:clientId?', (c, next) => { app.get('/cdp/:clientId?', (c, next) => {
const origin = c.req.header('origin') const origin = c.req.header('origin')
// Validate Origin header if present (Node.js clients don't send it) // Validate Origin header if present (Node.js clients don't send it)
if (origin) { if (origin) {
if (origin.startsWith('chrome-extension://')) { if (origin.startsWith('chrome-extension://')) {
@@ -736,7 +753,7 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
logger?.log(pc.red(`Rejecting /extension WebSocket: origin must be chrome-extension://, got: ${origin || 'none'}`)) logger?.log(pc.red(`Rejecting /extension WebSocket: origin must be chrome-extension://, got: ${origin || 'none'}`))
return c.text('Forbidden', 403) return c.text('Forbidden', 403)
} }
const extensionId = origin.replace('chrome-extension://', '') const extensionId = origin.replace('chrome-extension://', '')
if (!OUR_EXTENSION_IDS.includes(extensionId)) { if (!OUR_EXTENSION_IDS.includes(extensionId)) {
logger?.log(pc.red(`Rejecting /extension WebSocket from unknown extension: ${extensionId}`)) logger?.log(pc.red(`Rejecting /extension WebSocket from unknown extension: ${extensionId}`))
@@ -962,8 +979,8 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
// If this is an old connection closing after we've already established a new one, // If this is an old connection closing after we've already established a new one,
// don't clear the global state // don't clear the global state
if (extensionWs && extensionWs !== ws) { if (extensionWs && extensionWs !== ws) {
logger?.log('Old extension connection closed, keeping new one active') logger?.log('Old extension connection closed, keeping new one active')
return return
} }
for (const pending of extensionPendingRequests.values()) { for (const pending of extensionPendingRequests.values()) {