Skip to content

Commit cb4dc17

Browse files
committed
Clarify reverse transport direction
1 parent 39dfad4 commit cb4dc17

13 files changed

Lines changed: 53 additions & 66 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ pnpm run proxy -- --launcher-mode=local --upstream-mode=nats --upstream-nats-url
142142
# ✨ All ModCDP commands now work through playwright! you can modify/extend playwright behavior to your heart's content
143143
```
144144

145-
The proxy uses the same `--launcher-*`, `--injector-*`, `--upstream-*`, `--client-config='{"client_cdp_send_timeout_ms": 10000}'`, `--router='{"router_routes": {...}}'`, and `--server-config='{"router": {"router_routes": {...}}}'` config groups as `ModCDPClient`. CLI flags use kebab case and map to the owner-prefixed config fields, for example `--launcher-local-executable-path` maps to `launcher.launcher_local_executable_path`. `ws` keeps a transparent websocket-to-websocket fast path; `pipe`, `nativemessaging`, `nats`, and launched `reversews` proxy downstream CDP-shaped messages through the selected `ModCDPClient` upstream transport.
145+
The proxy uses the same `--launcher-*`, `--injector-*`, `--upstream-*`, `--client-config='{"client_cdp_send_timeout_ms": 10000}'`, `--router='{"router_routes": {...}}'`, and `--server-config='{"router": {"router_routes": {...}}}'` config groups as `ModCDPClient`. CLI flags use kebab case and map to the owner-prefixed config fields, for example `--launcher-local-executable-path` maps to `launcher.launcher_local_executable_path`. `ws` keeps a transparent websocket-to-websocket fast path; `pipe`, `nativemessaging`, `nats`, and launched `reversews` proxy downstream CDP-shaped messages through the selected client-side `ModCDPClient` transport.
146146

147147
Native messaging mode uses the configured browser native host name directly. The baked extension expects the default `com.modcdp.bridge` host, so changing `--upstream-nativemessaging-host-name` requires using an extension build that was baked for that host. Because Chrome owns native host process launch and stdio, native messaging is not a standalone `pnpm run proxy` mode.
148148

@@ -157,7 +157,7 @@ pnpm run proxy -- --launcher-mode=local --upstream-mode=reversews --upstream-rev
157157
# const browser = await playwright.chromium.connectOverCDP("http://127.0.0.1:9223")
158158
```
159159

160-
Reverse mode is opt-in. The shipped extension auto-connects to the fixed local reverse connector at `ws://127.0.0.1:29292`; the proxy/client listens there and waits for that extension connection. Keep `--upstream-reversews-bind` when using a custom extension build whose compiled autoconnect URL points at a different host or port. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits. Once connected, the extension identifies itself as a ModCDP service worker and the proxy uses that reverse websocket as its upstream. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker.
160+
Reverse mode is opt-in. The shipped extension auto-connects to the fixed local reverse connector at `ws://127.0.0.1:29292`; the proxy/client listens there and waits for that extension connection. Keep `--upstream-reversews-bind` when using a custom extension build whose compiled autoconnect URL points at a different host or port. `--upstream-reversews-wait-timeout-ms` controls how long the proxy/client waits. Once connected, the extension identifies itself as a ModCDP service worker and the proxy uses that reverse websocket as its client-side transport. From the service worker server's perspective, reversews is a downstream client connection. `Mod.*`, expression-backed `Custom.*` commands, custom event fanout, middleware, and normal CDP commands all stay routed through `globalThis.ModCDP.handleCommand(...)` in the service worker.
161161

162162
Reverse mode is intentionally scoped to one local browser and one reverse extension connection per proxy process. The browser may still have other extensions installed; ModCDP does not require `--disable-extensions-except`.
163163

go/modcdp/transport/UpstreamTransport.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,13 @@ func freePort() (int, error) {
4343
}
4444

4545
type UpstreamMode string
46+
type UpstreamPeerKind string
4647

4748
const (
4849
UpstreamModeWS UpstreamMode = "ws"
50+
51+
UpstreamPeerKindBrowserCDP UpstreamPeerKind = "browser_cdp"
52+
UpstreamPeerKindModCDPServer UpstreamPeerKind = "modcdp_server"
4953
)
5054

