@rynx-ai/browser-cdp 0.1.11-beta.37 → 0.1.11-beta.39
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/dist/client.d.ts +10 -2
- package/dist/client.js +257 -57
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -40,7 +40,8 @@ export interface BrowserAutomationSnapshotNode {
|
|
|
40
40
|
ignored: boolean;
|
|
41
41
|
/**
|
|
42
42
|
* Opaque, process-independent reference valid only for this endpoint's Page
|
|
43
|
-
*
|
|
43
|
+
* tree (including OOPIF targets) and current document. A navigation or Browser
|
|
44
|
+
* restart invalidates it.
|
|
44
45
|
*/
|
|
45
46
|
ref?: string;
|
|
46
47
|
properties: Readonly<Record<string, string | number | boolean>>;
|
|
@@ -87,13 +88,15 @@ export declare class BrowserAutomationStaleReferenceError extends Error {
|
|
|
87
88
|
*
|
|
88
89
|
* It does not infer selectors or wait for application-specific readiness.
|
|
89
90
|
* Snapshot refs can cross short-lived CLI processes, but are bound to the
|
|
90
|
-
* endpoint, Page target, and current
|
|
91
|
+
* endpoint, owning Page/OOPIF target, and that target's current document.
|
|
91
92
|
*/
|
|
92
93
|
export declare class BrowserAutomationClient {
|
|
93
94
|
private readonly connection;
|
|
94
95
|
private readonly pageSessionId;
|
|
96
|
+
private readonly pageTargetId;
|
|
95
97
|
private readonly referenceScope;
|
|
96
98
|
private readonly referenceKey;
|
|
99
|
+
private readonly iframeSessions;
|
|
97
100
|
private documentGenerationValue;
|
|
98
101
|
private accessibilityEnabled;
|
|
99
102
|
private domEnabled;
|
|
@@ -108,12 +111,17 @@ export declare class BrowserAutomationClient {
|
|
|
108
111
|
screenshot(options?: BrowserAutomationScreenshotOptions): Promise<BrowserAutomationScreenshot>;
|
|
109
112
|
close(): void;
|
|
110
113
|
private pageCall;
|
|
114
|
+
private targetCall;
|
|
115
|
+
private initializeIframeTracking;
|
|
116
|
+
private acceptAttachedIframe;
|
|
111
117
|
private ensureDomEnabled;
|
|
112
118
|
private snapshotNode;
|
|
113
119
|
private resolveElement;
|
|
114
120
|
private resolveObjectId;
|
|
115
121
|
private currentDocumentIdentity;
|
|
116
122
|
private elementCenter;
|
|
123
|
+
private targetForReference;
|
|
124
|
+
private pageCoordinates;
|
|
117
125
|
private invalidateDocument;
|
|
118
126
|
private assertOpen;
|
|
119
127
|
}
|
package/dist/client.js
CHANGED
|
@@ -7,13 +7,23 @@ const MAX_SNAPSHOT_NODES = 10_000;
|
|
|
7
7
|
const MAX_SELECTOR_CHARS = 4_096;
|
|
8
8
|
const MAX_INPUT_TEXT_BYTES = 64 * 1024;
|
|
9
9
|
const WEB_SOCKET_OPEN = 1;
|
|
10
|
-
const REFERENCE_VERSION =
|
|
10
|
+
const REFERENCE_VERSION = 2;
|
|
11
11
|
const REFERENCE_DOCUMENT_FINGERPRINT_BYTES = 12;
|
|
12
|
+
const REFERENCE_TARGET_FINGERPRINT_BYTES = 12;
|
|
12
13
|
const REFERENCE_BACKEND_NODE_BYTES = 8;
|
|
13
14
|
const REFERENCE_TAG_BYTES = 16;
|
|
14
|
-
const REFERENCE_PAYLOAD_BYTES = 1 +
|
|
15
|
+
const REFERENCE_PAYLOAD_BYTES = 1 +
|
|
16
|
+
REFERENCE_DOCUMENT_FINGERPRINT_BYTES +
|
|
17
|
+
REFERENCE_TARGET_FINGERPRINT_BYTES +
|
|
18
|
+
REFERENCE_BACKEND_NODE_BYTES;
|
|
15
19
|
const REFERENCE_BYTES = REFERENCE_PAYLOAD_BYTES + REFERENCE_TAG_BYTES;
|
|
16
|
-
const REFERENCE_DOMAIN = Buffer.from("rynx-browser-cdp-ref-
|
|
20
|
+
const REFERENCE_DOMAIN = Buffer.from("rynx-browser-cdp-ref-v2\0", "utf8");
|
|
21
|
+
const IFRAME_AUTO_ATTACH_PARAMS = {
|
|
22
|
+
autoAttach: true,
|
|
23
|
+
waitForDebuggerOnStart: false,
|
|
24
|
+
flatten: true,
|
|
25
|
+
filter: [{ type: "iframe" }, { exclude: true }],
|
|
26
|
+
};
|
|
17
27
|
export class BrowserAutomationError extends Error {
|
|
18
28
|
code;
|
|
19
29
|
name = "BrowserAutomationError";
|
|
@@ -35,24 +45,50 @@ export class BrowserAutomationStaleReferenceError extends Error {
|
|
|
35
45
|
*
|
|
36
46
|
* It does not infer selectors or wait for application-specific readiness.
|
|
37
47
|
* Snapshot refs can cross short-lived CLI processes, but are bound to the
|
|
38
|
-
* endpoint, Page target, and current
|
|
48
|
+
* endpoint, owning Page/OOPIF target, and that target's current document.
|
|
39
49
|
*/
|
|
40
50
|
export class BrowserAutomationClient {
|
|
41
51
|
connection;
|
|
42
52
|
pageSessionId;
|
|
53
|
+
pageTargetId;
|
|
43
54
|
referenceScope;
|
|
44
55
|
referenceKey;
|
|
56
|
+
iframeSessions = new Map();
|
|
45
57
|
documentGenerationValue = 1;
|
|
46
58
|
accessibilityEnabled = false;
|
|
47
59
|
domEnabled = false;
|
|
48
60
|
closed = false;
|
|
49
|
-
constructor(connection, referenceScope, referenceKey, pageSessionId) {
|
|
61
|
+
constructor(connection, referenceScope, referenceKey, pageTargetId, pageSessionId) {
|
|
50
62
|
this.connection = connection;
|
|
51
63
|
this.referenceScope = referenceScope;
|
|
52
64
|
this.referenceKey = referenceKey;
|
|
65
|
+
this.pageTargetId = pageTargetId;
|
|
53
66
|
this.pageSessionId = pageSessionId;
|
|
54
67
|
this.connection.onEvent((method, params, sessionId) => {
|
|
55
|
-
if (
|
|
68
|
+
if (method === "Target.attachedToTarget") {
|
|
69
|
+
this.acceptAttachedIframe(params);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (method === "Target.detachedFromTarget") {
|
|
73
|
+
const detachedSessionId = stringAt(params, "sessionId");
|
|
74
|
+
if (detachedSessionId) {
|
|
75
|
+
for (const [targetId, iframe] of this.iframeSessions) {
|
|
76
|
+
if (iframe.sessionId !== detachedSessionId)
|
|
77
|
+
continue;
|
|
78
|
+
this.iframeSessions.delete(targetId);
|
|
79
|
+
this.invalidateDocument();
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (detachedSessionId === this.pageSessionId)
|
|
84
|
+
this.invalidateDocument();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const knownIframeSession = sessionId && [...this.iframeSessions.values()].some((iframe) => iframe.sessionId === sessionId);
|
|
88
|
+
if (this.pageSessionId &&
|
|
89
|
+
sessionId &&
|
|
90
|
+
sessionId !== this.pageSessionId &&
|
|
91
|
+
!knownIframeSession)
|
|
56
92
|
return;
|
|
57
93
|
if (method === "DOM.documentUpdated") {
|
|
58
94
|
this.invalidateDocument();
|
|
@@ -64,9 +100,7 @@ export class BrowserAutomationClient {
|
|
|
64
100
|
this.invalidateDocument();
|
|
65
101
|
return;
|
|
66
102
|
}
|
|
67
|
-
if (method === "Inspector.detached"
|
|
68
|
-
(method === "Target.detachedFromTarget" &&
|
|
69
|
-
stringAt(params, "sessionId") === this.pageSessionId)) {
|
|
103
|
+
if (method === "Inspector.detached") {
|
|
70
104
|
this.invalidateDocument();
|
|
71
105
|
}
|
|
72
106
|
});
|
|
@@ -79,7 +113,9 @@ export class BrowserAutomationClient {
|
|
|
79
113
|
const connection = await CdpConnection.connect(endpoint.href, timeoutMs, options.createWebSocket ?? defaultWebSocketFactory);
|
|
80
114
|
try {
|
|
81
115
|
if (endpointKind === "page") {
|
|
82
|
-
|
|
116
|
+
const client = new BrowserAutomationClient(connection, endpoint.href, referenceKey, pageTargetIdFromEndpoint(endpoint));
|
|
117
|
+
await client.initializeIframeTracking();
|
|
118
|
+
return client;
|
|
83
119
|
}
|
|
84
120
|
const targetId = await selectPageTarget(connection, options.pageTargetId);
|
|
85
121
|
const attached = requireRecord(await connection.call("Target.attachToTarget", {
|
|
@@ -87,7 +123,9 @@ export class BrowserAutomationClient {
|
|
|
87
123
|
flatten: true,
|
|
88
124
|
}), "Target.attachToTarget result");
|
|
89
125
|
const sessionId = requiredString(attached.sessionId, "Target.attachToTarget sessionId");
|
|
90
|
-
|
|
126
|
+
const client = new BrowserAutomationClient(connection, `${endpoint.href}\0${targetId}`, referenceKey, targetId, sessionId);
|
|
127
|
+
await client.initializeIframeTracking();
|
|
128
|
+
return client;
|
|
91
129
|
}
|
|
92
130
|
catch (error) {
|
|
93
131
|
connection.close();
|
|
@@ -107,20 +145,45 @@ export class BrowserAutomationClient {
|
|
|
107
145
|
await this.pageCall("Accessibility.enable");
|
|
108
146
|
this.accessibilityEnabled = true;
|
|
109
147
|
}
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
148
|
+
await Promise.all([...this.iframeSessions.values()].map((iframe) => iframe.ready));
|
|
149
|
+
const targets = [
|
|
150
|
+
{ targetId: this.pageTargetId, sessionId: this.pageSessionId },
|
|
151
|
+
...[...this.iframeSessions.values()].map((iframe) => ({
|
|
152
|
+
targetId: iframe.targetId,
|
|
153
|
+
sessionId: iframe.sessionId,
|
|
154
|
+
})),
|
|
155
|
+
];
|
|
156
|
+
const nodes = [];
|
|
157
|
+
for (const target of targets) {
|
|
158
|
+
try {
|
|
159
|
+
const documentIdentity = await this.currentDocumentIdentity(target.sessionId);
|
|
160
|
+
const result = requireRecord(await this.targetCall("Accessibility.getFullAXTree", {}, target.sessionId), "Accessibility.getFullAXTree result");
|
|
161
|
+
if (!Array.isArray(result.nodes)) {
|
|
162
|
+
throw protocolError("Accessibility.getFullAXTree result nodes must be an array");
|
|
163
|
+
}
|
|
164
|
+
if (nodes.length + result.nodes.length > MAX_SNAPSHOT_NODES) {
|
|
165
|
+
throw protocolError(`Accessibility snapshot exceeds ${MAX_SNAPSHOT_NODES} nodes`);
|
|
166
|
+
}
|
|
167
|
+
if ((await this.currentDocumentIdentity(target.sessionId)) !== documentIdentity) {
|
|
168
|
+
throw protocolError("Browser document changed while taking the accessibility snapshot");
|
|
169
|
+
}
|
|
170
|
+
nodes.push(...result.nodes.map((value, index) => this.snapshotNode(value, index, documentIdentity, target.targetId)));
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (target.targetId === this.pageTargetId)
|
|
174
|
+
throw error;
|
|
175
|
+
// OOPIFs can detach while their parent remains stable. Skip only that
|
|
176
|
+
// stale frame; the next snapshot will discover its replacement target.
|
|
177
|
+
if (!isDetachedIframeError(error))
|
|
178
|
+
throw error;
|
|
179
|
+
if (this.iframeSessions.get(target.targetId)?.sessionId === target.sessionId) {
|
|
180
|
+
this.iframeSessions.delete(target.targetId);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
120
183
|
}
|
|
121
184
|
return {
|
|
122
185
|
documentGeneration: this.documentGenerationValue,
|
|
123
|
-
nodes
|
|
186
|
+
nodes,
|
|
124
187
|
};
|
|
125
188
|
}
|
|
126
189
|
async navigate(url) {
|
|
@@ -146,7 +209,7 @@ export class BrowserAutomationClient {
|
|
|
146
209
|
const element = "x" in target ? undefined : await this.resolveElement(target);
|
|
147
210
|
try {
|
|
148
211
|
const point = element
|
|
149
|
-
? await this.elementCenter(element)
|
|
212
|
+
? await this.pageCoordinates(element, await this.elementCenter(element))
|
|
150
213
|
: coordinateTarget(target);
|
|
151
214
|
await this.pageCall("Input.dispatchMouseEvent", {
|
|
152
215
|
type: "mouseMoved",
|
|
@@ -174,9 +237,7 @@ export class BrowserAutomationClient {
|
|
|
174
237
|
}
|
|
175
238
|
finally {
|
|
176
239
|
if (element?.objectId) {
|
|
177
|
-
await this.
|
|
178
|
-
objectId: element.objectId,
|
|
179
|
-
}).catch(() => undefined);
|
|
240
|
+
await this.targetCall("Runtime.releaseObject", { objectId: element.objectId }, element.sessionId).catch(() => undefined);
|
|
180
241
|
}
|
|
181
242
|
}
|
|
182
243
|
}
|
|
@@ -190,18 +251,18 @@ export class BrowserAutomationClient {
|
|
|
190
251
|
await this.ensureDomEnabled();
|
|
191
252
|
const objectId = element.objectId ?? await this.resolveObjectId(element);
|
|
192
253
|
try {
|
|
193
|
-
const focused = requireRecord(await this.
|
|
254
|
+
const focused = requireRecord(await this.targetCall("Runtime.callFunctionOn", {
|
|
194
255
|
objectId,
|
|
195
256
|
functionDeclaration: "function() { this.focus(); }",
|
|
196
257
|
returnByValue: true,
|
|
197
|
-
}), "Runtime.callFunctionOn result");
|
|
258
|
+
}, element.sessionId), "Runtime.callFunctionOn result");
|
|
198
259
|
if (focused.exceptionDetails !== undefined) {
|
|
199
260
|
throw protocolError("Browser element could not be focused");
|
|
200
261
|
}
|
|
201
262
|
await this.pageCall("Input.insertText", { text });
|
|
202
263
|
}
|
|
203
264
|
finally {
|
|
204
|
-
await this.
|
|
265
|
+
await this.targetCall("Runtime.releaseObject", { objectId }, element.sessionId).catch(() => undefined);
|
|
205
266
|
}
|
|
206
267
|
}
|
|
207
268
|
async screenshot(options = {}) {
|
|
@@ -247,15 +308,48 @@ export class BrowserAutomationClient {
|
|
|
247
308
|
async pageCall(method, params = {}) {
|
|
248
309
|
return await this.connection.call(method, params, this.pageSessionId);
|
|
249
310
|
}
|
|
311
|
+
async targetCall(method, params, sessionId) {
|
|
312
|
+
// A browser endpoint needs the attached main Page session on ordinary DOM
|
|
313
|
+
// operations. Only OOPIF elements override it with their child session.
|
|
314
|
+
return await this.connection.call(method, params, sessionId ?? this.pageSessionId);
|
|
315
|
+
}
|
|
316
|
+
async initializeIframeTracking() {
|
|
317
|
+
await this.pageCall("Target.setAutoAttach", IFRAME_AUTO_ATTACH_PARAMS);
|
|
318
|
+
await Promise.all([...this.iframeSessions.values()].map((iframe) => iframe.ready));
|
|
319
|
+
}
|
|
320
|
+
acceptAttachedIframe(params) {
|
|
321
|
+
const targetInfo = recordAt(params, "targetInfo");
|
|
322
|
+
const targetId = targetInfo && stringAt(targetInfo, "targetId");
|
|
323
|
+
const childSessionId = stringAt(params, "sessionId");
|
|
324
|
+
if (!targetInfo || targetInfo.type !== "iframe" || !targetId || !childSessionId)
|
|
325
|
+
return;
|
|
326
|
+
const existing = this.iframeSessions.get(targetId);
|
|
327
|
+
if (existing?.sessionId === childSessionId)
|
|
328
|
+
return;
|
|
329
|
+
let iframe;
|
|
330
|
+
const ready = (async () => {
|
|
331
|
+
await this.targetCall("DOM.enable", {}, childSessionId);
|
|
332
|
+
await this.targetCall("Accessibility.enable", {}, childSessionId);
|
|
333
|
+
await this.targetCall("Target.setAutoAttach", IFRAME_AUTO_ATTACH_PARAMS, childSessionId);
|
|
334
|
+
})().catch(() => {
|
|
335
|
+
if (this.iframeSessions.get(targetId) === iframe) {
|
|
336
|
+
this.iframeSessions.delete(targetId);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
iframe = { targetId, sessionId: childSessionId, ready };
|
|
340
|
+
this.iframeSessions.set(targetId, iframe);
|
|
341
|
+
this.invalidateDocument();
|
|
342
|
+
}
|
|
250
343
|
async ensureDomEnabled() {
|
|
251
344
|
if (this.domEnabled)
|
|
252
345
|
return;
|
|
253
346
|
await this.pageCall("DOM.enable");
|
|
254
347
|
this.domEnabled = true;
|
|
255
348
|
}
|
|
256
|
-
snapshotNode(value, index, documentIdentity) {
|
|
349
|
+
snapshotNode(value, index, documentIdentity, targetId) {
|
|
257
350
|
const node = requireRecord(value, `Accessibility node ${index}`);
|
|
258
|
-
const
|
|
351
|
+
const rawNodeId = requiredString(node.nodeId, `Accessibility node ${index} nodeId`);
|
|
352
|
+
const nodeId = scopedNodeId(targetId, rawNodeId);
|
|
259
353
|
const backendNodeId = positiveInteger(node.backendDOMNodeId);
|
|
260
354
|
const ignored = node.ignored === true;
|
|
261
355
|
const role = axString(node.role);
|
|
@@ -264,17 +358,17 @@ export class BrowserAutomationClient {
|
|
|
264
358
|
const childIds = rawChildIds === undefined
|
|
265
359
|
? []
|
|
266
360
|
: Array.isArray(rawChildIds)
|
|
267
|
-
? rawChildIds.map((child, childIndex) => requiredString(child, `Accessibility node ${index} childIds[${childIndex}]`))
|
|
361
|
+
? rawChildIds.map((child, childIndex) => scopedNodeId(targetId, requiredString(child, `Accessibility node ${index} childIds[${childIndex}]`)))
|
|
268
362
|
: (() => {
|
|
269
363
|
throw protocolError(`Accessibility node ${index} childIds must be an array`);
|
|
270
364
|
})();
|
|
271
365
|
const ref = !ignored && backendNodeId !== undefined
|
|
272
|
-
? encodeElementReference(this.referenceScope, documentIdentity, backendNodeId, this.referenceKey)
|
|
366
|
+
? encodeElementReference(this.referenceScope, targetId, documentIdentity, backendNodeId, this.referenceKey)
|
|
273
367
|
: undefined;
|
|
274
368
|
return {
|
|
275
369
|
nodeId,
|
|
276
370
|
...(optionalString(node.parentId)
|
|
277
|
-
? { parentId: optionalString(node.parentId) }
|
|
371
|
+
? { parentId: scopedNodeId(targetId, optionalString(node.parentId)) }
|
|
278
372
|
: {}),
|
|
279
373
|
childIds,
|
|
280
374
|
role,
|
|
@@ -295,12 +389,17 @@ export class BrowserAutomationClient {
|
|
|
295
389
|
if ("ref" in target) {
|
|
296
390
|
if (typeof target.ref !== "string")
|
|
297
391
|
throw invalidInput("ref must be a string");
|
|
298
|
-
const
|
|
299
|
-
const
|
|
392
|
+
const decoded = decodeElementReference(target.ref, this.referenceKey);
|
|
393
|
+
const targetSession = await this.targetForReference(target.ref, decoded);
|
|
300
394
|
try {
|
|
395
|
+
const element = {
|
|
396
|
+
backendNodeId: decoded.backendNodeId,
|
|
397
|
+
targetId: targetSession.targetId,
|
|
398
|
+
...(targetSession.sessionId ? { sessionId: targetSession.sessionId } : {}),
|
|
399
|
+
};
|
|
301
400
|
return {
|
|
302
|
-
|
|
303
|
-
objectId: await this.resolveObjectId(
|
|
401
|
+
...element,
|
|
402
|
+
objectId: await this.resolveObjectId(element),
|
|
304
403
|
};
|
|
305
404
|
}
|
|
306
405
|
catch (error) {
|
|
@@ -332,12 +431,12 @@ export class BrowserAutomationClient {
|
|
|
332
431
|
return { nodeId };
|
|
333
432
|
}
|
|
334
433
|
async resolveObjectId(element) {
|
|
335
|
-
const resolved = requireRecord(await this.
|
|
434
|
+
const resolved = requireRecord(await this.targetCall("DOM.resolveNode", domLocator(element), element.sessionId), "DOM.resolveNode result");
|
|
336
435
|
const object = requireRecord(resolved.object, "DOM.resolveNode object");
|
|
337
436
|
return requiredString(object.objectId, "DOM.resolveNode objectId");
|
|
338
437
|
}
|
|
339
|
-
async currentDocumentIdentity() {
|
|
340
|
-
const result = requireRecord(await this.
|
|
438
|
+
async currentDocumentIdentity(sessionId = this.pageSessionId) {
|
|
439
|
+
const result = requireRecord(await this.targetCall("Page.getFrameTree", {}, sessionId), "Page.getFrameTree result");
|
|
341
440
|
const frameTree = requireRecord(result.frameTree, "Page.getFrameTree frameTree");
|
|
342
441
|
const frame = requireRecord(frameTree.frame, "Page.getFrameTree frame");
|
|
343
442
|
return requiredString(frame.loaderId, "Page.getFrameTree loaderId");
|
|
@@ -345,8 +444,8 @@ export class BrowserAutomationClient {
|
|
|
345
444
|
async elementCenter(element) {
|
|
346
445
|
await this.ensureDomEnabled();
|
|
347
446
|
const locator = domLocator(element);
|
|
348
|
-
await this.
|
|
349
|
-
const result = requireRecord(await this.
|
|
447
|
+
await this.targetCall("DOM.scrollIntoViewIfNeeded", locator, element.sessionId);
|
|
448
|
+
const result = requireRecord(await this.targetCall("DOM.getBoxModel", locator, element.sessionId), "DOM.getBoxModel result");
|
|
350
449
|
const model = requireRecord(result.model, "DOM.getBoxModel model");
|
|
351
450
|
const quad = numericQuad(model.content ?? model.border);
|
|
352
451
|
return {
|
|
@@ -354,6 +453,60 @@ export class BrowserAutomationClient {
|
|
|
354
453
|
y: (quad[1] + quad[3] + quad[5] + quad[7]) / 4,
|
|
355
454
|
};
|
|
356
455
|
}
|
|
456
|
+
async targetForReference(ref, decoded) {
|
|
457
|
+
await Promise.all([...this.iframeSessions.values()].map((iframe) => iframe.ready));
|
|
458
|
+
const candidates = [
|
|
459
|
+
{ targetId: this.pageTargetId, sessionId: this.pageSessionId },
|
|
460
|
+
...[...this.iframeSessions.values()].map((iframe) => ({
|
|
461
|
+
targetId: iframe.targetId,
|
|
462
|
+
sessionId: iframe.sessionId,
|
|
463
|
+
})),
|
|
464
|
+
];
|
|
465
|
+
const selected = candidates.find((candidate) => timingSafeEqual(targetFingerprint(this.referenceScope, candidate.targetId), decoded.targetFingerprint));
|
|
466
|
+
if (!selected)
|
|
467
|
+
throw new BrowserAutomationStaleReferenceError(ref);
|
|
468
|
+
const documentIdentity = await this.currentDocumentIdentity(selected.sessionId);
|
|
469
|
+
const expectedDocument = documentFingerprint(this.referenceScope, selected.targetId, documentIdentity);
|
|
470
|
+
if (!timingSafeEqual(decoded.documentFingerprint, expectedDocument)) {
|
|
471
|
+
throw new BrowserAutomationStaleReferenceError(ref);
|
|
472
|
+
}
|
|
473
|
+
return selected;
|
|
474
|
+
}
|
|
475
|
+
async pageCoordinates(element, local) {
|
|
476
|
+
if (!element.sessionId || !element.targetId)
|
|
477
|
+
return local;
|
|
478
|
+
try {
|
|
479
|
+
const targetInfoResult = requireRecord(await this.pageCall("Target.getTargetInfo", { targetId: element.targetId }), "Target.getTargetInfo result");
|
|
480
|
+
const targetInfo = requireRecord(targetInfoResult.targetInfo, "Target.getTargetInfo targetInfo");
|
|
481
|
+
const targetUrl = optionalString(targetInfo.url);
|
|
482
|
+
const framesResult = requireRecord(await this.pageCall("Runtime.evaluate", {
|
|
483
|
+
expression: `(() => Array.from(document.querySelectorAll('iframe,frame'), frame => {
|
|
484
|
+
const rect = frame.getBoundingClientRect();
|
|
485
|
+
return { x: rect.x, y: rect.y, src: frame.src || '' };
|
|
486
|
+
}))()`,
|
|
487
|
+
returnByValue: true,
|
|
488
|
+
}), "Runtime.evaluate result");
|
|
489
|
+
const result = requireRecord(framesResult.result, "Runtime.evaluate result result");
|
|
490
|
+
const frames = Array.isArray(result.value)
|
|
491
|
+
? result.value.filter((value) => isRecord(value))
|
|
492
|
+
: [];
|
|
493
|
+
const exact = targetUrl
|
|
494
|
+
? frames.find((frame) => frame.src === targetUrl)
|
|
495
|
+
: undefined;
|
|
496
|
+
const originMatch = !exact && targetUrl
|
|
497
|
+
? frames.find((frame) => sameUrlOrigin(optionalString(frame.src), targetUrl))
|
|
498
|
+
: undefined;
|
|
499
|
+
const frame = exact ?? originMatch ?? (frames.length === 1 ? frames[0] : undefined);
|
|
500
|
+
if (frame && typeof frame.x === "number" && typeof frame.y === "number") {
|
|
501
|
+
return { x: local.x + frame.x, y: local.y + frame.y };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// The iframe may have navigated while resolving its parent offset. The
|
|
506
|
+
// local coordinates remain the best bounded fallback.
|
|
507
|
+
}
|
|
508
|
+
return local;
|
|
509
|
+
}
|
|
357
510
|
invalidateDocument() {
|
|
358
511
|
this.documentGenerationValue += 1;
|
|
359
512
|
}
|
|
@@ -397,8 +550,8 @@ class CdpConnection {
|
|
|
397
550
|
}
|
|
398
551
|
await new Promise((resolve, reject) => {
|
|
399
552
|
const timer = setTimeout(() => {
|
|
400
|
-
socket.close(1000, "connection timeout");
|
|
401
553
|
reject(new BrowserAutomationError(`CDP WebSocket did not open within ${timeoutMs}ms`, "timeout"));
|
|
554
|
+
socket.close(1000, "connection timeout");
|
|
402
555
|
}, timeoutMs);
|
|
403
556
|
socket.onopen = () => {
|
|
404
557
|
clearTimeout(timer);
|
|
@@ -571,15 +724,16 @@ function defaultWebSocketFactory(endpoint) {
|
|
|
571
724
|
}
|
|
572
725
|
return new Constructor(endpoint);
|
|
573
726
|
}
|
|
574
|
-
function encodeElementReference(referenceScope, documentIdentity, backendNodeId, referenceKey) {
|
|
727
|
+
function encodeElementReference(referenceScope, targetId, documentIdentity, backendNodeId, referenceKey) {
|
|
575
728
|
const payload = Buffer.alloc(REFERENCE_PAYLOAD_BYTES);
|
|
576
729
|
payload[0] = REFERENCE_VERSION;
|
|
577
|
-
documentFingerprint(referenceScope, documentIdentity).copy(payload, 1);
|
|
578
|
-
|
|
730
|
+
documentFingerprint(referenceScope, targetId, documentIdentity).copy(payload, 1);
|
|
731
|
+
targetFingerprint(referenceScope, targetId).copy(payload, 1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES);
|
|
732
|
+
payload.writeBigUInt64BE(BigInt(backendNodeId), 1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES + REFERENCE_TARGET_FINGERPRINT_BYTES);
|
|
579
733
|
const tag = referenceTag(payload, referenceKey);
|
|
580
734
|
return Buffer.concat([payload, tag]).toString("base64url");
|
|
581
735
|
}
|
|
582
|
-
function decodeElementReference(ref,
|
|
736
|
+
function decodeElementReference(ref, referenceKey) {
|
|
583
737
|
if (typeof ref !== "string" ||
|
|
584
738
|
ref.length === 0 ||
|
|
585
739
|
!/^[A-Za-z0-9_-]+$/.test(ref)) {
|
|
@@ -602,26 +756,38 @@ function decodeElementReference(ref, referenceScope, documentIdentity, reference
|
|
|
602
756
|
if (!timingSafeEqual(actualTag, referenceTag(payload, referenceKey))) {
|
|
603
757
|
throw new BrowserAutomationStaleReferenceError(ref);
|
|
604
758
|
}
|
|
605
|
-
const
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
throw new BrowserAutomationStaleReferenceError(ref);
|
|
609
|
-
}
|
|
610
|
-
const rawNodeId = payload.readBigUInt64BE(1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES);
|
|
759
|
+
const actualDocument = Buffer.from(payload.subarray(1, 1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES));
|
|
760
|
+
const actualTarget = Buffer.from(payload.subarray(1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES, 1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES + REFERENCE_TARGET_FINGERPRINT_BYTES));
|
|
761
|
+
const rawNodeId = payload.readBigUInt64BE(1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES + REFERENCE_TARGET_FINGERPRINT_BYTES);
|
|
611
762
|
if (rawNodeId < 1n || rawNodeId > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
612
763
|
throw new BrowserAutomationStaleReferenceError(ref);
|
|
613
764
|
}
|
|
614
|
-
return
|
|
765
|
+
return {
|
|
766
|
+
documentFingerprint: actualDocument,
|
|
767
|
+
targetFingerprint: actualTarget,
|
|
768
|
+
backendNodeId: Number(rawNodeId),
|
|
769
|
+
};
|
|
615
770
|
}
|
|
616
|
-
function documentFingerprint(referenceScope, documentIdentity) {
|
|
771
|
+
function documentFingerprint(referenceScope, targetId, documentIdentity) {
|
|
617
772
|
return createHash("sha256")
|
|
618
|
-
.update("rynx-browser-cdp-document-
|
|
773
|
+
.update("rynx-browser-cdp-document-v2\0")
|
|
619
774
|
.update(referenceScope)
|
|
620
775
|
.update("\0")
|
|
776
|
+
.update(targetId)
|
|
777
|
+
.update("\0")
|
|
621
778
|
.update(documentIdentity)
|
|
622
779
|
.digest()
|
|
623
780
|
.subarray(0, REFERENCE_DOCUMENT_FINGERPRINT_BYTES);
|
|
624
781
|
}
|
|
782
|
+
function targetFingerprint(referenceScope, targetId) {
|
|
783
|
+
return createHash("sha256")
|
|
784
|
+
.update("rynx-browser-cdp-target-v2\0")
|
|
785
|
+
.update(referenceScope)
|
|
786
|
+
.update("\0")
|
|
787
|
+
.update(targetId)
|
|
788
|
+
.digest()
|
|
789
|
+
.subarray(0, REFERENCE_TARGET_FINGERPRINT_BYTES);
|
|
790
|
+
}
|
|
625
791
|
function referenceTag(payload, referenceKey) {
|
|
626
792
|
return createHmac("sha256", referenceKey)
|
|
627
793
|
.update(REFERENCE_DOMAIN)
|
|
@@ -676,6 +842,40 @@ function inferEndpointKind(endpoint) {
|
|
|
676
842
|
return "page";
|
|
677
843
|
throw invalidInput("endpointKind is required for a non-standard CDP WebSocket path");
|
|
678
844
|
}
|
|
845
|
+
function pageTargetIdFromEndpoint(endpoint) {
|
|
846
|
+
const prefix = "/devtools/page/";
|
|
847
|
+
if (!endpoint.pathname.startsWith(prefix)) {
|
|
848
|
+
throw invalidInput("page endpoint must identify one CDP Page target");
|
|
849
|
+
}
|
|
850
|
+
const targetId = endpoint.pathname.slice(prefix.length);
|
|
851
|
+
if (targetId.length === 0 || targetId.includes("/")) {
|
|
852
|
+
throw invalidInput("page endpoint must identify one CDP Page target");
|
|
853
|
+
}
|
|
854
|
+
return targetId;
|
|
855
|
+
}
|
|
856
|
+
function scopedNodeId(targetId, nodeId) {
|
|
857
|
+
const target = createHash("sha256")
|
|
858
|
+
.update("rynx-browser-cdp-node-scope-v1\0")
|
|
859
|
+
.update(targetId)
|
|
860
|
+
.digest("base64url")
|
|
861
|
+
.slice(0, 12);
|
|
862
|
+
return `${target}:${nodeId}`;
|
|
863
|
+
}
|
|
864
|
+
function sameUrlOrigin(candidate, target) {
|
|
865
|
+
if (!candidate)
|
|
866
|
+
return false;
|
|
867
|
+
try {
|
|
868
|
+
return new URL(candidate).origin === new URL(target).origin;
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function isDetachedIframeError(error) {
|
|
875
|
+
return error instanceof BrowserAutomationError &&
|
|
876
|
+
error.code === "protocol" &&
|
|
877
|
+
/(?:no session|session.*not found|target.*(?:closed|not found)|not attached)/i.test(error.message);
|
|
878
|
+
}
|
|
679
879
|
function parseNavigationUrl(value) {
|
|
680
880
|
if (value === "about:blank")
|
|
681
881
|
return value;
|