@mefistofelix/cdp.js 0.2.0 → 0.4.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 +115 -20
- package/cdp.js +111 -13
- package/cdp_ext/cdp_ext.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# cdp.js
|
|
2
2
|
|
|
3
|
-
A low-level Chromium DevTools Protocol library for Node.js, with an
|
|
4
|
-
native Deno server for Chrome extension reverse connections. One ES module,
|
|
3
|
+
A low-level Chromium DevTools Protocol library for Node.js, Deno and Bun, with an
|
|
4
|
+
optional native Deno/Bun server for Chrome extension reverse connections. One ES module,
|
|
5
5
|
standard-library imports, no runtime dependencies or root `package.json`.
|
|
6
6
|
|
|
7
7
|
Use Node.js 22.19+ and an installed Chrome, Chromium or Edge. Import `cdp.js`
|
|
8
8
|
directly from an ES module, for example `example.mjs`, and run `node example.mjs`.
|
|
9
9
|
Browser discovery covers common Windows, macOS and Linux installation paths;
|
|
10
10
|
use `executable_path` or the `CDP_BROWSER` environment variable for other paths.
|
|
11
|
+
The same module also runs with `bun example.mjs` or `deno run -A example.mjs`.
|
|
12
|
+
Direct and reverse modes are tested with Bun 1.4.3; no Bun-specific package,
|
|
13
|
+
configuration file or installation step is required by the library.
|
|
11
14
|
|
|
12
15
|
For a published npm release:
|
|
13
16
|
|
|
@@ -24,7 +27,8 @@ below remain supported. Publication metadata is isolated in `npm/`.
|
|
|
24
27
|
The package includes both `cdp_ext/` files. With `cdp_ext: true`, the manager
|
|
25
28
|
writes a configured extension copy into the browser's profile directory and loads
|
|
26
29
|
it automatically; no manual extraction or build is needed. Reverse mode still
|
|
27
|
-
requires Deno. In Deno, import from `npm:@mefistofelix/cdp.js`.
|
|
30
|
+
requires Deno or Bun. In Deno, import from `npm:@mefistofelix/cdp.js`.
|
|
31
|
+
In Bun, use `bun add @mefistofelix/cdp.js` and the ordinary package import above.
|
|
28
32
|
|
|
29
33
|
## Features and scope
|
|
30
34
|
|
|
@@ -33,8 +37,9 @@ requires Deno. In Deno, import from `npm:@mefistofelix/cdp.js`.
|
|
|
33
37
|
- Browser proxies and named direct-mode contexts with independent proxies/storage
|
|
34
38
|
and automatic cleanup after their last target closes.
|
|
35
39
|
- Incognito targets in direct and reverse modes.
|
|
40
|
+
- Named, colored tab groups through the reverse Chrome extension.
|
|
36
41
|
- Attachment through WebSocket, HTTP discovery or a debugging port.
|
|
37
|
-
- Optional Chrome extension reverse connections through one native Deno server,
|
|
42
|
+
- Optional Chrome extension reverse connections through one native Deno/Bun server,
|
|
38
43
|
without browser remote-debugging arguments.
|
|
39
44
|
- Generic `chrome.x.y` extension API calls on that connection, including scripting,
|
|
40
45
|
tabs/windows, cookies, storage, downloads and browser settings.
|
|
@@ -116,8 +121,8 @@ import api, { util, jsrpc, browser, cdp } from './cdp.js'
|
|
|
116
121
|
| `client.call({ method, params = {} })` | Resolve routing and return a promise for the native CDP result, or a custom handler's result. |
|
|
117
122
|
| `client.evaluate_xpath(params, fn, options)` | Execute a serialized function with a normalized XPath in a target; return the native `Runtime.evaluate` result. |
|
|
118
123
|
| `client.close()` | Close owned processes, disconnect attached/reverse browsers, stop the reverse server and clear browser/socket mappings. Profiles and external browsers remain. |
|
|
119
|
-
| `new jsrpc(urlOrSocket, options = {})` | Open a raw WebSocket JSON-RPC connection or equip an accepted
|
|
120
|
-
| `client.listen_reverse(options = {})` | Start the native Deno WebSocket server; returns
|
|
124
|
+
| `new jsrpc(urlOrSocket, options = {})` | Open a raw WebSocket JSON-RPC connection or equip an accepted socket with the same RPC methods. |
|
|
125
|
+
| `client.listen_reverse(options = {})` | Start the native Deno or Bun WebSocket server; returns that server with common lifecycle fields documented below. |
|
|
121
126
|
| `socket.req(request, timeout_ms)` | Assign a request ID and await its native result. |
|
|
122
127
|
| `socket.notify(request)` | Serialize and send a raw message; no ID allocation or response wait. |
|
|
123
128
|
| `socket.close()` | Native WebSocket close; does not terminate the browser. |
|
|
@@ -237,8 +242,8 @@ local configuration.
|
|
|
237
242
|
| `request_timeout_ms` | number, `30000` | Default timeout for each protocol request on this browser's socket. |
|
|
238
243
|
| `waitForDebuggerOnStart` | boolean, `false` | Ask Chromium to pause newly auto-attached targets. Applied to `Target.setAutoAttach` when connecting; not a CLI argument or target option. |
|
|
239
244
|
| `websocket_url` | string, unset | Attach directly to a browser WebSocket endpoint. |
|
|
240
|
-
| `cdp_ext` | boolean, `false` | Launch with the reverse extension under Deno. Start one shared server automatically if needed, prepare a profile-specific extension copy and wait for its connection. |
|
|
241
|
-
| `reverse` | boolean, `false` | Attach to an externally launched extension instead of launching or discovering a browser. Starts the shared listener on first use under Deno. |
|
|
245
|
+
| `cdp_ext` | boolean, `false` | Launch with the reverse extension under Deno or Bun. Start one shared server automatically if needed, prepare a profile-specific extension copy and wait for its connection. |
|
|
246
|
+
| `reverse` | boolean, `false` | Attach to an externally launched extension instead of launching or discovering a browser. Starts the shared listener on first use under Deno or Bun. |
|
|
242
247
|
| `extension_id` | string, browser name for external reverse attachment | Incoming extension ID to associate with this logical browser. Set automatically for managed `cdp_ext` launches. |
|
|
243
248
|
| `http_url` | string, unset | Discover the endpoint from an HTTP(S) base URL or exact `/json/version` URL. |
|
|
244
249
|
| `port` | number, unset | Discover an existing browser at `http://<host>:<port>/json/version`. |
|
|
@@ -343,7 +348,7 @@ Native proxy rules can use semicolon-separated strings; the generic CLI converte
|
|
|
343
348
|
still comma-joins every array. Bypass lists accept both separators. Chrome gives
|
|
344
349
|
`no-proxy-server` precedence, so overriding `proxy-server` alone does not cancel
|
|
345
350
|
that flag; override `args['no-proxy-server']: false` too if needed.
|
|
346
|
-
Browser proxy options affect Chrome traffic, not
|
|
351
|
+
Browser proxy options affect Chrome traffic, not the runtime's endpoint discovery
|
|
347
352
|
or CDP transport. Chrome normally bypasses loopback addresses. To proxy them,
|
|
348
353
|
use `args['proxy-bypass-list']: '<-loopback>'`; in reverse mode keep the extension's
|
|
349
354
|
local WebSocket listener reachable. The library inserts no extra bypass rule.
|
|
@@ -491,12 +496,13 @@ browser instances on the same port; each extension announces its native ID
|
|
|
491
496
|
in its first message. No remote-debugging argument is needed.
|
|
492
497
|
|
|
493
498
|
Run the server under Deno using its native
|
|
494
|
-
[WebSocket upgrade](https://docs.deno.com/api/deno/websockets/)
|
|
499
|
+
[WebSocket upgrade](https://docs.deno.com/api/deno/websockets/) or under Bun using
|
|
500
|
+
[Bun.serve](https://bun.sh/docs/runtime/http/websockets).
|
|
495
501
|
Node's direct CDP mode remains supported; `listen_reverse()` under Node rejects
|
|
496
|
-
with an explicit Deno requirement. There is no WebSocket framing implementation,
|
|
502
|
+
with an explicit Deno/Bun requirement. There is no WebSocket framing implementation,
|
|
497
503
|
third-party server dependency or additional build step.
|
|
498
504
|
|
|
499
|
-
Run `deno run -A examples/reverse.mjs` to launch Chrome with the extension, create
|
|
505
|
+
Run `deno run -A examples/reverse.mjs` or `bun examples/reverse.mjs` to launch Chrome with the extension, create
|
|
500
506
|
a target and evaluate `21 * 2`. Ctrl+C closes the owned browser and server.
|
|
501
507
|
The same mode is available per browser:
|
|
502
508
|
|
|
@@ -589,15 +595,24 @@ retry. Direct browsers do not keep this listener alive.
|
|
|
589
595
|
`client.reverse_server` is the public shared configuration object. After startup
|
|
590
596
|
it contains the effective listening port, including when `port: 0` originally
|
|
591
597
|
requested an available port. Restarts reuse that port so existing extensions can
|
|
592
|
-
still reconnect. `client.server` contains the current native Deno `HttpServer`,
|
|
598
|
+
still reconnect. `client.server` contains the current native Deno `HttpServer` or Bun `Server`,
|
|
593
599
|
or is unset/null while stopped. A call arriving during shutdown waits for it to
|
|
594
600
|
finish before starting a replacement server.
|
|
595
601
|
|
|
596
602
|
`listen_reverse(options = client.reverse_server)` remains available to start the
|
|
597
|
-
listener explicitly ahead of a browser call. It returns the native
|
|
603
|
+
listener explicitly ahead of a browser call. It returns the native runtime server;
|
|
598
604
|
calling it while a server is running rejects. Explicit options replace the stored
|
|
599
605
|
settings, with omitted fields taking the hostname/port defaults above.
|
|
600
606
|
|
|
607
|
+
Both runtimes expose `server.addr.hostname`, `server.addr.port`,
|
|
608
|
+
`server.shutdown()` and `server.finished`. On Bun, the library adds these fields:
|
|
609
|
+
`shutdown()` calls native `stop()` and resolves `finished` after shutdown.
|
|
610
|
+
Use `client.close()` to also close owned browsers and incoming sockets.
|
|
611
|
+
Bun's callback-based server sockets receive a small EventTarget adapter with
|
|
612
|
+
native send/close and live ready state. The same `jsrpc` handles both runtimes;
|
|
613
|
+
registration, request IDs, pending waits, events and session routing are shared.
|
|
614
|
+
Native Bun WebSocket limits still apply.
|
|
615
|
+
|
|
601
616
|
Reverse calls reuse the normal target setup, custom handlers, mappings, routed
|
|
602
617
|
events, request correlation, timeout and error behavior. `reverse: true` alone
|
|
603
618
|
attaches externally: the manager neither launches nor updates that browser.
|
|
@@ -1259,6 +1274,7 @@ await client.call({
|
|
|
1259
1274
|
| `targetId` | `null` | Optional existing physical target ID. Reused if discovered; otherwise create a new target. |
|
|
1260
1275
|
| `context` | unset | Direct-mode context name or first-use configuration object; see named contexts above. Reverse mode rejects this option. |
|
|
1261
1276
|
| `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. |
|
|
1277
|
+
| `group` | unset | Reverse-only tab group name or first-use `{ name, color }` configuration. False skips grouping. |
|
|
1262
1278
|
| `create_params` | `{}` | Native creation overrides, merged over `url: 'about:blank'` and `background: true`; start from a blank target without requesting foreground activation. |
|
|
1263
1279
|
| `initialize` | `true` | Run the convenience setup sequence; false skips it. |
|
|
1264
1280
|
| `runtime` | `'bootstrap'` | Enable Runtime for setup, then disable reporting. `true` leaves it enabled; `false` skips both operations. |
|
|
@@ -1269,6 +1285,78 @@ await client.call({
|
|
|
1269
1285
|
| `binding` | `'_send_to_cdp'` | Default page-to-host JavaScript binding. String changes its name; false skips installation; true selects the default name. |
|
|
1270
1286
|
| `background_service` | `true` | Clear, observe and record `pushMessaging` background-service events in Chromium. |
|
|
1271
1287
|
|
|
1288
|
+
### Named tab groups
|
|
1289
|
+
|
|
1290
|
+
With `cdp_ext: true` or external `reverse: true`, set `target.group` when first
|
|
1291
|
+
configuring a target:
|
|
1292
|
+
|
|
1293
|
+
```js
|
|
1294
|
+
await client.call({
|
|
1295
|
+
method: 'Page.navigate',
|
|
1296
|
+
params: {
|
|
1297
|
+
browser: {
|
|
1298
|
+
name: 'work',
|
|
1299
|
+
cdp_ext: true,
|
|
1300
|
+
},
|
|
1301
|
+
target: {
|
|
1302
|
+
name: 'search',
|
|
1303
|
+
group: {
|
|
1304
|
+
name: 'Research',
|
|
1305
|
+
color: 'blue',
|
|
1306
|
+
},
|
|
1307
|
+
},
|
|
1308
|
+
url: 'https://example.com/',
|
|
1309
|
+
},
|
|
1310
|
+
})
|
|
1311
|
+
await client.call({
|
|
1312
|
+
method: 'Page.navigate',
|
|
1313
|
+
params: {
|
|
1314
|
+
browser: 'work',
|
|
1315
|
+
target: {
|
|
1316
|
+
name: 'notes',
|
|
1317
|
+
group: 'Research',
|
|
1318
|
+
},
|
|
1319
|
+
url: 'https://example.com/notes',
|
|
1320
|
+
},
|
|
1321
|
+
})
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
| Group option | Default | Meaning |
|
|
1325
|
+
| --- | --- | --- |
|
|
1326
|
+
| `name` | required in an object | Logical name in `record.groups`; also the visible group title. A string reference creates a default group if this name is new. |
|
|
1327
|
+
| `color` | unset | Chrome's native color selection for new groups; otherwise retain the current color. Explicit values: `grey`, `blue`, `red`, `yellow`, `green`, `pink`, `purple`, `cyan`, `orange`. |
|
|
1328
|
+
|
|
1329
|
+
The first use of a group name records its configuration. Concurrent target
|
|
1330
|
+
creation shares that definition and serializes group operations. Names are scoped
|
|
1331
|
+
to the logical browser. Each window has its own native group for that name:
|
|
1332
|
+
tabs stay in their current window, including separate regular/incognito windows.
|
|
1333
|
+
Groups are created by the library, not adopted from unrelated groups that happen
|
|
1334
|
+
to have the same visible title. Incognito still requires the bridge's user grant.
|
|
1335
|
+
|
|
1336
|
+
Grouping happens before optional CDP setup, including with `initialize: false`.
|
|
1337
|
+
It also works with `targetId` selecting an existing tab. As with other target
|
|
1338
|
+
options, passing a new group on a target label that was already configured does
|
|
1339
|
+
not reconfigure that label. Use another label with its physical `targetId`, or
|
|
1340
|
+
edit the public record and reinitialize the target.
|
|
1341
|
+
|
|
1342
|
+
Reconnection reapplies the name/color and rejoins a surviving group. If its last
|
|
1343
|
+
tab was closed or ungrouped, Chrome removes the native group; later target setup
|
|
1344
|
+
creates a new one. `record.groups[name].window_groups[windowId]` stores the last
|
|
1345
|
+
known native group ID. These IDs are checked against Chrome during setup and can
|
|
1346
|
+
remain stale between operations; no extra event subscriptions or persistence are
|
|
1347
|
+
added. In reverse target info, `groupId` is the current native ID, or `-1` when
|
|
1348
|
+
ungrouped. Generic `chrome.tabs` / `chrome.tabGroups` calls remain available for
|
|
1349
|
+
immediate native updates. Native errors, including invalid colors, reject setup
|
|
1350
|
+
with the usual RPC error context and can be retried after correcting the record.
|
|
1351
|
+
|
|
1352
|
+
Direct CDP does not expose browser tab-group commands. A non-false `group` option
|
|
1353
|
+
therefore rejects before connecting or launching in direct mode. The bridge's
|
|
1354
|
+
existing `tabGroups` permission is sufficient; no extra permission is added.
|
|
1355
|
+
See [Chrome tab groups](https://developer.chrome.com/docs/extensions/reference/api/tabGroups)
|
|
1356
|
+
and [grouping tabs](https://developer.chrome.com/docs/extensions/reference/api/tabs#method-group).
|
|
1357
|
+
|
|
1358
|
+
### Target initialization
|
|
1359
|
+
|
|
1272
1360
|
Domain options set to false skip setup commands; they do not actively disable a
|
|
1273
1361
|
domain already enabled by another caller. Likewise, `binding: false` does not
|
|
1274
1362
|
remove a previously installed binding. Use native CDP commands for immediate
|
|
@@ -1530,9 +1618,9 @@ dictionary containers have a null prototype.
|
|
|
1530
1618
|
| `client.reverse_server` | Shared listener configuration; the effective hostname/port are stored after startup. |
|
|
1531
1619
|
| `client.custom_methods[name]` | Handler function. |
|
|
1532
1620
|
| `client.browsers[name]` | Browser configuration fields plus live state. |
|
|
1533
|
-
| `client.reverse_sockets[extension_id]` | Announced incoming
|
|
1621
|
+
| `client.reverse_sockets[extension_id]` | Announced incoming socket, available even before the first call; native on Deno, an EventTarget adapter on Bun. |
|
|
1534
1622
|
| `record.extension_id`, `record.extension_path` | Expected extension identity and the managed copy's canonical directory. |
|
|
1535
|
-
| `client.server` | Current native Deno reverse
|
|
1623
|
+
| `client.server` | Current native Deno/Bun reverse server with shared lifecycle fields, or unset/null when not listening. |
|
|
1536
1624
|
| `client.server_closing` | Shutdown promise while the reverse listener is stopping, otherwise unset/null. |
|
|
1537
1625
|
| `record.socket` | Current `jsrpc` connection once created. |
|
|
1538
1626
|
| `record.proc` | Owned child process; absent for external attachment. |
|
|
@@ -1540,6 +1628,7 @@ dictionary containers have a null prototype.
|
|
|
1540
1628
|
| `record.targets[label]` | Target options, physical `targetId`, `sessionId`, `connecting` and `setup_errors` after setup. |
|
|
1541
1629
|
| `record.contexts[name]` | Recorded context options, native `browserContextId` or null, and pending `connecting`/`closing` promises. Direct mode only. |
|
|
1542
1630
|
| `record.context_checking` | Pending automatic empty-context cleanup, otherwise unset/null. |
|
|
1631
|
+
| `record.groups[name]` | Recorded tab-group options, per-window native IDs in `window_groups`, and a pending `connecting` operation. Reverse only. |
|
|
1543
1632
|
| `record.target_info[targetId]` | Discovered native target info plus cached `sessionId` when attached. |
|
|
1544
1633
|
| `record.session_targets[sessionId]` | Physical `targetId` for a session. |
|
|
1545
1634
|
| `socket.pending[id]` | Response-wait promise, removed on completion. |
|
|
@@ -1599,11 +1688,13 @@ subject to the debugging-port restrictions described above.
|
|
|
1599
1688
|
Readiness timeout or launch failure rejects and attempts to
|
|
1600
1689
|
terminate the spawned process.
|
|
1601
1690
|
|
|
1602
|
-
`jsrpc` takes a WebSocket URL or an accepted
|
|
1691
|
+
`jsrpc` takes a WebSocket URL or an accepted EventTarget socket and an options
|
|
1603
1692
|
object whose recognized option is
|
|
1604
1693
|
`request_timeout_ms` (default `30000`). Wait for the socket to open before `req`.
|
|
1605
|
-
For an accepted socket, it returns that same
|
|
1606
|
-
|
|
1694
|
+
For an accepted socket, it returns that same object with RPC fields/methods.
|
|
1695
|
+
The object supplies `send`, `close`, live `readyState` and normal message/close/error
|
|
1696
|
+
events. Deno sockets remain native WebSockets; Bun uses the adapter described
|
|
1697
|
+
above. Accepted sockets need not be instances of the `jsrpc` subclass.
|
|
1607
1698
|
The reverse server consumes the initial browser announcement before equipping
|
|
1608
1699
|
the socket with RPC behavior. Empty-text heartbeats are ignored.
|
|
1609
1700
|
The request is a raw envelope: `method`, optional `params`, and optional top-level
|
|
@@ -1625,6 +1716,7 @@ labels, target labels, custom methods or reconnection policy in this layer.
|
|
|
1625
1716
|
| Failed discovery or local launch | Reject the call establishing the connection. |
|
|
1626
1717
|
| Named context in reverse mode | Reject explicitly; no separate process emulation. |
|
|
1627
1718
|
| Reverse incognito without the extension's user grant | Reject with an instruction to enable Allow in incognito. |
|
|
1719
|
+
| Tab group in direct mode | Reject before connecting; tab groups require the reverse extension APIs. |
|
|
1628
1720
|
| Automatic context cleanup fails | Emit the manager's `error` event with browser label and original error. |
|
|
1629
1721
|
|
|
1630
1722
|
```js
|
|
@@ -1662,15 +1754,18 @@ node --check cdp.js
|
|
|
1662
1754
|
node --check cdp_ext/cdp_ext.js
|
|
1663
1755
|
node --test tests/cdp.test.js
|
|
1664
1756
|
deno test -A tests/reverse.test.js
|
|
1757
|
+
bun test tests/cdp.test.js tests/reverse.test.js --timeout 60000
|
|
1665
1758
|
```
|
|
1666
1759
|
|
|
1667
1760
|
Integration tests launch local headless Chromium with disposable profiles under
|
|
1668
1761
|
`build/tests/`. Set `CDP_BROWSER` if discovery does not match the machine. Direct
|
|
1669
1762
|
library use needs no build or package installation; npm packaging is optional.
|
|
1670
|
-
The Deno
|
|
1763
|
+
The shared Deno/Bun reverse suite loads the real extension into two headless Chrome instances on one
|
|
1671
1764
|
server port without remote-debugging arguments. It checks native/custom calls,
|
|
1672
1765
|
Network events, webRequest notifications without debugger attachment, bindings,
|
|
1673
1766
|
child sessions, reconnect, detach/recreate, incognito and isolation.
|
|
1767
|
+
Tab-group checks cover concurrent creation, existing tabs, title/color, per-window
|
|
1768
|
+
incognito separation, reconnection, recreation and native validation/retry.
|
|
1674
1769
|
Node coverage also checks actual browser/context proxy routing, shared private
|
|
1675
1770
|
storage, concurrent context creation, automatic disposal, unselected targets and
|
|
1676
1771
|
context ownership in externally attached browsers.
|
package/cdp.js
CHANGED
|
@@ -46,7 +46,7 @@ export class util {
|
|
|
46
46
|
|
|
47
47
|
export class jsrpc extends WebSocket {
|
|
48
48
|
constructor(url, options = {}) {
|
|
49
|
-
const socket = url instanceof
|
|
49
|
+
const socket = typeof url === 'string' || url instanceof URL ? super(url) : url
|
|
50
50
|
setMaxListeners(0, socket)
|
|
51
51
|
socket.id = 0
|
|
52
52
|
socket.pending = {}
|
|
@@ -355,18 +355,9 @@ export class cdp extends EventTarget {
|
|
|
355
355
|
hostname = '127.0.0.1',
|
|
356
356
|
port = 9223,
|
|
357
357
|
} = this.reverse_server) {
|
|
358
|
-
if (!globalThis.Deno) throw new Error('The reverse WebSocket server requires Deno')
|
|
358
|
+
if (!globalThis.Deno && !globalThis.Bun) throw new Error('The reverse WebSocket server requires Deno or Bun')
|
|
359
359
|
if (this.server) throw new Error('Reverse server is already listening')
|
|
360
|
-
|
|
361
|
-
hostname,
|
|
362
|
-
port,
|
|
363
|
-
}, request => {
|
|
364
|
-
const origin = request.headers.get('origin')
|
|
365
|
-
if (request.headers.get('upgrade') !== 'websocket' ||
|
|
366
|
-
!origin?.startsWith('chrome-extension://')) {
|
|
367
|
-
return new Response(null, { status: 403 })
|
|
368
|
-
}
|
|
369
|
-
const { socket, response } = Deno.upgradeWebSocket(request)
|
|
360
|
+
const accept = (socket, origin) => {
|
|
370
361
|
socket.addEventListener('message', event => {
|
|
371
362
|
const id = JSON.parse(event.data).browser
|
|
372
363
|
if (id !== origin.slice('chrome-extension://'.length) ||
|
|
@@ -380,8 +371,61 @@ export class cdp extends EventTarget {
|
|
|
380
371
|
})
|
|
381
372
|
util.emit(this, 'reverse_' + encodeURIComponent(id), socket)
|
|
382
373
|
}, { once: true })
|
|
374
|
+
}
|
|
375
|
+
const fetch = (request, server) => {
|
|
376
|
+
const origin = request.headers.get('origin')
|
|
377
|
+
if (request.headers.get('upgrade') !== 'websocket' ||
|
|
378
|
+
!origin?.startsWith('chrome-extension://')) {
|
|
379
|
+
return new Response(null, { status: 403 })
|
|
380
|
+
}
|
|
381
|
+
if (globalThis.Bun) {
|
|
382
|
+
if (server.upgrade(request, {
|
|
383
|
+
data: {
|
|
384
|
+
origin,
|
|
385
|
+
socket: new EventTarget(),
|
|
386
|
+
},
|
|
387
|
+
})) return
|
|
388
|
+
return new Response(null, { status: 400 })
|
|
389
|
+
}
|
|
390
|
+
const { socket, response } = Deno.upgradeWebSocket(request)
|
|
391
|
+
accept(socket, origin)
|
|
383
392
|
return response
|
|
384
|
-
}
|
|
393
|
+
}
|
|
394
|
+
if (globalThis.Bun) {
|
|
395
|
+
const server = this.server = Bun.serve({
|
|
396
|
+
hostname,
|
|
397
|
+
port,
|
|
398
|
+
fetch,
|
|
399
|
+
websocket: {
|
|
400
|
+
open(socket) {
|
|
401
|
+
const events = socket.data.socket
|
|
402
|
+
events.send = socket.send.bind(socket)
|
|
403
|
+
events.close = socket.close.bind(socket)
|
|
404
|
+
Object.defineProperty(events, 'readyState', { get: () => socket.readyState })
|
|
405
|
+
accept(events, socket.data.origin)
|
|
406
|
+
},
|
|
407
|
+
message(socket, data) {
|
|
408
|
+
socket.data.socket.dispatchEvent(new MessageEvent('message', { data }))
|
|
409
|
+
},
|
|
410
|
+
close(socket, code, reason) {
|
|
411
|
+
socket.data.socket.dispatchEvent(Object.assign(new Event('close'), {
|
|
412
|
+
code,
|
|
413
|
+
reason,
|
|
414
|
+
}))
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
})
|
|
418
|
+
server.addr = {
|
|
419
|
+
hostname,
|
|
420
|
+
port: server.port,
|
|
421
|
+
}
|
|
422
|
+
const finished = Promise.withResolvers()
|
|
423
|
+
server.finished = finished.promise
|
|
424
|
+
server.shutdown = () => server.stop().then(finished.resolve)
|
|
425
|
+
} else this.server = Deno.serve({
|
|
426
|
+
hostname,
|
|
427
|
+
port,
|
|
428
|
+
}, fetch)
|
|
385
429
|
this.reverse_server = {
|
|
386
430
|
hostname,
|
|
387
431
|
port: this.server.addr.port,
|
|
@@ -640,9 +684,58 @@ export class cdp extends EventTarget {
|
|
|
640
684
|
}
|
|
641
685
|
}
|
|
642
686
|
|
|
687
|
+
async _group(browser, target) {
|
|
688
|
+
const socket = browser.socket
|
|
689
|
+
const group = browser.groups[target.group]
|
|
690
|
+
while (group.connecting) await group.connecting
|
|
691
|
+
return group.connecting = (async () => {
|
|
692
|
+
const tab = await socket.req({
|
|
693
|
+
method: 'chrome.tabs.get',
|
|
694
|
+
params: { args: [Number(target.targetId)] },
|
|
695
|
+
})
|
|
696
|
+
const groups = await socket.req({
|
|
697
|
+
method: 'chrome.tabGroups.query',
|
|
698
|
+
params: { args: [{ windowId: tab.windowId }] },
|
|
699
|
+
})
|
|
700
|
+
const groupId = group.window_groups[tab.windowId]
|
|
701
|
+
const existing = groups.some(value => value.id === groupId)
|
|
702
|
+
const id = await socket.req({
|
|
703
|
+
method: 'chrome.tabs.group',
|
|
704
|
+
params: {
|
|
705
|
+
args: [{
|
|
706
|
+
tabIds: tab.id,
|
|
707
|
+
groupId: existing ? groupId : undefined,
|
|
708
|
+
createProperties: existing ? undefined : { windowId: tab.windowId },
|
|
709
|
+
}],
|
|
710
|
+
},
|
|
711
|
+
})
|
|
712
|
+
group.window_groups[tab.windowId] = id
|
|
713
|
+
await socket.req({
|
|
714
|
+
method: 'chrome.tabGroups.update',
|
|
715
|
+
params: {
|
|
716
|
+
args: [id, {
|
|
717
|
+
title: group.name,
|
|
718
|
+
color: group.color,
|
|
719
|
+
}],
|
|
720
|
+
},
|
|
721
|
+
})
|
|
722
|
+
return id
|
|
723
|
+
})().finally(() => {
|
|
724
|
+
group.connecting = null
|
|
725
|
+
})
|
|
726
|
+
}
|
|
727
|
+
|
|
643
728
|
async _target(browser, target) {
|
|
644
729
|
const socket = browser.socket
|
|
645
730
|
const reverse = browser.reverse || browser.cdp_ext
|
|
731
|
+
if (target.group != null && target.group !== false) {
|
|
732
|
+
const spec = typeof target.group === 'string' ? { name: target.group } : target.group
|
|
733
|
+
browser.groups[spec.name] ??= {
|
|
734
|
+
...spec,
|
|
735
|
+
window_groups: Object.create(null),
|
|
736
|
+
}
|
|
737
|
+
target.group = spec.name
|
|
738
|
+
}
|
|
646
739
|
if (target.incognito && !reverse && target.context == null && target.create_params?.browserContextId == null) {
|
|
647
740
|
target.context = 'incognito'
|
|
648
741
|
}
|
|
@@ -676,6 +769,7 @@ export class cdp extends EventTarget {
|
|
|
676
769
|
Object.assign(info, targetInfo)
|
|
677
770
|
if (!info.incognito) throw new Error('Existing target is not incognito')
|
|
678
771
|
}
|
|
772
|
+
if (target.group != null && target.group !== false) info.groupId = await this._group(browser, target)
|
|
679
773
|
if (!info.sessionId) {
|
|
680
774
|
info.sessionId = (await socket.req({
|
|
681
775
|
method: 'Target.attachToTarget',
|
|
@@ -752,10 +846,14 @@ export class cdp extends EventTarget {
|
|
|
752
846
|
},
|
|
753
847
|
targets: Object.create(null),
|
|
754
848
|
contexts: Object.create(null),
|
|
849
|
+
groups: Object.create(null),
|
|
755
850
|
}
|
|
756
851
|
if (target?.context != null && (record.reverse || record.cdp_ext)) {
|
|
757
852
|
throw new Error('Named browser contexts are unavailable through the reverse Chrome extension')
|
|
758
853
|
}
|
|
854
|
+
if (target?.group != null && target.group !== false && !record.reverse && !record.cdp_ext) {
|
|
855
|
+
throw new Error('Tab groups require the reverse Chrome extension (cdp_ext or reverse)')
|
|
856
|
+
}
|
|
759
857
|
if (!record.connecting && record.socket?.readyState !== WebSocket.OPEN) {
|
|
760
858
|
record.connecting = this._connect(record).catch(error => {
|
|
761
859
|
record.socket?.close()
|
package/cdp_ext/cdp_ext.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mefistofelix/cdp.js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Low-level Chromium DevTools Protocol client for Node.js, with no runtime dependencies.",
|
|
3
|
+
"version": "0.4.0",
|
|
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": [
|