From 06c19feba15c5ed4f5f56ce9efb4b4f91d97e550 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Fri, 12 Jun 2026 10:05:54 +0200 Subject: [PATCH] fix: align agent websocket origin Derive debugger WebSocket Origin from the selected inspector URL. Default local agent connections to 127.0.0.1 so React Native origin checks accept agent sessions. --- .changeset/agent-websocket-loopback-origin.md | 8 +++++ packages/agent-shared/src/index.ts | 2 +- .../__tests__/agent-session-manager.test.ts | 4 +-- .../src/__tests__/agent-session.test.ts | 31 +++++++++++++++---- packages/middleware/src/agent/session.ts | 16 +++++----- 5 files changed, 45 insertions(+), 16 deletions(-) create mode 100644 .changeset/agent-websocket-loopback-origin.md diff --git a/.changeset/agent-websocket-loopback-origin.md b/.changeset/agent-websocket-loopback-origin.md new file mode 100644 index 00000000..bc1dbd54 --- /dev/null +++ b/.changeset/agent-websocket-loopback-origin.md @@ -0,0 +1,8 @@ +--- +'@rozenite/agent-shared': patch +'@rozenite/agent-sdk': patch +'@rozenite/middleware': patch +'rozenite': patch +--- + +Derive the Agent debugger WebSocket `Origin` from the selected inspector URL and default local Agent connections to `127.0.0.1` so React Native origin checks accept Rozenite for Agents sessions. diff --git a/packages/agent-shared/src/index.ts b/packages/agent-shared/src/index.ts index 15654cbc..378b66a0 100644 --- a/packages/agent-shared/src/index.ts +++ b/packages/agent-shared/src/index.ts @@ -1,6 +1,6 @@ export const AGENT_PLUGIN_ID = 'rozenite-agent'; -export const DEFAULT_AGENT_HOST = 'localhost'; +export const DEFAULT_AGENT_HOST = '127.0.0.1'; export const DEFAULT_AGENT_PORT = 8081; export const AGENT_ROUTE_BASE = '/rozenite/agent'; diff --git a/packages/middleware/src/__tests__/agent-session-manager.test.ts b/packages/middleware/src/__tests__/agent-session-manager.test.ts index e2009492..54ca3461 100644 --- a/packages/middleware/src/__tests__/agent-session-manager.test.ts +++ b/packages/middleware/src/__tests__/agent-session-manager.test.ts @@ -81,7 +81,7 @@ describe('agent session manager', () => { ); }); - it('uses default localhost Metro endpoint', () => { + it('uses default loopback Metro endpoint', () => { const manager = createAgentSessionManager({ projectRoot: '/app' }); expect(manager.getInfo()).toEqual({ @@ -101,7 +101,7 @@ describe('agent session manager', () => { await expect(manager.listTargets()).resolves.toEqual([ { id: 'device-1', name: 'Phone' }, ]); - expect(mocks.getMetroTargets).toHaveBeenCalledWith('localhost', 8081); + expect(mocks.getMetroTargets).toHaveBeenCalledWith('127.0.0.1', 8081); }); it('creates and reuses the same session per device', async () => { diff --git a/packages/middleware/src/__tests__/agent-session.test.ts b/packages/middleware/src/__tests__/agent-session.test.ts index 269695a0..6eb106ad 100644 --- a/packages/middleware/src/__tests__/agent-session.test.ts +++ b/packages/middleware/src/__tests__/agent-session.test.ts @@ -240,7 +240,7 @@ vi.mock('../logger.js', () => ({ import { createAgentSession } from '../agent/session.js'; -const TARGET: MetroTarget = { +const createTarget = (overrides: Partial = {}): MetroTarget => ({ id: 'device-1', pageId: 'page-1', appId: 'com.example.app', @@ -248,7 +248,10 @@ const TARGET: MetroTarget = { title: 'App', description: 'Device target', webSocketDebuggerUrl: 'ws://localhost:8081/debug', -}; + ...overrides, +}); + +const TARGET = createTarget(); const flushMicrotasks = async () => { await Promise.resolve(); @@ -259,13 +262,14 @@ const createStartedSession = ( overrides?: Partial<{ cliVersion: string; metroVersion: string; + target: MetroTarget; }>, ) => { const session = createAgentSession({ projectRoot: '/app', host: 'localhost', port: 8081, - target: TARGET, + target: overrides?.target ?? TARGET, ...overrides, }); @@ -306,13 +310,13 @@ const emitRozeniteBindingPayload = async ( message: Record, ) => { mocks.parseRozeniteBindingPayload.mockImplementation( - (((rawMessage: Record) => + ((rawMessage: Record) => (rawMessage.bindingPayload as | { domain: string; message: Record; } - | undefined) ?? null) as unknown) as () => null, + | undefined) ?? null) as unknown as () => null, ); socket.emit( @@ -340,7 +344,7 @@ describe('agent session', () => { vi.useRealTimers(); }); - it('sets a localhost origin header for the debugger websocket', () => { + it('sets an origin header derived from the debugger websocket URL', () => { const { socket } = createStartedSession(); expect(socket.url).toBe(TARGET.webSocketDebuggerUrl); @@ -351,6 +355,21 @@ describe('agent session', () => { }); }); + it('preserves the debugger websocket host in the origin header', () => { + const target = createTarget({ + webSocketDebuggerUrl: 'ws://127.0.0.1:8081/debug', + }); + + const { socket } = createStartedSession({ target }); + + expect(socket.url).toBe(target.webSocketDebuggerUrl); + expect(socket.options).toEqual({ + headers: { + Origin: 'http://127.0.0.1:8081', + }, + }); + }); + it('does not resolve start before the bootstrap delay elapses', async () => { const { socket, startPromise } = createStartedSession(); const onResolved = vi.fn(); diff --git a/packages/middleware/src/agent/session.ts b/packages/middleware/src/agent/session.ts index 2416ccb8..f8b41df3 100644 --- a/packages/middleware/src/agent/session.ts +++ b/packages/middleware/src/agent/session.ts @@ -30,8 +30,11 @@ const DISPATCHER_INIT_RETRY_MS = 250; const PLUGIN_READINESS_QUIET_WINDOW_MS = 50; const PLUGIN_READINESS_MAX_WAIT_MS = 250; -const getDebuggerWebSocketOrigin = (port: number): string => { - return `http://localhost:${port}`; +const getDebuggerWebSocketOrigin = (webSocketDebuggerUrl: string): string => { + const url = new URL(webSocketDebuggerUrl); + const protocol = url.protocol === 'wss:' ? 'https:' : 'http:'; + + return `${protocol}//${url.host}`; }; type PendingCommand = { @@ -605,10 +608,7 @@ export const createAgentSession = (options: { notePluginReadinessActivity(); } - handler.handleDeviceMessage( - options.target.id, - devToolsMessage, - ); + handler.handleDeviceMessage(options.target.id, devToolsMessage); } else if (bindingPayload.domain === 'react-devtools') { for (const service of localServices) { if (service.captureReactDevToolsMessage) { @@ -624,7 +624,9 @@ export const createAgentSession = (options: { await new Promise((resolve, reject) => { const socket = new WebSocket(options.target.webSocketDebuggerUrl, { headers: { - Origin: getDebuggerWebSocketOrigin(options.port), + Origin: getDebuggerWebSocketOrigin( + options.target.webSocketDebuggerUrl, + ), }, }); let settled = false;