@olimsaidov/icdp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/dist/frame/ax-tree.mjs +2186 -0
- package/dist/frame/index.d.mts +17 -0
- package/dist/frame/index.mjs +782 -0
- package/dist/host/index.d.mts +99 -0
- package/dist/host/index.mjs +328 -0
- package/dist/protocol.d.mts +102 -0
- package/dist/protocol.mjs +16 -0
- package/dist/relay/core.d.mts +54 -0
- package/dist/relay/core.mjs +411 -0
- package/dist/relay/node.d.mts +24 -0
- package/dist/relay/node.mjs +99 -0
- package/package.json +77 -0
- package/src/frame/ax-tree.ts +2393 -0
- package/src/frame/index.ts +1048 -0
- package/src/host/index.ts +422 -0
- package/src/protocol.ts +125 -0
- package/src/relay/core.ts +499 -0
- package/src/relay/node.ts +135 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { CDP_METHOD_NOT_FOUND, CDP_SERVER_ERROR, parseJson } from "../protocol.mjs";
|
|
2
|
+
//#region src/relay/core.ts
|
|
3
|
+
/** Sentinel: the method was handled and a response was already sent. */
|
|
4
|
+
const RESPONDED = Symbol("responded");
|
|
5
|
+
var RelayCore = class {
|
|
6
|
+
product;
|
|
7
|
+
browserWsUrl;
|
|
8
|
+
hostSocket = null;
|
|
9
|
+
clients = /* @__PURE__ */ new Map();
|
|
10
|
+
sessions = /* @__PURE__ */ new Map();
|
|
11
|
+
targets = /* @__PURE__ */ new Map();
|
|
12
|
+
pending = /* @__PURE__ */ new Map();
|
|
13
|
+
nextBridgeId = 1;
|
|
14
|
+
nextSessionId = 1;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.product = options.product ?? "icdp/0.1";
|
|
17
|
+
this.browserWsUrl = options.browserWsUrl ?? "";
|
|
18
|
+
}
|
|
19
|
+
/** A Host bridge connected. New-wins: any previous Host is dropped. */
|
|
20
|
+
hostConnected(socket) {
|
|
21
|
+
if (this.hostSocket && this.hostSocket !== socket) {
|
|
22
|
+
const previous = this.hostSocket;
|
|
23
|
+
this.hostSocket = null;
|
|
24
|
+
this.dropAllTargets("Host replaced by a newer connection");
|
|
25
|
+
try {
|
|
26
|
+
previous.close(1008, "replaced by a newer host");
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
this.hostSocket = socket;
|
|
30
|
+
}
|
|
31
|
+
hostDisconnected(socket) {
|
|
32
|
+
if (this.hostSocket !== socket) return;
|
|
33
|
+
this.hostSocket = null;
|
|
34
|
+
this.dropAllTargets("Host disconnected");
|
|
35
|
+
}
|
|
36
|
+
hostMessage(socket, raw) {
|
|
37
|
+
if (this.hostSocket !== socket) return;
|
|
38
|
+
const message = parseJson(raw);
|
|
39
|
+
if (!message) return;
|
|
40
|
+
switch (message.kind) {
|
|
41
|
+
case "ready":
|
|
42
|
+
this.dropAllTargets("Host re-announced");
|
|
43
|
+
for (const target of message.targets) this.addTarget(target);
|
|
44
|
+
return;
|
|
45
|
+
case "targetCreated":
|
|
46
|
+
this.addTarget(message.target);
|
|
47
|
+
return;
|
|
48
|
+
case "targetDestroyed":
|
|
49
|
+
this.removeTarget(message.targetId, "Target destroyed");
|
|
50
|
+
return;
|
|
51
|
+
case "targetInfoChanged":
|
|
52
|
+
this.targets.set(message.target.targetId, message.target);
|
|
53
|
+
this.broadcastTargetEvent("Target.targetInfoChanged", { targetInfo: this.targetInfo(message.target) });
|
|
54
|
+
return;
|
|
55
|
+
case "response": {
|
|
56
|
+
const id = Number(message.id);
|
|
57
|
+
const call = this.pending.get(id);
|
|
58
|
+
if (!call) return;
|
|
59
|
+
this.pending.delete(id);
|
|
60
|
+
this.sendToClient(call.client, {
|
|
61
|
+
id: call.clientId,
|
|
62
|
+
sessionId: call.sessionId,
|
|
63
|
+
...message.error ? { error: message.error } : { result: message.result ?? {} }
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
case "event":
|
|
68
|
+
for (const session of this.sessions.values()) {
|
|
69
|
+
if (session.targetId !== message.targetId) continue;
|
|
70
|
+
this.sendToClient(session.client, {
|
|
71
|
+
method: message.method,
|
|
72
|
+
params: message.params,
|
|
73
|
+
sessionId: session.sessionId
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
clientConnected(socket) {
|
|
80
|
+
this.clients.set(socket, {
|
|
81
|
+
socket,
|
|
82
|
+
autoAttach: false,
|
|
83
|
+
discoverTargets: false,
|
|
84
|
+
sessions: /* @__PURE__ */ new Set()
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
clientDisconnected(socket) {
|
|
88
|
+
const client = this.clients.get(socket);
|
|
89
|
+
if (!client) return;
|
|
90
|
+
this.clients.delete(socket);
|
|
91
|
+
for (const sessionId of client.sessions) this.endSession(sessionId, { notifyClient: false });
|
|
92
|
+
}
|
|
93
|
+
clientMessage(socket, raw) {
|
|
94
|
+
const client = this.clients.get(socket);
|
|
95
|
+
if (!client) return;
|
|
96
|
+
const message = parseJson(raw);
|
|
97
|
+
if (!message) return;
|
|
98
|
+
if (message.sessionId) {
|
|
99
|
+
this.routeSessionCommand(client, message);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const local = this.browserLevelResult(client, message);
|
|
103
|
+
if (local === RESPONDED) return;
|
|
104
|
+
if (local !== void 0) {
|
|
105
|
+
this.sendToClient(client, {
|
|
106
|
+
id: message.id,
|
|
107
|
+
result: local
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.sendToClient(client, {
|
|
112
|
+
id: message.id,
|
|
113
|
+
error: {
|
|
114
|
+
code: CDP_METHOD_NOT_FOUND,
|
|
115
|
+
message: `Method not available on the browser target: ${message.method ?? "<missing>"}. Attach to a target and send it with a sessionId.`
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
jsonVersion() {
|
|
120
|
+
return {
|
|
121
|
+
Browser: this.product,
|
|
122
|
+
"Protocol-Version": "1.3",
|
|
123
|
+
"User-Agent": this.product,
|
|
124
|
+
"V8-Version": "synthetic",
|
|
125
|
+
"WebKit-Version": "synthetic",
|
|
126
|
+
webSocketDebuggerUrl: this.browserWsUrl
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
jsonList() {
|
|
130
|
+
return Array.from(this.targets.values(), (target) => ({
|
|
131
|
+
description: "icdp iframe target",
|
|
132
|
+
devtoolsFrontendUrl: "",
|
|
133
|
+
id: target.targetId,
|
|
134
|
+
title: target.title,
|
|
135
|
+
type: "page",
|
|
136
|
+
url: target.url,
|
|
137
|
+
webSocketDebuggerUrl: this.browserWsUrl
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
status() {
|
|
141
|
+
return {
|
|
142
|
+
hostConnected: this.hostSocket !== null,
|
|
143
|
+
targets: Array.from(this.targets.values()),
|
|
144
|
+
clients: this.clients.size
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
targetInfo(target) {
|
|
148
|
+
return {
|
|
149
|
+
targetId: target.targetId,
|
|
150
|
+
type: "page",
|
|
151
|
+
title: target.title,
|
|
152
|
+
url: target.url,
|
|
153
|
+
attached: Array.from(this.sessions.values()).some((session) => session.targetId === target.targetId),
|
|
154
|
+
canAccessOpener: false
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
sendToClient(client, message) {
|
|
158
|
+
try {
|
|
159
|
+
client.socket.send(JSON.stringify(message));
|
|
160
|
+
} catch {}
|
|
161
|
+
}
|
|
162
|
+
sendToHost(message) {
|
|
163
|
+
try {
|
|
164
|
+
this.hostSocket?.send(JSON.stringify(message));
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
broadcastTargetEvent(method, params) {
|
|
168
|
+
for (const client of this.clients.values()) {
|
|
169
|
+
if (!client.discoverTargets) continue;
|
|
170
|
+
this.sendToClient(client, {
|
|
171
|
+
method,
|
|
172
|
+
params
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
addTarget(target) {
|
|
177
|
+
this.targets.set(target.targetId, target);
|
|
178
|
+
this.broadcastTargetEvent("Target.targetCreated", { targetInfo: this.targetInfo(target) });
|
|
179
|
+
for (const client of this.clients.values()) if (client.autoAttach) this.startSession(client, target.targetId);
|
|
180
|
+
}
|
|
181
|
+
removeTarget(targetId, reason) {
|
|
182
|
+
if (!this.targets.delete(targetId)) return;
|
|
183
|
+
for (const session of Array.from(this.sessions.values())) if (session.targetId === targetId) this.endSession(session.sessionId, {
|
|
184
|
+
notifyClient: true,
|
|
185
|
+
failReason: reason
|
|
186
|
+
});
|
|
187
|
+
this.broadcastTargetEvent("Target.targetDestroyed", { targetId });
|
|
188
|
+
}
|
|
189
|
+
dropAllTargets(reason) {
|
|
190
|
+
for (const targetId of Array.from(this.targets.keys())) this.removeTarget(targetId, reason);
|
|
191
|
+
}
|
|
192
|
+
startSession(client, targetId) {
|
|
193
|
+
const session = {
|
|
194
|
+
sessionId: `icdp-session-${this.nextSessionId++}`,
|
|
195
|
+
targetId,
|
|
196
|
+
client
|
|
197
|
+
};
|
|
198
|
+
this.sessions.set(session.sessionId, session);
|
|
199
|
+
client.sessions.add(session.sessionId);
|
|
200
|
+
const target = this.targets.get(targetId);
|
|
201
|
+
if (target) this.sendToClient(client, {
|
|
202
|
+
method: "Target.attachedToTarget",
|
|
203
|
+
params: {
|
|
204
|
+
sessionId: session.sessionId,
|
|
205
|
+
targetInfo: this.targetInfo(target),
|
|
206
|
+
waitingForDebugger: false
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
return session;
|
|
210
|
+
}
|
|
211
|
+
endSession(sessionId, options) {
|
|
212
|
+
const session = this.sessions.get(sessionId);
|
|
213
|
+
if (!session) return;
|
|
214
|
+
this.sessions.delete(sessionId);
|
|
215
|
+
session.client.sessions.delete(sessionId);
|
|
216
|
+
for (const [bridgeId, call] of this.pending) {
|
|
217
|
+
if (call.sessionId !== sessionId) continue;
|
|
218
|
+
this.pending.delete(bridgeId);
|
|
219
|
+
if (options.notifyClient) this.sendToClient(session.client, {
|
|
220
|
+
id: call.clientId,
|
|
221
|
+
sessionId,
|
|
222
|
+
error: {
|
|
223
|
+
code: CDP_SERVER_ERROR,
|
|
224
|
+
message: options.failReason ?? "Session detached"
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (options.notifyClient) this.sendToClient(session.client, {
|
|
229
|
+
method: "Target.detachedFromTarget",
|
|
230
|
+
params: {
|
|
231
|
+
sessionId,
|
|
232
|
+
targetId: session.targetId
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
this.sendToHost({
|
|
236
|
+
kind: "detached",
|
|
237
|
+
sessionId,
|
|
238
|
+
targetId: session.targetId
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
routeSessionCommand(client, message) {
|
|
242
|
+
const sessionId = message.sessionId;
|
|
243
|
+
const session = this.sessions.get(sessionId);
|
|
244
|
+
if (!session || session.client !== client) {
|
|
245
|
+
this.sendToClient(client, {
|
|
246
|
+
id: message.id,
|
|
247
|
+
sessionId,
|
|
248
|
+
error: {
|
|
249
|
+
code: CDP_SERVER_ERROR,
|
|
250
|
+
message: `Session not found: ${sessionId}`
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (!message.method) {
|
|
256
|
+
this.sendToClient(client, {
|
|
257
|
+
id: message.id,
|
|
258
|
+
sessionId,
|
|
259
|
+
error: {
|
|
260
|
+
code: CDP_METHOD_NOT_FOUND,
|
|
261
|
+
message: "Method not found: <missing>"
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const local = this.sessionLevelResult(session, message);
|
|
267
|
+
if (local !== void 0) {
|
|
268
|
+
this.sendToClient(client, {
|
|
269
|
+
id: message.id,
|
|
270
|
+
sessionId,
|
|
271
|
+
result: local
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!this.hostSocket) {
|
|
276
|
+
this.sendToClient(client, {
|
|
277
|
+
id: message.id,
|
|
278
|
+
sessionId,
|
|
279
|
+
error: {
|
|
280
|
+
code: CDP_SERVER_ERROR,
|
|
281
|
+
message: "Host is not connected"
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const bridgeId = this.nextBridgeId++;
|
|
287
|
+
this.pending.set(bridgeId, {
|
|
288
|
+
client,
|
|
289
|
+
clientId: message.id,
|
|
290
|
+
sessionId
|
|
291
|
+
});
|
|
292
|
+
this.sendToHost({
|
|
293
|
+
kind: "command",
|
|
294
|
+
sessionId,
|
|
295
|
+
targetId: session.targetId,
|
|
296
|
+
id: bridgeId,
|
|
297
|
+
method: message.method,
|
|
298
|
+
params: message.params ?? {}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/** Session-scoped methods the Relay answers itself; undefined = forward to the frame. */
|
|
302
|
+
sessionLevelResult(session, message) {
|
|
303
|
+
switch (message.method) {
|
|
304
|
+
case "Browser.getVersion": return {
|
|
305
|
+
protocolVersion: "1.3",
|
|
306
|
+
product: this.product,
|
|
307
|
+
revision: `icdp-v1`,
|
|
308
|
+
userAgent: this.product,
|
|
309
|
+
jsVersion: "synthetic"
|
|
310
|
+
};
|
|
311
|
+
case "Schema.getDomains": return { domains: [] };
|
|
312
|
+
case "Target.setAutoAttach":
|
|
313
|
+
case "Target.setDiscoverTargets":
|
|
314
|
+
case "Target.setRemoteLocations":
|
|
315
|
+
case "Target.activateTarget": return {};
|
|
316
|
+
case "Target.getTargetInfo": {
|
|
317
|
+
const target = this.targets.get(session.targetId);
|
|
318
|
+
if (!target) return { targetInfo: {
|
|
319
|
+
targetId: session.targetId,
|
|
320
|
+
type: "page",
|
|
321
|
+
title: "",
|
|
322
|
+
url: "",
|
|
323
|
+
attached: true
|
|
324
|
+
} };
|
|
325
|
+
return { targetInfo: this.targetInfo(target) };
|
|
326
|
+
}
|
|
327
|
+
default: return;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
/** Browser-level methods the Relay answers itself; undefined = not local. */
|
|
331
|
+
browserLevelResult(client, message) {
|
|
332
|
+
switch (message.method) {
|
|
333
|
+
case "Browser.getVersion": return {
|
|
334
|
+
protocolVersion: "1.3",
|
|
335
|
+
product: this.product,
|
|
336
|
+
revision: `icdp-v1`,
|
|
337
|
+
userAgent: this.product,
|
|
338
|
+
jsVersion: "synthetic"
|
|
339
|
+
};
|
|
340
|
+
case "Browser.close":
|
|
341
|
+
case "Browser.setDownloadBehavior":
|
|
342
|
+
case "Browser.setWindowBounds":
|
|
343
|
+
case "Security.setIgnoreCertificateErrors":
|
|
344
|
+
case "Target.setRemoteLocations":
|
|
345
|
+
case "Target.activateTarget":
|
|
346
|
+
case "Target.closeTarget": return {};
|
|
347
|
+
case "Schema.getDomains": return { domains: [] };
|
|
348
|
+
case "Target.getTargets": return { targetInfos: Array.from(this.targets.values(), (target) => this.targetInfo(target)) };
|
|
349
|
+
case "Target.getTargetInfo": {
|
|
350
|
+
const targetId = String(message.params?.targetId ?? "");
|
|
351
|
+
const target = this.targets.get(targetId);
|
|
352
|
+
if (!target) return { targetInfo: {
|
|
353
|
+
targetId,
|
|
354
|
+
type: "page",
|
|
355
|
+
title: "",
|
|
356
|
+
url: "",
|
|
357
|
+
attached: false
|
|
358
|
+
} };
|
|
359
|
+
return { targetInfo: this.targetInfo(target) };
|
|
360
|
+
}
|
|
361
|
+
case "Target.setDiscoverTargets": {
|
|
362
|
+
const discover = Boolean(message.params?.discover);
|
|
363
|
+
client.discoverTargets = discover;
|
|
364
|
+
if (discover) for (const target of this.targets.values()) this.sendToClient(client, {
|
|
365
|
+
method: "Target.targetCreated",
|
|
366
|
+
params: { targetInfo: this.targetInfo(target) }
|
|
367
|
+
});
|
|
368
|
+
return {};
|
|
369
|
+
}
|
|
370
|
+
case "Target.setAutoAttach": {
|
|
371
|
+
const autoAttach = Boolean(message.params?.autoAttach);
|
|
372
|
+
client.autoAttach = autoAttach;
|
|
373
|
+
if (autoAttach) {
|
|
374
|
+
for (const target of this.targets.values()) if (!Array.from(client.sessions).some((sessionId) => this.sessions.get(sessionId)?.targetId === target.targetId)) this.startSession(client, target.targetId);
|
|
375
|
+
}
|
|
376
|
+
return {};
|
|
377
|
+
}
|
|
378
|
+
case "Target.attachToTarget": {
|
|
379
|
+
const targetId = String(message.params?.targetId ?? "");
|
|
380
|
+
if (!this.targets.has(targetId)) {
|
|
381
|
+
this.sendToClient(client, {
|
|
382
|
+
id: message.id,
|
|
383
|
+
error: {
|
|
384
|
+
code: CDP_SERVER_ERROR,
|
|
385
|
+
message: `No target with given id found: ${targetId}`
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
return RESPONDED;
|
|
389
|
+
}
|
|
390
|
+
return { sessionId: this.startSession(client, targetId).sessionId };
|
|
391
|
+
}
|
|
392
|
+
case "Target.detachFromTarget": {
|
|
393
|
+
const sessionId = String(message.params?.sessionId ?? "");
|
|
394
|
+
this.endSession(sessionId, { notifyClient: false });
|
|
395
|
+
return {};
|
|
396
|
+
}
|
|
397
|
+
case "Target.createTarget":
|
|
398
|
+
this.sendToClient(client, {
|
|
399
|
+
id: message.id,
|
|
400
|
+
error: {
|
|
401
|
+
code: CDP_SERVER_ERROR,
|
|
402
|
+
message: "Target.createTarget is not supported: icdp targets are iframes paired by the Host."
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
return RESPONDED;
|
|
406
|
+
default: return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
//#endregion
|
|
411
|
+
export { RelayCore };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { RelayCore } from "./core.mjs";
|
|
2
|
+
import { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
3
|
+
|
|
4
|
+
//#region src/relay/node.d.ts
|
|
5
|
+
type ServeRelayOptions = {
|
|
6
|
+
port?: number;
|
|
7
|
+
hostname?: string;
|
|
8
|
+
product?: string; /** Path Clients connect to. Advertised by /json/version. */
|
|
9
|
+
browserPath?: string; /** Path the Host bridge connects to. */
|
|
10
|
+
hostPath?: string; /** Handles requests the relay doesn't own (anything but its WS + /json + /icdp paths). */
|
|
11
|
+
fallback?: (request: IncomingMessage, response: ServerResponse) => void;
|
|
12
|
+
};
|
|
13
|
+
type RelayServer = {
|
|
14
|
+
core: RelayCore;
|
|
15
|
+
server: Server;
|
|
16
|
+
port: number;
|
|
17
|
+
browserWsUrl: string;
|
|
18
|
+
hostWsUrl: string;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
/** Serve a Relay on Node. One Host, many Clients, flat-session protocol only. */
|
|
22
|
+
declare function serveRelay(options?: ServeRelayOptions): Promise<RelayServer>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { RelayServer, ServeRelayOptions, serveRelay };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { RelayCore } from "./core.mjs";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { WebSocketServer } from "ws";
|
|
4
|
+
//#region src/relay/node.ts
|
|
5
|
+
function asSocketLike(ws) {
|
|
6
|
+
return {
|
|
7
|
+
send: (data) => {
|
|
8
|
+
try {
|
|
9
|
+
ws.send(data);
|
|
10
|
+
} catch {}
|
|
11
|
+
},
|
|
12
|
+
close: (code, reason) => {
|
|
13
|
+
try {
|
|
14
|
+
ws.close(code, reason);
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function sendJson(response, payload) {
|
|
20
|
+
response.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
21
|
+
response.end(JSON.stringify(payload));
|
|
22
|
+
}
|
|
23
|
+
/** Serve a Relay on Node. One Host, many Clients, flat-session protocol only. */
|
|
24
|
+
async function serveRelay(options = {}) {
|
|
25
|
+
const hostname = options.hostname ?? "127.0.0.1";
|
|
26
|
+
const browserPath = options.browserPath ?? "/devtools/browser";
|
|
27
|
+
const hostPath = options.hostPath ?? "/icdp/host";
|
|
28
|
+
const server = createServer((request, response) => {
|
|
29
|
+
const url = new URL(request.url ?? "/", `http://${hostname}`);
|
|
30
|
+
if (process.env.ICDP_DEBUG === "1") console.log(`[icdp:http] ${request.method} ${url.pathname}`);
|
|
31
|
+
if (url.pathname === "/json/version") return sendJson(response, core.jsonVersion());
|
|
32
|
+
if (url.pathname === "/json" || url.pathname === "/json/list") return sendJson(response, core.jsonList());
|
|
33
|
+
if (url.pathname === "/icdp/status") return sendJson(response, core.status());
|
|
34
|
+
if (options.fallback) return options.fallback(request, response);
|
|
35
|
+
response.writeHead(404);
|
|
36
|
+
response.end("not found");
|
|
37
|
+
});
|
|
38
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
39
|
+
const socketLikes = /* @__PURE__ */ new WeakMap();
|
|
40
|
+
const wrap = (ws) => {
|
|
41
|
+
let like = socketLikes.get(ws);
|
|
42
|
+
if (!like) {
|
|
43
|
+
like = asSocketLike(ws);
|
|
44
|
+
socketLikes.set(ws, like);
|
|
45
|
+
}
|
|
46
|
+
return like;
|
|
47
|
+
};
|
|
48
|
+
server.on("upgrade", (request, socket, head) => {
|
|
49
|
+
const url = new URL(request.url ?? "/", `http://${hostname}`);
|
|
50
|
+
const kind = url.pathname === browserPath ? "client" : url.pathname === hostPath ? "host" : null;
|
|
51
|
+
if (process.env.ICDP_DEBUG === "1") console.log(`[icdp:http] UPGRADE ${url.pathname} -> ${kind ?? "reject"}`);
|
|
52
|
+
if (!kind) {
|
|
53
|
+
socket.destroy();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
57
|
+
if (kind === "host") core.hostConnected(wrap(ws));
|
|
58
|
+
else core.clientConnected(wrap(ws));
|
|
59
|
+
ws.on("message", (data) => {
|
|
60
|
+
const raw = data.toString();
|
|
61
|
+
if (process.env.ICDP_DEBUG === "1") console.log(`[icdp:${kind}]`, raw.slice(0, 400));
|
|
62
|
+
if (kind === "host") core.hostMessage(wrap(ws), raw);
|
|
63
|
+
else core.clientMessage(wrap(ws), raw);
|
|
64
|
+
});
|
|
65
|
+
ws.on("close", () => {
|
|
66
|
+
if (kind === "host") core.hostDisconnected(wrap(ws));
|
|
67
|
+
else core.clientDisconnected(wrap(ws));
|
|
68
|
+
});
|
|
69
|
+
ws.on("error", () => ws.close());
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
await new Promise((resolve, reject) => {
|
|
73
|
+
server.once("error", reject);
|
|
74
|
+
server.listen(options.port ?? 0, hostname, resolve);
|
|
75
|
+
});
|
|
76
|
+
const address = server.address();
|
|
77
|
+
if (address === null || typeof address === "string") throw new Error("relay server has no TCP address");
|
|
78
|
+
const port = address.port;
|
|
79
|
+
const browserWsUrl = `ws://${hostname}:${port}${browserPath}`;
|
|
80
|
+
const core = new RelayCore({
|
|
81
|
+
product: options.product,
|
|
82
|
+
browserWsUrl
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
core,
|
|
86
|
+
server,
|
|
87
|
+
port,
|
|
88
|
+
browserWsUrl,
|
|
89
|
+
hostWsUrl: `ws://${hostname}:${port}${hostPath}`,
|
|
90
|
+
stop: () => new Promise((resolve) => {
|
|
91
|
+
for (const ws of wss.clients) ws.terminate();
|
|
92
|
+
wss.close();
|
|
93
|
+
server.closeAllConnections();
|
|
94
|
+
server.close(() => resolve());
|
|
95
|
+
})
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
export { serveRelay };
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@olimsaidov/icdp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Chrome DevTools Protocol over an iframe boundary: drive and inspect embedded (including cross-origin) apps with CDP tools, without a real browser debugging session.",
|
|
5
|
+
"homepage": "https://github.com/olimsaidov/icdp#readme",
|
|
6
|
+
"bugs": "https://github.com/olimsaidov/icdp/issues",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Olim Saidov",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/olimsaidov/icdp.git"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"exports": {
|
|
20
|
+
"./frame": {
|
|
21
|
+
"types": "./dist/frame/index.d.mts",
|
|
22
|
+
"default": "./dist/frame/index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./host": {
|
|
25
|
+
"types": "./dist/host/index.d.mts",
|
|
26
|
+
"default": "./dist/host/index.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./relay": {
|
|
29
|
+
"types": "./dist/relay/core.d.mts",
|
|
30
|
+
"default": "./dist/relay/core.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./relay/node": {
|
|
33
|
+
"types": "./dist/relay/node.d.mts",
|
|
34
|
+
"default": "./dist/relay/node.mjs"
|
|
35
|
+
},
|
|
36
|
+
"./protocol": {
|
|
37
|
+
"types": "./dist/protocol.d.mts",
|
|
38
|
+
"default": "./dist/protocol.mjs"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"registry": "https://registry.npmjs.org/"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsdown",
|
|
47
|
+
"check": "tsc --noEmit && oxlint && oxfmt --check .",
|
|
48
|
+
"fmt": "oxfmt .",
|
|
49
|
+
"gen:conformance": "node scripts/gen-conformance.ts",
|
|
50
|
+
"playground": "node playground/server.ts",
|
|
51
|
+
"test": "vitest run --exclude '**/*.e2e.test.ts'",
|
|
52
|
+
"test:e2e": "vitest run tests/agent-browser.e2e.test.ts",
|
|
53
|
+
"test:all": "vitest run"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"aria-query": "^5.3.2",
|
|
57
|
+
"chobitsu": "1.8.6",
|
|
58
|
+
"devtools-protocol": "^0.0.1624250",
|
|
59
|
+
"dom-accessibility-api": "^0.7.1",
|
|
60
|
+
"ws": "^8.21.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/aria-query": "^5.0.4",
|
|
64
|
+
"@types/node": "^24",
|
|
65
|
+
"@types/ws": "^8",
|
|
66
|
+
"jsdom": "^29.1.1",
|
|
67
|
+
"oxfmt": "^0.54.0",
|
|
68
|
+
"oxlint": "^1.69.0",
|
|
69
|
+
"rolldown": "^1.1.1",
|
|
70
|
+
"tsdown": "^0.22.2",
|
|
71
|
+
"typescript": "^5",
|
|
72
|
+
"vitest": "^4.1.8"
|
|
73
|
+
},
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=22"
|
|
76
|
+
}
|
|
77
|
+
}
|