5155
type HostPort struct {
@@ -54,17 +58,17 @@ type HostPort struct {
5458
}
5559

5660
type UpstreamTransport struct {
57-
Config UpstreamTransportConfig
58-
UpstreamIsModCDPServer bool
59-
recvListeners []recvListener
60-
closeListeners []closeListener
61-
eventListeners map[string][]upstreamEventListener
62-
listenerMu sync.Mutex
63-
nextListenerID int64
64-
nextID int64
65-
pending map[int64]chan map[string]any
66-
pendingMu sync.Mutex
67-
writeCommand func(map[string]any) error
61+
Config UpstreamTransportConfig
62+
PeerKind UpstreamPeerKind
63+
recvListeners []recvListener
64+
closeListeners []closeListener
65+
eventListeners map[string][]upstreamEventListener
66+
listenerMu sync.Mutex
67+
nextListenerID int64
68+
nextID int64
69+
pending map[int64]chan map[string]any
70+
pendingMu sync.Mutex
71+
writeCommand func(map[string]any) error
6872
}
6973

7074
type recvListener struct {
@@ -94,6 +98,7 @@ func NewUpstreamTransport(config UpstreamTransportConfig) UpstreamTransport {
9498
}
9599
return UpstreamTransport{
96100
Config: config,
101+
PeerKind: UpstreamPeerKindBrowserCDP,
97102
eventListeners: map[string][]upstreamEventListener{},
98103
pending: map[int64]chan map[string]any{},
99104
writeCommand: func(map[string]any) error {

js/src/client/ModCDPClient.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ export class ModCDPClient<
323323
return this;
324324
}
325325

326-
if (this.upstream.upstream_is_modcdp_server) {
326+
if (this.upstream.peer_kind === "modcdp_server") {
327327
const configure_started_at = Date.now();
328328
if (this.server_config !== null) await this.upstream.send("Mod.configure", this._serverConfigureParams());
329329
const configure_completed_at = Date.now();
@@ -395,7 +395,7 @@ export class ModCDPClient<
395395
async send(method: string, params: unknown = {}, session_id: string | null = null): Promise<Record<string, unknown>> {
396396
const started_at = Date.now();
397397
const can_register_locally =
398-
!this.upstream.upstream_is_modcdp_server &&
398+
this.upstream.peer_kind !== "modcdp_server" &&
399399
(method === "Mod.addCustomCommand" ||
400400
(method === "Mod.addCustomEvent" && !this.injector?.session_id) ||
401401
(method === "Mod.addMiddleware" && !this.injector?.session_id));
@@ -416,7 +416,7 @@ export class ModCDPClient<
416416
};
417417
return this.types.parseCommandResult(method, prepared.local_result);
418418
}
419-
if (this.upstream.upstream_is_modcdp_server) {
419+
if (this.upstream.peer_kind === "modcdp_server") {
420420
const result = await this.upstream.send(method, command_params as ProtocolPayload, session_id, {
421421
timeout_ms: this.config.client_cdp_send_timeout_ms,
422422
});

js/src/transport/NATSUpstreamTransport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ type NatsSocket = WebSocket | NatsTcpSocket;
3737

3838
class NATSUpstreamTransport extends UpstreamTransport {
3939
declare config: NATSUpstreamTransportConfig;
40-
override upstream_is_modcdp_server = true;
40+
override peer_kind = "modcdp_server" as const;
4141
private socket: NatsSocket | null = null;
4242
private tcp_buffer = Buffer.alloc(0);
4343
private ws_buffer = "";

js/src/transport/NativeMessagingUpstreamTransport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type NativeMessagingUpstreamTransportConfig = z.infer<typeof NativeMessagingUpst
1717

1818
class NativeMessagingUpstreamTransport extends UpstreamTransport {
1919
declare config: NativeMessagingUpstreamTransportConfig;
20-
override upstream_is_modcdp_server = true;
20+
override peer_kind = "modcdp_server" as const;
2121
private buffer: Buffer<ArrayBufferLike> = Buffer.alloc(0);
2222
private read_native_message: ((chunk: Buffer) => void) | null = null;
2323

js/src/transport/ReverseWSDownstreamTransport.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const DEFAULT_REVERSE_BRIDGE_URL = "ws://127.0.0.1:29292";
1414

1515
const ReverseWSDownstreamTransportConfigSchema = z
1616
.object({
17-
upstream_reversews_url: z.string().default(DEFAULT_REVERSE_BRIDGE_URL),
17+
downstream_reversews_url: z.string().default(DEFAULT_REVERSE_BRIDGE_URL),
1818
reconnect_interval_ms: z.number().positive().default(DEFAULT_REVERSE_BRIDGE_RECONNECT_INTERVAL_MS),
1919
})
2020
.strict();
@@ -69,19 +69,19 @@ class ReverseWSDownstreamTransport extends DownstreamTransport {
6969
start(endpoint?: string, config: z.input<typeof ReverseWSDownstreamTransportConfigSchema> = {}) {
7070
this.config = ReverseWSDownstreamTransportConfigSchema.parse({
7171
...config,
72-
upstream_reversews_url: endpoint,
72+
downstream_reversews_url: endpoint,
7373
});
74-
if (!/^wss?:\/\//i.test(this.config.upstream_reversews_url)) {
74+
if (!/^wss?:\/\//i.test(this.config.downstream_reversews_url)) {
7575
throw new Error(
76-
`reverse proxy endpoint must be a ws:// or wss:// URL, got ${this.config.upstream_reversews_url}.`,
76+
`reverse proxy endpoint must be a ws:// or wss:// URL, got ${this.config.downstream_reversews_url}.`,
7777
);
7878
}
7979
this.started = true;
80-
void this.connect(this.config.upstream_reversews_url).catch(() => {
80+
void this.connect(this.config.downstream_reversews_url).catch(() => {
8181
this.scheduleReconnect();
8282
});
8383
return {
84-
upstream_reversews_url: this.config.upstream_reversews_url,
84+
downstream_reversews_url: this.config.downstream_reversews_url,
8585
reconnect_interval_ms: this.config.reconnect_interval_ms,
8686
connecting: true,
8787
};
@@ -94,7 +94,7 @@ class ReverseWSDownstreamTransport extends DownstreamTransport {
9494

9595
/** Stop reconnecting and close the active reversews socket. */
9696
stop(reason = "stopped") {
97-
const upstream_reversews_url = this.started ? this.config.upstream_reversews_url : null;
97+
const downstream_reversews_url = this.started ? this.config.downstream_reversews_url : null;
9898
this.started = false;
9999
if (this.reconnect_timer) {
100100
clearTimeout(this.reconnect_timer);
@@ -105,7 +105,7 @@ class ReverseWSDownstreamTransport extends DownstreamTransport {
105105
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) {
106106
socket.close(1000, reason);
107107
}
108-
return { upstream_reversews_url, stopped: true, reason };
108+
return { downstream_reversews_url, stopped: true, reason };
109109
}
110110

111111
/** Send one CDP response to the reversews client that sent the request. */
@@ -138,14 +138,14 @@ class ReverseWSDownstreamTransport extends DownstreamTransport {
138138
this.reconnect_timer = setTimeout(() => {
139139
this.reconnect_timer = null;
140140
if (!this.started) return;
141-
void this.connect(this.config.upstream_reversews_url).catch(() => {});
141+
void this.connect(this.config.downstream_reversews_url).catch(() => {});
142142
}, this.config.reconnect_interval_ms);
143143
}
144144

145145
private async connect(endpoint: string) {
146146
if (this.socket?.readyState === WebSocket.OPEN || this.socket?.readyState === WebSocket.CONNECTING) {
147147
return {
148-
upstream_reversews_url: endpoint,
148+
downstream_reversews_url: endpoint,
149149
connected: this.socket.readyState === WebSocket.OPEN,
150150
};
151151
}
@@ -173,7 +173,7 @@ class ReverseWSDownstreamTransport extends DownstreamTransport {
173173
if (this.socket === ws) this.socket = null;
174174
this.scheduleReconnect();
175175
});
176-
return { upstream_reversews_url: endpoint, connected: false };
176+
return { downstream_reversews_url: endpoint, connected: false };
177177
}
178178

179179
private async handleMessage(ws: WebSocket, data: unknown) {

js/src/transport/ReverseWSUpstreamTransport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ type ReverseHello = {
2828

2929
class ReverseWSUpstreamTransport extends UpstreamTransport {
3030
declare config: ReverseWSUpstreamTransportConfig;
31-
override upstream_is_modcdp_server = true;
31+
override peer_kind = "modcdp_server" as const;
3232
endpoint_url: string;
3333
private reversews_listener: WsServer | null = null;
3434
private socket: WsSocket | null = null;

js/src/transport/UpstreamTransport.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type TargetRoute = {
3333
sessionId?: cdp.types.ts.Target.SessionID | null;
3434
};
3535
type UpstreamPeerWaitConfig = { connected_after_ms?: number | null };
36+
type UpstreamPeerKind = "browser_cdp" | "modcdp_server";
3637

3738
type UpstreamEventListener = (
3839
payload: ProtocolPayload,
@@ -42,10 +43,11 @@ type UpstreamEventListener = (
4243

4344
class UpstreamTransport {
4445
config: UpstreamTransportBaseConfig;
45-
// True when this transport terminates at a ModCDPServer peer instead of a
46-
// raw browser CDP endpoint. ModCDPClient reads this to skip local
47-
// target/session bootstrap and send CDP-shaped commands directly upstream.
48-
upstream_is_modcdp_server = false;
46+
// The kind of remote peer this client-side transport talks to. Most
47+
// transports talk to raw browser CDP. Reverse client transports talk to a
48+
// ModCDPServer downstream connection and therefore do not use the local
49+
// AutoSessionRouter bootstrap path.
50+
peer_kind: UpstreamPeerKind = "browser_cdp";
4951
private next_id = 1;
5052
private pending = new Map<
5153
number,
@@ -278,5 +280,6 @@ export type {
278280
UpstreamTransportConfig,
279281
TargetRoute,
280282
UpstreamPeerWaitConfig,
283+
UpstreamPeerKind,
281284
UpstreamEventListener,
282285
};

js/test/test.ReverseWSUpstreamTransport.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { test } from "vitest";
1111
import { LocalBrowserLauncher } from "../src/launcher/LocalBrowserLauncher.js";
1212
import { ReverseWSUpstreamTransport } from "../src/transport/ReverseWSUpstreamTransport.js";
1313

14-
test("reversews upstream config owns bind updates and wait timeout", async () => {
14+
test("reversews client transport config owns bind updates and wait timeout", async () => {
1515
const transport = new ReverseWSUpstreamTransport({
1616
upstream_reversews_bind: "127.0.0.1:29292",
1717
upstream_reversews_wait_timeout_ms: 10,
@@ -32,7 +32,7 @@ test("reversews upstream config owns bind updates and wait timeout", async () =>
3232
await assert.rejects(() => transport.waitForPeer(), /Timed out waiting 5ms/);
3333
});
3434

35-
test("reversews upstream close rejects pending peer waits", async () => {
35+
test("reversews client transport close rejects pending peer waits", async () => {
3636
const reverse_port = await LocalBrowserLauncher.freePort();
3737
const transport = new ReverseWSUpstreamTransport({
3838
upstream_reversews_bind: `127.0.0.1:${reverse_port}`,
@@ -48,7 +48,7 @@ test("reversews upstream close rejects pending peer waits", async () => {
4848
);
4949
});
5050

51-
test("reversews upstream close resets peer wait state", async () => {
51+
test("reversews client transport close resets peer wait state", async () => {
5252
const reverse_port = await LocalBrowserLauncher.freePort();
5353
const transport = new ReverseWSUpstreamTransport({
5454
upstream_reversews_bind: `127.0.0.1:${reverse_port}`,
@@ -72,7 +72,7 @@ test("reversews upstream close resets peer wait state", async () => {
7272
}
7373
});
7474

75-
test("reversews upstream waits again after a peer disconnects", async () => {
75+
test("reversews client transport waits again after a peer disconnects", async () => {
7676
const reverse_port = await LocalBrowserLauncher.freePort();
7777
const transport = new ReverseWSUpstreamTransport({
7878
upstream_reversews_bind: `127.0.0.1:${reverse_port}`,
@@ -95,7 +95,7 @@ test("reversews upstream waits again after a peer disconnects", async () => {
9595
}
9696
});
9797

98-
test("reversews upstream accepts a replacement peer after disconnect", async () => {
98+
test("reversews client transport accepts a replacement peer after disconnect", async () => {
9999
const reverse_port = await LocalBrowserLauncher.freePort();
100100
const transport = new ReverseWSUpstreamTransport({
101101
upstream_reversews_bind: `127.0.0.1:${reverse_port}`,

js/test/test.proxy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ test("proxy CLI maps user-facing flags into a real reversews browser session", a
330330
}
331331
}, 90_000);
332332

333-
test("proxy upgrades a vanilla CDP websocket to ModCDP against a real browser over reversews upstream", async () => {
333+
test("proxy upgrades a vanilla CDP websocket to ModCDP against a real browser over reversews client transport", async () => {
334334
const proxy_port = await LocalBrowserLauncher.freePort();
335335
const proxy = await startProxy({
336336
proxy_listen_port: proxy_port,

0 commit comments

Comments
 (0)