@mefistofelix/cdp.js 0.1.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +872 -28
- package/cdp.js +379 -36
- package/cdp_ext/cdp_ext.js +263 -0
- package/cdp_ext/manifest.json +76 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# cdp.js
|
|
2
2
|
|
|
3
|
-
A low-level Chromium DevTools Protocol library for Node.js
|
|
3
|
+
A low-level Chromium DevTools Protocol library for Node.js, Deno and Bun, with an
|
|
4
|
+
optional native Deno/Bun server for Chrome extension reverse connections. One ES module,
|
|
4
5
|
standard-library imports, no runtime dependencies or root `package.json`.
|
|
5
6
|
|
|
6
7
|
Use Node.js 22.19+ and an installed Chrome, Chromium or Edge. Import `cdp.js`
|
|
7
8
|
directly from an ES module, for example `example.mjs`, and run `node example.mjs`.
|
|
8
9
|
Browser discovery covers common Windows, macOS and Linux installation paths;
|
|
9
10
|
use `executable_path` or the `CDP_BROWSER` environment variable for other paths.
|
|
11
|
+
The same module also runs with `bun example.mjs` or `deno run -A example.mjs`.
|
|
12
|
+
Direct and reverse modes are tested with Bun 1.4.3; no Bun-specific package,
|
|
13
|
+
configuration file or installation step is required by the library.
|
|
10
14
|
|
|
11
15
|
For a published npm release:
|
|
12
16
|
|
|
@@ -20,24 +24,39 @@ import api, { util, jsrpc, browser, cdp } from '@mefistofelix/cdp.js'
|
|
|
20
24
|
|
|
21
25
|
The npm package exports the same file and API. Direct file imports in the examples
|
|
22
26
|
below remain supported. Publication metadata is isolated in `npm/`.
|
|
27
|
+
The package includes both `cdp_ext/` files. With `cdp_ext: true`, the manager
|
|
28
|
+
writes a configured extension copy into the browser's profile directory and loads
|
|
29
|
+
it automatically; no manual extraction or build is needed. Reverse mode still
|
|
30
|
+
requires Deno or Bun. In Deno, import from `npm:@mefistofelix/cdp.js`.
|
|
31
|
+
In Bun, use `bun add @mefistofelix/cdp.js` and the ordinary package import above.
|
|
23
32
|
|
|
24
33
|
## Features and scope
|
|
25
34
|
|
|
26
35
|
- Native CDP requests through `call({ method, params })`, with native results.
|
|
27
36
|
- Named browsers and targets, created and connected on demand.
|
|
37
|
+
- Browser proxies and named direct-mode contexts with independent proxies/storage
|
|
38
|
+
and automatic cleanup after their last target closes.
|
|
39
|
+
- Incognito targets in direct and reverse modes.
|
|
28
40
|
- Attachment through WebSocket, HTTP discovery or a debugging port.
|
|
41
|
+
- Optional Chrome extension reverse connections through one native Deno/Bun server,
|
|
42
|
+
without browser remote-debugging arguments.
|
|
43
|
+
- Generic `chrome.x.y` extension API calls on that connection, including scripting,
|
|
44
|
+
tabs/windows, cookies, storage, downloads and browser settings.
|
|
29
45
|
- Shared initialization for concurrent calls; reconnection and target recreation
|
|
30
46
|
on subsequent use, without replaying failed commands.
|
|
31
47
|
- Public object mappings and directly replaceable custom method handlers.
|
|
32
48
|
- Configurable target initialization, JavaScript bindings and direct events.
|
|
49
|
+
- Reverse network notifications through CDP Network or extension webRequest,
|
|
50
|
+
including observation without debugger attachment.
|
|
33
51
|
- XPath `_.click` and `_.find`, including two XPath expression rewrites.
|
|
34
52
|
- Configurable browser launch arguments and profile preferences.
|
|
35
53
|
- Raw WebSocket JSON-RPC transport and reusable static helpers.
|
|
36
54
|
|
|
37
55
|
There is no Page class, batch API, image-processing helper, event buffer/filter
|
|
38
|
-
API or database. Browser, target and session mappings live in memory. Chromium
|
|
56
|
+
API or database. Browser, context, target and session mappings live in memory. Chromium
|
|
39
57
|
profiles and their preferences are ordinary files on disk. Any native CDP method
|
|
40
|
-
supported by the connected browser remains available through `call`.
|
|
58
|
+
supported by the connected browser remains available through direct `call`.
|
|
59
|
+
The optional extension transport has the command/domain limits documented below.
|
|
41
60
|
|
|
42
61
|
## Quick start
|
|
43
62
|
|
|
@@ -100,13 +119,16 @@ import api, { util, jsrpc, browser, cdp } from './cdp.js'
|
|
|
100
119
|
| `new cdp(options = {})` | Create a manager; does not launch a browser yet. |
|
|
101
120
|
| `client.call({ method, params = {} })` | Resolve routing and return a promise for the native CDP result, or a custom handler's result. |
|
|
102
121
|
| `client.evaluate_xpath(params, fn, options)` | Execute a serialized function with a normalized XPath in a target; return the native `Runtime.evaluate` result. |
|
|
103
|
-
| `client.close()` | Close owned
|
|
104
|
-
| `new jsrpc(
|
|
122
|
+
| `client.close()` | Close owned processes, disconnect attached/reverse browsers, stop the reverse server and clear browser/socket mappings. Profiles and external browsers remain. |
|
|
123
|
+
| `new jsrpc(urlOrSocket, options = {})` | Open a raw WebSocket JSON-RPC connection or equip an accepted socket with the same RPC methods. |
|
|
124
|
+
| `client.listen_reverse(options = {})` | Start the native Deno or Bun WebSocket server; returns that server with common lifecycle fields documented below. |
|
|
105
125
|
| `socket.req(request, timeout_ms)` | Assign a request ID and await its native result. |
|
|
106
126
|
| `socket.notify(request)` | Serialize and send a raw message; no ID allocation or response wait. |
|
|
107
127
|
| `socket.close()` | Native WebSocket close; does not terminate the browser. |
|
|
108
128
|
| `browser.find_executable_path()` | Synchronously return an executable path from the environment or common locations. |
|
|
109
129
|
| `browser.build_args(options = {})` | Return a fresh, editable object of launch switches. |
|
|
130
|
+
| `browser.extension_id(directory)` | Compute Chromium's path-derived unpacked extension ID. Use the canonical absolute directory without a manifest `key`. |
|
|
131
|
+
| `browser.profile_extension_paths(args)` | Read the effective profile and return a promise for an object keyed by extension ID, with `{ path, enabled }` values for enabled and disabled extensions. |
|
|
110
132
|
| `browser.update_profile_preferences(filename, preferences)` | Update dotted keys in a JSON preferences file; return a promise. |
|
|
111
133
|
| `browser.launch(options)` | Start a local browser and return a promise for `{ proc, url }`. The caller owns the process. |
|
|
112
134
|
| `util.normalize_xpath(xpath)` | Return an XPath string with the supported rewrites. |
|
|
@@ -122,7 +144,7 @@ connections or target/session routing.
|
|
|
122
144
|
|
|
123
145
|
| Field | Meaning |
|
|
124
146
|
| --- | --- |
|
|
125
|
-
| `method` | Native name such as `Page.navigate`,
|
|
147
|
+
| `method` | Native name such as `Page.navigate`, a custom name beginning with `_.`, or an extension API name such as `chrome.tabs.query` in reverse mode. |
|
|
126
148
|
| `params` | Native parameters plus the two routing fields below. Defaults to `{}`. |
|
|
127
149
|
| `params.browser` | Browser label or configuration object with `name`. Defaults to `'main'`. |
|
|
128
150
|
| `params.target` | Target label or configuration object with `name`. Omitted or `null` means a browser-level request. |
|
|
@@ -170,7 +192,8 @@ For a new browser record, configuration is assembled in this order:
|
|
|
170
192
|
2. Constructor browser options.
|
|
171
193
|
3. The `params.browser` configuration object.
|
|
172
194
|
|
|
173
|
-
`args` and `
|
|
195
|
+
`args`, `preferences` and `local_state` are shallow-merged separately between
|
|
196
|
+
steps 2 and 3.
|
|
174
197
|
For duplicate keys, the per-browser value wins. Arrays replace previous arrays;
|
|
175
198
|
they are not concatenated during this merge.
|
|
176
199
|
|
|
@@ -179,6 +202,10 @@ At local launch, another two independent merges happen:
|
|
|
179
202
|
- Generated CLI defaults, adjusted by high-level browser options, then raw `args`.
|
|
180
203
|
- Generated profile preference defaults, then raw `preferences`.
|
|
181
204
|
|
|
205
|
+
After the CLI merge, a non-null/non-false `disable-extensions-except` adds
|
|
206
|
+
`DisableDisableExtensionsExceptCommandLineSwitch` to the final `disable-features`
|
|
207
|
+
list. This requested argument convention also applies to raw feature overrides.
|
|
208
|
+
|
|
182
209
|
There is no conflict resolver across CLI flags and preferences. Each is passed
|
|
183
210
|
to Chromium through its own mechanism. Raw settings can therefore contradict a
|
|
184
211
|
high-level option; the library does not silently reconcile them.
|
|
@@ -195,21 +222,28 @@ local configuration.
|
|
|
195
222
|
| Option | Type / default | Meaning |
|
|
196
223
|
| --- | --- | --- |
|
|
197
224
|
| `base_path` | string, `'.cdp'` | Constructor-only profile root, resolved against the working directory. |
|
|
225
|
+
| `reverse_server` | object, `{}` | Constructor-only settings for the shared lazy listener: `hostname` defaults to `'127.0.0.1'`, `port` to `9223`. Independent of browser attachment `host`/`port`. |
|
|
198
226
|
| `name` | string, `'main'` when routing is omitted | Required in a browser configuration object; dictionary key and default profile directory label. |
|
|
199
|
-
| `user_data_dir` | absolute path string
|
|
227
|
+
| `user_data_dir` | absolute path string or `null`, `<base_path>/<name>` | Chromium user-data directory containing profiles such as `Default`. Explicit paths should be absolute. `null` omits the switch and skips manager preference-file writes; Chromium selects its system directory. |
|
|
200
228
|
| `executable_path` | string, automatic discovery | Executable to spawn; takes precedence over `CDP_BROWSER`. |
|
|
201
229
|
| `headless` | boolean, `false` | When true, add `headless: 'new'` and default `window-size: '1440,900'`. |
|
|
202
230
|
| `windowsHide` | boolean, effective headless mode | Hide the spawned process's console on Windows. Defaults to true when the final arguments include `--headless`, false otherwise; an explicit value wins. Does not hide the parent terminal. |
|
|
203
|
-
| `extensions` | boolean, `false` |
|
|
231
|
+
| `extensions` | boolean or array of directory strings, `false` | True enables normal profile extensions. An array also generates `disable-extensions-except` to load the listed local directories at startup; include `'*'` to retain enabled profile extensions. False adds the two extension-disabling switches. |
|
|
204
232
|
| `images` | boolean, `true` | When false, request `blink-settings: 'imagesEnabled=false'`; controls loading, not image processing. |
|
|
233
|
+
| `proxy` | string or `false`, unset | A string generates `proxy-server` using native Chromium syntax; false generates `no-proxy-server`. Unset/null adds neither flag. Applies at launch, including managed extension launches. |
|
|
205
234
|
| `translations` | boolean, `false` | Set translation preferences and, unless true, disable translation feature IDs. |
|
|
206
235
|
| `login` | boolean, `false` | Set browser sign-in preferences and, unless true, disable sync and sign-in promotions. Does not prevent website login. |
|
|
207
236
|
| `args` | object, `{}` | Raw CLI overrides applied after generated defaults; keys omit `--`. |
|
|
237
|
+
| `update_preferences` | boolean, presence of an effective user-data directory | Manager-only switch for writing `Preferences` and `Local State` before launch. `false` skips both files. No files are written without a directory, including when explicitly true. |
|
|
208
238
|
| `preferences` | object, `{}` | Dotted JSON preference overrides applied after generated preference defaults. |
|
|
239
|
+
| `local_state` | object, `{}` | Dotted JSON overrides for `<user-data-dir>/Local State`, shared by profiles in that directory. No generated defaults. |
|
|
209
240
|
| `connect_timeout_ms` | number, `15000` | Timeout for each discovery, launch-readiness or socket-opening wait; not one combined deadline. |
|
|
210
241
|
| `request_timeout_ms` | number, `30000` | Default timeout for each protocol request on this browser's socket. |
|
|
211
242
|
| `waitForDebuggerOnStart` | boolean, `false` | Ask Chromium to pause newly auto-attached targets. Applied to `Target.setAutoAttach` when connecting; not a CLI argument or target option. |
|
|
212
243
|
| `websocket_url` | string, unset | Attach directly to a browser WebSocket endpoint. |
|
|
244
|
+
| `cdp_ext` | boolean, `false` | Launch with the reverse extension under Deno or Bun. Start one shared server automatically if needed, prepare a profile-specific extension copy and wait for its connection. |
|
|
245
|
+
| `reverse` | boolean, `false` | Attach to an externally launched extension instead of launching or discovering a browser. Starts the shared listener on first use under Deno or Bun. |
|
|
246
|
+
| `extension_id` | string, browser name for external reverse attachment | Incoming extension ID to associate with this logical browser. Set automatically for managed `cdp_ext` launches. |
|
|
213
247
|
| `http_url` | string, unset | Discover the endpoint from an HTTP(S) base URL or exact `/json/version` URL. |
|
|
214
248
|
| `port` | number, unset | Discover an existing browser at `http://<host>:<port>/json/version`. |
|
|
215
249
|
| `host` | string, `'127.0.0.1'` | Host used with `port`. |
|
|
@@ -240,8 +274,10 @@ arbitrary locations in `PATH`.
|
|
|
240
274
|
|
|
241
275
|
### Attach to an existing browser
|
|
242
276
|
|
|
243
|
-
`
|
|
244
|
-
|
|
277
|
+
With `reverse: true`, the manager waits for the extension and ignores attachment
|
|
278
|
+
endpoints and launch options. With `cdp_ext: true`, it launches through the extension
|
|
279
|
+
and ignores attachment endpoints. Otherwise, `websocket_url` takes precedence over
|
|
280
|
+
HTTP discovery; `http_url` takes precedence over `host`/`port`. Use one endpoint form:
|
|
245
281
|
|
|
246
282
|
```js
|
|
247
283
|
await client.call({
|
|
@@ -255,13 +291,607 @@ await client.call({
|
|
|
255
291
|
})
|
|
256
292
|
```
|
|
257
293
|
|
|
258
|
-
The existing browser must already expose CDP. Launch flags
|
|
259
|
-
|
|
260
|
-
leaves that browser running.
|
|
294
|
+
The existing browser must already expose CDP. Launch flags, profile preferences
|
|
295
|
+
and Local State overrides are not applied to it. `client.close()` disconnects its
|
|
296
|
+
socket and leaves that browser running.
|
|
261
297
|
|
|
262
298
|
`port: 9222` means **attach**. To **launch** on that port, use
|
|
263
299
|
`args: { 'remote-debugging-port': 9222 }` without an attachment endpoint.
|
|
264
300
|
|
|
301
|
+
## Proxy configuration
|
|
302
|
+
|
|
303
|
+
Set `proxy` in constructor browser defaults, a per-browser `params.browser`
|
|
304
|
+
object, or standalone `browser.launch` options:
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
const client = new cdp({
|
|
308
|
+
proxy: 'http://127.0.0.1:8080',
|
|
309
|
+
})
|
|
310
|
+
await client.call({
|
|
311
|
+
method: 'Page.navigate',
|
|
312
|
+
params: {
|
|
313
|
+
browser: {
|
|
314
|
+
name: 'socks',
|
|
315
|
+
proxy: 'socks5://127.0.0.1:1080',
|
|
316
|
+
},
|
|
317
|
+
target: 'page',
|
|
318
|
+
url: 'https://example.com/',
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
await client.close()
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
`proxy: false` requests direct connections through `--no-proxy-server`.
|
|
325
|
+
Unset/null adds no proxy flags, preserving Chrome's system/profile selection.
|
|
326
|
+
Configuration is recorded on first use of a browser label and applies at launch;
|
|
327
|
+
it does not change an already running or externally attached browser.
|
|
328
|
+
HTTP, HTTPS and SOCKS proxy syntax passes through unchanged. Credentials embedded
|
|
329
|
+
in proxy URLs are not supported by Chrome; use native authentication instead.
|
|
330
|
+
The library does not add an authentication responder.
|
|
331
|
+
|
|
332
|
+
Raw `args` override generated keys independently. Use them for other proxy
|
|
333
|
+
controls, including bypass rules and PAC:
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
const client = new cdp({
|
|
337
|
+
args: {
|
|
338
|
+
'proxy-server': 'http://127.0.0.1:8080',
|
|
339
|
+
'proxy-bypass-list': '*.internal.example;localhost',
|
|
340
|
+
},
|
|
341
|
+
})
|
|
342
|
+
// Alternatively: args: { 'proxy-pac-url': 'https://example.com/proxy.pac' }
|
|
343
|
+
await client.close()
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Native proxy rules can use semicolon-separated strings; the generic CLI converter
|
|
347
|
+
still comma-joins every array. Bypass lists accept both separators. Chrome gives
|
|
348
|
+
`no-proxy-server` precedence, so overriding `proxy-server` alone does not cancel
|
|
349
|
+
that flag; override `args['no-proxy-server']: false` too if needed.
|
|
350
|
+
Browser proxy options affect Chrome traffic, not the runtime's endpoint discovery
|
|
351
|
+
or CDP transport. Chrome normally bypasses loopback addresses. To proxy them,
|
|
352
|
+
use `args['proxy-bypass-list']: '<-loopback>'`; in reverse mode keep the extension's
|
|
353
|
+
local WebSocket listener reachable. The library inserts no extra bypass rule.
|
|
354
|
+
See [Chromium proxy configuration](https://chromium.googlesource.com/chromium/src/+/HEAD/net/docs/proxy.md)
|
|
355
|
+
and [proxy switches](https://www.chromium.org/developers/design-documents/network-settings/).
|
|
356
|
+
|
|
357
|
+
### Named contexts and different tab proxies
|
|
358
|
+
|
|
359
|
+
Direct CDP supports separate proxies and storage through browser contexts.
|
|
360
|
+
Configure a named context on its first target, then reference that name on other
|
|
361
|
+
targets in the same browser:
|
|
362
|
+
|
|
363
|
+
```js
|
|
364
|
+
await client.call({
|
|
365
|
+
method: 'Page.navigate',
|
|
366
|
+
params: {
|
|
367
|
+
browser: 'main',
|
|
368
|
+
target: {
|
|
369
|
+
name: 'proxied-tab',
|
|
370
|
+
context: {
|
|
371
|
+
name: 'tenant-a',
|
|
372
|
+
proxy: 'http://127.0.0.1:8081',
|
|
373
|
+
create_params: {
|
|
374
|
+
proxyBypassList: '*.internal.example',
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
url: 'https://example.com/',
|
|
379
|
+
},
|
|
380
|
+
})
|
|
381
|
+
await client.call({
|
|
382
|
+
method: 'Page.navigate',
|
|
383
|
+
params: {
|
|
384
|
+
browser: 'main',
|
|
385
|
+
target: {
|
|
386
|
+
name: 'another-tab',
|
|
387
|
+
context: 'tenant-a',
|
|
388
|
+
},
|
|
389
|
+
url: 'https://example.com/another',
|
|
390
|
+
},
|
|
391
|
+
})
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
| Context option | Default | Meaning |
|
|
395
|
+
| --- | --- | --- |
|
|
396
|
+
| `name` | required in an object | Label in this browser's public `contexts` object. A previously unused string label creates a context with native defaults. |
|
|
397
|
+
| `proxy` | unset | Native context proxy override. String sets `proxyServer`; false sets `'direct://'`; unset/null uses Chrome's native selection. |
|
|
398
|
+
| `create_params` | `{}` | Native `Target.createBrowserContext` parameters, overriding the generated `proxyServer`. |
|
|
399
|
+
|
|
400
|
+
Configuration is recorded on first use of the context label. Context names are
|
|
401
|
+
scoped to a browser; constructor options remain browser defaults. Concurrent
|
|
402
|
+
targets sharing a name create one native context. There is no `target.proxy`
|
|
403
|
+
option: put proxy settings on `target.context`.
|
|
404
|
+
|
|
405
|
+
Contexts have private cookies/storage and do not inherit ordinary-profile website
|
|
406
|
+
logins. Tabs sharing a context share its storage and proxy. Existing physical tabs
|
|
407
|
+
cannot move between contexts; specifying a context for an existing target in
|
|
408
|
+
another context rejects. Native proxy fields are experimental and unsupported
|
|
409
|
+
browsers return normal CDP errors.
|
|
410
|
+
|
|
411
|
+
The manager disposes a named context after its last application target disappears.
|
|
412
|
+
It checks native targets, including unselected popups/workers, and waits for pending
|
|
413
|
+
target initialization. Internal Chrome `browser_ui` targets do not retain it.
|
|
414
|
+
Cleanup follows destruction events and can finish after `Target.closeTarget`
|
|
415
|
+
returns. The `context_closed` event signals completion. Cleanup failures emit
|
|
416
|
+
`error` with `{ browser, error }`; the context remains available for a later attempt.
|
|
417
|
+
The logical record remains in `record.contexts`; its `browserContextId` becomes
|
|
418
|
+
null. Reusing the name creates a fresh private context with the recorded options.
|
|
419
|
+
Socket reconnection preserves a surviving native context; browser restart creates
|
|
420
|
+
a new one. Context maps are in memory, not written to profile files.
|
|
421
|
+
|
|
422
|
+
`client.close()` disposes managed contexts, including their tabs in externally
|
|
423
|
+
attached browsers, while preserving other external tabs and unmanaged contexts.
|
|
424
|
+
You can also call `Target.disposeBrowserContext` with the ID from
|
|
425
|
+
`client.browsers.main.contexts['tenant-a'].browserContextId`; managed state is
|
|
426
|
+
invalidated when that call succeeds. Raw native context creation remains available:
|
|
427
|
+
pass its ID through `target.create_params.browserContextId` and dispose it yourself.
|
|
428
|
+
Do not combine that raw ID with `target.context`. No automatic `disposeOnDetach`
|
|
429
|
+
option is added. See the native
|
|
430
|
+
[Target context API](https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-createBrowserContext).
|
|
431
|
+
|
|
432
|
+
Named contexts reject explicitly in both reverse attachment and managed extension
|
|
433
|
+
mode. Chrome's extension/debugger APIs cannot create independent browser contexts.
|
|
434
|
+
Launch-time `proxy`/raw CLI flags work with `cdp_ext: true`. For changes through
|
|
435
|
+
an already connected extension, the existing generic `chrome.proxy.settings`
|
|
436
|
+
methods accept native `params.args`:
|
|
437
|
+
|
|
438
|
+
```js
|
|
439
|
+
await client.call({
|
|
440
|
+
method: 'chrome.proxy.settings.set',
|
|
441
|
+
params: {
|
|
442
|
+
browser: 'work',
|
|
443
|
+
args: [{
|
|
444
|
+
value: {
|
|
445
|
+
mode: 'fixed_servers',
|
|
446
|
+
rules: {
|
|
447
|
+
singleProxy: {
|
|
448
|
+
scheme: 'http',
|
|
449
|
+
host: '127.0.0.1',
|
|
450
|
+
port: 8080,
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
scope: 'regular',
|
|
455
|
+
}],
|
|
456
|
+
},
|
|
457
|
+
})
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
The extension already declares `proxy` permission. ChromeSetting scopes apply to
|
|
461
|
+
profiles/incognito, not individual tabs. `scope: 'regular'` can persist the setting
|
|
462
|
+
in the profile. Use the appropriate native scope or explicitly call
|
|
463
|
+
`chrome.proxy.settings.clear` with the same scope. Policy/extension conflicts are
|
|
464
|
+
reported by `chrome.proxy.settings.get`. See the
|
|
465
|
+
[Chrome proxy API](https://developer.chrome.com/docs/extensions/reference/api/proxy).
|
|
466
|
+
|
|
467
|
+
### Incognito targets
|
|
468
|
+
|
|
469
|
+
Set `target: { name: 'private', incognito: true }` to create a private target.
|
|
470
|
+
The default is false. In direct mode, this selects the shared named context
|
|
471
|
+
`'incognito'` unless an explicit `context` or raw `create_params.browserContextId`
|
|
472
|
+
was supplied. All named CDP contexts are already private. Use different context
|
|
473
|
+
names for independent private sessions or proxies.
|
|
474
|
+
|
|
475
|
+
In reverse mode, this creates a native incognito tab, reusing an existing incognito
|
|
476
|
+
window or opening one. Enable **Allow in incognito** for the bridge in
|
|
477
|
+
`chrome://extensions` first; otherwise creation returns an explicit error.
|
|
478
|
+
The manifest's `incognito: 'spanning'` setting does not grant this user permission,
|
|
479
|
+
and the library never enables it automatically. Existing regular targets cannot
|
|
480
|
+
be changed into incognito targets. `create_params.incognito` is the native reverse
|
|
481
|
+
creation override; the high-level `incognito` flag also checks existing targets.
|
|
482
|
+
|
|
483
|
+
All native Chrome incognito windows share one private session. Storage clears
|
|
484
|
+
when the last incognito window closes; these are not independent named contexts.
|
|
485
|
+
Reverse incognito does not add per-tab proxies. The generic `chrome.proxy` API
|
|
486
|
+
can configure its native incognito scope. See
|
|
487
|
+
[Chrome incognito behavior](https://support.google.com/chrome/answer/95464).
|
|
488
|
+
|
|
489
|
+
## Reverse Chrome extension transport
|
|
490
|
+
|
|
491
|
+
[cdp_ext/manifest.json](cdp_ext/manifest.json) and
|
|
492
|
+
[cdp_ext/cdp_ext.js](cdp_ext/cdp_ext.js) form a Manifest V3 extension for Chrome
|
|
493
|
+
125+. The extension connects out to one WebSocket server. The server serves all
|
|
494
|
+
browser instances on the same port; each extension announces its native ID
|
|
495
|
+
in its first message. No remote-debugging argument is needed.
|
|
496
|
+
|
|
497
|
+
Run the server under Deno using its native
|
|
498
|
+
[WebSocket upgrade](https://docs.deno.com/api/deno/websockets/) or under Bun using
|
|
499
|
+
[Bun.serve](https://bun.sh/docs/runtime/http/websockets).
|
|
500
|
+
Node's direct CDP mode remains supported; `listen_reverse()` under Node rejects
|
|
501
|
+
with an explicit Deno/Bun requirement. There is no WebSocket framing implementation,
|
|
502
|
+
third-party server dependency or additional build step.
|
|
503
|
+
|
|
504
|
+
Run `deno run -A examples/reverse.mjs` or `bun examples/reverse.mjs` to launch Chrome with the extension, create
|
|
505
|
+
a target and evaluate `21 * 2`. Ctrl+C closes the owned browser and server.
|
|
506
|
+
The same mode is available per browser:
|
|
507
|
+
|
|
508
|
+
```js
|
|
509
|
+
const result = await client.call({
|
|
510
|
+
method: 'Runtime.evaluate',
|
|
511
|
+
params: {
|
|
512
|
+
browser: {
|
|
513
|
+
name: 'work',
|
|
514
|
+
cdp_ext: true,
|
|
515
|
+
headless: true,
|
|
516
|
+
},
|
|
517
|
+
target: 'demo',
|
|
518
|
+
expression: '21 * 2',
|
|
519
|
+
},
|
|
520
|
+
})
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
The manager copies the two extension files into `cdp_ext/` inside the effective
|
|
524
|
+
profile directory, sets the shared server URL in that copy's manifest and loads
|
|
525
|
+
it before browser startup. Without an effective user-data directory, the copy
|
|
526
|
+
lives under `base_path/<name>/<profile-directory>/` instead (`Default` when unset);
|
|
527
|
+
Chromium still uses its system profile. Profile preferences follow the existing
|
|
528
|
+
`update_preferences` rules.
|
|
529
|
+
The generated launch arguments omit remote debugging and enable this extension.
|
|
530
|
+
An `extensions` array also selects those directories; `extensions: true` keeps
|
|
531
|
+
normally enabled profile extensions through `'*'`. Raw `args` still win and can
|
|
532
|
+
prevent the bridge from loading or connecting, causing a connection timeout.
|
|
533
|
+
|
|
534
|
+
For external attachment, load unpacked `cdp_ext/` through `chrome://extensions`
|
|
535
|
+
or the [local-extension argument options](#local-extensions-at-startup).
|
|
536
|
+
Use its ID shown in Chrome, or `browser.extension_id(directory)`, as the
|
|
537
|
+
browser option `extension_id`. The logical `name` stays in the manager.
|
|
538
|
+
|
|
539
|
+
The extension configuration is read from `chrome.runtime.getManifest()`:
|
|
540
|
+
|
|
541
|
+
| Manifest `cdp` key | Default | Meaning |
|
|
542
|
+
| --- | --- | --- |
|
|
543
|
+
| `websocket_url` | `ws://127.0.0.1:9223` | The shared server endpoint. Port 9223 avoids the usual direct CDP port 9222. |
|
|
544
|
+
| `reconnect_ms` | `5000` | Connection retry interval while the extension worker is awake; also the empty-message heartbeat interval while connected. Use a positive value below 30000. |
|
|
545
|
+
|
|
546
|
+
The first message uses `chrome.runtime.id`, not a configured logical browser name.
|
|
547
|
+
For an unpacked extension without a manifest `key`, Chromium derives this ID from
|
|
548
|
+
its directory. Managed launches use a separate directory for each profile, so
|
|
549
|
+
the ID is stable across reconnects and restarts, and the manager knows which
|
|
550
|
+
browser it belongs to. External instances must likewise use distinct extension
|
|
551
|
+
directories. Moving the directory changes the ID. The server rejects duplicate
|
|
552
|
+
live IDs and announcements that differ from the WebSocket's `chrome-extension://`
|
|
553
|
+
origin. This is routing information, not an authentication credential.
|
|
554
|
+
|
|
555
|
+
```js
|
|
556
|
+
import { browser, cdp } from './cdp.js'
|
|
557
|
+
|
|
558
|
+
const client = new cdp({
|
|
559
|
+
reverse: true,
|
|
560
|
+
connect_timeout_ms: 60000,
|
|
561
|
+
reverse_server: {
|
|
562
|
+
hostname: '127.0.0.1',
|
|
563
|
+
port: 9223,
|
|
564
|
+
},
|
|
565
|
+
})
|
|
566
|
+
const result = await client.call({
|
|
567
|
+
method: 'Runtime.evaluate',
|
|
568
|
+
params: {
|
|
569
|
+
browser: {
|
|
570
|
+
name: 'other',
|
|
571
|
+
reverse: true,
|
|
572
|
+
extension_id: browser.extension_id('/absolute/path/to/cdp_ext'),
|
|
573
|
+
},
|
|
574
|
+
target: 'demo',
|
|
575
|
+
expression: 'document.title',
|
|
576
|
+
},
|
|
577
|
+
})
|
|
578
|
+
console.log(result.result.value)
|
|
579
|
+
await client.close()
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Construction and direct CDP calls do not start a listener. The first browser call
|
|
583
|
+
using `reverse: true` or `cdp_ext: true` starts the shared server lazily, with the
|
|
584
|
+
constructor's `reverse_server` settings. To mix direct and reverse browsers, leave the
|
|
585
|
+
constructor's `reverse` default false and specify `reverse: true` in that browser's
|
|
586
|
+
`params.browser` object. Constructor `port` remains a direct attachment option.
|
|
587
|
+
|
|
588
|
+
The listener closes when no reverse connection, pending reverse initialization
|
|
589
|
+
or live owned reverse process needs it. A live owned browser keeps it available
|
|
590
|
+
through temporary disconnects. External disconnects can release it when nothing
|
|
591
|
+
else needs it; the next reverse call restarts it and waits for the extension's
|
|
592
|
+
retry. Direct browsers do not keep this listener alive.
|
|
593
|
+
|
|
594
|
+
`client.reverse_server` is the public shared configuration object. After startup
|
|
595
|
+
it contains the effective listening port, including when `port: 0` originally
|
|
596
|
+
requested an available port. Restarts reuse that port so existing extensions can
|
|
597
|
+
still reconnect. `client.server` contains the current native Deno `HttpServer` or Bun `Server`,
|
|
598
|
+
or is unset/null while stopped. A call arriving during shutdown waits for it to
|
|
599
|
+
finish before starting a replacement server.
|
|
600
|
+
|
|
601
|
+
`listen_reverse(options = client.reverse_server)` remains available to start the
|
|
602
|
+
listener explicitly ahead of a browser call. It returns the native runtime server;
|
|
603
|
+
calling it while a server is running rejects. Explicit options replace the stored
|
|
604
|
+
settings, with omitted fields taking the hostname/port defaults above.
|
|
605
|
+
|
|
606
|
+
Both runtimes expose `server.addr.hostname`, `server.addr.port`,
|
|
607
|
+
`server.shutdown()` and `server.finished`. On Bun, the library adds these fields:
|
|
608
|
+
`shutdown()` calls native `stop()` and resolves `finished` after shutdown.
|
|
609
|
+
Use `client.close()` to also close owned browsers and incoming sockets.
|
|
610
|
+
Bun's callback-based server sockets receive a small EventTarget adapter with
|
|
611
|
+
native send/close and live ready state. The same `jsrpc` handles both runtimes;
|
|
612
|
+
registration, request IDs, pending waits, events and session routing are shared.
|
|
613
|
+
Native Bun WebSocket limits still apply.
|
|
614
|
+
|
|
615
|
+
Reverse calls reuse the normal target setup, custom handlers, mappings, routed
|
|
616
|
+
events, request correlation, timeout and error behavior. `reverse: true` alone
|
|
617
|
+
attaches externally: the manager neither launches nor updates that browser.
|
|
618
|
+
`cdp_ext: true` owns the launched process and applies normal launch preferences.
|
|
619
|
+
Closing the manager closes owned processes and the server, while external
|
|
620
|
+
browsers and tabs remain open. The extension retries when the server returns;
|
|
621
|
+
the next call rebuilds discovery and sessions, preserving surviving target IDs.
|
|
622
|
+
Commands interrupted by disconnect are never replayed.
|
|
623
|
+
|
|
624
|
+
A WebSocket close is not proof that Chrome exited. A managed reverse connection
|
|
625
|
+
loss or connection timeout leaves its owned process alive. Subsequent calls wait
|
|
626
|
+
for that extension to reconnect; they do not spawn another process. Only the
|
|
627
|
+
child process's confirmed exit permits a new launch. Explicit manager `close()`
|
|
628
|
+
still terminates owned browsers.
|
|
629
|
+
|
|
630
|
+
The current reverse registration uses a profile-specific extension copy. It does
|
|
631
|
+
not assign a new logical name to an already running Chrome using a shared extension
|
|
632
|
+
directory. Unpacked `chrome.runtime.id` identifies the extension path, not the
|
|
633
|
+
user-data directory or browser executable. Shared-path registration and URL-based
|
|
634
|
+
bootstrap are tracked in [TODO.md](TODO.md), not implemented behavior.
|
|
635
|
+
|
|
636
|
+
Chrome's [process singleton](https://github.com/chromium/chromium/blob/main/chrome/browser/process_singleton.h)
|
|
637
|
+
uses the user-data directory to decide whether to forward a new launch to an
|
|
638
|
+
existing instance. `no-default-browser-check` only suppresses the default-browser
|
|
639
|
+
prompt. The current Chromium [notification callback](https://github.com/chromium/chromium/blob/main/chrome/browser/chrome_browser_main.cc)
|
|
640
|
+
rejects forwarding when either command line enables automation or uses headless
|
|
641
|
+
mode. This library generates `enable-automation` by default; a normal browser
|
|
642
|
+
started with `args['enable-automation']: false` can behave differently. Do not
|
|
643
|
+
infer ownership of the real browser from a short-lived forwarding process.
|
|
644
|
+
|
|
645
|
+
The [service-worker lifecycle](https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle)
|
|
646
|
+
allows termination while offline. A 30-second Chrome alarm wakes the worker for
|
|
647
|
+
another connection attempt; the configured interval applies while it is awake.
|
|
648
|
+
Connected empty-text heartbeats keep an otherwise idle WebSocket worker active.
|
|
649
|
+
These are transport messages, not event buffering or polling of browser state.
|
|
650
|
+
|
|
651
|
+
### Reverse protocol and supported commands
|
|
652
|
+
|
|
653
|
+
The first extension-to-server message is `{ "browser": "<extension ID>" }`. Subsequent
|
|
654
|
+
messages use existing CDP envelopes:
|
|
655
|
+
|
|
656
|
+
- Request: `{ id, method, params, sessionId? }`.
|
|
657
|
+
- Response: `{ id, result }` or `{ id, error: { code, message } }`.
|
|
658
|
+
- Notification: `{ method, params, sessionId? }`.
|
|
659
|
+
- Empty text: heartbeat, ignored by `jsrpc`.
|
|
660
|
+
|
|
661
|
+
`jsrpc` assigns increasing request IDs per socket, shared by all its targets and
|
|
662
|
+
sessions. The extension echoes each request's ID in its response; IDs internal
|
|
663
|
+
to `chrome.debugger` are not exposed. Replies can arrive out of order. A timed-out
|
|
664
|
+
request's late reply cannot resolve a newer request. A new socket starts a new
|
|
665
|
+
ID sequence; the extension sends replies only on the connection that received
|
|
666
|
+
the request, so old completions cannot enter the replacement connection.
|
|
667
|
+
|
|
668
|
+
Top-level target IDs are stringified Chrome tab IDs, stable while a tab survives.
|
|
669
|
+
For root commands, the envelope's `sessionId` is that tab ID: the extension sends
|
|
670
|
+
to its registered `{ tabId }` debuggee without a generated UUID.
|
|
671
|
+
`Target.attachToTarget` returns the same tab ID in `sessionId`, and root
|
|
672
|
+
notifications use it too. This keeps the shared manager and RPC flow unchanged;
|
|
673
|
+
it is a reverse routing reference, not a native root CDP session.
|
|
674
|
+
|
|
675
|
+
Worker and out-of-process iframe commands still use real flattened CDP sessions.
|
|
676
|
+
Their native session IDs pass through unchanged, without prefixes. The extension
|
|
677
|
+
uses one `sessions` object for both roots and children: a tab ID maps to `{ tabId }`,
|
|
678
|
+
and a native child session ID maps to `{ tabId, sessionId }`. Every target command
|
|
679
|
+
looks up that object and passes the debuggee directly to `chrome.debugger.sendCommand`;
|
|
680
|
+
there is no fallback that interprets unknown session IDs as tab IDs.
|
|
681
|
+
Explicit detach takes `params.sessionId` for either a tab or child
|
|
682
|
+
session. Logical target labels remain inside the manager. Reconnecting invalidates
|
|
683
|
+
attachment/setup state even though a surviving root tab's routing ID stays the same.
|
|
684
|
+
|
|
685
|
+
The extension adapts browser-level `Target.getTargets`, `getTargetInfo`,
|
|
686
|
+
`setDiscoverTargets`, `setAutoAttach`, `createTarget`, `attachToTarget`,
|
|
687
|
+
`detachFromTarget`, `closeTarget` and `activateTarget` to Chrome tab/debugger APIs.
|
|
688
|
+
Discovery lists tabs; attaching a selected tab exposes its native child target
|
|
689
|
+
events and flattened sessions. It does not attach every unselected tab.
|
|
690
|
+
`createTarget` accepts only `url`, `background` and `incognito`; `activateTarget` selects the
|
|
691
|
+
tab and focuses its window. `Browser.getVersion` returns product/user-agent data;
|
|
692
|
+
its protocol version is `1.3`, with empty `revision` and `jsVersion` fields.
|
|
693
|
+
Other browser-level commands return explicit unsupported-command errors.
|
|
694
|
+
|
|
695
|
+
Target/session commands go through
|
|
696
|
+
[chrome.debugger](https://developer.chrome.com/docs/extensions/reference/api/debugger),
|
|
697
|
+
which supports a restricted set of CDP domains. Runtime, Page, Network, DOM,
|
|
698
|
+
Input and Emulation are available; ServiceWorker and BackgroundService are not.
|
|
699
|
+
Their default setup attempts therefore appear in `target.setup_errors`; disable
|
|
700
|
+
those setup flags if desired. Chrome restrictions, policies and debugger detach
|
|
701
|
+
events remain visible. Opening DevTools on an attached tab can detach this debugger.
|
|
702
|
+
Firefox/Camoufox support remains planned.
|
|
703
|
+
|
|
704
|
+
Browser-level `waitForDebuggerOnStart: true` returns an explicit error in reverse
|
|
705
|
+
mode: the extension cannot pause newly created root tabs before attachment.
|
|
706
|
+
Keep its default false and create `about:blank`, finish setup, then navigate.
|
|
707
|
+
Normal target-side `Target.setAutoAttach` is still native pass-through for children.
|
|
708
|
+
|
|
709
|
+
### Chrome extension API commands
|
|
710
|
+
|
|
711
|
+
In reverse mode, `chrome.<namespace>.<method>` calls the corresponding Chrome
|
|
712
|
+
extension method directly, including nested objects such as
|
|
713
|
+
`chrome.storage.session.get` and `chrome.privacy.websites.referrersEnabled.get`.
|
|
714
|
+
There is no method registry or CDP-name translation. A name such as
|
|
715
|
+
`Runtime.evaluate` still uses `chrome.debugger`; a name such as
|
|
716
|
+
`chrome.debugger.getTargets` calls the extension API with its native signature.
|
|
717
|
+
The same connection, request IDs, timeouts and RPC error envelopes handle both.
|
|
718
|
+
These names are extension-only; direct CDP connections do not implement them.
|
|
719
|
+
|
|
720
|
+
| Parameter | Meaning |
|
|
721
|
+
| --- | --- |
|
|
722
|
+
| `browser` | Usual manager browser label/configuration. |
|
|
723
|
+
| `args` | Positional native arguments as a JSON array; defaults to `[]`. One object argument is `[object]`; two arguments are `[first, second]`. |
|
|
724
|
+
| `callback` | Default `false`: await a native Promise or return a synchronous result. Set `true` for callback APIs: append a callback, await its first result and reject on `chrome.runtime.lastError`. |
|
|
725
|
+
| `target` | Optional manager target selection/setup. It does not supply a native tab ID to the extension API. Put native `target` objects inside `args`. |
|
|
726
|
+
|
|
727
|
+
```js
|
|
728
|
+
const tabs = await client.call({
|
|
729
|
+
method: 'chrome.tabs.query',
|
|
730
|
+
params: {
|
|
731
|
+
browser: 'work',
|
|
732
|
+
args: [{}],
|
|
733
|
+
},
|
|
734
|
+
})
|
|
735
|
+
await client.call({
|
|
736
|
+
method: 'chrome.scripting.insertCSS',
|
|
737
|
+
params: {
|
|
738
|
+
browser: 'work',
|
|
739
|
+
args: [{
|
|
740
|
+
target: {
|
|
741
|
+
tabId: tabs[0].id,
|
|
742
|
+
},
|
|
743
|
+
css: 'body { background: white; }',
|
|
744
|
+
}],
|
|
745
|
+
},
|
|
746
|
+
})
|
|
747
|
+
await client.call({
|
|
748
|
+
method: 'chrome.storage.session.set',
|
|
749
|
+
params: {
|
|
750
|
+
browser: 'work',
|
|
751
|
+
args: [{
|
|
752
|
+
example: true,
|
|
753
|
+
}],
|
|
754
|
+
},
|
|
755
|
+
})
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
Native arrays, objects, strings, booleans, numbers and `null` are returned directly;
|
|
759
|
+
only `undefined` becomes `{}`. The method is called on its owning object, so nested
|
|
760
|
+
API objects retain their native receiver. Arguments and results must be JSON data.
|
|
761
|
+
Callbacks can only be supplied through `callback: true`; functions, event listeners,
|
|
762
|
+
Ports, Blobs and other live/non-JSON objects have no remote representation.
|
|
763
|
+
The bridge forwards the network notifications documented below. It does not
|
|
764
|
+
provide generic remote registration of other extension event listeners.
|
|
765
|
+
|
|
766
|
+
`chrome.scripting.executeScript` supports `files` relative to the loaded extension
|
|
767
|
+
directory and returns the native array of frame results. The bridge also accepts
|
|
768
|
+
`func` as a function-expression string, including arrow and async functions:
|
|
769
|
+
|
|
770
|
+
```js
|
|
771
|
+
const frames = await client.call({
|
|
772
|
+
method: 'chrome.scripting.executeScript',
|
|
773
|
+
params: {
|
|
774
|
+
browser: 'work',
|
|
775
|
+
args: [{
|
|
776
|
+
target: {
|
|
777
|
+
tabId,
|
|
778
|
+
},
|
|
779
|
+
world: 'MAIN',
|
|
780
|
+
func: '(first, second) => first + second',
|
|
781
|
+
args: [19, 23],
|
|
782
|
+
}],
|
|
783
|
+
},
|
|
784
|
+
})
|
|
785
|
+
console.log(frames[0].result) // 42
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
Only string-valued `func` is adapted: a fixed native function evaluates that source
|
|
789
|
+
inside the destination world, then calls it with the supplied injection `args`.
|
|
790
|
+
The worker does not evaluate incoming source. Other injection fields remain native;
|
|
791
|
+
Chrome awaits returned Promises and returns its frame-result array. A bare body
|
|
792
|
+
such as `return 42` is not a function expression. To execute arbitrary script text,
|
|
793
|
+
use `func: 'code => (0, eval)(code)'` and `args: [code]`; the last expression or
|
|
794
|
+
resolved Promise supplies the result. Both paths have access to the destination DOM.
|
|
795
|
+
|
|
796
|
+
The native world default remains `ISOLATED`, whose content-script CSP blocks `eval`
|
|
797
|
+
and `new Function`. Select `MAIN` explicitly for string functions; the page's CSP
|
|
798
|
+
must allow string evaluation there. Both evaluators require the same `unsafe-eval`
|
|
799
|
+
permission, so switching between them does not bypass CSP. Native execution errors
|
|
800
|
+
may leave a frame result without `result`; consult the tab console as well. See
|
|
801
|
+
[content-script CSP](https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#content_security_policy).
|
|
802
|
+
|
|
803
|
+
CSP changes are explicit calls, never automatic. Two existing API paths are:
|
|
804
|
+
|
|
805
|
+
- With a debugger attachment, call `Page.setBypassCSP` with `enabled: true` on the
|
|
806
|
+
selected target **before navigation or reload**, then inject into `MAIN`.
|
|
807
|
+
Set `enabled: false` and reload to restore normal enforcement. This native CDP
|
|
808
|
+
method works through the reverse extension too; `Runtime.evaluate` already
|
|
809
|
+
accepts script text through the same debugger connection.
|
|
810
|
+
- Without a debugger, call `chrome.declarativeNetRequest.updateSessionRules` with a
|
|
811
|
+
`modifyHeaders` rule removing the `content-security-policy` response header.
|
|
812
|
+
Scope its `condition.tabIds` to the intended tab and its `resourceTypes` to
|
|
813
|
+
`main_frame` (include `sub_frame` for iframe navigations), then reload the tab.
|
|
814
|
+
Remove the rule through `removeRuleIds` when finished and reload again. Session
|
|
815
|
+
rules expire on browser shutdown. Header removal does not remove a CSP supplied
|
|
816
|
+
by HTML `<meta>`; the policy already applied to a loaded document also remains
|
|
817
|
+
until navigation. CSP affects the page's scripts as well as the injection.
|
|
818
|
+
|
|
819
|
+
See [Page.setBypassCSP](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-setBypassCSP)
|
|
820
|
+
and [declarative request rules](https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest).
|
|
821
|
+
Tested on Chrome 154: both explicit paths allow `MAIN` string evaluation and DOM
|
|
822
|
+
access on a page served with `script-src 'none'`.
|
|
823
|
+
|
|
824
|
+
These operations are observable. Page JavaScript can probe `eval` or `new Function`
|
|
825
|
+
and compare the outcome with its intended CSP. Code in `MAIN` shares the page's
|
|
826
|
+
globals, including any replacements of those functions. There is no standard page
|
|
827
|
+
API reporting which extension attached a debugger, but inspector operations can
|
|
828
|
+
have observable effects; attachment is not guaranteed to be invisible. V8 explicitly
|
|
829
|
+
treats prevention of [inspector detection](https://chromium.googlesource.com/v8/v8/+/c195efb1d03b5171c376a02bca885c68792ae0f7/src/inspector/SECURITY.md)
|
|
830
|
+
as a non-goal. A current behavioral probe does not provide a reliable history of
|
|
831
|
+
every past attachment or identify the caller by itself.
|
|
832
|
+
|
|
833
|
+
For native script-text injection without changing the page's CSP,
|
|
834
|
+
`chrome.userScripts.execute` (Chrome 135+) accepts `js[].code`. Its default
|
|
835
|
+
`USER_SCRIPT` world shares the DOM but has separate JavaScript globals and is exempt
|
|
836
|
+
from the page's CSP. `MAIN` instead shares the page's globals and CSP.
|
|
837
|
+
|
|
838
|
+
```js
|
|
839
|
+
const frames = await client.call({
|
|
840
|
+
method: 'chrome.userScripts.execute',
|
|
841
|
+
params: {
|
|
842
|
+
browser: 'work',
|
|
843
|
+
args: [{
|
|
844
|
+
target: {
|
|
845
|
+
tabId,
|
|
846
|
+
},
|
|
847
|
+
js: [{
|
|
848
|
+
code: 'document.querySelector("h1").dataset.injected = "yes"; Promise.resolve(42)',
|
|
849
|
+
}],
|
|
850
|
+
}],
|
|
851
|
+
},
|
|
852
|
+
})
|
|
853
|
+
console.log(frames[0].result) // 42
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
The manifest includes the permission, but Chrome also requires explicit user
|
|
857
|
+
activation: Developer mode before Chrome 138, or **Allow User Scripts** on the
|
|
858
|
+
extension's details page from Chrome 138 onward. Open
|
|
859
|
+
`chrome://extensions/?id=<extension_id>` using `client.browsers.work.extension_id`.
|
|
860
|
+
If necessary, reload the extension after enabling the toggle. Neither `cdp_ext: true`
|
|
861
|
+
nor `extensions` enables it or adds a default profile setting. A previously enabled
|
|
862
|
+
profile retains its choice. Chrome stores the extension-specific
|
|
863
|
+
`extensions.settings.<extension_id>.user_scripts_enabled` setting; on Windows it
|
|
864
|
+
is in protected `Secure Preferences`, and simply writing the key into a fresh
|
|
865
|
+
profile did not enable access in our test. Use Chrome's toggle rather than assuming
|
|
866
|
+
a normal `preferences` entry can update protected extension settings.
|
|
867
|
+
See the native [userScripts API and activation steps](https://developer.chrome.com/docs/extensions/reference/api/userScripts).
|
|
868
|
+
|
|
869
|
+
Chrome's native `scripting.executeScript` still rejects strings supplied directly
|
|
870
|
+
as `func`; the bridge's fixed function makes this adaptation possible. Overriding
|
|
871
|
+
an actual function's `toString`, `Symbol.toPrimitive` or `Function.prototype.toString`
|
|
872
|
+
does not replace the source Chrome serializes. Tests on Chrome 154 confirmed that
|
|
873
|
+
these overrides were never called and the original function executed.
|
|
874
|
+
|
|
875
|
+
The manifest grants `<all_urls>` host access and broad desktop extension API
|
|
876
|
+
permissions for scripting/debugging, tabs/groups/sessions, bookmarks/history/reading
|
|
877
|
+
list/search, cookies/content settings, storage, downloads, management, privacy/proxy,
|
|
878
|
+
accessibility/font settings, alarms/idle/power, system information, notifications,
|
|
879
|
+
context menus, capture, clipboard/geolocation access, identity, native messaging,
|
|
880
|
+
messaging through GCM, offscreen documents, side panels, speech/engines,
|
|
881
|
+
navigation/network requests and declarative rules.
|
|
882
|
+
Its `action` declaration also enables `chrome.action` methods. The exact permission
|
|
883
|
+
list is in [cdp_ext/manifest.json](cdp_ext/manifest.json); managed launches copy it.
|
|
884
|
+
There are no per-method permission prompts added by the bridge.
|
|
885
|
+
|
|
886
|
+
Permissions do not bypass Chrome's native restrictions: platform-specific,
|
|
887
|
+
enterprise-only and channel-specific APIs are not enabled by this manifest, and
|
|
888
|
+
Manifest V3's policy-only `webRequestBlocking` is not requested. User gestures,
|
|
889
|
+
incognito/file-access grants, capture dialogs, native messaging host installations
|
|
890
|
+
and required extension resources still apply. Unavailable methods and native errors
|
|
891
|
+
reject normally. See the official [extension API reference](https://developer.chrome.com/docs/extensions/reference/api),
|
|
892
|
+
[permission list](https://developer.chrome.com/docs/extensions/reference/permissions-list)
|
|
893
|
+
and [scripting API](https://developer.chrome.com/docs/extensions/reference/api/scripting).
|
|
894
|
+
|
|
265
895
|
## Command-line arguments
|
|
266
896
|
|
|
267
897
|
The library exposes a JavaScript API, not a command-line program. `args` refers
|
|
@@ -351,9 +981,12 @@ and [content switches](https://raw.githubusercontent.com/chromium/chromium/main/
|
|
|
351
981
|
| `disable-features` | array below | Disable named Chromium features. |
|
|
352
982
|
| `headless` | `'new'` when `headless` is true | Run without a visible browser window. |
|
|
353
983
|
| `window-size` | `'1440,900'` when headless | Set initial window dimensions. |
|
|
354
|
-
| `disable-extensions` | `true` unless `extensions` is true | Disable browser extensions. |
|
|
984
|
+
| `disable-extensions` | `true` unless `extensions` is true or an array | Disable browser extensions. |
|
|
355
985
|
| `disable-component-extensions-with-background-pages` | same condition | Disable those component extensions. |
|
|
986
|
+
| `disable-extensions-except` | the `extensions` array, otherwise unset | Load local directories and exclude other normal extensions. Raw overrides win. |
|
|
356
987
|
| `blink-settings` | `'imagesEnabled=false'` when `images` is false | Disable image loading through Blink settings. |
|
|
988
|
+
| `proxy-server` | `proxy` when it is a string | Configure the launched browser's proxy using native Chromium syntax. |
|
|
989
|
+
| `no-proxy-server` | `true` when `proxy` is false | Request direct connections; Chrome gives this flag precedence over other proxy flags. |
|
|
357
990
|
| `disable-signin-promo-on-avatar-pill-for-testing` | `true` unless `login` is true | Suppress the avatar sign-in promotion. |
|
|
358
991
|
| `disable-sync` | same condition | Disable browser sync. |
|
|
359
992
|
|
|
@@ -395,12 +1028,74 @@ These are native Chromium feature identifiers, not additional library options.
|
|
|
395
1028
|
Unless `translations` is true, the library appends `Translate`, `TranslateToast`
|
|
396
1029
|
and `EnableTranslatePdf`. Unless `login` is true, it appends
|
|
397
1030
|
`DiceWebSigninInterception` and `SigninPromoOnAvatarPill`. A raw
|
|
398
|
-
`args['disable-features']` replaces that
|
|
1031
|
+
`args['disable-features']` replaces that generated array; an active
|
|
1032
|
+
`disable-extensions-except` then appends its required feature ID as described below.
|
|
1033
|
+
|
|
1034
|
+
## Local extensions at startup
|
|
1035
|
+
|
|
1036
|
+
These conventions operate before Chromium starts, without CDP installation calls
|
|
1037
|
+
or manual extension registration in preference files:
|
|
1038
|
+
|
|
1039
|
+
1. An `extensions` array generates `disable-extensions-except` and omits the two
|
|
1040
|
+
normal extension-disabling defaults. `extensions: true` alone keeps normal
|
|
1041
|
+
profile loading without generating an exception list.
|
|
1042
|
+
2. Raw `args` override generated switches. A final non-null/non-false
|
|
1043
|
+
`disable-extensions-except` automatically appends
|
|
1044
|
+
`DisableDisableExtensionsExceptCommandLineSwitch` to `disable-features`, once.
|
|
1045
|
+
Raw feature arrays or comma-separated strings retain their entries.
|
|
1046
|
+
3. At launch, an exact `'*'` entry expands to enabled extension paths from the
|
|
1047
|
+
effective profile; duplicate paths are removed. Explicit paths remain selected.
|
|
1048
|
+
|
|
1049
|
+
```js
|
|
1050
|
+
const client = new cdp({
|
|
1051
|
+
extensions: [
|
|
1052
|
+
'*',
|
|
1053
|
+
'/absolute/path/to/local-extension',
|
|
1054
|
+
],
|
|
1055
|
+
})
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
The equivalent raw selection is `args['disable-extensions-except']` containing
|
|
1059
|
+
that array or a comma-separated string. It works independently of `extensions`;
|
|
1060
|
+
other raw switches remain effective. A null/false selector suppresses the generated
|
|
1061
|
+
exception list and its automatic feature addition. An empty array selects no
|
|
1062
|
+
normal extensions. Arrays passed by the caller are not mutated.
|
|
1063
|
+
|
|
1064
|
+
`browser.build_args` leaves `'*'` unexpanded. `browser.launch` reads
|
|
1065
|
+
`<user-data-dir>/<profile-directory or Default>/Preferences` and `Secure Preferences`
|
|
1066
|
+
before spawning, using the final raw directory arguments. Secure records take
|
|
1067
|
+
precedence for duplicate IDs. `browser.profile_extension_paths(args)` returns a
|
|
1068
|
+
plain object keyed by extension ID, containing both enabled and disabled records
|
|
1069
|
+
as `{ path, enabled }`. Paths refer to the current recorded version, including
|
|
1070
|
+
absolute unpacked paths. Disable reasons and legacy disabled state set
|
|
1071
|
+
`enabled: false`; internal component extensions and records without a path are
|
|
1072
|
+
omitted. Wildcard expansion takes only the entries with `enabled: true`, without
|
|
1073
|
+
scanning obsolete versions or activating disabled extensions. Missing files contribute no
|
|
1074
|
+
entries; malformed JSON and other read errors reject launch. `'*'` requires an
|
|
1075
|
+
explicit effective user-data directory; the system directory is not inferred.
|
|
1076
|
+
`update_preferences: false` does not disable this read-only expansion.
|
|
1077
|
+
|
|
1078
|
+
```js
|
|
1079
|
+
const extensions = await browser.profile_extension_paths({
|
|
1080
|
+
'user-data-dir': '/absolute/path/to/profile-root',
|
|
1081
|
+
'profile-directory': 'Default',
|
|
1082
|
+
})
|
|
1083
|
+
// extensions[extensionId] contains { path, enabled }.
|
|
1084
|
+
const disabled = Object.entries(extensions).filter(([, extension]) => !extension.enabled)
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
Explicit directories must contain unpacked extensions with `manifest.json`.
|
|
1088
|
+
The native argument excludes other normal extensions, so include `'*'` when
|
|
1089
|
+
preserving enabled profile extensions. Chromium still decides whether an extension
|
|
1090
|
+
can load. The feature override was verified on Chrome 154; it depends on Chrome
|
|
1091
|
+
retaining that internal switch, rather than a stable extension-installation API.
|
|
1092
|
+
See [Chromium's startup loader](https://github.com/chromium/chromium/blob/main/chrome/browser/extensions/extension_service.cc).
|
|
399
1093
|
|
|
400
1094
|
## Profile preferences
|
|
401
1095
|
|
|
402
|
-
|
|
403
|
-
Chromium
|
|
1096
|
+
The `cdp` manager edits the profile's JSON `Preferences` file before spawning
|
|
1097
|
+
Chromium when the effective `user-data-dir` argument specifies a directory and
|
|
1098
|
+
`update_preferences` is not false. The path is:
|
|
404
1099
|
|
|
405
1100
|
```text
|
|
406
1101
|
<args['user-data-dir']>/<args['profile-directory'] or Default>/Preferences
|
|
@@ -417,6 +1112,10 @@ preference defaults are:
|
|
|
417
1112
|
|
|
418
1113
|
`options.preferences` overwrites these defaults by key. Preferences and switches
|
|
419
1114
|
are separate layers; overriding one does not remove the other.
|
|
1115
|
+
`update_preferences: false` skips generated and raw preference writes, including
|
|
1116
|
+
`local_state`. It does not change generated CLI arguments. The default follows
|
|
1117
|
+
the effective directory, including raw overrides; the manager's generated named
|
|
1118
|
+
directory counts as a configured directory.
|
|
420
1119
|
|
|
421
1120
|
```js
|
|
422
1121
|
await client.call({
|
|
@@ -454,6 +1153,49 @@ supplied object. `null` is stored as JSON null. Malformed JSON, incompatible
|
|
|
454
1153
|
existing path values and filesystem errors propagate. It does not coordinate
|
|
455
1154
|
writes with a running browser, which may overwrite the file itself.
|
|
456
1155
|
|
|
1156
|
+
### Local State and the remote-debugging checkbox
|
|
1157
|
+
|
|
1158
|
+
Chrome stores settings shared by profiles in the same user-data directory in
|
|
1159
|
+
`<user-data-dir>/Local State`, outside `Default` or `Profile 1`. `local_state`
|
|
1160
|
+
instructs the `cdp` manager to update this file before launch using the same
|
|
1161
|
+
`browser.update_profile_preferences` helper and preservation rules.
|
|
1162
|
+
The effective raw `args['user-data-dir']` override
|
|
1163
|
+
selects its location; `profile-directory` does not affect it.
|
|
1164
|
+
|
|
1165
|
+
For Chrome's `chrome://inspect/#remote-debugging` checkbox:
|
|
1166
|
+
|
|
1167
|
+
```js
|
|
1168
|
+
const client = new cdp({
|
|
1169
|
+
local_state: {
|
|
1170
|
+
'devtools.remote_debugging.user-enabled': true,
|
|
1171
|
+
},
|
|
1172
|
+
})
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
Set the key to `false` to disable it. The helper can also edit this file directly,
|
|
1176
|
+
before starting Chrome:
|
|
1177
|
+
|
|
1178
|
+
```js
|
|
1179
|
+
await browser.update_profile_preferences('/absolute/user-data-dir/Local State', {
|
|
1180
|
+
'devtools.remote_debugging.user-enabled': true,
|
|
1181
|
+
})
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
This is the user-enabled setting, distinct from the administrator policy
|
|
1185
|
+
`devtools.remote_debugging.allowed`. Chrome's native mode requires approval for
|
|
1186
|
+
incoming connections; setting the preference does not remove that prompt.
|
|
1187
|
+
The library's normal launch still uses `--remote-debugging-port`, which takes
|
|
1188
|
+
precedence over this mode. See the [Chromium implementation](https://github.com/chromium/chromium/blob/main/chrome/browser/devtools/remote_debugging_server.cc).
|
|
1189
|
+
|
|
1190
|
+
`user_data_dir: null`, or a raw `args['user-data-dir']: null`/`false`, omits the
|
|
1191
|
+
directory switch. Direct `browser.launch` also omits it when no directory is
|
|
1192
|
+
provided. The manager skips both `preferences` and `local_state` writes in that
|
|
1193
|
+
case; it does not discover or edit the system directory. Chrome 136+ ignores
|
|
1194
|
+
the debugging-port switch for its system directory, so omitting the directory
|
|
1195
|
+
does not provide a working CDP connection through the normal launcher.
|
|
1196
|
+
Attach using an existing endpoint when using Chrome's native approval mode.
|
|
1197
|
+
See [Chrome's debugging-port restrictions](https://developer.chrome.com/blog/remote-debugging-port).
|
|
1198
|
+
|
|
457
1199
|
## Overlaps and override examples
|
|
458
1200
|
|
|
459
1201
|
| Settings | Effective library behavior |
|
|
@@ -464,10 +1206,15 @@ writes with a running browser, which may overwrite the file itself.
|
|
|
464
1206
|
| Explicit `windowsHide: true` or `false` | Pass that value to Node's `spawn`, regardless of headless mode. |
|
|
465
1207
|
| `extensions: true` plus `args['disable-extensions']: true` | The raw extension-disabling switch still wins. |
|
|
466
1208
|
| `extensions: false` plus only `args['disable-extensions']: false` | Remove that flag, but retain `disable-component-extensions-with-background-pages`. Use `extensions: true` to omit both generated flags. |
|
|
1209
|
+
| `extensions: ['*', directory]` | Select enabled profile extensions plus the explicit local directory before startup. |
|
|
1210
|
+
| An `extensions` array plus a raw `disable-extensions-except` | The raw selection replaces the generated array; null/false suppresses that selection. |
|
|
467
1211
|
| `images: false` plus an overridden `blink-settings` | Replace the whole string; image disabling is not appended to the caller's value. |
|
|
1212
|
+
| A string `proxy` plus `args['proxy-server']` | The raw proxy-server value replaces the generated one. |
|
|
1213
|
+
| `proxy: false` plus only `args['proxy-server']` | Retain `no-proxy-server`; Chrome still selects direct connections. Override `args['no-proxy-server']: false` to remove it. |
|
|
1214
|
+
| `proxy` plus an attachment endpoint | The launch option is unused; no running browser proxy is changed. |
|
|
468
1215
|
| `translations: false` plus `preferences['translate.enabled']: true` | Store true in the profile, while generated translation-disabling feature IDs remain on the CLI. |
|
|
469
1216
|
| `login: true` plus `args['disable-sync']: true` | Allow profile sign-in through generated preferences while still requesting sync to be disabled. |
|
|
470
|
-
| Any high-level settings plus a raw `disable-features` array | Replace the
|
|
1217
|
+
| Any high-level settings plus a raw `disable-features` array | Replace the generated list, including translation/sign-in additions; an active `disable-extensions-except` still appends its required feature ID. |
|
|
471
1218
|
| `user_data_dir` plus `args['user-data-dir']` | The raw argument determines both the profile write location and the launched browser directory. |
|
|
472
1219
|
| `port` plus `args['remote-debugging-port']` | Attach using `port`; no launch occurs, so the raw launch argument is unused. |
|
|
473
1220
|
| Constructor options plus an existing browser label | Reuse the stored record; configuration is not merged again on every call. |
|
|
@@ -524,6 +1271,8 @@ await client.call({
|
|
|
524
1271
|
| --- | --- | --- |
|
|
525
1272
|
| `name` | required in an object | Logical label in this browser's `targets` mapping. |
|
|
526
1273
|
| `targetId` | `null` | Optional existing physical target ID. Reused if discovered; otherwise create a new target. |
|
|
1274
|
+
| `context` | unset | Direct-mode context name or first-use configuration object; see named contexts above. Reverse mode rejects this option. |
|
|
1275
|
+
| `incognito` | `false` | Create/select a private target; direct mode uses the shared `'incognito'` context by default, reverse uses a native incognito window with the user's extension grant. |
|
|
527
1276
|
| `create_params` | `{}` | Native creation overrides, merged over `url: 'about:blank'` and `background: true`; start from a blank target without requesting foreground activation. |
|
|
528
1277
|
| `initialize` | `true` | Run the convenience setup sequence; false skips it. |
|
|
529
1278
|
| `runtime` | `'bootstrap'` | Enable Runtime for setup, then disable reporting. `true` leaves it enabled; `false` skips both operations. |
|
|
@@ -696,6 +1445,46 @@ browser/target resolution, may be synchronous or asynchronous, and define their
|
|
|
696
1445
|
own return value. Their exceptions propagate. An unregistered `_.` name rejects
|
|
697
1446
|
without launching a browser.
|
|
698
1447
|
|
|
1448
|
+
## Reverse network notifications
|
|
1449
|
+
|
|
1450
|
+
Reverse mode exposes two independent streams through the same JSON-RPC connection:
|
|
1451
|
+
|
|
1452
|
+
| Stream | Debugger required | Data |
|
|
1453
|
+
| --- | --- | --- |
|
|
1454
|
+
| `Network.*` | Yes | Native CDP events after `Network.enable`, normally enabled by target setup. Native commands can retrieve response bodies where supported. |
|
|
1455
|
+
| `chrome.webRequest.*` | No | Native extension request lifecycle details for accessible tabs, including request/response headers. No response bodies or WebSocket frames. |
|
|
1456
|
+
|
|
1457
|
+
The extension always forwards `onBeforeRequest`, `onBeforeSendHeaders`,
|
|
1458
|
+
`onSendHeaders`, `onHeadersReceived`, `onAuthRequired`, `onResponseStarted`,
|
|
1459
|
+
`onBeforeRedirect`, `onCompleted` and `onErrorOccurred` under the
|
|
1460
|
+
`chrome.webRequest.` prefix while connected. `network: false` skips debugger
|
|
1461
|
+
Network setup but does not disable this extension stream. No debugger attachment
|
|
1462
|
+
is required to observe a tab created/navigated through `chrome.tabs` calls.
|
|
1463
|
+
|
|
1464
|
+
```js
|
|
1465
|
+
client.addEventListener('chrome.webRequest.onCompleted', event => {
|
|
1466
|
+
const { browser, target, tabId, requestId, url, statusCode } = event.detail
|
|
1467
|
+
console.log(browser, target, tabId, requestId, url, statusCode)
|
|
1468
|
+
})
|
|
1469
|
+
```
|
|
1470
|
+
|
|
1471
|
+
Raw notifications have `{ method, params, sessionId }` and no request `id`.
|
|
1472
|
+
`params` contains native Chrome details. When `tabId >= 0`, `sessionId` is its
|
|
1473
|
+
string form, allowing normal manager routing even without a debugger session.
|
|
1474
|
+
Requests with `tabId: -1` have no target routing reference. Browser labels are
|
|
1475
|
+
added by the manager; target labels are added only for known logical targets.
|
|
1476
|
+
`requestId` belongs to the extension API and is not a CDP Network request ID.
|
|
1477
|
+
Raw socket notifications remain unchanged.
|
|
1478
|
+
|
|
1479
|
+
The manifest includes `webRequest`, `webRequestAuthProvider` and `<all_urls>`.
|
|
1480
|
+
`onAuthRequired` is observation only: no authentication response is supplied.
|
|
1481
|
+
Incognito traffic requires the bridge's **Allow in incognito** grant. Chrome
|
|
1482
|
+
restrictions, cached requests and protected URLs can limit visibility. Sensitive
|
|
1483
|
+
headers and some preflights require native `extraHeaders`, which this stream does
|
|
1484
|
+
not request. There is no library filtering, buffering, replay, interception or
|
|
1485
|
+
generic remote event registration; disconnected notifications are dropped.
|
|
1486
|
+
See the [Chrome webRequest API](https://developer.chrome.com/docs/extensions/reference/api/webRequest).
|
|
1487
|
+
|
|
699
1488
|
## Events and bindings
|
|
700
1489
|
|
|
701
1490
|
| Emitter / event | `event.detail` |
|
|
@@ -703,6 +1492,9 @@ without launching a browser.
|
|
|
703
1492
|
| `client`, `notify` | CDP message with browser and, when known, target labels added to `params`. |
|
|
704
1493
|
| `client`, native method such as `Runtime.bindingCalled` | That message's routed parameters. |
|
|
705
1494
|
| `client`, `close` | `{ browser, code }` for closure of the current browser socket. |
|
|
1495
|
+
| `client`, `context_closed` | `{ browser, context, browserContextId }` after successful managed context disposal. |
|
|
1496
|
+
| `client`, `error` | `{ browser, error }` when asynchronous managed context cleanup fails. |
|
|
1497
|
+
| `client`, `chrome.webRequest.*` | Native extension request details with browser and, when known, target labels. Reverse only; does not require debugger attachment. |
|
|
706
1498
|
| `socket`, `notify` | The unmodified CDP notification from the raw transport. |
|
|
707
1499
|
|
|
708
1500
|
Labels are strings, not record objects. Native `sessionId` remains available on
|
|
@@ -748,13 +1540,20 @@ dictionary containers have a null prototype.
|
|
|
748
1540
|
| Path | Contents |
|
|
749
1541
|
| --- | --- |
|
|
750
1542
|
| `client.base_path` | Resolved profile root. |
|
|
751
|
-
| `client.options` | Constructor browser defaults, excluding `base_path`. |
|
|
1543
|
+
| `client.options` | Constructor browser defaults, excluding `base_path` and `reverse_server`. |
|
|
1544
|
+
| `client.reverse_server` | Shared listener configuration; the effective hostname/port are stored after startup. |
|
|
752
1545
|
| `client.custom_methods[name]` | Handler function. |
|
|
753
1546
|
| `client.browsers[name]` | Browser configuration fields plus live state. |
|
|
1547
|
+
| `client.reverse_sockets[extension_id]` | Announced incoming socket, available even before the first call; native on Deno, an EventTarget adapter on Bun. |
|
|
1548
|
+
| `record.extension_id`, `record.extension_path` | Expected extension identity and the managed copy's canonical directory. |
|
|
1549
|
+
| `client.server` | Current native Deno/Bun reverse server with shared lifecycle fields, or unset/null when not listening. |
|
|
1550
|
+
| `client.server_closing` | Shutdown promise while the reverse listener is stopping, otherwise unset/null. |
|
|
754
1551
|
| `record.socket` | Current `jsrpc` connection once created. |
|
|
755
1552
|
| `record.proc` | Owned child process; absent for external attachment. |
|
|
756
1553
|
| `record.connecting` | In-progress connection promise, cleared after settlement. |
|
|
757
1554
|
| `record.targets[label]` | Target options, physical `targetId`, `sessionId`, `connecting` and `setup_errors` after setup. |
|
|
1555
|
+
| `record.contexts[name]` | Recorded context options, native `browserContextId` or null, and pending `connecting`/`closing` promises. Direct mode only. |
|
|
1556
|
+
| `record.context_checking` | Pending automatic empty-context cleanup, otherwise unset/null. |
|
|
758
1557
|
| `record.target_info[targetId]` | Discovered native target info plus cached `sessionId` when attached. |
|
|
759
1558
|
| `record.session_targets[sessionId]` | Physical `targetId` for a session. |
|
|
760
1559
|
| `socket.pending[id]` | Response-wait promise, removed on completion. |
|
|
@@ -800,15 +1599,29 @@ try {
|
|
|
800
1599
|
}
|
|
801
1600
|
```
|
|
802
1601
|
|
|
803
|
-
`browser.launch`
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
a CDP connection
|
|
1602
|
+
`browser.launch` builds arguments, starts the process, reads the
|
|
1603
|
+
DevTools WebSocket URL from stderr and returns `{ proc, url }`. With `cdp_ext: true`,
|
|
1604
|
+
it waits for process spawn instead and returns `url: undefined`; the caller must
|
|
1605
|
+
prepare the extension and supply its directory as `extension_path`.
|
|
1606
|
+
It does not open a CDP connection or write preference files; `update_preferences`, `preferences`
|
|
1607
|
+
and `local_state` are manager options, ignored by this helper.
|
|
1608
|
+
To prepare files independently, call
|
|
1609
|
+
`browser.update_profile_preferences` explicitly before launch. The helper does
|
|
1610
|
+
not resolve `user_data_dir` or `args['user-data-dir']`; explicit paths should be
|
|
1611
|
+
absolute. Omitting the directory lets Chromium select its system directory,
|
|
1612
|
+
subject to the debugging-port restrictions described above.
|
|
1613
|
+
Readiness timeout or launch failure rejects and attempts to
|
|
808
1614
|
terminate the spawned process.
|
|
809
1615
|
|
|
810
|
-
`jsrpc` takes a WebSocket URL
|
|
1616
|
+
`jsrpc` takes a WebSocket URL or an accepted EventTarget socket and an options
|
|
1617
|
+
object whose recognized option is
|
|
811
1618
|
`request_timeout_ms` (default `30000`). Wait for the socket to open before `req`.
|
|
1619
|
+
For an accepted socket, it returns that same object with RPC fields/methods.
|
|
1620
|
+
The object supplies `send`, `close`, live `readyState` and normal message/close/error
|
|
1621
|
+
events. Deno sockets remain native WebSockets; Bun uses the adapter described
|
|
1622
|
+
above. Accepted sockets need not be instances of the `jsrpc` subclass.
|
|
1623
|
+
The reverse server consumes the initial browser announcement before equipping
|
|
1624
|
+
the socket with RPC behavior. Empty-text heartbeats are ignored.
|
|
812
1625
|
The request is a raw envelope: `method`, optional `params`, and optional top-level
|
|
813
1626
|
`sessionId`. `req` assigns its own incrementing ID even if the input contains one.
|
|
814
1627
|
Its second argument overrides the timeout for that request. There are no browser
|
|
@@ -826,6 +1639,9 @@ labels, target labels, custom methods or reconnection policy in this layer.
|
|
|
826
1639
|
| Unknown `_.` handler | Reject before browser creation. |
|
|
827
1640
|
| Invalid profile JSON or filesystem failure | Propagate the parsing/filesystem error. |
|
|
828
1641
|
| Failed discovery or local launch | Reject the call establishing the connection. |
|
|
1642
|
+
| Named context in reverse mode | Reject explicitly; no separate process emulation. |
|
|
1643
|
+
| Reverse incognito without the extension's user grant | Reject with an instruction to enable Allow in incognito. |
|
|
1644
|
+
| Automatic context cleanup fails | Emit the manager's `error` event with browser label and original error. |
|
|
829
1645
|
|
|
830
1646
|
```js
|
|
831
1647
|
try {
|
|
@@ -845,6 +1661,7 @@ try {
|
|
|
845
1661
|
|
|
846
1662
|
Contributors and coding agents should read [AGENTS.md](AGENTS.md) and
|
|
847
1663
|
[DEV_PREF.md](DEV_PREF.md). The maintained implementation is [cdp.js](cdp.js).
|
|
1664
|
+
Planned work is tracked in [TODO.md](TODO.md).
|
|
848
1665
|
`extra/`, when present locally, holds ignored reference files and is not part of
|
|
849
1666
|
the tracked source or runtime dependencies.
|
|
850
1667
|
|
|
@@ -853,14 +1670,41 @@ workflow for detecting new conversations and extracting connector session handle
|
|
|
853
1670
|
from tool-call history. It is a caller-side recipe, not additional library API.
|
|
854
1671
|
|
|
855
1672
|
For npm packaging, account setup, and GitHub publishing, see
|
|
856
|
-
[Publishing](npm/PUBLISHING.md). Publishing
|
|
857
|
-
into `npm/` alongside its manifest; no build script is needed.
|
|
1673
|
+
[Publishing](npm/PUBLISHING.md). Publishing copies `cdp.js`, this README and the
|
|
1674
|
+
two `cdp_ext/` files into `npm/` alongside its manifest; no build script is needed.
|
|
858
1675
|
|
|
859
1676
|
```sh
|
|
860
1677
|
node --check cdp.js
|
|
1678
|
+
node --check cdp_ext/cdp_ext.js
|
|
861
1679
|
node --test tests/cdp.test.js
|
|
1680
|
+
deno test -A tests/reverse.test.js
|
|
1681
|
+
bun test tests/cdp.test.js tests/reverse.test.js --timeout 60000
|
|
862
1682
|
```
|
|
863
1683
|
|
|
864
1684
|
Integration tests launch local headless Chromium with disposable profiles under
|
|
865
1685
|
`build/tests/`. Set `CDP_BROWSER` if discovery does not match the machine. Direct
|
|
866
1686
|
library use needs no build or package installation; npm packaging is optional.
|
|
1687
|
+
The shared Deno/Bun reverse suite loads the real extension into two headless Chrome instances on one
|
|
1688
|
+
server port without remote-debugging arguments. It checks native/custom calls,
|
|
1689
|
+
Network events, webRequest notifications without debugger attachment, bindings,
|
|
1690
|
+
child sessions, reconnect, detach/recreate, incognito and isolation.
|
|
1691
|
+
Node coverage also checks actual browser/context proxy routing, shared private
|
|
1692
|
+
storage, concurrent context creation, automatic disposal, unselected targets and
|
|
1693
|
+
context ownership in externally attached browsers.
|
|
1694
|
+
Extension API coverage checks granted permissions, positional arguments, nested
|
|
1695
|
+
API receivers, synchronous/Promise/callback results, CSS/script injection, storage,
|
|
1696
|
+
native validation errors and false/null/undefined results. Function-injection checks
|
|
1697
|
+
cover native string rejection, ignored custom string conversions, the bridge's
|
|
1698
|
+
string-function adapter and async arguments/results. Real page CSP checks cover
|
|
1699
|
+
both evaluators in both worlds, explicit debugger bypass after reload, and explicit
|
|
1700
|
+
header removal through session rules. User-script checks cover default denied
|
|
1701
|
+
access and explicit activation, DOM changes and Promise results under restrictive
|
|
1702
|
+
page CSP. Only disposable-profile preparation uses direct CDP to operate Chrome's
|
|
1703
|
+
Allow User Scripts and Allow in incognito toggles; subsequent reverse execution
|
|
1704
|
+
uses no remote-debugging flags and never enables these grants automatically.
|
|
1705
|
+
Managed-launch coverage checks concurrent first use, profile-specific identity,
|
|
1706
|
+
connection timeout without process replacement, lazy listener startup/shutdown
|
|
1707
|
+
independent of direct browsers, and relaunch after confirmed exit. External reverse
|
|
1708
|
+
timeouts release the listening port; other live reverse connections retain it.
|
|
1709
|
+
The npm tarball contains `package.json`, `cdp.js`, `README.md` and the two extension
|
|
1710
|
+
files required by managed reverse launches.
|