@olimsaidov/icdp 0.2.1 → 0.3.1

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.
@@ -1,4 +1,4 @@
1
- import { CdpError, FrameInfo, TargetSummary } from "../protocol.mjs";
1
+ import { C as TargetSummary, g as FrameInfo, p as CdpError } from "../protocol-CZthc7CO.mjs";
2
2
 
3
3
  //#region src/host/index.d.ts
4
4
  /** Minimal structural view of an iframe, so tests can fake it. */
@@ -0,0 +1,121 @@
1
+ import Protocol from "devtools-protocol";
2
+
3
+ //#region src/protocol.d.ts
4
+ declare const PROTOCOL_VERSION = 1;
5
+ type CdpId = Protocol.integer | string;
6
+ /** A raw CDP message: command, response, or event. */
7
+ type CdpMessage = {
8
+ id?: CdpId;
9
+ method?: string;
10
+ params?: Record<string, unknown>;
11
+ sessionId?: string;
12
+ result?: unknown;
13
+ error?: CdpError;
14
+ };
15
+ type CdpError = {
16
+ code: number;
17
+ message: string;
18
+ };
19
+ declare const CDP_SERVER_ERROR = -32000;
20
+ declare const CDP_METHOD_NOT_FOUND = -32601;
21
+ /** Metadata a Frame Agent reports about its document. */
22
+ type FrameInfo = {
23
+ title: string;
24
+ url: string;
25
+ };
26
+ /** Target metadata as the Host reports it to the Relay. */
27
+ type TargetSummary = FrameInfo & {
28
+ targetId: string;
29
+ };
30
+ /** Sent by the Frame Agent to window.parent when it boots (and on probe). */
31
+ type HelloMessage = {
32
+ icdp: "hello";
33
+ v: number;
34
+ } & FrameInfo;
35
+ /** Sent by the Host to an iframe it doesn't yet have a channel for. */
36
+ type ProbeMessage = {
37
+ icdp: "probe";
38
+ v: number;
39
+ };
40
+ /** Sent by the Host in reply to hello, transferring a MessagePort. */
41
+ type WelcomeMessage = {
42
+ icdp: "welcome";
43
+ v: number;
44
+ };
45
+ type HandshakeMessage = HelloMessage | ProbeMessage | WelcomeMessage;
46
+ declare function isHandshakeMessage(data: unknown): data is HandshakeMessage;
47
+ /** Host -> Relay: announces itself and its current targets. New-wins: the Relay drops any previous Host. */
48
+ type BridgeReady = {
49
+ kind: "ready";
50
+ v: number;
51
+ targets: TargetSummary[];
52
+ /** Browser-level methods (e.g. "Target.createTarget") the Host handles itself.
53
+ * The Relay forwards these as a BridgeBrowserRequest instead of using its
54
+ * built-in default; omitted/empty means the Relay keeps its defaults. */
55
+ handles?: string[];
56
+ };
57
+ /** Host -> Relay: a Pairing appeared. */
58
+ type BridgeTargetCreated = {
59
+ kind: "targetCreated";
60
+ target: TargetSummary;
61
+ };
62
+ /** Host -> Relay: a Pairing was destroyed by the Host. */
63
+ type BridgeTargetDestroyed = {
64
+ kind: "targetDestroyed";
65
+ targetId: string;
66
+ };
67
+ /** Host -> Relay: a Target's document changed (reload / navigation under a stable targetId). */
68
+ type BridgeTargetInfoChanged = {
69
+ kind: "targetInfoChanged";
70
+ target: TargetSummary;
71
+ };
72
+ /** Relay -> Host: a Client command routed to one session. */
73
+ type BridgeCommand = {
74
+ kind: "command";
75
+ sessionId: string;
76
+ targetId: string;
77
+ id: number;
78
+ method: string;
79
+ params: Record<string, unknown>;
80
+ };
81
+ /** Host -> Relay: the response to a BridgeCommand. */
82
+ type BridgeResponse = {
83
+ kind: "response";
84
+ sessionId: string;
85
+ id: number;
86
+ result?: unknown;
87
+ error?: CdpError;
88
+ };
89
+ /** Host -> Relay: a CDP event from a Target; the Relay fans it out to every session attached to it. */
90
+ type BridgeEvent = {
91
+ kind: "event";
92
+ targetId: string;
93
+ method: string;
94
+ params: Record<string, unknown>;
95
+ };
96
+ /** Relay -> Host: a session detached (Client disconnected or detached explicitly). */
97
+ type BridgeDetached = {
98
+ kind: "detached";
99
+ sessionId: string;
100
+ targetId: string;
101
+ };
102
+ /** Relay -> Host: a browser-level method the Host advertised it handles
103
+ * (e.g. Target.createTarget / Target.closeTarget). Not session-scoped. */
104
+ type BridgeBrowserRequest = {
105
+ kind: "browserRequest";
106
+ id: number;
107
+ method: string;
108
+ params: Record<string, unknown>;
109
+ };
110
+ /** Host -> Relay: the response to a BridgeBrowserRequest. */
111
+ type BridgeBrowserResult = {
112
+ kind: "browserResult";
113
+ id: number;
114
+ result?: unknown;
115
+ error?: CdpError;
116
+ };
117
+ type HostToRelayMessage = BridgeReady | BridgeTargetCreated | BridgeTargetDestroyed | BridgeTargetInfoChanged | BridgeResponse | BridgeEvent | BridgeBrowserResult;
118
+ type RelayToHostMessage = BridgeCommand | BridgeDetached | BridgeBrowserRequest;
119
+ declare function parseJson<T>(raw: string | Buffer | ArrayBuffer | Uint8Array): T | null;
120
+ //#endregion
121
+ export { TargetSummary as C, parseJson as E, RelayToHostMessage as S, isHandshakeMessage as T, HandshakeMessage as _, BridgeEvent as a, PROTOCOL_VERSION as b, BridgeTargetCreated as c, CDP_METHOD_NOT_FOUND as d, CDP_SERVER_ERROR as f, FrameInfo as g, CdpMessage as h, BridgeDetached as i, BridgeTargetDestroyed as l, CdpId as m, BridgeBrowserResult as n, BridgeReady as o, CdpError as p, BridgeCommand as r, BridgeResponse as s, BridgeBrowserRequest as t, BridgeTargetInfoChanged as u, HelloMessage as v, WelcomeMessage as w, ProbeMessage as x, HostToRelayMessage as y };
@@ -1,121 +1,2 @@
1
- import Protocol from "devtools-protocol";
2
-
3
- //#region src/protocol.d.ts
4
- declare const PROTOCOL_VERSION = 1;
5
- type CdpId = Protocol.integer | string;
6
- /** A raw CDP message: command, response, or event. */
7
- type CdpMessage = {
8
- id?: CdpId;
9
- method?: string;
10
- params?: Record<string, unknown>;
11
- sessionId?: string;
12
- result?: unknown;
13
- error?: CdpError;
14
- };
15
- type CdpError = {
16
- code: number;
17
- message: string;
18
- };
19
- declare const CDP_SERVER_ERROR = -32000;
20
- declare const CDP_METHOD_NOT_FOUND = -32601;
21
- /** Metadata a Frame Agent reports about its document. */
22
- type FrameInfo = {
23
- title: string;
24
- url: string;
25
- };
26
- /** Target metadata as the Host reports it to the Relay. */
27
- type TargetSummary = FrameInfo & {
28
- targetId: string;
29
- };
30
- /** Sent by the Frame Agent to window.parent when it boots (and on probe). */
31
- type HelloMessage = {
32
- icdp: "hello";
33
- v: number;
34
- } & FrameInfo;
35
- /** Sent by the Host to an iframe it doesn't yet have a channel for. */
36
- type ProbeMessage = {
37
- icdp: "probe";
38
- v: number;
39
- };
40
- /** Sent by the Host in reply to hello, transferring a MessagePort. */
41
- type WelcomeMessage = {
42
- icdp: "welcome";
43
- v: number;
44
- };
45
- type HandshakeMessage = HelloMessage | ProbeMessage | WelcomeMessage;
46
- declare function isHandshakeMessage(data: unknown): data is HandshakeMessage;
47
- /** Host -> Relay: announces itself and its current targets. New-wins: the Relay drops any previous Host. */
48
- type BridgeReady = {
49
- kind: "ready";
50
- v: number;
51
- targets: TargetSummary[];
52
- /** Browser-level methods (e.g. "Target.createTarget") the Host handles itself.
53
- * The Relay forwards these as a BridgeBrowserRequest instead of using its
54
- * built-in default; omitted/empty means the Relay keeps its defaults. */
55
- handles?: string[];
56
- };
57
- /** Host -> Relay: a Pairing appeared. */
58
- type BridgeTargetCreated = {
59
- kind: "targetCreated";
60
- target: TargetSummary;
61
- };
62
- /** Host -> Relay: a Pairing was destroyed by the Host. */
63
- type BridgeTargetDestroyed = {
64
- kind: "targetDestroyed";
65
- targetId: string;
66
- };
67
- /** Host -> Relay: a Target's document changed (reload / navigation under a stable targetId). */
68
- type BridgeTargetInfoChanged = {
69
- kind: "targetInfoChanged";
70
- target: TargetSummary;
71
- };
72
- /** Relay -> Host: a Client command routed to one session. */
73
- type BridgeCommand = {
74
- kind: "command";
75
- sessionId: string;
76
- targetId: string;
77
- id: number;
78
- method: string;
79
- params: Record<string, unknown>;
80
- };
81
- /** Host -> Relay: the response to a BridgeCommand. */
82
- type BridgeResponse = {
83
- kind: "response";
84
- sessionId: string;
85
- id: number;
86
- result?: unknown;
87
- error?: CdpError;
88
- };
89
- /** Host -> Relay: a CDP event from a Target; the Relay fans it out to every session attached to it. */
90
- type BridgeEvent = {
91
- kind: "event";
92
- targetId: string;
93
- method: string;
94
- params: Record<string, unknown>;
95
- };
96
- /** Relay -> Host: a session detached (Client disconnected or detached explicitly). */
97
- type BridgeDetached = {
98
- kind: "detached";
99
- sessionId: string;
100
- targetId: string;
101
- };
102
- /** Relay -> Host: a browser-level method the Host advertised it handles
103
- * (e.g. Target.createTarget / Target.closeTarget). Not session-scoped. */
104
- type BridgeBrowserRequest = {
105
- kind: "browserRequest";
106
- id: number;
107
- method: string;
108
- params: Record<string, unknown>;
109
- };
110
- /** Host -> Relay: the response to a BridgeBrowserRequest. */
111
- type BridgeBrowserResult = {
112
- kind: "browserResult";
113
- id: number;
114
- result?: unknown;
115
- error?: CdpError;
116
- };
117
- type HostToRelayMessage = BridgeReady | BridgeTargetCreated | BridgeTargetDestroyed | BridgeTargetInfoChanged | BridgeResponse | BridgeEvent | BridgeBrowserResult;
118
- type RelayToHostMessage = BridgeCommand | BridgeDetached | BridgeBrowserRequest;
119
- declare function parseJson<T>(raw: string | Buffer | ArrayBuffer | Uint8Array): T | null;
120
- //#endregion
1
+ import { C as TargetSummary, E as parseJson, S as RelayToHostMessage, T as isHandshakeMessage, _ as HandshakeMessage, a as BridgeEvent, b as PROTOCOL_VERSION, c as BridgeTargetCreated, d as CDP_METHOD_NOT_FOUND, f as CDP_SERVER_ERROR, g as FrameInfo, h as CdpMessage, i as BridgeDetached, l as BridgeTargetDestroyed, m as CdpId, n as BridgeBrowserResult, o as BridgeReady, p as CdpError, r as BridgeCommand, s as BridgeResponse, t as BridgeBrowserRequest, u as BridgeTargetInfoChanged, v as HelloMessage, w as WelcomeMessage, x as ProbeMessage, y as HostToRelayMessage } from "./protocol-CZthc7CO.mjs";
121
2
  export { BridgeBrowserRequest, BridgeBrowserResult, BridgeCommand, BridgeDetached, BridgeEvent, BridgeReady, BridgeResponse, BridgeTargetCreated, BridgeTargetDestroyed, BridgeTargetInfoChanged, CDP_METHOD_NOT_FOUND, CDP_SERVER_ERROR, CdpError, CdpId, CdpMessage, FrameInfo, HandshakeMessage, HelloMessage, HostToRelayMessage, PROTOCOL_VERSION, ProbeMessage, RelayToHostMessage, TargetSummary, WelcomeMessage, isHandshakeMessage, parseJson };
