@mefistofelix/cdp.js 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,263 @@
1
+ const config = chrome.runtime.getManifest().cdp ?? {}
2
+ const sessions = {}
3
+ const attached = {}
4
+ const attaching = {}
5
+ let socket
6
+ let disconnecting
7
+ let discovering = false
8
+ let auto_attach = {}
9
+
10
+ function send(message) {
11
+ if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message))
12
+ }
13
+
14
+ function target_info(tab) {
15
+ return {
16
+ targetId: String(tab.id),
17
+ type: 'page',
18
+ title: tab.title ?? '',
19
+ url: tab.url ?? tab.pendingUrl ?? '',
20
+ attached: !!attached[tab.id],
21
+ incognito: tab.incognito,
22
+ }
23
+ }
24
+
25
+ function detached(source) {
26
+ if (!attached[source.tabId]) return
27
+ delete attached[source.tabId]
28
+ for (const [id, session] of Object.entries(sessions)) {
29
+ if (session.tabId === source.tabId) delete sessions[id]
30
+ }
31
+ send({
32
+ method: 'Target.detachedFromTarget',
33
+ params: {
34
+ sessionId: String(source.tabId),
35
+ targetId: String(source.tabId),
36
+ },
37
+ })
38
+ }
39
+
40
+ async function attach_tab(tabId) {
41
+ const source = { tabId }
42
+ await chrome.debugger.attach(source, '1.3')
43
+ if (socket.readyState !== WebSocket.OPEN) {
44
+ await chrome.debugger.detach(source)
45
+ throw new Error('Reverse connection closed during attachment')
46
+ }
47
+ const sessionId = String(tabId)
48
+ sessions[sessionId] = source
49
+ attached[tabId] = true
50
+ await chrome.debugger.sendCommand(source, 'Target.setAutoAttach', auto_attach)
51
+ send({
52
+ method: 'Target.attachedToTarget',
53
+ params: {
54
+ sessionId,
55
+ targetInfo: target_info(await chrome.tabs.get(tabId)),
56
+ waitingForDebugger: false,
57
+ },
58
+ })
59
+ return { sessionId }
60
+ }
61
+
62
+ const methods = {
63
+ 'Browser.getVersion'() {
64
+ return {
65
+ protocolVersion: '1.3',
66
+ product: navigator.userAgent.match(/Chrome\/[^ ]+/)?.[0] ?? 'Chrome',
67
+ userAgent: navigator.userAgent,
68
+ revision: '',
69
+ jsVersion: '',
70
+ }
71
+ },
72
+ async 'Target.getTargets'() {
73
+ return { targetInfos: (await chrome.tabs.query({})).map(target_info) }
74
+ },
75
+ async 'Target.getTargetInfo'(params) {
76
+ return { targetInfo: target_info(await chrome.tabs.get(Number(params.targetId))) }
77
+ },
78
+ async 'Target.setDiscoverTargets'(params) {
79
+ discovering = params.discover
80
+ if (discovering) {
81
+ for (const tab of await chrome.tabs.query({})) {
82
+ send({
83
+ method: 'Target.targetCreated',
84
+ params: { targetInfo: target_info(tab) },
85
+ })
86
+ }
87
+ }
88
+ },
89
+ 'Target.setAutoAttach'(params) {
90
+ if (params.waitForDebuggerOnStart) throw new Error('Pausing new browser tabs is unavailable through chrome.debugger')
91
+ auto_attach = params
92
+ },
93
+ async 'Target.createTarget'(params) {
94
+ if (Object.keys(params).some(key => !['url', 'background', 'incognito'].includes(key))) {
95
+ throw new Error('Reverse Target.createTarget supports only url, background and incognito')
96
+ }
97
+ const options = {
98
+ url: params.url ?? 'about:blank',
99
+ active: params.background !== true,
100
+ }
101
+ let tab
102
+ if (params.incognito) {
103
+ if (!await chrome.extension.isAllowedIncognitoAccess()) {
104
+ throw new Error('Enable Allow in incognito for cdp_ext in chrome://extensions')
105
+ }
106
+ const window = (await chrome.windows.getAll()).find(window => window.incognito)
107
+ if (window) tab = await chrome.tabs.create({
108
+ ...options,
109
+ windowId: window.id,
110
+ })
111
+ else tab = (await chrome.windows.create({
112
+ incognito: true,
113
+ url: options.url,
114
+ focused: options.active,
115
+ })).tabs[0]
116
+ } else tab = await chrome.tabs.create(options)
117
+ return { targetId: String(tab.id) }
118
+ },
119
+ async 'Target.attachToTarget'(params) {
120
+ const tabId = Number(params.targetId)
121
+ if (attaching[tabId]) return attaching[tabId]
122
+ if (attached[tabId]) return { sessionId: String(tabId) }
123
+ return attaching[tabId] = attach_tab(tabId).finally(() => delete attaching[tabId])
124
+ },
125
+ async 'Target.detachFromTarget'(params) {
126
+ const source = sessions[params.sessionId]
127
+ if (!source) throw new Error('Unknown debugger session')
128
+ if (source.sessionId) {
129
+ await chrome.debugger.sendCommand({ tabId: source.tabId }, 'Target.detachFromTarget', { sessionId: source.sessionId })
130
+ } else {
131
+ await chrome.debugger.detach(source)
132
+ detached(source)
133
+ }
134
+ },
135
+ async 'Target.closeTarget'(params) {
136
+ await chrome.tabs.remove(Number(params.targetId))
137
+ return { success: true }
138
+ },
139
+ async 'Target.activateTarget'(params) {
140
+ const tab = await chrome.tabs.update(Number(params.targetId), { active: true })
141
+ await chrome.windows.update(tab.windowId, { focused: true })
142
+ },
143
+ }
144
+
145
+ chrome.debugger.onEvent.addListener((source, method, params) => {
146
+ if (!attached[source.tabId]) return
147
+ if (method === 'Target.attachedToTarget' || method === 'Target.detachedFromTarget') {
148
+ if (method === 'Target.attachedToTarget') sessions[params.sessionId] = {
149
+ tabId: source.tabId,
150
+ sessionId: params.sessionId,
151
+ }
152
+ else delete sessions[params.sessionId]
153
+ }
154
+ send({
155
+ method,
156
+ params,
157
+ sessionId: source.sessionId ?? String(source.tabId),
158
+ })
159
+ })
160
+ chrome.debugger.onDetach.addListener(detached)
161
+ chrome.tabs.onCreated.addListener(tab => {
162
+ if (discovering) send({
163
+ method: 'Target.targetCreated',
164
+ params: { targetInfo: target_info(tab) },
165
+ })
166
+ })
167
+ chrome.tabs.onUpdated.addListener((id, changes, tab) => {
168
+ if (discovering) send({
169
+ method: 'Target.targetInfoChanged',
170
+ params: { targetInfo: target_info(tab) },
171
+ })
172
+ })
173
+ chrome.tabs.onRemoved.addListener(id => {
174
+ if (discovering) send({
175
+ method: 'Target.targetDestroyed',
176
+ params: { targetId: String(id) },
177
+ })
178
+ })
179
+
180
+ for (const [name, options] of Object.entries({
181
+ onBeforeRequest: [],
182
+ onBeforeSendHeaders: ['requestHeaders'],
183
+ onSendHeaders: ['requestHeaders'],
184
+ onHeadersReceived: ['responseHeaders'],
185
+ onAuthRequired: ['responseHeaders'],
186
+ onResponseStarted: ['responseHeaders'],
187
+ onBeforeRedirect: ['responseHeaders'],
188
+ onCompleted: ['responseHeaders'],
189
+ onErrorOccurred: [],
190
+ })) {
191
+ chrome.webRequest[name].addListener(params => send({
192
+ method: 'chrome.webRequest.' + name,
193
+ params,
194
+ sessionId: params.tabId >= 0 ? String(params.tabId) : undefined,
195
+ }), { urls: ['<all_urls>'] }, options)
196
+ }
197
+
198
+ function connect() {
199
+ if (disconnecting) return
200
+ if (socket?.readyState === WebSocket.CONNECTING) return
201
+ if (socket?.readyState === WebSocket.OPEN) return socket.send('')
202
+ const connection = socket = new WebSocket(config.websocket_url ?? 'ws://127.0.0.1:9223')
203
+ connection.onopen = () => connection.send(JSON.stringify({ browser: chrome.runtime.id }))
204
+ connection.onmessage = async event => {
205
+ if (!event.data) return
206
+ const request = JSON.parse(event.data)
207
+ const { method, params = {}, sessionId, id } = request
208
+ let response
209
+ try {
210
+ let result
211
+ if (method.startsWith('chrome.')) {
212
+ const names = method.split('.').slice(1)
213
+ const name = names.pop()
214
+ const object = names.reduce((object, name) => object[name], chrome)
215
+ const args = params.args ?? []
216
+ if (method === 'chrome.scripting.executeScript' && typeof args[0]?.func === 'string') {
217
+ const injection = args[0]
218
+ injection.args = [injection.func, ...(injection.args ?? [])]
219
+ injection.func = (source, ...args) => (0, eval)('(' + source + ')')(...args)
220
+ }
221
+ result = params.callback
222
+ ? await new Promise((resolve, reject) => object[name](...args, value => {
223
+ const error = chrome.runtime.lastError
224
+ if (error) reject(new Error(error.message))
225
+ else resolve(value)
226
+ }))
227
+ : await object[name](...args)
228
+ } else {
229
+ if (sessionId == null && !methods[method]) throw new Error('Unsupported reverse browser command: ' + method)
230
+ result = sessionId != null
231
+ ? await chrome.debugger.sendCommand(sessions[sessionId], method, params)
232
+ : await methods[method](params)
233
+ }
234
+ response = {
235
+ id,
236
+ result: result === undefined ? {} : result,
237
+ }
238
+ } catch (error) {
239
+ response = {
240
+ id,
241
+ error: {
242
+ code: -32000,
243
+ message: error.message,
244
+ },
245
+ }
246
+ }
247
+ if (id != null && connection.readyState === WebSocket.OPEN) connection.send(JSON.stringify(response))
248
+ }
249
+ connection.onclose = () => {
250
+ discovering = false
251
+ disconnecting = Promise.all(Object.keys(attached).map(id => {
252
+ const source = { tabId: Number(id) }
253
+ detached(source)
254
+ return chrome.debugger.detach(source).catch(() => {})
255
+ })).finally(() => disconnecting = null)
256
+ }
257
+ }
258
+
259
+ chrome.runtime.onStartup.addListener(connect)
260
+ chrome.alarms.onAlarm.addListener(connect)
261
+ chrome.alarms.create('cdp-reconnect', { periodInMinutes: 0.5 })
262
+ setInterval(connect, config.reconnect_ms ?? 5000)
263
+ connect()
@@ -0,0 +1,76 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "CDP reverse connection",
4
+ "version": "0.2.0",
5
+ "minimum_chrome_version": "125",
6
+ "permissions": [
7
+ "accessibilityFeatures.read",
8
+ "accessibilityFeatures.modify",
9
+ "alarms",
10
+ "bookmarks",
11
+ "browsingData",
12
+ "clipboardRead",
13
+ "clipboardWrite",
14
+ "contentSettings",
15
+ "contextMenus",
16
+ "cookies",
17
+ "debugger",
18
+ "declarativeContent",
19
+ "declarativeNetRequest",
20
+ "declarativeNetRequestFeedback",
21
+ "desktopCapture",
22
+ "downloads",
23
+ "downloads.open",
24
+ "downloads.ui",
25
+ "favicon",
26
+ "fontSettings",
27
+ "gcm",
28
+ "geolocation",
29
+ "history",
30
+ "identity",
31
+ "identity.email",
32
+ "idle",
33
+ "management",
34
+ "nativeMessaging",
35
+ "notifications",
36
+ "offscreen",
37
+ "pageCapture",
38
+ "power",
39
+ "privacy",
40
+ "proxy",
41
+ "readingList",
42
+ "scripting",
43
+ "search",
44
+ "sessions",
45
+ "sidePanel",
46
+ "storage",
47
+ "system.cpu",
48
+ "system.display",
49
+ "system.memory",
50
+ "system.storage",
51
+ "tabCapture",
52
+ "tabGroups",
53
+ "tabs",
54
+ "topSites",
55
+ "tts",
56
+ "ttsEngine",
57
+ "unlimitedStorage",
58
+ "userScripts",
59
+ "webAuthenticationProxy",
60
+ "webNavigation",
61
+ "webRequest",
62
+ "webRequestAuthProvider"
63
+ ],
64
+ "host_permissions": [
65
+ "<all_urls>"
66
+ ],
67
+ "action": {},
68
+ "incognito": "spanning",
69
+ "background": {
70
+ "service_worker": "cdp_ext.js"
71
+ },
72
+ "cdp": {
73
+ "websocket_url": "ws://127.0.0.1:9223",
74
+ "reconnect_ms": 5000
75
+ }
76
+ }
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@mefistofelix/cdp.js",
3
- "version": "0.1.2",
4
- "description": "Low-level Chromium DevTools Protocol client for Node.js, with no runtime dependencies.",
3
+ "version": "0.3.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": [
8
- "cdp.js"
8
+ "cdp.js",
9
+ "cdp_ext/manifest.json",
10
+ "cdp_ext/cdp_ext.js"
9
11
  ],
10
12
  "engines": {
11
13
  "node": ">=22.19.0"