@mefistofelix/cdp.js 0.5.1 → 0.7.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 +192 -19
- package/cdp.js +66 -18
- package/cdp_ext/cdp_ext.js +178 -28
- package/cdp_ext/manifest.json +7 -2
- package/cdp_ext/sidebar.html +37 -0
- package/cdp_ext/sidebar.js +54 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ import api, { util, jsrpc, browser, cdp } from '@mefistofelix/cdp.js'
|
|
|
24
24
|
|
|
25
25
|
The npm package exports the same file and API. Direct file imports in the examples
|
|
26
26
|
below remain supported. Publication metadata is isolated in `npm/`.
|
|
27
|
-
The package includes
|
|
27
|
+
The package includes the worker, manifest and debug sidebar files. With `cdp_ext: true`, the manager
|
|
28
28
|
prepares and loads the extension automatically with a custom user-data directory.
|
|
29
29
|
The system profile uses an already installed extension by default. Reverse mode still
|
|
30
30
|
requires Deno or Bun. In Deno, import from `npm:@mefistofelix/cdp.js`.
|
|
@@ -43,6 +43,8 @@ In Bun, use `bun add @mefistofelix/cdp.js` and the ordinary package import above
|
|
|
43
43
|
without browser remote-debugging arguments.
|
|
44
44
|
- Generic `chrome.x.y` extension API calls on that connection, including scripting,
|
|
45
45
|
tabs/windows, cookies, storage, downloads and browser settings.
|
|
46
|
+
- Reverse target management and XPath helpers default to browser APIs, with
|
|
47
|
+
optional debugger execution; explicit native CDP commands still execute.
|
|
46
48
|
- Shared initialization for concurrent calls; reconnection and target recreation
|
|
47
49
|
on subsequent use, without replaying failed commands.
|
|
48
50
|
- Public object mappings and directly replaceable custom method handlers.
|
|
@@ -119,7 +121,7 @@ import api, { util, jsrpc, browser, cdp } from './cdp.js'
|
|
|
119
121
|
| --- | --- |
|
|
120
122
|
| `new cdp(options = {})` | Create a manager; does not launch a browser yet. |
|
|
121
123
|
| `client.call({ method, params = {} })` | Resolve routing and return a promise for the native CDP result, or a custom handler's result. |
|
|
122
|
-
| `client.evaluate_xpath(params, fn, options)` | Execute a serialized function with a normalized XPath
|
|
124
|
+
| `client.evaluate_xpath(params, fn, options)` | Execute a serialized function with a normalized XPath. Direct/debugger mode returns native `Runtime.evaluate` data; reverse API mode returns `{ result: { type, value } }`. |
|
|
123
125
|
| `client.close()` | Close owned processes, disconnect attached/reverse browsers, stop the reverse server and clear browser/socket mappings. Profiles and external browsers remain. |
|
|
124
126
|
| `new jsrpc(urlOrSocket, options = {})` | Open a raw WebSocket JSON-RPC connection or equip an accepted socket with the same RPC methods. |
|
|
125
127
|
| `client.listen_reverse(options = {})` | Start the native Deno or Bun WebSocket server; returns that server with common lifecycle fields documented below. |
|
|
@@ -491,7 +493,7 @@ can configure its native incognito scope. See
|
|
|
491
493
|
## Reverse Chrome extension transport
|
|
492
494
|
|
|
493
495
|
[cdp_ext/manifest.json](cdp_ext/manifest.json) and
|
|
494
|
-
[cdp_ext/cdp_ext.js](cdp_ext/cdp_ext.js) form a Manifest V3 extension for Chrome
|
|
496
|
+
[cdp_ext/cdp_ext.js](cdp_ext/cdp_ext.js), with the local debug sidebar, form a Manifest V3 extension for Chrome
|
|
495
497
|
125+. The extension connects out to one WebSocket server. The server serves all
|
|
496
498
|
browser instances on the same port; bootstrapped extensions announce the logical
|
|
497
499
|
browser name and native extension ID. No remote-debugging argument is needed.
|
|
@@ -529,6 +531,19 @@ the local URL and connects to the waiting manager. The user enters no extension
|
|
|
529
531
|
ID or WebSocket address. Load the package's `cdp_ext/` once through
|
|
530
532
|
`chrome://extensions` (developer mode, Load unpacked); reload it after upgrading.
|
|
531
533
|
|
|
534
|
+
After accepting a trigger, the extension records `cdp_ext.trigger` in its debug
|
|
535
|
+
sidebar and closes that trigger tab automatically, including when the server is
|
|
536
|
+
currently offline. If it is Chrome's only tab, it becomes the extension's
|
|
537
|
+
**CDP.js connection** page instead, keeping Chrome available for subsequent calls.
|
|
538
|
+
This reuses the debug view and displays the logical browser and live connected/
|
|
539
|
+
waiting status; it never claims a connection before one exists. No extra
|
|
540
|
+
tab is created. Other loopback pages are preserved: only the exact `/cdp_ext`
|
|
541
|
+
route with non-empty `browser`, `websocket_url` and `token` parameters and a
|
|
542
|
+
same-host/port `ws:` endpoint is accepted. The tab URL is checked again before
|
|
543
|
+
cleanup, so a tab already navigated elsewhere is left alone. Duplicate tab updates
|
|
544
|
+
reuse the trigger's diagnostic entry. A conflicting live browser binding is
|
|
545
|
+
reported and its trigger page is cleaned up without replacing the connection.
|
|
546
|
+
|
|
532
547
|
```js
|
|
533
548
|
const client = new cdp({
|
|
534
549
|
cdp_ext: true,
|
|
@@ -541,7 +556,7 @@ await client.call({
|
|
|
541
556
|
|
|
542
557
|
Provide `user_data_dir` or raw `args['user-data-dir']` to choose a custom
|
|
543
558
|
directory. An effective custom directory makes `cdp_ext_install` default to true:
|
|
544
|
-
the manager copies the
|
|
559
|
+
the manager copies the four extension files into
|
|
545
560
|
`<user-data-dir>/<profile-directory>/cdp_ext/` (`Default` when unset) and adds
|
|
546
561
|
startup loading flags. The server address comes only from the trigger URL. Normal launch
|
|
547
562
|
options and profile preferences then apply. Set `cdp_ext_install: false` to use
|
|
@@ -648,8 +663,9 @@ native send/close and live ready state. The same `jsrpc` handles both runtimes;
|
|
|
648
663
|
registration, request IDs, pending waits, events and session routing are shared.
|
|
649
664
|
Native Bun WebSocket limits still apply.
|
|
650
665
|
|
|
651
|
-
Reverse calls
|
|
652
|
-
|
|
666
|
+
Reverse calls share target selection, custom handlers, mappings, routed events,
|
|
667
|
+
request correlation, timeout and error behavior. Implicit CDP setup is opt-in
|
|
668
|
+
through `target.execution_mode: 'debugger'`. `reverse: true` alone
|
|
653
669
|
attaches externally: the manager neither launches nor updates that browser.
|
|
654
670
|
`cdp_ext_install: true` owns the launched process and applies normal launch preferences.
|
|
655
671
|
With installation off, Chrome is external and remains open on manager close.
|
|
@@ -685,6 +701,40 @@ WebSocket attempt on every timer tick. Successful connections and later
|
|
|
685
701
|
disconnects log their status once per transition. Chrome can still emit a native
|
|
686
702
|
network diagnostic if the server disappears between the HTTP check and upgrade.
|
|
687
703
|
|
|
704
|
+
### Debug sidebar
|
|
705
|
+
|
|
706
|
+
Click the extension's toolbar icon to open its native Chrome side panel.
|
|
707
|
+
If the icon is hidden, open Chrome's Extensions menu (the puzzle piece), find
|
|
708
|
+
**CDP reverse connection**, and pin it or click its entry. The panel opens on a
|
|
709
|
+
user action; loading the extension or connecting a browser does not open it.
|
|
710
|
+
After updating the package, an already running Chrome still has its previously
|
|
711
|
+
loaded extension: reload it in `chrome://extensions` after updating its directory,
|
|
712
|
+
or restart that custom-profile browser through a fresh manager. For a manually
|
|
713
|
+
installed extension, update its installed files too. Reopening an old side panel
|
|
714
|
+
alone does not upgrade the worker.
|
|
715
|
+
|
|
716
|
+
It shows the current connection status, logical browser name, WebSocket endpoint,
|
|
717
|
+
native extension ID, number of routing sessions and number of debugger-attached
|
|
718
|
+
root tabs. A routing session does not imply debugger attachment. It works before
|
|
719
|
+
the first trigger too, displaying **Waiting for trigger** without starting a
|
|
720
|
+
server connection.
|
|
721
|
+
|
|
722
|
+
The most recent 50 commands and trigger entries appear newest first, with request ID,
|
|
723
|
+
method, optional session ID/execution mode, running/done/error status, elapsed milliseconds and
|
|
724
|
+
error text. This is command metadata only: parameters, results and the trigger
|
|
725
|
+
token are not included. Trigger entries show the tab ID, browser label, endpoint
|
|
726
|
+
and cleanup action; the full trigger URL is never recorded. Empty heartbeat
|
|
727
|
+
messages and CDP events are not logged.
|
|
728
|
+
**Clear** empties this history without cancelling commands or changing the
|
|
729
|
+
connection. Reopening the panel retrieves the current snapshot; a worker restart
|
|
730
|
+
or extension reload clears the history. No debug history is written to storage.
|
|
731
|
+
|
|
732
|
+
The panel receives updates through a local extension port. While open, a
|
|
733
|
+
20-second port heartbeat keeps the worker available, including before a trigger.
|
|
734
|
+
Closing the panel does not disconnect CDP. After reloading the extension, reopen
|
|
735
|
+
the panel if its old document reports that it has disconnected. The HTML and
|
|
736
|
+
external script follow the normal extension CSP and require no build or dependency.
|
|
737
|
+
|
|
688
738
|
### Reverse protocol and supported commands
|
|
689
739
|
|
|
690
740
|
The current extension's first announcement contains `{ browser, extension_id, token }`
|
|
@@ -714,8 +764,10 @@ Worker and out-of-process iframe commands still use real flattened CDP sessions.
|
|
|
714
764
|
Their native session IDs pass through unchanged, without prefixes. The extension
|
|
715
765
|
uses one `sessions` object for both roots and children: a tab ID maps to `{ tabId }`,
|
|
716
766
|
and a native child session ID maps to `{ tabId, sessionId }`. Every target command
|
|
717
|
-
looks up that object
|
|
718
|
-
|
|
767
|
+
looks up that object. API-mode root selection only registers the routing reference;
|
|
768
|
+
an explicit session CDP command attaches its root lazily, sharing concurrent
|
|
769
|
+
attachment, then calls `chrome.debugger.sendCommand`. There is no fallback that
|
|
770
|
+
interprets unknown session IDs as tab IDs.
|
|
719
771
|
Explicit detach takes `params.sessionId` for either a tab or child
|
|
720
772
|
session. Logical target labels remain inside the manager. Reconnecting invalidates
|
|
721
773
|
attachment/setup state even though a surviving root tab's routing ID stays the same.
|
|
@@ -730,12 +782,13 @@ tab and focuses its window. `Browser.getVersion` returns product/user-agent data
|
|
|
730
782
|
its protocol version is `1.3`, with empty `revision` and `jsVersion` fields.
|
|
731
783
|
Other browser-level commands return explicit unsupported-command errors.
|
|
732
784
|
|
|
733
|
-
|
|
785
|
+
Explicit native target/session CDP commands go through
|
|
734
786
|
[chrome.debugger](https://developer.chrome.com/docs/extensions/reference/api/debugger),
|
|
735
787
|
which supports a restricted set of CDP domains. Runtime, Page, Network, DOM,
|
|
736
788
|
Input and Emulation are available; ServiceWorker and BackgroundService are not.
|
|
737
|
-
Their
|
|
738
|
-
those setup flags if desired.
|
|
789
|
+
Their setup attempts in debugger target mode therefore appear in
|
|
790
|
+
`target.setup_errors`; disable those setup flags if desired. API targets skip
|
|
791
|
+
that setup. Chrome restrictions, policies and debugger detach
|
|
739
792
|
events remain visible. Opening DevTools on an attached tab can detach this debugger.
|
|
740
793
|
Firefox/Camoufox support remains planned.
|
|
741
794
|
|
|
@@ -1313,7 +1366,8 @@ await client.call({
|
|
|
1313
1366
|
| `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. |
|
|
1314
1367
|
| `group` | unset | Reverse-only tab group name or first-use `{ name, color }` configuration. False skips grouping. |
|
|
1315
1368
|
| `create_params` | `{}` | Native creation overrides, merged over `url: 'about:blank'` and `background: true`; start from a blank target without requesting foreground activation. |
|
|
1316
|
-
| `
|
|
1369
|
+
| `execution_mode` | `'api'` in reverse | Reverse-only choice for implicit target setup and built-in XPath helpers: `'api'` uses extension APIs; `'debugger'` attaches and runs CDP setup. Direct CDP ignores it. |
|
|
1370
|
+
| `initialize` | `true` | Run the convenience CDP setup sequence in direct/debugger mode; false skips it. API mode skips that sequence regardless. |
|
|
1317
1371
|
| `runtime` | `'bootstrap'` | Enable Runtime for setup, then disable reporting. `true` leaves it enabled; `false` skips both operations. |
|
|
1318
1372
|
| `page` | `true` | Enable Page domain reporting for navigation/page operations and events. |
|
|
1319
1373
|
| `network` | `true` | Enable Network reporting so native network events are available. |
|
|
@@ -1322,6 +1376,102 @@ await client.call({
|
|
|
1322
1376
|
| `binding` | `'_send_to_cdp'` | Default page-to-host JavaScript binding. String changes its name; false skips installation; true selects the default name. |
|
|
1323
1377
|
| `background_service` | `true` | Clear, observe and record `pushMessaging` background-service events in Chromium. |
|
|
1324
1378
|
|
|
1379
|
+
### Reverse execution mode
|
|
1380
|
+
|
|
1381
|
+
In extension mode (`cdp_ext: true` or passive `reverse: true`), a target defaults
|
|
1382
|
+
to `execution_mode: 'api'`. Creating/selecting it, grouping it and running
|
|
1383
|
+
`_.find`/`_.click` do not attach the debugger. Root tabs are created through
|
|
1384
|
+
`chrome.tabs`/`chrome.windows`; choosing debugger mode then attaches the selected
|
|
1385
|
+
tab before setup. Chrome's debugger API needs an existing debuggee.
|
|
1386
|
+
|
|
1387
|
+
| Operation | Reverse API target (default) | Reverse debugger target |
|
|
1388
|
+
| --- | --- | --- |
|
|
1389
|
+
| Target selection and root routing | Register the stable tab ID without debugger attachment. | Attach the selected tab and register the same routing ID. |
|
|
1390
|
+
| Implicit initialization | No Runtime/Page/Network enabling, binding, focus emulation, background-service setup or resume command. | Apply the documented initialization options and resume after setup. |
|
|
1391
|
+
| `_.find`, `_.click` | Fixed native `chrome.scripting.executeScript` functions in the isolated world; shared DOM, no string evaluation. | `Runtime.evaluate` in the page world. |
|
|
1392
|
+
| Explicit session CDP, such as `Runtime.evaluate` or `Page.navigate` | Attach lazily and execute the exact CDP request. | Execute the CDP request. |
|
|
1393
|
+
| Explicit `chrome.*` method | Invoke the named native extension API with the supplied positional arguments. | Same native extension API dispatch. |
|
|
1394
|
+
|
|
1395
|
+
The target option is recorded on first use of its logical name, like the other
|
|
1396
|
+
target options. It belongs in `params.target`, not constructor/browser defaults.
|
|
1397
|
+
`runtime: true`, `network: true` or a binding name alone do not opt an API target
|
|
1398
|
+
into debugger setup. To retain the previous reverse convenience initialization,
|
|
1399
|
+
set `target.execution_mode: 'debugger'` explicitly. Direct CDP defaults are unchanged.
|
|
1400
|
+
As with the existing XPath helper, `_.find` does not wait for navigation or retry;
|
|
1401
|
+
wait for your page's readiness before querying it.
|
|
1402
|
+
|
|
1403
|
+
```js
|
|
1404
|
+
const found = await client.call({
|
|
1405
|
+
method: '_.find',
|
|
1406
|
+
params: {
|
|
1407
|
+
browser: {
|
|
1408
|
+
name: 'work',
|
|
1409
|
+
cdp_ext: true,
|
|
1410
|
+
},
|
|
1411
|
+
target: {
|
|
1412
|
+
name: 'search',
|
|
1413
|
+
create_params: {
|
|
1414
|
+
url: 'https://example.com/',
|
|
1415
|
+
},
|
|
1416
|
+
},
|
|
1417
|
+
xpath: '//h1',
|
|
1418
|
+
},
|
|
1419
|
+
})
|
|
1420
|
+
console.log(found.result.value)
|
|
1421
|
+
|
|
1422
|
+
// Select debugger execution for this helper call only.
|
|
1423
|
+
await client.call({
|
|
1424
|
+
method: '_.click',
|
|
1425
|
+
params: {
|
|
1426
|
+
browser: 'work',
|
|
1427
|
+
target: 'search',
|
|
1428
|
+
execution_mode: 'debugger',
|
|
1429
|
+
xpath: '//button',
|
|
1430
|
+
},
|
|
1431
|
+
})
|
|
1432
|
+
|
|
1433
|
+
// Native CDP executes even though the target's default remains API mode.
|
|
1434
|
+
await client.call({
|
|
1435
|
+
method: 'Runtime.evaluate',
|
|
1436
|
+
params: {
|
|
1437
|
+
browser: 'work',
|
|
1438
|
+
target: 'search',
|
|
1439
|
+
expression: 'document.title',
|
|
1440
|
+
},
|
|
1441
|
+
})
|
|
1442
|
+
|
|
1443
|
+
// A separate target with the full convenience debugger setup.
|
|
1444
|
+
await client.call({
|
|
1445
|
+
method: 'Page.navigate',
|
|
1446
|
+
params: {
|
|
1447
|
+
browser: 'work',
|
|
1448
|
+
target: {
|
|
1449
|
+
name: 'instrumented',
|
|
1450
|
+
execution_mode: 'debugger',
|
|
1451
|
+
runtime: true,
|
|
1452
|
+
},
|
|
1453
|
+
url: 'https://example.com/',
|
|
1454
|
+
},
|
|
1455
|
+
})
|
|
1456
|
+
```
|
|
1457
|
+
|
|
1458
|
+
For the built-in helpers and `evaluate_xpath`, `params.execution_mode` overrides
|
|
1459
|
+
the target's choice for that call only; it does not enable optional target setup
|
|
1460
|
+
or change the recorded configuration. Valid reverse values are `'api'` and
|
|
1461
|
+
`'debugger'`. Native CDP and `chrome.*` methods keep their explicit dispatch and
|
|
1462
|
+
ignore this helper override; the router removes it from native parameters.
|
|
1463
|
+
Custom handlers still receive the original parameters and choose their own behavior.
|
|
1464
|
+
|
|
1465
|
+
API selection does not detach a debugger previously attached by an explicit
|
|
1466
|
+
command. Use `Target.detachFromTarget` explicitly if needed. After detachment,
|
|
1467
|
+
an API target can be selected and used through API helpers again without
|
|
1468
|
+
reattachment. Native CDP attachment by itself does not enable domains or install
|
|
1469
|
+
bindings; send the desired commands explicitly or choose debugger target setup.
|
|
1470
|
+
Only debugger-attached targets expose flattened worker/iframe sessions. Browser
|
|
1471
|
+
API helpers operate on the registered root tab's main frame; extension permissions
|
|
1472
|
+
and Chrome's restricted-page rules still apply. There is no automatic fallback
|
|
1473
|
+
from API execution to debugger execution.
|
|
1474
|
+
|
|
1325
1475
|
### Named tab groups
|
|
1326
1476
|
|
|
1327
1477
|
With `cdp_ext: true` or external `reverse: true`, set `target.group` when first
|
|
@@ -1394,6 +1544,10 @@ and [grouping tabs](https://developer.chrome.com/docs/extensions/reference/api/t
|
|
|
1394
1544
|
|
|
1395
1545
|
### Target initialization
|
|
1396
1546
|
|
|
1547
|
+
The following CDP setup applies to direct mode and reverse targets configured
|
|
1548
|
+
with `execution_mode: 'debugger'`. Reverse API targets skip it entirely, including
|
|
1549
|
+
`Runtime.runIfWaitingForDebugger`; `initialize: false` does not opt into debugging.
|
|
1550
|
+
|
|
1397
1551
|
Domain options set to false skip setup commands; they do not actively disable a
|
|
1398
1552
|
domain already enabled by another caller. Likewise, `binding: false` does not
|
|
1399
1553
|
remove a previously installed binding. Use native CDP commands for immediate
|
|
@@ -1466,10 +1620,16 @@ await loaded
|
|
|
1466
1620
|
## XPath custom methods
|
|
1467
1621
|
|
|
1468
1622
|
Both built-ins require `params.target`; `params.browser` defaults to `main`
|
|
1469
|
-
through the normal router.
|
|
1623
|
+
through the normal router. In direct/debugger execution they use `Runtime.evaluate` with `returnByValue`,
|
|
1470
1624
|
`awaitPromise`, `silent` and `userGesture` set to true: serialize the result,
|
|
1471
1625
|
await async page code, suppress evaluation exception reporting, and evaluate
|
|
1472
|
-
with a user gesture respectively.
|
|
1626
|
+
with a user gesture respectively. In reverse API execution, fixed native scripting
|
|
1627
|
+
functions perform the same DOM operations in the isolated world, without eval,
|
|
1628
|
+
user-script permission or CSP changes. They keep `response.result.value` and the
|
|
1629
|
+
same click/find value shapes. API results contain `result.type` and `result.value`;
|
|
1630
|
+
they do not manufacture CDP remote-object metadata or `exceptionDetails`.
|
|
1631
|
+
Native injection errors reject; failed script execution also rejects. Debugger
|
|
1632
|
+
evaluation retains native `exceptionDetails` for page-side exceptions.
|
|
1473
1633
|
|
|
1474
1634
|
| Method | Parameters | Remote result in `response.result.value` |
|
|
1475
1635
|
| --- | --- | --- |
|
|
@@ -1514,10 +1674,19 @@ const text = await client.evaluate_xpath({
|
|
|
1514
1674
|
console.log(text.result.value)
|
|
1515
1675
|
```
|
|
1516
1676
|
|
|
1517
|
-
`params` supplies `browser`, `target
|
|
1677
|
+
`params` supplies `browser`, `target`, `xpath` and optional per-call
|
|
1678
|
+
`execution_mode`. `fn` is serialized and called in
|
|
1518
1679
|
the target as `fn(normalizedXPath, options)`; it cannot capture Node.js variables
|
|
1519
1680
|
or imported helpers. `options` must be JSON-serializable; omission becomes null
|
|
1520
|
-
in the argument array.
|
|
1681
|
+
in the argument array. Direct/debugger results retain native `exceptionDetails`
|
|
1682
|
+
if page code throws. In reverse API mode this arbitrary-function helper uses the
|
|
1683
|
+
existing string-function scripting adapter in the **MAIN** world and returns
|
|
1684
|
+
`{ result: { type, value } }`. Unlike the two fixed built-ins, arbitrary source
|
|
1685
|
+
evaluation requires the page CSP to allow eval. Injection errors or missing
|
|
1686
|
+
execution results reject; no debugger, CSP bypass or user-script toggle is enabled
|
|
1687
|
+
automatically. Choose `execution_mode: 'debugger'` explicitly when native CDP
|
|
1688
|
+
evaluation is required. Native `chrome.scripting.executeScript` calls retain their
|
|
1689
|
+
existing native world default and native result array.
|
|
1521
1690
|
|
|
1522
1691
|
### Register, replace or remove handlers
|
|
1523
1692
|
|
|
@@ -1623,6 +1792,7 @@ await client.call({
|
|
|
1623
1792
|
params: {
|
|
1624
1793
|
target: {
|
|
1625
1794
|
name: 'bridge',
|
|
1795
|
+
execution_mode: 'debugger',
|
|
1626
1796
|
binding: 'send_to_host',
|
|
1627
1797
|
runtime: true,
|
|
1628
1798
|
},
|
|
@@ -1787,11 +1957,12 @@ from tool-call history. It is a caller-side recipe, not additional library API.
|
|
|
1787
1957
|
|
|
1788
1958
|
For npm packaging, account setup, and GitHub publishing, see
|
|
1789
1959
|
[Publishing](npm/PUBLISHING.md). Publishing copies `cdp.js`, this README and the
|
|
1790
|
-
|
|
1960
|
+
four `cdp_ext/` files into `npm/` alongside its manifest; no build script is needed.
|
|
1791
1961
|
|
|
1792
1962
|
```sh
|
|
1793
1963
|
node --check cdp.js
|
|
1794
1964
|
node --check cdp_ext/cdp_ext.js
|
|
1965
|
+
node --check cdp_ext/sidebar.js
|
|
1795
1966
|
node --test tests/cdp.test.js
|
|
1796
1967
|
deno test -A tests/reverse.test.js
|
|
1797
1968
|
bun test tests/cdp.test.js tests/reverse.test.js --timeout 60000
|
|
@@ -1835,5 +2006,7 @@ combinations include default/custom directories, null/false raw directory
|
|
|
1835
2006
|
overrides, forced installation and independent preference-write controls.
|
|
1836
2007
|
Offline worker checks verify silence before a trigger, deduplicated waiting logs,
|
|
1837
2008
|
no WebSocket attempts against absent/incompatible servers and concurrent retry checks.
|
|
1838
|
-
The npm tarball contains `package.json`, `cdp.js`, `README.md` and the
|
|
1839
|
-
files required by managed reverse launches.
|
|
2009
|
+
The npm tarball contains `package.json`, `cdp.js`, `README.md` and the four extension
|
|
2010
|
+
files required by managed reverse launches and their debug sidebar. Tests cover
|
|
2011
|
+
live command metadata, its 50-entry limit, error/session details, sidebar rendering,
|
|
2012
|
+
safe text display, reopening and clearing through native extension messaging.
|
package/cdp.js
CHANGED
|
@@ -320,7 +320,7 @@ const custom_methods = {
|
|
|
320
320
|
if (i + 1 < options.attempts) await new Promise(resolve => setTimeout(resolve, options.interval_ms))
|
|
321
321
|
}
|
|
322
322
|
return false
|
|
323
|
-
}, options)
|
|
323
|
+
}, options, '_.click')
|
|
324
324
|
},
|
|
325
325
|
'_.find'(params) {
|
|
326
326
|
const limit = params.limit ?? 20
|
|
@@ -346,7 +346,7 @@ const custom_methods = {
|
|
|
346
346
|
count: nodes.snapshotLength,
|
|
347
347
|
items,
|
|
348
348
|
}
|
|
349
|
-
}, limit)
|
|
349
|
+
}, limit, '_.find')
|
|
350
350
|
},
|
|
351
351
|
}
|
|
352
352
|
|
|
@@ -472,20 +472,52 @@ export class cdp extends EventTarget {
|
|
|
472
472
|
})
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
evaluate_xpath(params, fn, options) {
|
|
475
|
+
async evaluate_xpath(params, fn, options, method) {
|
|
476
476
|
if (params.target == null) throw new Error('Custom method requires target')
|
|
477
477
|
const xpath = util.normalize_xpath(params.xpath)
|
|
478
|
-
|
|
478
|
+
const { record, entry } = await this._resolve(params)
|
|
479
|
+
const reverse = record.reverse || record.cdp_ext
|
|
480
|
+
const execution_mode = params.execution_mode ?? entry.execution_mode ?? 'api'
|
|
481
|
+
if (reverse && !['api', 'debugger'].includes(execution_mode)) throw new Error('execution_mode must be api or debugger')
|
|
482
|
+
if (reverse && execution_mode === 'api') {
|
|
483
|
+
if (method) return record.socket.req({
|
|
484
|
+
method,
|
|
485
|
+
params: {
|
|
486
|
+
xpath,
|
|
487
|
+
options,
|
|
488
|
+
},
|
|
489
|
+
sessionId: entry.sessionId,
|
|
490
|
+
})
|
|
491
|
+
const results = await record.socket.req({
|
|
492
|
+
method: 'chrome.scripting.executeScript',
|
|
493
|
+
params: {
|
|
494
|
+
args: [{
|
|
495
|
+
target: { tabId: Number(entry.targetId) },
|
|
496
|
+
world: 'MAIN',
|
|
497
|
+
func: 'async (...args) => ({ value: await (' + fn + ')(...args) })',
|
|
498
|
+
args: [xpath, options ?? null],
|
|
499
|
+
}],
|
|
500
|
+
},
|
|
501
|
+
})
|
|
502
|
+
const result = results[0]?.result
|
|
503
|
+
if (!result) throw new Error('Browser API evaluation failed; check page CSP and extension access. No debugger fallback was used.')
|
|
504
|
+
return {
|
|
505
|
+
result: {
|
|
506
|
+
type: typeof result.value,
|
|
507
|
+
value: result.value,
|
|
508
|
+
},
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return record.socket.req({
|
|
479
512
|
method: 'Runtime.evaluate',
|
|
480
513
|
params: {
|
|
481
|
-
browser: params.browser,
|
|
482
|
-
target: params.target,
|
|
483
514
|
expression: '(' + fn + ')(...' + JSON.stringify([xpath, options]) + ')',
|
|
484
515
|
returnByValue: true,
|
|
485
516
|
awaitPromise: true,
|
|
486
517
|
silent: true,
|
|
487
518
|
userGesture: true,
|
|
488
519
|
},
|
|
520
|
+
sessionId: entry.sessionId,
|
|
489
521
|
})
|
|
490
522
|
}
|
|
491
523
|
|
|
@@ -513,7 +545,7 @@ export class cdp extends EventTarget {
|
|
|
513
545
|
}
|
|
514
546
|
const info = browser.target_info[targetId]
|
|
515
547
|
if (message.method === 'Target.attachedToTarget') {
|
|
516
|
-
this._detach(browser, info?.sessionId)
|
|
548
|
+
if (info?.sessionId !== sessionId) this._detach(browser, info?.sessionId)
|
|
517
549
|
info.sessionId = sessionId
|
|
518
550
|
browser.session_targets[sessionId] = targetId
|
|
519
551
|
}
|
|
@@ -658,8 +690,9 @@ export class cdp extends EventTarget {
|
|
|
658
690
|
await fs.mkdir(record.extension_path, { recursive: true })
|
|
659
691
|
record.extension_path = await fs.realpath(record.extension_path)
|
|
660
692
|
record.extension_id = browser.extension_id(record.extension_path)
|
|
661
|
-
|
|
662
|
-
|
|
693
|
+
for (const filename of ['manifest.json', 'cdp_ext.js', 'sidebar.html', 'sidebar.js']) {
|
|
694
|
+
await fs.copyFile(new URL('./cdp_ext/' + filename, import.meta.url), path.join(record.extension_path, filename))
|
|
695
|
+
}
|
|
663
696
|
}
|
|
664
697
|
if (args['user-data-dir'] && record.update_preferences !== false) {
|
|
665
698
|
await browser.update_profile_preferences(path.join(args['user-data-dir'], args['profile-directory'] || 'Default', 'Preferences'), {
|
|
@@ -776,6 +809,8 @@ export class cdp extends EventTarget {
|
|
|
776
809
|
async _target(browser, target) {
|
|
777
810
|
const socket = browser.socket
|
|
778
811
|
const reverse = browser.reverse || browser.cdp_ext
|
|
812
|
+
const execution_mode = target.execution_mode ?? 'api'
|
|
813
|
+
if (reverse && !['api', 'debugger'].includes(execution_mode)) throw new Error('execution_mode must be api or debugger')
|
|
779
814
|
if (target.group != null && target.group !== false) {
|
|
780
815
|
const spec = typeof target.group === 'string' ? { name: target.group } : target.group
|
|
781
816
|
browser.groups[spec.name] ??= {
|
|
@@ -818,17 +853,23 @@ export class cdp extends EventTarget {
|
|
|
818
853
|
if (!info.incognito) throw new Error('Existing target is not incognito')
|
|
819
854
|
}
|
|
820
855
|
if (target.group != null && target.group !== false) info.groupId = await this._group(browser, target)
|
|
821
|
-
if (!info.sessionId) {
|
|
856
|
+
if (!info.sessionId || (reverse && execution_mode === 'debugger')) {
|
|
822
857
|
info.sessionId = (await socket.req({
|
|
823
858
|
method: 'Target.attachToTarget',
|
|
824
859
|
params: {
|
|
825
860
|
targetId: target.targetId,
|
|
826
861
|
flatten: true,
|
|
862
|
+
...(reverse ? { execution_mode } : {}),
|
|
827
863
|
},
|
|
828
864
|
})).sessionId
|
|
829
865
|
browser.session_targets[info.sessionId] = target.targetId
|
|
830
866
|
}
|
|
831
867
|
const sessionId = info.sessionId
|
|
868
|
+
target.setup_errors = []
|
|
869
|
+
if (reverse && execution_mode === 'api') {
|
|
870
|
+
target.sessionId = sessionId
|
|
871
|
+
return
|
|
872
|
+
}
|
|
832
873
|
const steps = []
|
|
833
874
|
if (target.initialize !== false) {
|
|
834
875
|
for (const [option, domain] of Object.entries({
|
|
@@ -852,7 +893,6 @@ export class cdp extends EventTarget {
|
|
|
852
893
|
if ((target.runtime ?? 'bootstrap') === 'bootstrap') steps.push(['Runtime.disable'])
|
|
853
894
|
}
|
|
854
895
|
steps.push(['Runtime.runIfWaitingForDebugger'])
|
|
855
|
-
target.setup_errors = []
|
|
856
896
|
for (const [method, params] of steps) {
|
|
857
897
|
try {
|
|
858
898
|
await socket.req({
|
|
@@ -868,13 +908,7 @@ export class cdp extends EventTarget {
|
|
|
868
908
|
target.sessionId = sessionId
|
|
869
909
|
}
|
|
870
910
|
|
|
871
|
-
async
|
|
872
|
-
if (method.startsWith('_.')) {
|
|
873
|
-
const handler = this.custom_methods[method]
|
|
874
|
-
if (!handler) throw new Error('Unknown CDP extension: ' + method)
|
|
875
|
-
return handler.call(this, params)
|
|
876
|
-
}
|
|
877
|
-
const { browser = 'main', target, ...native } = params
|
|
911
|
+
async _resolve({ browser = 'main', target }) {
|
|
878
912
|
const spec = typeof browser === 'string' ? { name: browser } : browser
|
|
879
913
|
const record = this.browsers[spec.name] ??= {
|
|
880
914
|
user_data_dir: ((spec.reverse ?? this.options.reverse) || (spec.cdp_ext ?? this.options.cdp_ext)) ? null : path.join(this.base_path, spec.name),
|
|
@@ -932,6 +966,20 @@ export class cdp extends EventTarget {
|
|
|
932
966
|
await entry.connecting
|
|
933
967
|
}
|
|
934
968
|
}
|
|
969
|
+
return {
|
|
970
|
+
record,
|
|
971
|
+
entry,
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async call({ method, params = {} }) {
|
|
976
|
+
if (method.startsWith('_.')) {
|
|
977
|
+
const handler = this.custom_methods[method]
|
|
978
|
+
if (!handler) throw new Error('Unknown CDP extension: ' + method)
|
|
979
|
+
return handler.call(this, params)
|
|
980
|
+
}
|
|
981
|
+
const { browser, target, execution_mode, ...native } = params
|
|
982
|
+
const { record, entry } = await this._resolve(params)
|
|
935
983
|
if (method === 'Target.disposeBrowserContext' && target == null) {
|
|
936
984
|
const context = Object.values(record.contexts).find(value => value.browserContextId === native.browserContextId)
|
|
937
985
|
if (context) return this._dispose_context(record, context, native)
|
package/cdp_ext/cdp_ext.js
CHANGED
|
@@ -12,11 +12,57 @@ let auto_attach = {}
|
|
|
12
12
|
let bootstrapping
|
|
13
13
|
let checking = false
|
|
14
14
|
let connection_message
|
|
15
|
+
const debug_commands = []
|
|
16
|
+
const debug_ports = []
|
|
17
|
+
|
|
18
|
+
function debug_record(fields) {
|
|
19
|
+
const record = {
|
|
20
|
+
time: Date.now(),
|
|
21
|
+
...fields,
|
|
22
|
+
status: 'running',
|
|
23
|
+
}
|
|
24
|
+
debug_commands.unshift(record)
|
|
25
|
+
debug_commands.length = Math.min(debug_commands.length, 50)
|
|
26
|
+
debug_update()
|
|
27
|
+
return record
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function debug_update() {
|
|
31
|
+
const state = {
|
|
32
|
+
status: connection_message ?? (config.browser ? 'Connecting to server' : 'Waiting for trigger'),
|
|
33
|
+
connected: socket?.readyState === WebSocket.OPEN,
|
|
34
|
+
browser: config.browser ?? '',
|
|
35
|
+
endpoint: config.websocket_url ?? '',
|
|
36
|
+
extension_id: chrome.runtime.id,
|
|
37
|
+
sessions: Object.keys(sessions).length,
|
|
38
|
+
debugger_tabs: Object.keys(attached).length,
|
|
39
|
+
commands: debug_commands,
|
|
40
|
+
}
|
|
41
|
+
for (const port of debug_ports) {
|
|
42
|
+
try {
|
|
43
|
+
port.postMessage(state)
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
chrome.runtime.onConnect.addListener(port => {
|
|
49
|
+
if (port.name !== 'cdp_debug') return
|
|
50
|
+
debug_ports.push(port)
|
|
51
|
+
port.onDisconnect.addListener(() => debug_ports.splice(debug_ports.indexOf(port), 1))
|
|
52
|
+
port.onMessage.addListener(message => {
|
|
53
|
+
if (message !== 'clear') return
|
|
54
|
+
debug_commands.length = 0
|
|
55
|
+
debug_update()
|
|
56
|
+
})
|
|
57
|
+
configured.then(debug_update)
|
|
58
|
+
})
|
|
59
|
+
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })
|
|
15
60
|
|
|
16
61
|
function report_connection(message) {
|
|
17
62
|
if (connection_message === message) return
|
|
18
63
|
connection_message = message
|
|
19
64
|
console.info('[cdp_ext] ' + message)
|
|
65
|
+
debug_update()
|
|
20
66
|
}
|
|
21
67
|
|
|
22
68
|
async function bootstrap(tab) {
|
|
@@ -34,29 +80,62 @@ async function bootstrap(tab) {
|
|
|
34
80
|
await configured
|
|
35
81
|
while (bootstrapping) await bootstrapping
|
|
36
82
|
return bootstrapping = (async () => {
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
83
|
+
const tabs = await chrome.tabs.query({})
|
|
84
|
+
if (!tabs.some(current => current.id === tab.id && (current.pendingUrl ?? current.url) === text)) return
|
|
85
|
+
const trigger = debug_commands.find(record => record.method === 'cdp_ext.trigger' &&
|
|
86
|
+
record.tabId === tab.id && record.browser === settings.browser && record.endpoint === settings.websocket_url) ?? debug_record({
|
|
87
|
+
method: 'cdp_ext.trigger',
|
|
88
|
+
tabId: tab.id,
|
|
89
|
+
browser: settings.browser,
|
|
90
|
+
endpoint: settings.websocket_url,
|
|
91
|
+
})
|
|
92
|
+
try {
|
|
93
|
+
const changed = ['browser', 'websocket_url', 'token'].some(key => settings[key] !== config[key])
|
|
94
|
+
if (changed && config.browser && socket?.readyState < WebSocket.CLOSING) {
|
|
95
|
+
const probe = new WebSocket(settings.websocket_url)
|
|
96
|
+
trigger.error = 'This Chrome profile is already connected as ' + config.browser
|
|
97
|
+
probe.onopen = () => probe.send(JSON.stringify({
|
|
98
|
+
browser: settings.browser,
|
|
99
|
+
extension_id: chrome.runtime.id,
|
|
100
|
+
token: settings.token,
|
|
101
|
+
error: trigger.error,
|
|
102
|
+
}))
|
|
103
|
+
} else {
|
|
104
|
+
if (changed) {
|
|
105
|
+
Object.assign(config, settings)
|
|
106
|
+
connection_message = undefined
|
|
107
|
+
debug_update()
|
|
108
|
+
await chrome.storage.session.set({ cdp_bootstrap: settings })
|
|
109
|
+
if (socket && socket.readyState !== WebSocket.CLOSED) {
|
|
110
|
+
await new Promise(resolve => {
|
|
111
|
+
socket.addEventListener('close', resolve, { once: true })
|
|
112
|
+
socket.close()
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
await disconnecting
|
|
116
|
+
}
|
|
117
|
+
await connect()
|
|
56
118
|
}
|
|
57
|
-
await
|
|
119
|
+
const current_tabs = await chrome.tabs.query({})
|
|
120
|
+
const current = current_tabs.find(current => current.id === tab.id)
|
|
121
|
+
if ((current?.pendingUrl ?? current?.url) === text) {
|
|
122
|
+
if (current_tabs.length === 1) {
|
|
123
|
+
await chrome.tabs.update(tab.id, { url: chrome.runtime.getURL('sidebar.html?connection') })
|
|
124
|
+
trigger.action = 'opened connection page'
|
|
125
|
+
} else {
|
|
126
|
+
await chrome.tabs.remove(tab.id)
|
|
127
|
+
trigger.action = 'closed tab'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
trigger.status = trigger.error ? 'error' : 'done'
|
|
131
|
+
} catch (error) {
|
|
132
|
+
trigger.status = 'error'
|
|
133
|
+
trigger.error = error.message
|
|
134
|
+
throw error
|
|
135
|
+
} finally {
|
|
136
|
+
trigger.duration_ms = Date.now() - trigger.time
|
|
137
|
+
debug_update()
|
|
58
138
|
}
|
|
59
|
-
await connect()
|
|
60
139
|
})().finally(() => {
|
|
61
140
|
bootstrapping = null
|
|
62
141
|
})
|
|
@@ -79,7 +158,7 @@ function target_info(tab) {
|
|
|
79
158
|
}
|
|
80
159
|
|
|
81
160
|
function detached(source) {
|
|
82
|
-
if (!
|
|
161
|
+
if (!sessions[String(source.tabId)]) return
|
|
83
162
|
delete attached[source.tabId]
|
|
84
163
|
for (const [id, session] of Object.entries(sessions)) {
|
|
85
164
|
if (session.tabId === source.tabId) delete sessions[id]
|
|
@@ -91,6 +170,7 @@ function detached(source) {
|
|
|
91
170
|
targetId: String(source.tabId),
|
|
92
171
|
},
|
|
93
172
|
})
|
|
173
|
+
debug_update()
|
|
94
174
|
}
|
|
95
175
|
|
|
96
176
|
async function attach_tab(tabId) {
|
|
@@ -174,6 +254,12 @@ const methods = {
|
|
|
174
254
|
},
|
|
175
255
|
async 'Target.attachToTarget'(params) {
|
|
176
256
|
const tabId = Number(params.targetId)
|
|
257
|
+
if (params.execution_mode === 'api') {
|
|
258
|
+
await chrome.tabs.get(tabId)
|
|
259
|
+
sessions[String(tabId)] = { tabId }
|
|
260
|
+
debug_update()
|
|
261
|
+
return { sessionId: String(tabId) }
|
|
262
|
+
}
|
|
177
263
|
if (attaching[tabId]) return attaching[tabId]
|
|
178
264
|
if (attached[tabId]) return { sessionId: String(tabId) }
|
|
179
265
|
return attaching[tabId] = attach_tab(tabId).finally(() => delete attaching[tabId])
|
|
@@ -184,7 +270,7 @@ const methods = {
|
|
|
184
270
|
if (source.sessionId) {
|
|
185
271
|
await chrome.debugger.sendCommand({ tabId: source.tabId }, 'Target.detachFromTarget', { sessionId: source.sessionId })
|
|
186
272
|
} else {
|
|
187
|
-
await chrome.debugger.detach(source)
|
|
273
|
+
if (attached[source.tabId]) await chrome.debugger.detach(source)
|
|
188
274
|
detached(source)
|
|
189
275
|
}
|
|
190
276
|
},
|
|
@@ -206,6 +292,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
206
292
|
sessionId: params.sessionId,
|
|
207
293
|
}
|
|
208
294
|
else delete sessions[params.sessionId]
|
|
295
|
+
debug_update()
|
|
209
296
|
}
|
|
210
297
|
send({
|
|
211
298
|
method,
|
|
@@ -229,6 +316,7 @@ chrome.tabs.onUpdated.addListener((id, changes, tab) => {
|
|
|
229
316
|
})
|
|
230
317
|
})
|
|
231
318
|
chrome.tabs.onRemoved.addListener(id => {
|
|
319
|
+
detached({ tabId: id })
|
|
232
320
|
if (discovering) send({
|
|
233
321
|
method: 'Target.targetDestroyed',
|
|
234
322
|
params: { targetId: String(id) },
|
|
@@ -296,6 +384,12 @@ async function connect() {
|
|
|
296
384
|
if (!event.data) return
|
|
297
385
|
const request = JSON.parse(event.data)
|
|
298
386
|
const { method, params = {}, sessionId, id } = request
|
|
387
|
+
const command = debug_record({
|
|
388
|
+
id,
|
|
389
|
+
method,
|
|
390
|
+
sessionId,
|
|
391
|
+
execution_mode: params.execution_mode,
|
|
392
|
+
})
|
|
299
393
|
let response
|
|
300
394
|
try {
|
|
301
395
|
let result
|
|
@@ -316,11 +410,62 @@ async function connect() {
|
|
|
316
410
|
else resolve(value)
|
|
317
411
|
}))
|
|
318
412
|
: await object[name](...args)
|
|
413
|
+
} else if (method === '_.click' || method === '_.find') {
|
|
414
|
+
const source = sessions[sessionId]
|
|
415
|
+
if (!source || source.sessionId) throw new Error('XPath browser API commands require a registered root tab')
|
|
416
|
+
const results = await chrome.scripting.executeScript({
|
|
417
|
+
target: { tabId: source.tabId },
|
|
418
|
+
args: [method, params.xpath, params.options],
|
|
419
|
+
func: async (method, xpath, options) => {
|
|
420
|
+
if (method === '_.click') {
|
|
421
|
+
for (let i = 0; i < options.attempts; i++) {
|
|
422
|
+
const node = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
|
|
423
|
+
if (node) {
|
|
424
|
+
if (typeof node.click !== 'function') return false
|
|
425
|
+
node.click()
|
|
426
|
+
return true
|
|
427
|
+
}
|
|
428
|
+
if (i + 1 < options.attempts) await new Promise(resolve => setTimeout(resolve, options.interval_ms))
|
|
429
|
+
}
|
|
430
|
+
return false
|
|
431
|
+
}
|
|
432
|
+
const nodes = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
|
|
433
|
+
const items = []
|
|
434
|
+
for (let i = 0; i < Math.min(nodes.snapshotLength, options); i++) {
|
|
435
|
+
const node = nodes.snapshotItem(i)
|
|
436
|
+
items.push({
|
|
437
|
+
node_type: node.nodeType,
|
|
438
|
+
node_name: node.nodeName,
|
|
439
|
+
text: String(node.innerText ?? node.textContent ?? '').trim().slice(0, 2000),
|
|
440
|
+
tag: node.tagName?.toLowerCase(),
|
|
441
|
+
id: node.id,
|
|
442
|
+
class: node.className == null ? undefined : String(node.className),
|
|
443
|
+
href: node.href,
|
|
444
|
+
value: node.value,
|
|
445
|
+
rect: node.getBoundingClientRect?.().toJSON(),
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
count: nodes.snapshotLength,
|
|
450
|
+
items,
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
})
|
|
454
|
+
if (!Object.hasOwn(results[0] ?? {}, 'result')) throw new Error('XPath browser API execution failed')
|
|
455
|
+
result = {
|
|
456
|
+
result: {
|
|
457
|
+
type: typeof results[0].result,
|
|
458
|
+
value: results[0].result,
|
|
459
|
+
},
|
|
460
|
+
}
|
|
319
461
|
} else {
|
|
320
462
|
if (sessionId == null && !methods[method]) throw new Error('Unsupported reverse browser command: ' + method)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
:
|
|
463
|
+
if (sessionId != null) {
|
|
464
|
+
const source = sessions[sessionId]
|
|
465
|
+
if (!source) throw new Error('Unknown debugger session: ' + sessionId)
|
|
466
|
+
await methods['Target.attachToTarget']({ targetId: String(source.tabId) })
|
|
467
|
+
result = await chrome.debugger.sendCommand(source, method, params)
|
|
468
|
+
} else result = await methods[method](params)
|
|
324
469
|
}
|
|
325
470
|
response = {
|
|
326
471
|
id,
|
|
@@ -335,15 +480,20 @@ async function connect() {
|
|
|
335
480
|
},
|
|
336
481
|
}
|
|
337
482
|
}
|
|
483
|
+
command.status = response.error ? 'error' : 'done'
|
|
484
|
+
command.error = response.error?.message
|
|
485
|
+
command.duration_ms = Date.now() - command.time
|
|
486
|
+
debug_update()
|
|
338
487
|
if (id != null && connection.readyState === WebSocket.OPEN) connection.send(JSON.stringify(response))
|
|
339
488
|
}
|
|
340
489
|
connection.onclose = () => {
|
|
341
490
|
report_connection('Waiting for CDP.js server at ' + endpoint + '; retrying automatically.')
|
|
342
491
|
discovering = false
|
|
343
|
-
disconnecting = Promise.all(Object.keys(
|
|
492
|
+
disconnecting = Promise.all(Object.keys(sessions).filter(id => !sessions[id].sessionId).map(id => {
|
|
344
493
|
const source = { tabId: Number(id) }
|
|
494
|
+
const was_attached = attached[id]
|
|
345
495
|
detached(source)
|
|
346
|
-
return chrome.debugger.detach(source).catch(() => {})
|
|
496
|
+
if (was_attached) return chrome.debugger.detach(source).catch(() => {})
|
|
347
497
|
})).finally(() => disconnecting = null)
|
|
348
498
|
}
|
|
349
499
|
}
|
package/cdp_ext/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "CDP reverse connection",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"minimum_chrome_version": "125",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"accessibilityFeatures.read",
|
|
@@ -64,7 +64,12 @@
|
|
|
64
64
|
"host_permissions": [
|
|
65
65
|
"<all_urls>"
|
|
66
66
|
],
|
|
67
|
-
"action": {
|
|
67
|
+
"action": {
|
|
68
|
+
"default_title": "CDP.js debug"
|
|
69
|
+
},
|
|
70
|
+
"side_panel": {
|
|
71
|
+
"default_path": "sidebar.html"
|
|
72
|
+
},
|
|
68
73
|
"incognito": "spanning",
|
|
69
74
|
"background": {
|
|
70
75
|
"service_worker": "cdp_ext.js"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<meta charset="utf-8">
|
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
5
|
+
<title>CDP.js debug</title>
|
|
6
|
+
<style>
|
|
7
|
+
:root { color-scheme: light dark; font: 14px system-ui, sans-serif; }
|
|
8
|
+
body { margin: 16px; }
|
|
9
|
+
header { display: flex; align-items: center; justify-content: space-between; }
|
|
10
|
+
h1 { font-size: 18px; margin: 0; }
|
|
11
|
+
h2 { font-size: 14px; margin: 24px 0 12px; }
|
|
12
|
+
button { font: inherit; cursor: pointer; padding: 5px 10px; }
|
|
13
|
+
#status { border-left: 3px solid #d99b24; padding-left: 10px; overflow-wrap: anywhere; }
|
|
14
|
+
#status.connected { border-color: #35a76a; }
|
|
15
|
+
dt { opacity: .65; margin-top: 12px; }
|
|
16
|
+
dd { margin: 3px 0; overflow-wrap: anywhere; font-family: monospace; }
|
|
17
|
+
#commands { padding: 0; list-style: none; }
|
|
18
|
+
li { border-top: 1px solid #8885; padding: 10px 0; }
|
|
19
|
+
strong { display: block; overflow-wrap: anywhere; font: 13px monospace; }
|
|
20
|
+
small { display: block; opacity: .7; margin-top: 5px; font-size: 12px; }
|
|
21
|
+
.error { color: light-dark(#b42318, #ff938a); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
22
|
+
</style>
|
|
23
|
+
<header><h1>CDP.js debug</h1><button id="clear" type="button" title="Clear diagnostic history">Clear</button></header>
|
|
24
|
+
<p id="connection-message" hidden>Browser bridge configured. Live connection status is shown below.</p>
|
|
25
|
+
<p id="status" role="status">Connecting to extension…</p>
|
|
26
|
+
<dl>
|
|
27
|
+
<dt>Browser</dt><dd id="browser">—</dd>
|
|
28
|
+
<dt>Server</dt><dd id="endpoint">—</dd>
|
|
29
|
+
<dt>Extension</dt><dd id="extension">—</dd>
|
|
30
|
+
<dt>Routing sessions</dt><dd id="sessions">0</dd>
|
|
31
|
+
<dt>Debugger tabs</dt><dd id="debugger_tabs">0</dd>
|
|
32
|
+
</dl>
|
|
33
|
+
<h2>Recent activity <small>Latest 50 · memory only</small></h2>
|
|
34
|
+
<p id="empty">No activity received.</p>
|
|
35
|
+
<ol id="commands"></ol>
|
|
36
|
+
<script src="sidebar.js"></script>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const port = chrome.runtime.connect({ name: 'cdp_debug' })
|
|
2
|
+
const status = document.querySelector('#status')
|
|
3
|
+
const heartbeat = setInterval(() => port.postMessage('ping'), 20000)
|
|
4
|
+
if (location.search === '?connection') {
|
|
5
|
+
document.title = 'CDP.js connection'
|
|
6
|
+
document.querySelector('h1').textContent = document.title
|
|
7
|
+
document.querySelector('#connection-message').hidden = false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
port.onMessage.addListener(state => {
|
|
11
|
+
status.textContent = state.status
|
|
12
|
+
status.classList.toggle('connected', state.connected)
|
|
13
|
+
for (const [id, value] of Object.entries({
|
|
14
|
+
browser: state.browser,
|
|
15
|
+
endpoint: state.endpoint,
|
|
16
|
+
extension: state.extension_id,
|
|
17
|
+
sessions: state.sessions,
|
|
18
|
+
debugger_tabs: state.debugger_tabs ?? 0,
|
|
19
|
+
})) document.getElementById(id).textContent = value === '' ? '—' : value
|
|
20
|
+
document.querySelector('#empty').hidden = state.commands.length > 0
|
|
21
|
+
document.querySelector('#commands').replaceChildren(...state.commands.map(command => {
|
|
22
|
+
const row = document.createElement('li')
|
|
23
|
+
const method = document.createElement('strong')
|
|
24
|
+
method.textContent = command.method
|
|
25
|
+
const details = document.createElement('small')
|
|
26
|
+
details.textContent = [
|
|
27
|
+
new Date(command.time).toLocaleTimeString(),
|
|
28
|
+
command.id == null ? (command.method === 'cdp_ext.trigger' ? 'trigger' : 'notification') : '#' + command.id,
|
|
29
|
+
command.sessionId ? 'session ' + command.sessionId : 'browser',
|
|
30
|
+
command.status,
|
|
31
|
+
command.execution_mode,
|
|
32
|
+
command.tabId == null ? '' : 'tab ' + command.tabId,
|
|
33
|
+
command.browser,
|
|
34
|
+
command.endpoint,
|
|
35
|
+
command.action,
|
|
36
|
+
command.duration_ms == null ? '' : command.duration_ms + ' ms',
|
|
37
|
+
].filter(Boolean).join(' · ')
|
|
38
|
+
row.append(method, details)
|
|
39
|
+
if (command.error) {
|
|
40
|
+
const error = document.createElement('p')
|
|
41
|
+
error.className = 'error'
|
|
42
|
+
error.textContent = command.error
|
|
43
|
+
row.append(error)
|
|
44
|
+
}
|
|
45
|
+
return row
|
|
46
|
+
}))
|
|
47
|
+
})
|
|
48
|
+
port.onDisconnect.addListener(() => {
|
|
49
|
+
clearInterval(heartbeat)
|
|
50
|
+
status.textContent = 'Extension disconnected. Reopen this panel after reloading the extension.'
|
|
51
|
+
status.classList.remove('connected')
|
|
52
|
+
document.querySelector('#clear').disabled = true
|
|
53
|
+
})
|
|
54
|
+
document.querySelector('#clear').addEventListener('click', () => port.postMessage('clear'))
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mefistofelix/cdp.js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Low-level Chromium DevTools Protocol client for Node.js, Deno and Bun, with no runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": "./cdp.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"cdp.js",
|
|
9
9
|
"cdp_ext/manifest.json",
|
|
10
|
-
"cdp_ext/cdp_ext.js"
|
|
10
|
+
"cdp_ext/cdp_ext.js",
|
|
11
|
+
"cdp_ext/sidebar.html",
|
|
12
|
+
"cdp_ext/sidebar.js"
|
|
11
13
|
],
|
|
12
14
|
"engines": {
|
|
13
15
|
"node": ">=22.19.0"
|