@@ -1,4 +1,4 @@
1
- import { TargetSummary } from "../protocol.mjs";
1
+ import { C as TargetSummary } from "../protocol-CZthc7CO.mjs";
2
2
 
3
3
  //#region src/relay/core.d.ts
4
4
  /** Minimal socket surface the adapter must provide for each connection. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olimsaidov/icdp",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
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
5
  "homepage": "https://github.com/olimsaidov/icdp#readme",
6
6
  "bugs": "https://github.com/olimsaidov/icdp/issues",
@@ -45,6 +45,10 @@
45
45
  "scripts": {
46
46
  "build": "tsdown",
47
47
  "check": "tsc --noEmit && oxlint && oxfmt --check .",
48
+ "docs:build": "node docs/scripts/build-demo-frame.mjs && vitepress build docs",
49
+ "docs:demo-frame": "node docs/scripts/build-demo-frame.mjs",
50
+ "docs:dev": "node docs/scripts/build-demo-frame.mjs && vitepress dev docs",
51
+ "docs:preview": "vitepress preview docs",
48
52
  "fmt": "oxfmt .",
49
53
  "gen:conformance": "node scripts/gen-conformance.ts",
50
54
  "playground": "node playground/server.ts",
@@ -60,15 +64,21 @@
60
64
  "ws": "^8.21.0"
61
65
  },
62
66
  "devDependencies": {
67
+ "@olimsaidov/agent-browser-wasm": "^0.27.3",
63
68
  "@types/aria-query": "^5.0.4",
64
69
  "@types/node": "^24",
65
70
  "@types/ws": "^8",
71
+ "@xterm/addon-fit": "^0.11.0",
72
+ "@xterm/xterm": "^6.0.0",
66
73
  "jsdom": "^29.1.1",
74
+ "mermaid": "^11.15.0",
67
75
  "oxfmt": "^0.54.0",
68
76
  "oxlint": "^1.69.0",
69
77
  "rolldown": "^1.1.1",
70
78
  "tsdown": "^0.22.2",
71
79
  "typescript": "^5",
80
+ "vitepress": "^1.6.4",
81
+ "vitepress-plugin-mermaid": "^2.0.17",
72
82
  "vitest": "^4.1.8"
73
83
  },
74
84
  "engines": {
@@ -382,24 +382,6 @@ function wrapConsoleMethod(fn: ConsoleMethod, type: string): ConsoleMethod {
382
382
  return wrapped;
383
383
  }
384
384
 
385
- function highlight(el: Element): void {
386
- const rect = el.getBoundingClientRect();
387
- const marker = document.createElement("div");
388
- marker.style.cssText = [
389
- "position:fixed",
390
- `left:${rect.left}px`,
391
- `top:${rect.top}px`,
392
- `width:${rect.width}px`,
393
- `height:${rect.height}px`,
394
- "z-index:2147483647",
395
- "pointer-events:none",
396
- "outline:2px solid #005fb8",
397
- "background:rgba(0,95,184,.08)",
398
- ].join(";");
399
- document.documentElement.appendChild(marker);
400
- setTimeout(() => marker.remove(), 500);
401
- }
402
-
403
385
  function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): void {
404
386
  const proto =
405
387
  el instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
@@ -414,7 +396,6 @@ function canSelectText(el: HTMLInputElement): boolean {
414
396
  function insertText(text: string): void {
415
397
  const el = document.activeElement as HTMLElement | null;
416
398
  if (!el) return;
417
- highlight(el);
418
399
  if (el instanceof HTMLInputElement && !canSelectText(el)) {
419
400
  setNativeValue(el, text);
420
401
  el.dispatchEvent(
@@ -665,20 +646,45 @@ function getOuterHTML(params: CdpParams<"DOM.getOuterHTML">): Protocol.DOM.GetOu
665
646
 
666
647
  function focusNode(params: CdpParams<"DOM.focus">): Record<string, never> {
667
648
  const element = elementForBackendId(Number(params.backendNodeId ?? params.nodeId));
668
- if (element instanceof HTMLElement) element.focus();
649
+ // preventScroll: a default focus() scrolls the element into view across every
650
+ // ancestor — including the embedding page across the iframe boundary, yanking
651
+ // the host. Revealing the element is scrollIntoViewIfNeeded's job (and it stays
652
+ // within the frame); focus must not move the parent.
653
+ if (element instanceof HTMLElement) element.focus({ preventScroll: true });
669
654
  return {};
670
655
  }
671
656
 
672
657
  function scrollIntoViewIfNeeded(
673
658
  params: CdpParams<"DOM.scrollIntoViewIfNeeded">,
674
659
  ): Record<string, never> {
675
- elementForBackendId(Number(params.backendNodeId ?? params.nodeId)).scrollIntoView({
676
- block: "center",
677
- inline: "center",
678
- });
660
+ revealWithinFrame(elementForBackendId(Number(params.backendNodeId ?? params.nodeId)));
679
661
  return {};
680
662
  }
681
663
 
664
+ /**
665
+ * Bring an element into view by scrolling its own scroll containers and the
666
+ * frame's viewport — never `Element.scrollIntoView`, which cascades across the
667
+ * iframe boundary and yanks the embedding page to re-position the frame. Honours
668
+ * "IfNeeded": a no-op when the element is already on screen, minimal scroll
669
+ * otherwise.
670
+ */
671
+ function revealWithinFrame(el: Element): void {
672
+ const container = scrollableAncestor(el);
673
+ if (container instanceof Element && container !== document.scrollingElement) {
674
+ const c = container.getBoundingClientRect();
675
+ const r = el.getBoundingClientRect();
676
+ if (r.top < c.top) container.scrollTop += r.top - c.top;
677
+ else if (r.bottom > c.bottom) container.scrollTop += r.bottom - c.bottom;
678
+ if (r.left < c.left) container.scrollLeft += r.left - c.left;
679
+ else if (r.right > c.right) container.scrollLeft += r.right - c.right;
680
+ }
681
+ // window.scrollBy scrolls this frame's own document, not the parent's.
682
+ const r = el.getBoundingClientRect();
683
+ const dy = r.top < 0 ? r.top : r.bottom > window.innerHeight ? r.bottom - window.innerHeight : 0;
684
+ const dx = r.left < 0 ? r.left : r.right > window.innerWidth ? r.right - window.innerWidth : 0;
685
+ if (dx || dy) window.scrollBy(dx, dy);
686
+ }
687
+
682
688
  function requestChildNodes(params: CdpParams<"DOM.requestChildNodes">): Record<string, never> {
683
689
  const node = registry.nodeForBackendId(Number(params.nodeId));
684
690
  if (!node) throw new Error(`No element for backendDOMNodeId=${params.nodeId}`);
@@ -1022,6 +1028,19 @@ export function startFrameAgent(options: FrameAgentOptions): void {
1022
1028
  if (started || window.parent === window) return;
1023
1029
  started = true;
1024
1030
 
1031
+ // The embedded app shipped this agent to be automated, and automation must
1032
+ // never scroll the *host* page. Element.scrollIntoView cascades across the
1033
+ // iframe boundary — it scrolls every scrollable ancestor, including the
1034
+ // embedding document — and agent-browser evals it to position elements before
1035
+ // clicking. Replace it (and the legacy scrollIntoViewIfNeeded) with a
1036
+ // frame-local reveal so commands move the frame, never the page around it.
1037
+ const revealOnly = function (this: Element): void {
1038
+ revealWithinFrame(this);
1039
+ };
1040
+ Element.prototype.scrollIntoView = revealOnly as Element["scrollIntoView"];
1041
+ const proto = Element.prototype as Element & { scrollIntoViewIfNeeded?: () => void };
1042
+ if (typeof proto.scrollIntoViewIfNeeded === "function") proto.scrollIntoViewIfNeeded = revealOnly;
1043
+
1025
1044
  const allowed = options.allowedParents;
1026
1045
 
1027
1046
  window.addEventListener("message", (event) => {