@mefistofelix/cdp.js 0.1.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.
Files changed (3) hide show
  1. package/README.md +835 -0
  2. package/cdp.js +544 -0
  3. package/package.json +32 -0
package/README.md ADDED
@@ -0,0 +1,835 @@
1
+ # cdp.js
2
+
3
+ A low-level Chromium DevTools Protocol library for Node.js. One ES module,
4
+ standard-library imports, no runtime dependencies or root `package.json`.
5
+
6
+ Use Node.js 22.19+ and an installed Chrome, Chromium or Edge. Import `cdp.js`
7
+ directly from an ES module, for example `example.mjs`, and run `node example.mjs`.
8
+ Browser discovery covers common Windows, macOS and Linux installation paths;
9
+ use `executable_path` or the `CDP_BROWSER` environment variable for other paths.
10
+
11
+ For a published npm release:
12
+
13
+ ```sh
14
+ npm install @mefistofelix/cdp.js
15
+ ```
16
+
17
+ ```js
18
+ import api, { util, jsrpc, browser, cdp } from '@mefistofelix/cdp.js'
19
+ ```
20
+
21
+ The npm package exports the same file and API. Direct file imports in the examples
22
+ below remain supported. Publication metadata is isolated in `npm/`.
23
+
24
+ ## Features and scope
25
+
26
+ - Native CDP requests through `call({ method, params })`, with native results.
27
+ - Named browsers and targets, created and connected on demand.
28
+ - Attachment through WebSocket, HTTP discovery or a debugging port.
29
+ - Shared initialization for concurrent calls; reconnection and target recreation
30
+ on subsequent use, without replaying failed commands.
31
+ - Public object mappings and directly replaceable custom method handlers.
32
+ - Configurable target initialization, JavaScript bindings and direct events.
33
+ - XPath `_.click` and `_.find`, including two XPath expression rewrites.
34
+ - Configurable browser launch arguments and profile preferences.
35
+ - Raw WebSocket JSON-RPC transport and reusable static helpers.
36
+
37
+ 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
39
+ profiles and their preferences are ordinary files on disk. Any native CDP method
40
+ supported by the connected browser remains available through `call`.
41
+
42
+ ## Quick start
43
+
44
+ This complete example uses a local blank target and needs no website:
45
+
46
+ ```js
47
+ import { cdp } from './cdp.js'
48
+
49
+ const client = new cdp({
50
+ base_path: './profiles',
51
+ headless: true,
52
+ })
53
+
54
+ try {
55
+ await client.call({
56
+ method: 'Runtime.evaluate',
57
+ params: {
58
+ browser: 'main',
59
+ target: 'demo',
60
+ expression: `
61
+ document.body.innerHTML = '<button id="hello">Hello</button>';
62
+ window.clicks = 0;
63
+ document.querySelector('button').onclick = () => window.clicks++;
64
+ `,
65
+ },
66
+ })
67
+ const found = await client.call({
68
+ method: '_.find',
69
+ params: {
70
+ target: 'demo',
71
+ xpath: '//button',
72
+ },
73
+ })
74
+ console.log(found.result.value.items)
75
+
76
+ const clicked = await client.call({
77
+ method: '_.click',
78
+ params: {
79
+ target: 'demo',
80
+ xpath: '//button[@id="hello"]',
81
+ },
82
+ })
83
+ console.log(clicked.result.value) // true
84
+ } finally {
85
+ await client.close()
86
+ }
87
+ ```
88
+
89
+ The following short examples assume a `client` that has not yet been closed.
90
+
91
+ ## Exports and API
92
+
93
+ ```js
94
+ import api, { util, jsrpc, browser, cdp } from './cdp.js'
95
+ // api contains the same four classes.
96
+ ```
97
+
98
+ | API | Purpose / result |
99
+ | --- | --- |
100
+ | `new cdp(options = {})` | Create a manager; does not launch a browser yet. |
101
+ | `client.call({ method, params = {} })` | Resolve routing and return a promise for the native CDP result, or a custom handler's result. |
102
+ | `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 browser processes, disconnect attached browsers, then clear `client.browsers`. Profile directories remain. |
104
+ | `new jsrpc(url, options = {})` | Open a raw WebSocket JSON-RPC connection. |
105
+ | `socket.req(request, timeout_ms)` | Assign a request ID and await its native result. |
106
+ | `socket.notify(request)` | Serialize and send a raw message; no ID allocation or response wait. |
107
+ | `socket.close()` | Native WebSocket close; does not terminate the browser. |
108
+ | `browser.find_executable_path()` | Synchronously return an executable path from the environment or common locations. |
109
+ | `browser.build_args(options = {})` | Return a fresh, editable object of launch switches. |
110
+ | `browser.update_profile_preferences(filename, preferences)` | Update dotted keys in a JSON preferences file; return a promise. |
111
+ | `browser.launch(options)` | Start a local browser and return a promise for `{ proc, url }`. The caller owns the process. |
112
+ | `util.normalize_xpath(xpath)` | Return an XPath string with the supported rewrites. |
113
+ | `util.args_to_strings(args)` | Convert a switch object to an array of command-line argument strings. |
114
+ | `util.emit(target, name, detail)` | Dispatch a `CustomEvent`; return `dispatchEvent`'s boolean result. |
115
+ | `util.on_first(target, names, timeout_ms = 30000)` | Await the first space-separated event name; return `{ type, args }`. |
116
+
117
+ `cdp` is an `EventTarget`; `jsrpc` extends the native `WebSocket`. The `browser`
118
+ and `util` classes contain static helpers only. `browser` does not own CDP
119
+ connections or target/session routing.
120
+
121
+ ## Calls and routing
122
+
123
+ | Field | Meaning |
124
+ | --- | --- |
125
+ | `method` | Native name such as `Page.navigate`, or a custom name beginning with `_.`. |
126
+ | `params` | Native parameters plus the two routing fields below. Defaults to `{}`. |
127
+ | `params.browser` | Browser label or configuration object with `name`. Defaults to `'main'`. |
128
+ | `params.target` | Target label or configuration object with `name`. Omitted or `null` means a browser-level request. |
129
+
130
+ For native requests, the manager removes `browser` and `target` from a copy of
131
+ `params` and adds the resolved `sessionId` to the protocol envelope. The caller's
132
+ object is not mutated. A string label is shorthand for `{ name: 'label' }`.
133
+ Put routing inside `params`, not beside `method`.
134
+
135
+ ```js
136
+ const version = await client.call({
137
+ method: 'Browser.getVersion',
138
+ params: {
139
+ browser: 'main',
140
+ },
141
+ })
142
+ console.log(version.product)
143
+
144
+ const result = await client.call({
145
+ method: 'Runtime.evaluate',
146
+ params: {
147
+ browser: 'main',
148
+ target: 'demo',
149
+ expression: 'window.clicks',
150
+ returnByValue: true,
151
+ },
152
+ })
153
+ console.log(result.result.value)
154
+ ```
155
+
156
+ `call` unwraps only the outer protocol response. `result.result.value` above is
157
+ the remote value inside `Runtime.evaluate`'s own result object. Other methods
158
+ have their own result shapes and parameters, defined by the
159
+ [CDP reference](https://chromedevtools.github.io/devtools-protocol/).
160
+
161
+ ## Configuration scope and precedence
162
+
163
+ `new cdp({ base_path, ...options })` sets the profile root and default **browser**
164
+ options. `params.browser` overrides them for a particular label. Target options
165
+ belong in `params.target` and do not inherit from the constructor.
166
+
167
+ For a new browser record, configuration is assembled in this order:
168
+
169
+ 1. Default `user_data_dir` derived from `base_path` and the browser name.
170
+ 2. Constructor browser options.
171
+ 3. The `params.browser` configuration object.
172
+
173
+ `args` and `preferences` are shallow-merged separately between steps 2 and 3.
174
+ For duplicate keys, the per-browser value wins. Arrays replace previous arrays;
175
+ they are not concatenated during this merge.
176
+
177
+ At local launch, another two independent merges happen:
178
+
179
+ - Generated CLI defaults, adjusted by high-level browser options, then raw `args`.
180
+ - Generated profile preference defaults, then raw `preferences`.
181
+
182
+ There is no conflict resolver across CLI flags and preferences. Each is passed
183
+ to Chromium through its own mechanism. Raw settings can therefore contradict a
184
+ high-level option; the library does not silently reconcile them.
185
+
186
+ Configuration is recorded on the first use of a label. A subsequent configuration
187
+ object with the same name does not reconfigure that record. Public records can
188
+ be edited directly: launch settings apply at a later launch, target settings at
189
+ a later initialization. Direct edits do not immediately send CDP commands. Use
190
+ a new browser label, or close the manager and configure it again, for a fresh
191
+ local configuration.
192
+
193
+ ## Browser options and defaults
194
+
195
+ | Option | Type / default | Meaning |
196
+ | --- | --- | --- |
197
+ | `base_path` | string, `'.cdp'` | Constructor-only profile root, resolved against the working directory. |
198
+ | `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, `<base_path>/<name>` | Chromium user-data directory containing profiles such as `Default`. The generated default is absolute; explicit overrides should be absolute too. |
200
+ | `executable_path` | string, automatic discovery | Executable to spawn; takes precedence over `CDP_BROWSER`. |
201
+ | `headless` | boolean, `false` | When true, add `headless: 'new'` and default `window-size: '1440,900'`. |
202
+ | `extensions` | boolean, `false` | When true, omit the two extension-disabling switches; does not install extensions. |
203
+ | `images` | boolean, `true` | When false, request `blink-settings: 'imagesEnabled=false'`; controls loading, not image processing. |
204
+ | `translations` | boolean, `false` | Set translation preferences and, unless true, disable translation feature IDs. |
205
+ | `login` | boolean, `false` | Set browser sign-in preferences and, unless true, disable sync and sign-in promotions. Does not prevent website login. |
206
+ | `args` | object, `{}` | Raw CLI overrides applied after generated defaults; keys omit `--`. |
207
+ | `preferences` | object, `{}` | Dotted JSON preference overrides applied after generated preference defaults. |
208
+ | `connect_timeout_ms` | number, `15000` | Timeout for each discovery, launch-readiness or socket-opening wait; not one combined deadline. |
209
+ | `request_timeout_ms` | number, `30000` | Default timeout for each protocol request on this browser's socket. |
210
+ | `websocket_url` | string, unset | Attach directly to a browser WebSocket endpoint. |
211
+ | `http_url` | string, unset | Discover the endpoint from an HTTP(S) base URL or exact `/json/version` URL. |
212
+ | `port` | number, unset | Discover an existing browser at `http://<host>:<port>/json/version`. |
213
+ | `host` | string, `'127.0.0.1'` | Host used with `port`. |
214
+
215
+ ### Why these defaults
216
+
217
+ The preset aims to make local automation predictable while keeping the browser
218
+ visible by default. These are practical purposes and tradeoffs of the current
219
+ settings, not a claim that every flag is required by every workload:
220
+
221
+ | Default | Purpose / tradeoff |
222
+ | --- | --- |
223
+ | Visible browser | Allows direct inspection and manual interaction; use `headless: true` for unattended runs. |
224
+ | Images enabled | Preserves normal page content and layout; disabling them can reduce loading work. |
225
+ | Extensions disabled | Avoids extension behavior and background pages affecting a run; enable them when the profile needs them. |
226
+ | Translations disabled | Avoids browser translation changing page content or presenting translation UI. |
227
+ | Browser login/sync disabled | Keeps browser-account integration out of the automation profile; website authentication remains available. |
228
+ | Separate named profile directories | Separates browser state by label while retaining normal profile data between launches. |
229
+ | Debugging port `0` | Lets Chromium choose an available port; launch reads the actual endpoint from stderr. |
230
+ | Bounded connection/request waits | Prevents indefinite local waits; timeout does not cancel execution already sent to Chromium. |
231
+ | Raw results and direct events | Keeps native protocol data available without another response or subscription abstraction. |
232
+
233
+ Automatic discovery checks `CDP_BROWSER` first. On Windows it checks Chrome and
234
+ Edge below `ProgramFiles`, `ProgramFiles(x86)` and `LOCALAPPDATA`. On macOS/Linux
235
+ it checks the application paths and `/usr/bin` candidates listed in
236
+ `browser.find_executable_path()`. It does not download a browser or search
237
+ arbitrary locations in `PATH`.
238
+
239
+ ### Attach to an existing browser
240
+
241
+ `websocket_url` takes precedence over HTTP discovery; `http_url` takes precedence
242
+ over `host`/`port`. Use one endpoint form:
243
+
244
+ ```js
245
+ await client.call({
246
+ method: 'Browser.getVersion',
247
+ params: {
248
+ browser: {
249
+ name: 'existing',
250
+ port: 9222,
251
+ },
252
+ },
253
+ })
254
+ ```
255
+
256
+ The existing browser must already expose CDP. Launch flags and profile
257
+ preferences are not applied to it. `client.close()` disconnects its socket and
258
+ leaves that browser running.
259
+
260
+ `port: 9222` means **attach**. To **launch** on that port, use
261
+ `args: { 'remote-debugging-port': 9222 }` without an attachment endpoint.
262
+
263
+ ## Command-line arguments
264
+
265
+ The library exposes a JavaScript API, not a command-line program. `args` refers
266
+ to the switches passed to the Chromium executable.
267
+
268
+ `browser.build_args(options)` builds defaults, applies boolean options, then
269
+ overwrites keys with `options.args`. The result remains an object until
270
+ `util.args_to_strings` materializes it for `child_process.spawn`. No shell command
271
+ is assembled; a value containing spaces stays inside one argument.
272
+
273
+ | Object value | Generated argument |
274
+ | --- | --- |
275
+ | `true` | `--key` |
276
+ | `false`, `null`, `undefined` | Omitted |
277
+ | string or number | `--key=value` |
278
+ | array | `--key=first,second` |
279
+
280
+ The array rule applies to every switch. An empty array emits `--key=`. There is
281
+ no separate `disabled_features` option. Setting a generated flag to `false`
282
+ removes it; it does not automatically emit an opposite flag.
283
+
284
+ ```js
285
+ import { browser, util } from './cdp.js'
286
+
287
+ const args = browser.build_args({
288
+ user_data_dir: './profiles/example',
289
+ headless: true,
290
+ args: {
291
+ 'mute-audio': false,
292
+ 'window-size': '800,600',
293
+ 'enable-features': ['FeatureOne', 'FeatureTwo'],
294
+ },
295
+ })
296
+ args['disable-features'].push('AnotherFeature')
297
+ delete args['hide-crash-restore-bubble']
298
+ console.log(util.args_to_strings(args))
299
+ ```
300
+
301
+ To launch with a modified object, supply it as `options.args`; defaults are built
302
+ again, so suppress a default with `false` in that override object. Deleting a key
303
+ from a standalone object suppresses it only when that object itself is
304
+ materialized directly.
305
+
306
+ ### Generated switches
307
+
308
+ These are the library's requested defaults and their intended purpose. The
309
+ installed Chromium build determines which switches and feature IDs it honors;
310
+ some are platform-specific or retained compatibility switches. The preset does
311
+ not guarantee that every browser version implements every entry. Native
312
+ reference definitions include Chromium's [Chrome switches](https://raw.githubusercontent.com/chromium/chromium/main/chrome/common/chrome_switches.h)
313
+ and [content switches](https://raw.githubusercontent.com/chromium/chromium/main/content/public/common/content_switches.cc).
314
+
315
+ | Switch key | Value / condition | Purpose |
316
+ | --- | --- | --- |
317
+ | `user-data-dir` | `user_data_dir` | Select user-data storage. |
318
+ | `remote-debugging-port` | `0` | Enable CDP on a port chosen by Chromium. |
319
+ | `enable-automation` | `true` | Enable Chromium automation mode. |
320
+ | `mute-audio` | `true` | Mute browser audio. |
321
+ | `disable-blink-features` | `'AutomationControlled'` | Disable that Blink runtime feature. |
322
+ | `hide-crash-restore-bubble` | `true` | Suppress crash-restore UI where supported. |
323
+ | `disable-field-trial-config` | `true` | Disable field-trial configuration. |
324
+ | `disable-background-networking` | `true` | Reduce subsystem background networking. |
325
+ | `enable-features` | array below | Enable named Chromium features. |
326
+ | `disable-background-timer-throttling` | `true` | Disable background-page timer throttling. |
327
+ | `disable-backgrounding-occluded-windows` | `true` | Avoid backgrounding occluded windows. |
328
+ | `disable-renderer-backgrounding` | `true` | Avoid lowering renderer priority in background. |
329
+ | `disable-back-forward-cache` | `true` | Disable the back/forward page cache. |
330
+ | `disable-breakpad` | `true` | Disable Breakpad crash reporting where supported. |
331
+ | `disable-client-side-phishing-detection` | `true` | Disable client-side phishing detection. |
332
+ | `disable-component-update` | `true` | Disable browser component updates. |
333
+ | `no-default-browser-check` | `true` | Skip the default-browser check. |
334
+ | `disable-default-apps` | `true` | Skip default application installation. |
335
+ | `disable-dev-shm-usage` | `true` | Avoid `/dev/shm` storage on Linux. |
336
+ | `allow-pre-commit-input` | `true` | Allow input before frame commit where supported. |
337
+ | `disable-hang-monitor` | `true` | Disable renderer hang monitoring. |
338
+ | `disable-ipc-flooding-protection` | `true` | Disable IPC flooding rate limits. |
339
+ | `disable-popup-blocking` | `true` | Disable automatic popup blocking. |
340
+ | `disable-prompt-on-repost` | `true` | Suppress form-repost confirmation. |
341
+ | `force-color-profile` | `'srgb'` | Select the sRGB color profile. |
342
+ | `metrics-recording-only` | `true` | Request metrics recording without reporting. |
343
+ | `no-first-run` | `true` | Skip the first-run experience. |
344
+ | `password-store` | `'basic'` | Select the basic password-store backend. |
345
+ | `use-mock-keychain` | `true` | Use the test keychain where supported. |
346
+ | `no-service-autorun` | `true` | Suppress service autorun where supported. |
347
+ | `export-tagged-pdf` | `true` | Request tagged PDF output where supported. |
348
+ | `disable-search-engine-choice-screen` | `true` | Suppress search-engine selection UI. |
349
+ | `disable-features` | array below | Disable named Chromium features. |
350
+ | `headless` | `'new'` when `headless` is true | Run without a visible browser window. |
351
+ | `window-size` | `'1440,900'` when headless | Set initial window dimensions. |
352
+ | `disable-extensions` | `true` unless `extensions` is true | Disable browser extensions. |
353
+ | `disable-component-extensions-with-background-pages` | same condition | Disable those component extensions. |
354
+ | `blink-settings` | `'imagesEnabled=false'` when `images` is false | Disable image loading through Blink settings. |
355
+ | `disable-signin-promo-on-avatar-pill-for-testing` | `true` unless `login` is true | Suppress the avatar sign-in promotion. |
356
+ | `disable-sync` | same condition | Disable browser sync. |
357
+
358
+ The sign-in promotion switch is defined in Chromium's
359
+ [sign-in switches](https://raw.githubusercontent.com/chromium/chromium/main/components/signin/public/base/signin_switches.cc).
360
+ `about:blank` is appended as the initial URL. Launching or creating a managed
361
+ target does not close other tabs.
362
+
363
+ The initial `enable-features` array is:
364
+
365
+ ```js
366
+ [
367
+ 'NetworkService',
368
+ 'NetworkServiceInProcess',
369
+ ]
370
+ ```
371
+
372
+ The initial `disable-features` array is:
373
+
374
+ ```js
375
+ [
376
+ 'InfiniteSessionRestore',
377
+ 'ImprovedCookieControls',
378
+ 'LazyFrameLoading',
379
+ 'GlobalMediaControls',
380
+ 'DestroyProfileOnBrowserClose',
381
+ 'MediaRouter',
382
+ 'DialMediaRouteProvider',
383
+ 'AcceptCHFrame',
384
+ 'AutoExpandDetailsElement',
385
+ 'CertificateTransparencyComponentUpdater',
386
+ 'AvoidUnnecessaryBeforeUnloadCheckSync',
387
+ 'HttpsUpgrades',
388
+ 'PaintHolding',
389
+ ]
390
+ ```
391
+
392
+ These are native Chromium feature identifiers, not additional library options.
393
+ Unless `translations` is true, the library appends `Translate`, `TranslateToast`
394
+ and `EnableTranslatePdf`. Unless `login` is true, it appends
395
+ `DiceWebSigninInterception` and `SigninPromoOnAvatarPill`. A raw
396
+ `args['disable-features']` replaces that entire generated array.
397
+
398
+ ## Profile preferences
399
+
400
+ Local launch edits the profile's JSON `Preferences` file before spawning
401
+ Chromium. The effective path is:
402
+
403
+ ```text
404
+ <args['user-data-dir']>/<args['profile-directory'] or Default>/Preferences
405
+ ```
406
+
407
+ `profile-directory` is a raw switch, for example `'Profile 1'`. The generated
408
+ preference defaults are:
409
+
410
+ | Dotted key | Value | Meaning |
411
+ | --- | --- | --- |
412
+ | `translate.enabled` | `translations === true` | Allow browser translation. |
413
+ | `signin.allowed` | `login === true` | Allow browser profile sign-in. |
414
+ | `signin.allowed_on_next_startup` | `login === true` | Apply sign-in permission at startup. |
415
+
416
+ `options.preferences` overwrites these defaults by key. Preferences and switches
417
+ are separate layers; overriding one does not remove the other.
418
+
419
+ ```js
420
+ await client.call({
421
+ method: 'Browser.getVersion',
422
+ params: {
423
+ browser: {
424
+ name: 'downloads',
425
+ headless: true,
426
+ translations: true,
427
+ preferences: {
428
+ 'download.prompt_for_download': false,
429
+ 'download.default_directory': '/absolute/path/to/downloads',
430
+ },
431
+ args: {
432
+ 'profile-directory': 'Default',
433
+ },
434
+ },
435
+ },
436
+ })
437
+ ```
438
+
439
+ The helper is also usable independently, before starting the browser:
440
+
441
+ ```js
442
+ await browser.update_profile_preferences('./profiles/example/Default/Preferences', {
443
+ 'translate.enabled': false,
444
+ 'download.prompt_for_download': false,
445
+ })
446
+ ```
447
+
448
+ It creates missing directories and a missing JSON file, traverses dotted keys,
449
+ assigns JSON-compatible values and preserves unrelated values. An object
450
+ assigned at a key replaces that value; this is not a recursive merge of the
451
+ supplied object. `null` is stored as JSON null. Malformed JSON, incompatible
452
+ existing path values and filesystem errors propagate. It does not coordinate
453
+ writes with a running browser, which may overwrite the file itself.
454
+
455
+ ## Overlaps and override examples
456
+
457
+ | Settings | Effective library behavior |
458
+ | --- | --- |
459
+ | `headless: true` plus `args.headless: false` | Omit `--headless`; the generated window-size argument remains unless also overridden. |
460
+ | `headless: false` plus `args.headless: 'new'` | Send `--headless=new`; no automatic headless window-size default was generated. |
461
+ | `extensions: true` plus `args['disable-extensions']: true` | The raw extension-disabling switch still wins. |
462
+ | `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. |
463
+ | `images: false` plus an overridden `blink-settings` | Replace the whole string; image disabling is not appended to the caller's value. |
464
+ | `translations: false` plus `preferences['translate.enabled']: true` | Store true in the profile, while generated translation-disabling feature IDs remain on the CLI. |
465
+ | `login: true` plus `args['disable-sync']: true` | Allow profile sign-in through generated preferences while still requesting sync to be disabled. |
466
+ | Any high-level settings plus a raw `disable-features` array | Replace the whole generated list, including translation/sign-in additions. |
467
+ | `user_data_dir` plus `args['user-data-dir']` | The raw argument determines both the profile write location and the launched browser directory. |
468
+ | `port` plus `args['remote-debugging-port']` | Attach using `port`; no launch occurs, so the raw launch argument is unused. |
469
+ | Constructor options plus an existing browser label | Reuse the stored record; configuration is not merged again on every call. |
470
+ | `initialize: false` plus `runtime: true` or a binding name | Skip optional target setup, including domain enabling and binding installation. |
471
+ | `binding: false` plus `runtime: true` | Skip binding installation but retain Runtime enabling/reporting. |
472
+
473
+ For translation/sign-in behavior, prefer the high-level option unless a mixed
474
+ configuration is intentional. Browser versions and policies ultimately determine
475
+ the outcome of contradictory native settings; the library only constructs the
476
+ arguments and writes the preferences described here.
477
+
478
+ A raw feature list can be extended without losing the generated entries:
479
+
480
+ ```js
481
+ const generated = browser.build_args({
482
+ translations: true,
483
+ login: false,
484
+ })
485
+
486
+ const clientWithOverrides = new cdp({
487
+ translations: true,
488
+ login: false,
489
+ args: {
490
+ 'disable-features': [...generated['disable-features'], 'AnotherFeature'],
491
+ },
492
+ })
493
+ await clientWithOverrides.close()
494
+ ```
495
+
496
+ ## Target options and lifecycle
497
+
498
+ Target configuration belongs in `params.target`:
499
+
500
+ ```js
501
+ await client.call({
502
+ method: 'Runtime.evaluate',
503
+ params: {
504
+ target: {
505
+ name: 'events',
506
+ binding: 'send_to_host',
507
+ runtime: true,
508
+ network: false,
509
+ create_params: {
510
+ background: false,
511
+ },
512
+ },
513
+ expression: 'document.title',
514
+ returnByValue: true,
515
+ },
516
+ })
517
+ ```
518
+
519
+ | Option | Default | Meaning / purpose of default |
520
+ | --- | --- | --- |
521
+ | `name` | required in an object | Logical label in this browser's `targets` mapping. |
522
+ | `targetId` | `null` | Optional existing physical target ID. Reused if discovered; otherwise create a new target. |
523
+ | `create_params` | `{}` | Native creation overrides, merged over `url: 'about:blank'` and `background: true`; start from a blank target without requesting foreground activation. |
524
+ | `initialize` | `true` | Run the convenience setup sequence; false skips it. |
525
+ | `runtime` | `'bootstrap'` | Enable Runtime for setup, then disable reporting. `true` leaves it enabled; `false` skips both operations. |
526
+ | `page` | `true` | Enable Page domain reporting for navigation/page operations and events. |
527
+ | `network` | `true` | Enable Network reporting so native network events are available. |
528
+ | `service_worker` | `true` | Enable ServiceWorker reporting. |
529
+ | `focus_emulation` | `true` | Request target focus emulation, including when the actual window is not focused. |
530
+ | `binding` | `'_send_to_cdp'` | Default page-to-host JavaScript binding. String changes its name; false skips installation; true selects the default name. |
531
+ | `background_service` | `true` | Clear, observe and record `pushMessaging` background-service events in Chromium. |
532
+
533
+ Domain options set to false skip setup commands; they do not actively disable a
534
+ domain already enabled by another caller. Likewise, `binding: false` does not
535
+ remove a previously installed binding. Use native CDP commands for immediate
536
+ changes. `initialize: false` skips binding installation too;
537
+ `Runtime.runIfWaitingForDebugger` is still sent at the end. Auto-attachment uses
538
+ `waitForDebuggerOnStart: false`.
539
+
540
+ Setup CDP errors are collected as strings in `target.setup_errors`, allowing the
541
+ sequence to continue when a browser/target does not support a setup command.
542
+ Transport failures and timeouts reject initialization. Errors from the caller's
543
+ actual native request reject normally.
544
+
545
+ The first request creates/selects a target, attaches a flattened session and
546
+ runs setup. Concurrent requests share that work. Detachment invalidates the
547
+ cached session; destruction/crash invalidates target discovery information.
548
+ Later use reattaches or creates a target as needed. Reconnection retains labels
549
+ and configuration, reuses surviving physical targets and reapplies setup.
550
+ Failed commands are never replayed.
551
+
552
+ Navigation readiness is the caller's responsibility. For example, with one
553
+ selected target and no other concurrent navigation:
554
+
555
+ ```js
556
+ import { util } from './cdp.js'
557
+
558
+ await client.call({
559
+ method: 'Page.enable',
560
+ params: {
561
+ target: 'demo',
562
+ },
563
+ })
564
+ const loaded = util.on_first(client, 'Page.loadEventFired')
565
+ await client.call({
566
+ method: 'Page.navigate',
567
+ params: {
568
+ target: 'demo',
569
+ url: 'data:text/html,<title>Example</title><p>Ready</p>',
570
+ },
571
+ })
572
+ await loaded
573
+ ```
574
+
575
+ ## XPath custom methods
576
+
577
+ Both built-ins require `params.target`; `params.browser` defaults to `main`
578
+ through the normal router. They use `Runtime.evaluate` with `returnByValue`,
579
+ `awaitPromise`, `silent` and `userGesture` set to true: serialize the result,
580
+ await async page code, suppress evaluation exception reporting, and evaluate
581
+ with a user gesture respectively.
582
+
583
+ | Method | Parameters | Remote result in `response.result.value` |
584
+ | --- | --- | --- |
585
+ | `_.click` | `xpath` string; `attempts` number, default `5`; `interval_ms` number, default `300` | True after the first matching node's `.click()`. False when no node is found after the attempts or the node has no click function. |
586
+ | `_.find` | `xpath` string; `limit` number, default `20` | `{ count, items }`: total matches and at most `limit` serialized nodes. |
587
+
588
+ The click defaults provide a short wait for a node to appear; the find limit
589
+ bounds returned item data. Use positive integer attempt counts and non-negative
590
+ integer limits. Click waits only between unsuccessful attempts. It calls DOM
591
+ `.click()`, not mouse movement or coordinate input. `_.find` does not retry.
592
+
593
+ | Find item field | Meaning |
594
+ | --- | --- |
595
+ | `node_type`, `node_name` | DOM `nodeType` and `nodeName`; non-element nodes are supported. |
596
+ | `text` | Trimmed `innerText` or `textContent`, limited to 2,000 characters to bound returned text. |
597
+ | `tag` | Lowercase `tagName`, when present. |
598
+ | `id`, `href`, `value` | Corresponding DOM properties, when present. |
599
+ | `class` | String form of `className`, when present. |
600
+ | `rect` | `getBoundingClientRect().toJSON()` for nodes that support it. |
601
+
602
+ Absent optional properties may be omitted by serialization. `count` is the full
603
+ XPath snapshot length, even when `items` is limited or empty.
604
+
605
+ `util.normalize_xpath(xpath)` rewrites `ends-with(a, b)` and ASCII
606
+ `icontains(a, b)` into XPath 1 expressions. These are regular-expression rewrites,
607
+ not a complete parser; arbitrary nested or comma-containing arguments are outside
608
+ that rewriting logic. Ordinary XPath expressions pass through unchanged.
609
+
610
+ `client.evaluate_xpath(params, fn, options)` is available for other helpers:
611
+
612
+ ```js
613
+ const text = await client.evaluate_xpath({
614
+ target: 'demo',
615
+ xpath: '//p',
616
+ }, xpath => document.evaluate(
617
+ xpath,
618
+ document,
619
+ null,
620
+ XPathResult.FIRST_ORDERED_NODE_TYPE,
621
+ null,
622
+ ).singleNodeValue?.textContent)
623
+ console.log(text.result.value)
624
+ ```
625
+
626
+ `params` supplies `browser`, `target` and `xpath`. `fn` is serialized and called in
627
+ the target as `fn(normalizedXPath, options)`; it cannot capture Node.js variables
628
+ or imported helpers. `options` must be JSON-serializable; omission becomes null
629
+ in the argument array. Results retain native `exceptionDetails` if page code throws.
630
+
631
+ ### Register, replace or remove handlers
632
+
633
+ `client.custom_methods` is an ordinary public object with a separate copy of the
634
+ built-ins per client. Assign or delete keys directly; there are no registration
635
+ methods. Only method names starting with `_.` use this dispatch path.
636
+
637
+ ```js
638
+ client.custom_methods['_.title'] = function (params) {
639
+ return this.call({
640
+ method: 'Runtime.evaluate',
641
+ params: {
642
+ ...params,
643
+ expression: 'document.title',
644
+ returnByValue: true,
645
+ },
646
+ })
647
+ }
648
+ const title = await client.call({
649
+ method: '_.title',
650
+ params: {
651
+ target: 'demo',
652
+ },
653
+ })
654
+ console.log(title.result.value)
655
+
656
+ client.custom_methods['_.click'] = async function (params) {
657
+ return { handled: params.xpath }
658
+ }
659
+ delete client.custom_methods['_.find']
660
+ ```
661
+
662
+ Handlers receive the original `params`, with `this` bound to the `cdp` instance.
663
+ Use a regular function when that binding is needed. They run before automatic
664
+ browser/target resolution, may be synchronous or asynchronous, and define their
665
+ own return value. Their exceptions propagate. An unregistered `_.` name rejects
666
+ without launching a browser.
667
+
668
+ ## Events and bindings
669
+
670
+ | Emitter / event | `event.detail` |
671
+ | --- | --- |
672
+ | `client`, `notify` | CDP message with browser and, when known, target labels added to `params`. |
673
+ | `client`, native method such as `Runtime.bindingCalled` | That message's routed parameters. |
674
+ | `client`, `close` | `{ browser, code }` for closure of the current browser socket. |
675
+ | `socket`, `notify` | The unmodified CDP notification from the raw transport. |
676
+
677
+ Labels are strings, not record objects. Native `sessionId` remains available on
678
+ messages that carry it. Events are delivered directly: there is no history,
679
+ polling API or library-level filtering. Use normal event listeners and inspect
680
+ labels when handling several targets.
681
+
682
+ ```js
683
+ client.addEventListener('Runtime.bindingCalled', event => {
684
+ const { browser, target, name, payload } = event.detail
685
+ console.log(browser, target, name, payload)
686
+ })
687
+ await client.call({
688
+ method: 'Runtime.evaluate',
689
+ params: {
690
+ target: {
691
+ name: 'bridge',
692
+ binding: 'send_to_host',
693
+ runtime: true,
694
+ },
695
+ expression: 'send_to_host(JSON.stringify({ hello: "world" }))',
696
+ },
697
+ })
698
+ console.log(client.browsers.main.targets.bridge.binding) // send_to_host
699
+ ```
700
+
701
+ Bindings accept a string payload. `runtime: true` keeps reporting enabled for
702
+ ongoing binding and console events; bootstrap mode disables Runtime after setup.
703
+ The configured binding name lives on the target record and is reapplied during
704
+ subsequent initialization.
705
+
706
+ `util.on_first(target, 'open error close', timeout_ms)` supports EventTarget and
707
+ EventEmitter. For EventTarget, `args[0]` is the event; for EventEmitter, `args` is
708
+ the emitted argument list. The helper removes listeners and cancels its timer
709
+ after completion. Timeout rejects with `AbortError`; EventEmitter error handling
710
+ follows Node.js `events.once` semantics.
711
+
712
+ ## Public records
713
+
714
+ Mappings use object indexing, not `Map` instances or accessor methods. Some
715
+ dictionary containers have a null prototype.
716
+
717
+ | Path | Contents |
718
+ | --- | --- |
719
+ | `client.base_path` | Resolved profile root. |
720
+ | `client.options` | Constructor browser defaults, excluding `base_path`. |
721
+ | `client.custom_methods[name]` | Handler function. |
722
+ | `client.browsers[name]` | Browser configuration fields plus live state. |
723
+ | `record.socket` | Current `jsrpc` connection once created. |
724
+ | `record.proc` | Owned child process; absent for external attachment. |
725
+ | `record.connecting` | In-progress connection promise, cleared after settlement. |
726
+ | `record.targets[label]` | Target options, physical `targetId`, `sessionId`, `connecting` and `setup_errors` after setup. |
727
+ | `record.target_info[targetId]` | Discovered native target info plus cached `sessionId` when attached. |
728
+ | `record.session_targets[sessionId]` | Physical `targetId` for a session. |
729
+ | `socket.pending[id]` | Response-wait promise, removed on completion. |
730
+ | `socket.id`, `socket.timeout_ms` | Request counter and effective timeout. |
731
+
732
+ ```js
733
+ const record = client.browsers.main
734
+ const target = record.targets.demo
735
+ console.log(target.targetId, target.sessionId, target.binding)
736
+ console.log(target.setup_errors)
737
+ console.log(record.target_info[target.targetId])
738
+ console.log(record.session_targets[target.sessionId])
739
+ ```
740
+
741
+ Mappings appear as their connection/setup steps run and are updated by events.
742
+ `target_info` and `session_targets` are rebuilt on reconnection. They are live,
743
+ mutable state, not persistent snapshots.
744
+
745
+ ## Raw transport and standalone launch
746
+
747
+ This complete example bypasses the manager:
748
+
749
+ ```js
750
+ import { browser, jsrpc, util } from './cdp.js'
751
+ import * as path from 'node:path'
752
+
753
+ const launched = await browser.launch({
754
+ user_data_dir: path.resolve('./profiles/raw'),
755
+ headless: true,
756
+ })
757
+ const socket = new jsrpc(launched.url, {
758
+ request_timeout_ms: 10000,
759
+ })
760
+ try {
761
+ await util.on_first(socket, 'open error close')
762
+ console.log(await socket.req({
763
+ method: 'Browser.getVersion',
764
+ }, 5000))
765
+ await socket.req({ method: 'Browser.close' })
766
+ } finally {
767
+ socket.close()
768
+ launched.proc.kill()
769
+ }
770
+ ```
771
+
772
+ `browser.launch` requires a usable absolute directory through `user_data_dir` or
773
+ `args['user-data-dir']`; this helper does not resolve that path for you. It writes
774
+ preferences, starts the process, reads the
775
+ DevTools WebSocket URL from stderr and returns `{ proc, url }`. It does not open
776
+ a CDP connection. Readiness timeout or launch failure rejects and attempts to
777
+ terminate the spawned process.
778
+
779
+ `jsrpc` takes a WebSocket URL and an options object whose recognized option is
780
+ `request_timeout_ms` (default `30000`). Wait for the socket to open before `req`.
781
+ The request is a raw envelope: `method`, optional `params`, and optional top-level
782
+ `sessionId`. `req` assigns its own incrementing ID even if the input contains one.
783
+ Its second argument overrides the timeout for that request. There are no browser
784
+ labels, target labels, custom methods or reconnection policy in this layer.
785
+
786
+ ## Errors and timeouts
787
+
788
+ | Situation | Behavior |
789
+ | --- | --- |
790
+ | CDP returns `error` | Reject with `Error`; response in `error.cdp`, request/response in `error.cause.req` / `.ret`. |
791
+ | Page JavaScript throws during evaluation | Native result may contain `exceptionDetails`; not converted to a rejected promise. |
792
+ | Request times out | Reject with `AbortError`, clear local waiting state; Chromium may still execute the command. |
793
+ | Socket closes during a request | Reject the pending request; no automatic replay. |
794
+ | `req` before the socket opens | Reject with a connection-state error. |
795
+ | Unknown `_.` handler | Reject before browser creation. |
796
+ | Invalid profile JSON or filesystem failure | Propagate the parsing/filesystem error. |
797
+ | Failed discovery or local launch | Reject the call establishing the connection. |
798
+
799
+ ```js
800
+ try {
801
+ await client.call({
802
+ method: 'NoSuchDomain.noSuchMethod',
803
+ params: {
804
+ target: 'demo',
805
+ },
806
+ })
807
+ } catch (error) {
808
+ console.error(error.message)
809
+ if (error.cdp) console.error(error.cdp.error.code)
810
+ }
811
+ ```
812
+
813
+ ## Development
814
+
815
+ Contributors and coding agents should read [AGENTS.md](AGENTS.md) and
816
+ [DEV_PREF.md](DEV_PREF.md). The maintained implementation is [cdp.js](cdp.js).
817
+ `extra/`, when present locally, holds ignored reference files and is not part of
818
+ the tracked source or runtime dependencies.
819
+
820
+ [Manual chat monitoring](tests/chat-monitoring.md) documents the observed network
821
+ workflow for detecting new conversations and extracting connector session handles
822
+ from tool-call history. It is a caller-side recipe, not additional library API.
823
+
824
+ For npm packaging, account setup, and GitHub publishing, see
825
+ [Publishing](npm/PUBLISHING.md). `build.bat` prepares the package on Windows;
826
+ `node npm/prepare.mjs` prepares its files on any supported Node platform.
827
+
828
+ ```sh
829
+ node --check cdp.js
830
+ node --test tests/cdp.test.js
831
+ ```
832
+
833
+ Integration tests launch local headless Chromium with disposable profiles under
834
+ `build/tests/`. Set `CDP_BROWSER` if discovery does not match the machine. Direct
835
+ library use needs no build or package installation; npm packaging is optional.