@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 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
- * target and current document. A navigation or Browser restart invalidates it.
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 main-document loader.
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 = 1;
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 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES + REFERENCE_BACKEND_NODE_BYTES;
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-v1\0", "utf8");
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 main-document loader.
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 (this.pageSessionId && sessionId && sessionId !== this.pageSessionId)
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
- return new BrowserAutomationClient(connection, endpoint.href, referenceKey);
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
- return new BrowserAutomationClient(connection, `${endpoint.href}\0${targetId}`, referenceKey, sessionId);
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
- const documentIdentity = await this.currentDocumentIdentity();
111
- const result = requireRecord(await this.pageCall("Accessibility.getFullAXTree"), "Accessibility.getFullAXTree result");
112
- if (!Array.isArray(result.nodes)) {
113
- throw protocolError("Accessibility.getFullAXTree result nodes must be an array");
114
- }
115
- if (result.nodes.length > MAX_SNAPSHOT_NODES) {
116
- throw protocolError(`Accessibility snapshot exceeds ${MAX_SNAPSHOT_NODES} nodes`);
117
- }
118
- if ((await this.currentDocumentIdentity()) !== documentIdentity) {
119
- throw protocolError("Browser document changed while taking the accessibility snapshot");
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: result.nodes.map((value, index) => this.snapshotNode(value, index, documentIdentity)),
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.pageCall("Runtime.releaseObject", {
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.pageCall("Runtime.callFunctionOn", {
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.pageCall("Runtime.releaseObject", { objectId }).catch(() => undefined);
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 nodeId = requiredString(node.nodeId, `Accessibility node ${index} nodeId`);
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 documentIdentity = await this.currentDocumentIdentity();
299
- const backendNodeId = decodeElementReference(target.ref, this.referenceScope, documentIdentity, this.referenceKey);
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
- backendNodeId,
303
- objectId: await this.resolveObjectId({ backendNodeId }),
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.pageCall("DOM.resolveNode", domLocator(element)), "DOM.resolveNode result");
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.pageCall("Page.getFrameTree"), "Page.getFrameTree result");
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.pageCall("DOM.scrollIntoViewIfNeeded", locator);
349
- const result = requireRecord(await this.pageCall("DOM.getBoxModel", locator), "DOM.getBoxModel result");
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
- payload.writeBigUInt64BE(BigInt(backendNodeId), 1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES);
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, referenceScope, documentIdentity, referenceKey) {
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 expectedDocument = documentFingerprint(referenceScope, documentIdentity);
606
- const actualDocument = payload.subarray(1, 1 + REFERENCE_DOCUMENT_FINGERPRINT_BYTES);
607
- if (!timingSafeEqual(actualDocument, expectedDocument)) {
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 Number(rawNodeId);
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-v1\0")
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/browser-cdp",
3
- "version": "0.1.11-beta.37",
3
+ "version": "0.1.11-beta.39",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",