Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/agent-websocket-loopback-origin.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion packages/agent-shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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 () => {
Expand Down
31 changes: 25 additions & 6 deletions packages/middleware/src/__tests__/agent-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,15 +240,18 @@ vi.mock('../logger.js', () => ({

import { createAgentSession } from '../agent/session.js';

const TARGET: MetroTarget = {
const createTarget = (overrides: Partial<MetroTarget> = {}): MetroTarget => ({
id: 'device-1',
pageId: 'page-1',
appId: 'com.example.app',
name: 'iPhone 16',
title: 'App',
description: 'Device target',
webSocketDebuggerUrl: 'ws://localhost:8081/debug',
};
...overrides,
});

const TARGET = createTarget();

const flushMicrotasks = async () => {
await Promise.resolve();
Expand All @@ -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,
});

Expand Down Expand Up @@ -306,13 +310,13 @@ const emitRozeniteBindingPayload = async (
message: Record<string, unknown>,
) => {
mocks.parseRozeniteBindingPayload.mockImplementation(
(((rawMessage: Record<string, unknown>) =>
((rawMessage: Record<string, unknown>) =>
(rawMessage.bindingPayload as
| {
domain: string;
message: Record<string, unknown>;
}
| undefined) ?? null) as unknown) as () => null,
| undefined) ?? null) as unknown as () => null,
);

socket.emit(
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down
16 changes: 9 additions & 7 deletions packages/middleware/src/agent/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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) {
Expand All @@ -624,7 +624,9 @@ export const createAgentSession = (options: {
await new Promise<void>((resolve, reject) => {
const socket = new WebSocket(options.target.webSocketDebuggerUrl, {
headers: {
Origin: getDebuggerWebSocketOrigin(options.port),
Origin: getDebuggerWebSocketOrigin(
options.target.webSocketDebuggerUrl,
),
},
});
let settled = false;
Expand Down
Loading