diff --git a/README.md b/README.md index f88ce44fa..0cfcc2635 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ When the site you need is not yet covered, use the `opencli-adapter-author` skil | `OPENCLI_BROWSER_CONNECT_TIMEOUT` | `45` | Seconds to wait for browser connection | | `OPENCLI_BROWSER_COMMAND_TIMEOUT` | `60` | Seconds to wait for a single browser command | | `OPENCLI_CDP_ENDPOINT` | — | Chrome DevTools Protocol endpoint for remote browser or Electron apps | +| `OPENCLI_CDP_ORIGIN` | — | Optional WebSocket `Origin` header for CDP brokers that enforce an origin allowlist | | `OPENCLI_CDP_TARGET` | — | Filter CDP targets by URL substring (e.g. `detail.1688.com`) | | `OPENCLI_VERBOSE` | `false` | Enable verbose logging (`-v` flag also works) | | `DEBUG_SNAPSHOT` | — | Set to `1` for DOM snapshot debug output | diff --git a/src/browser/cdp.test.ts b/src/browser/cdp.test.ts index 6ad3b8365..46b90a180 100644 --- a/src/browser/cdp.test.ts +++ b/src/browser/cdp.test.ts @@ -3,10 +3,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const { MockWebSocket } = vi.hoisted(() => { class MockWebSocket { static OPEN = 1; + static constructorCalls: Array<{ url: string; options: unknown }> = []; readyState = 1; private handlers = new Map void>>(); - constructor(_url: string) { + constructor(url: string, options?: unknown) { + MockWebSocket.constructorCalls.push({ url, options }); queueMicrotask(() => this.emit('open')); } @@ -41,6 +43,31 @@ import { CDPBridge } from './cdp.js'; describe('CDPBridge cookies', () => { beforeEach(() => { vi.unstubAllEnvs(); + MockWebSocket.constructorCalls = []; + }); + + it('uses an optional WebSocket Origin for private CDP brokers', async () => { + vi.stubEnv('OPENCLI_CDP_ENDPOINT', 'ws://127.0.0.1:9222/devtools/page/1'); + vi.stubEnv('OPENCLI_CDP_ORIGIN', 'opencli://trusted-broker'); + + const bridge = new CDPBridge(); + vi.spyOn(bridge, 'send').mockResolvedValue({}); + await bridge.connect(); + + expect(MockWebSocket.constructorCalls).toEqual([{ + url: 'ws://127.0.0.1:9222/devtools/page/1', + options: { origin: 'opencli://trusted-broker' }, + }]); + }); + + it('preserves the default WebSocket handshake without an Origin override', async () => { + vi.stubEnv('OPENCLI_CDP_ENDPOINT', 'ws://127.0.0.1:9222/devtools/page/1'); + + const bridge = new CDPBridge(); + vi.spyOn(bridge, 'send').mockResolvedValue({}); + await bridge.connect(); + + expect(MockWebSocket.constructorCalls[0]?.options).toBeUndefined(); }); it('filters cookies by actual domain match instead of substring match', async () => { diff --git a/src/browser/cdp.ts b/src/browser/cdp.ts index 7ac2aa7bc..4b3b01ee4 100644 --- a/src/browser/cdp.ts +++ b/src/browser/cdp.ts @@ -70,7 +70,8 @@ export class CDPBridge implements IBrowserFactory { } return new Promise((resolve, reject) => { - const ws = new WebSocket(wsUrl); + const origin = process.env.OPENCLI_CDP_ORIGIN; + const ws = new WebSocket(wsUrl, origin ? { origin } : undefined); const timeoutMs = (opts?.timeout ?? 10) * 1000; const timeout = setTimeout(() => { this._ws = null;