@silbercue/chrome 0.2.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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/build/cache/a11y-tree.d.ts +252 -0
  4. package/build/cache/a11y-tree.js +1956 -0
  5. package/build/cache/index.d.ts +8 -0
  6. package/build/cache/index.js +4 -0
  7. package/build/cache/selector-cache.d.ts +47 -0
  8. package/build/cache/selector-cache.js +119 -0
  9. package/build/cache/session-defaults.d.ts +27 -0
  10. package/build/cache/session-defaults.js +130 -0
  11. package/build/cache/tab-state-cache.d.ts +39 -0
  12. package/build/cache/tab-state-cache.js +171 -0
  13. package/build/cdp/cdp-client.d.ts +25 -0
  14. package/build/cdp/cdp-client.js +146 -0
  15. package/build/cdp/chrome-launcher.d.ts +85 -0
  16. package/build/cdp/chrome-launcher.js +502 -0
  17. package/build/cdp/console-collector.d.ts +53 -0
  18. package/build/cdp/console-collector.js +147 -0
  19. package/build/cdp/debug.d.ts +1 -0
  20. package/build/cdp/debug.js +6 -0
  21. package/build/cdp/dialog-handler.d.ts +54 -0
  22. package/build/cdp/dialog-handler.js +129 -0
  23. package/build/cdp/dom-watcher.d.ts +45 -0
  24. package/build/cdp/dom-watcher.js +195 -0
  25. package/build/cdp/emulation.d.ts +12 -0
  26. package/build/cdp/emulation.js +17 -0
  27. package/build/cdp/index.d.ts +11 -0
  28. package/build/cdp/index.js +6 -0
  29. package/build/cdp/network-collector.d.ts +77 -0
  30. package/build/cdp/network-collector.js +257 -0
  31. package/build/cdp/protocol.d.ts +20 -0
  32. package/build/cdp/protocol.js +1 -0
  33. package/build/cdp/session-manager.d.ts +62 -0
  34. package/build/cdp/session-manager.js +205 -0
  35. package/build/cdp/settle.d.ts +16 -0
  36. package/build/cdp/settle.js +71 -0
  37. package/build/cli/license-commands.d.ts +19 -0
  38. package/build/cli/license-commands.js +199 -0
  39. package/build/cli/top-level-commands.d.ts +49 -0
  40. package/build/cli/top-level-commands.js +222 -0
  41. package/build/hooks/index.d.ts +2 -0
  42. package/build/hooks/index.js +1 -0
  43. package/build/hooks/pro-hooks.d.ts +126 -0
  44. package/build/hooks/pro-hooks.js +17 -0
  45. package/build/index.d.ts +4 -0
  46. package/build/index.js +86 -0
  47. package/build/license/free-tier-config.d.ts +14 -0
  48. package/build/license/free-tier-config.js +18 -0
  49. package/build/license/index.d.ts +4 -0
  50. package/build/license/index.js +2 -0
  51. package/build/license/license-status.d.ts +15 -0
  52. package/build/license/license-status.js +9 -0
  53. package/build/overlay/session-overlay.d.ts +22 -0
  54. package/build/overlay/session-overlay.js +372 -0
  55. package/build/plan/index.d.ts +7 -0
  56. package/build/plan/index.js +4 -0
  57. package/build/plan/plan-conditions.d.ts +12 -0
  58. package/build/plan/plan-conditions.js +242 -0
  59. package/build/plan/plan-executor.d.ts +49 -0
  60. package/build/plan/plan-executor.js +259 -0
  61. package/build/plan/plan-state-store.d.ts +24 -0
  62. package/build/plan/plan-state-store.js +43 -0
  63. package/build/plan/plan-variables.d.ts +16 -0
  64. package/build/plan/plan-variables.js +71 -0
  65. package/build/registry.d.ts +124 -0
  66. package/build/registry.js +884 -0
  67. package/build/server.d.ts +1 -0
  68. package/build/server.js +245 -0
  69. package/build/tools/click.d.ts +34 -0
  70. package/build/tools/click.js +293 -0
  71. package/build/tools/configure-session.d.ts +15 -0
  72. package/build/tools/configure-session.js +45 -0
  73. package/build/tools/console-logs.d.ts +18 -0
  74. package/build/tools/console-logs.js +44 -0
  75. package/build/tools/dom-snapshot.d.ts +13 -0
  76. package/build/tools/dom-snapshot.js +259 -0
  77. package/build/tools/element-utils.d.ts +23 -0
  78. package/build/tools/element-utils.js +133 -0
  79. package/build/tools/error-utils.d.ts +8 -0
  80. package/build/tools/error-utils.js +27 -0
  81. package/build/tools/evaluate.d.ts +34 -0
  82. package/build/tools/evaluate.js +217 -0
  83. package/build/tools/file-upload.d.ts +20 -0
  84. package/build/tools/file-upload.js +174 -0
  85. package/build/tools/fill-form.d.ts +39 -0
  86. package/build/tools/fill-form.js +256 -0
  87. package/build/tools/handle-dialog.d.ts +15 -0
  88. package/build/tools/handle-dialog.js +48 -0
  89. package/build/tools/index.d.ts +35 -0
  90. package/build/tools/index.js +18 -0
  91. package/build/tools/navigate.d.ts +18 -0
  92. package/build/tools/navigate.js +111 -0
  93. package/build/tools/network-monitor.d.ts +18 -0
  94. package/build/tools/network-monitor.js +66 -0
  95. package/build/tools/observe.d.ts +44 -0
  96. package/build/tools/observe.js +339 -0
  97. package/build/tools/press-key.d.ts +33 -0
  98. package/build/tools/press-key.js +155 -0
  99. package/build/tools/read-page.d.ts +22 -0
  100. package/build/tools/read-page.js +100 -0
  101. package/build/tools/run-plan.d.ts +205 -0
  102. package/build/tools/run-plan.js +215 -0
  103. package/build/tools/screenshot.d.ts +16 -0
  104. package/build/tools/screenshot.js +283 -0
  105. package/build/tools/scroll.d.ts +28 -0
  106. package/build/tools/scroll.js +143 -0
  107. package/build/tools/switch-tab.d.ts +26 -0
  108. package/build/tools/switch-tab.js +355 -0
  109. package/build/tools/tab-status.d.ts +7 -0
  110. package/build/tools/tab-status.js +50 -0
  111. package/build/tools/type.d.ts +31 -0
  112. package/build/tools/type.js +247 -0
  113. package/build/tools/virtual-desk.d.ts +7 -0
  114. package/build/tools/virtual-desk.js +108 -0
  115. package/build/tools/visual-constants.d.ts +3 -0
  116. package/build/tools/visual-constants.js +10 -0
  117. package/build/tools/wait-for.d.ts +26 -0
  118. package/build/tools/wait-for.js +323 -0
  119. package/build/transport/index.d.ts +3 -0
  120. package/build/transport/index.js +2 -0
  121. package/build/transport/pipe-transport.d.ts +18 -0
  122. package/build/transport/pipe-transport.js +63 -0
  123. package/build/transport/transport.d.ts +8 -0
  124. package/build/transport/transport.js +1 -0
  125. package/build/transport/websocket-transport.d.ts +22 -0
  126. package/build/transport/websocket-transport.js +200 -0
  127. package/build/types.d.ts +21 -0
  128. package/build/types.js +1 -0
  129. package/package.json +62 -0
@@ -0,0 +1,257 @@
1
+ import { debug } from "./debug.js";
2
+ // --- NetworkCollector ---
3
+ export class NetworkCollector {
4
+ _buffer = [];
5
+ _pending = new Map();
6
+ _maxEntries;
7
+ _maxPending;
8
+ _cdpClient;
9
+ _sessionId;
10
+ _monitoring = false;
11
+ _monitoringSince = 0; // performance.now() beim Start
12
+ _callbacks = null;
13
+ constructor(cdpClient, sessionId, options) {
14
+ this._cdpClient = cdpClient;
15
+ this._sessionId = sessionId;
16
+ this._maxEntries = options?.maxEntries ?? 500;
17
+ this._maxPending = options?.maxPending ?? 200;
18
+ }
19
+ /**
20
+ * Start Network monitoring. Calls Network.enable and registers 4 event listeners.
21
+ * Idempotent — calling twice has no effect.
22
+ */
23
+ async start() {
24
+ if (this._monitoring)
25
+ return;
26
+ await this._cdpClient.send("Network.enable", {}, this._sessionId);
27
+ this._callbacks = {
28
+ requestWillBeSent: (params) => this._onRequestWillBeSent(params),
29
+ responseReceived: (params) => this._onResponseReceived(params),
30
+ loadingFinished: (params) => this._onLoadingFinished(params),
31
+ loadingFailed: (params) => this._onLoadingFailed(params),
32
+ };
33
+ this._cdpClient.on("Network.requestWillBeSent", this._callbacks.requestWillBeSent, this._sessionId);
34
+ this._cdpClient.on("Network.responseReceived", this._callbacks.responseReceived, this._sessionId);
35
+ this._cdpClient.on("Network.loadingFinished", this._callbacks.loadingFinished, this._sessionId);
36
+ this._cdpClient.on("Network.loadingFailed", this._callbacks.loadingFailed, this._sessionId);
37
+ this._monitoring = true;
38
+ this._monitoringSince = performance.now();
39
+ this._buffer = [];
40
+ this._pending = new Map();
41
+ debug("NetworkCollector started on session %s", this._sessionId);
42
+ }
43
+ /**
44
+ * Stop Network monitoring. Returns collected requests, clears buffer, calls Network.disable.
45
+ * If not monitoring, returns empty array (graceful).
46
+ */
47
+ async stop() {
48
+ if (!this._monitoring)
49
+ return [];
50
+ this._removeListeners();
51
+ // H1: Wrap Network.disable in try/catch — even if CDP is disconnected,
52
+ // the collector must reset cleanly so start() can be called again.
53
+ try {
54
+ await this._cdpClient.send("Network.disable", {}, this._sessionId);
55
+ }
56
+ catch (err) {
57
+ debug("NetworkCollector: Network.disable failed (ignored): %O", err);
58
+ }
59
+ // H3: Flush all in-flight pending requests into the buffer before clearing.
60
+ // They are marked as incomplete (status 0, size 0) so the caller can distinguish them.
61
+ this._flushPending();
62
+ this._monitoring = false;
63
+ const result = [...this._buffer];
64
+ this._buffer = [];
65
+ this._pending.clear();
66
+ debug("NetworkCollector stopped, returning %d requests", result.length);
67
+ return result;
68
+ }
69
+ /**
70
+ * Remove event listeners without calling Network.disable (for shutdown — sync, no CDP call).
71
+ */
72
+ detach() {
73
+ this._removeListeners();
74
+ this._monitoring = false;
75
+ debug("NetworkCollector detached");
76
+ }
77
+ /**
78
+ * Re-initialize after reconnect or tab switch.
79
+ * Detaches, sets new client/session, clears buffer.
80
+ * Monitoring stays OFF — agent must call start() again.
81
+ */
82
+ reinit(cdpClient, sessionId) {
83
+ this.detach();
84
+ this._cdpClient = cdpClient;
85
+ this._sessionId = sessionId;
86
+ this._buffer = [];
87
+ this._pending = new Map();
88
+ debug("NetworkCollector reinit on session %s", sessionId);
89
+ }
90
+ /**
91
+ * Return a copy of all buffered request entries.
92
+ */
93
+ getAll() {
94
+ return [...this._buffer];
95
+ }
96
+ /**
97
+ * Return filtered request entries. Both filters are combined with AND.
98
+ * Throws if the regex pattern is invalid.
99
+ */
100
+ getFiltered(filter, pattern) {
101
+ if (!filter && !pattern)
102
+ return this.getAll();
103
+ let regex;
104
+ if (pattern) {
105
+ regex = new RegExp(pattern);
106
+ }
107
+ return this._buffer.filter((entry) => {
108
+ if (filter === "failed" && !(entry.failed || entry.status >= 400))
109
+ return false;
110
+ if (regex && !regex.test(entry.url))
111
+ return false;
112
+ return true;
113
+ });
114
+ }
115
+ get isMonitoring() {
116
+ return this._monitoring;
117
+ }
118
+ get monitoringSince() {
119
+ return this._monitoringSince;
120
+ }
121
+ get count() {
122
+ return this._buffer.length;
123
+ }
124
+ // --- Internal ---
125
+ _removeListeners() {
126
+ if (this._callbacks) {
127
+ this._cdpClient.off("Network.requestWillBeSent", this._callbacks.requestWillBeSent);
128
+ this._cdpClient.off("Network.responseReceived", this._callbacks.responseReceived);
129
+ this._cdpClient.off("Network.loadingFinished", this._callbacks.loadingFinished);
130
+ this._cdpClient.off("Network.loadingFailed", this._callbacks.loadingFailed);
131
+ this._callbacks = null;
132
+ }
133
+ }
134
+ _pushEntry(entry) {
135
+ if (this._buffer.length >= this._maxEntries) {
136
+ this._buffer.shift();
137
+ }
138
+ this._buffer.push(entry);
139
+ }
140
+ /**
141
+ * H3: Flush all pending requests into the ring buffer as incomplete entries.
142
+ * Called by stop() so in-flight requests are not silently discarded.
143
+ */
144
+ _flushPending() {
145
+ for (const [, pending] of this._pending) {
146
+ this._pushEntry({
147
+ url: pending.url,
148
+ method: pending.method,
149
+ status: pending.status ?? 0,
150
+ mimeType: pending.mimeType ?? "",
151
+ size: pending.encodedDataLength ?? 0,
152
+ duration: 0,
153
+ initiator: pending.initiator,
154
+ failed: false,
155
+ });
156
+ }
157
+ }
158
+ /**
159
+ * H2: Evict oldest pending entries when the pending map exceeds _maxPending.
160
+ * Evicted entries are pushed into the ring buffer as incomplete.
161
+ */
162
+ _evictOldestPending() {
163
+ if (this._pending.size <= this._maxPending)
164
+ return;
165
+ const excess = this._pending.size - this._maxPending;
166
+ const keys = this._pending.keys();
167
+ for (let i = 0; i < excess; i++) {
168
+ const { value: key, done } = keys.next();
169
+ if (done)
170
+ break;
171
+ const pending = this._pending.get(key);
172
+ this._pushEntry({
173
+ url: pending.url,
174
+ method: pending.method,
175
+ status: pending.status ?? 0,
176
+ mimeType: pending.mimeType ?? "",
177
+ size: pending.encodedDataLength ?? 0,
178
+ duration: 0,
179
+ initiator: pending.initiator,
180
+ failed: false,
181
+ });
182
+ this._pending.delete(key);
183
+ }
184
+ }
185
+ _finishRequest(requestId) {
186
+ const pending = this._pending.get(requestId);
187
+ if (!pending)
188
+ return;
189
+ this._pending.delete(requestId);
190
+ const entry = {
191
+ url: pending.url,
192
+ method: pending.method,
193
+ status: pending.status ?? 0,
194
+ mimeType: pending.mimeType ?? "",
195
+ size: pending.encodedDataLength ?? 0,
196
+ duration: Math.round(((pending.endTimestamp ?? pending.startTimestamp) - pending.startTimestamp) * 1000),
197
+ initiator: pending.initiator,
198
+ failed: pending.failed ?? false,
199
+ };
200
+ if (pending.errorText) {
201
+ entry.errorText = pending.errorText;
202
+ }
203
+ this._pushEntry(entry);
204
+ }
205
+ _onRequestWillBeSent(params) {
206
+ const p = params;
207
+ const requestId = p.requestId;
208
+ if (!requestId)
209
+ return;
210
+ // Redirect handling: overwrite existing pending request with same requestId
211
+ // Only the final URL matters
212
+ this._pending.set(requestId, {
213
+ url: p.request?.url ?? "",
214
+ method: p.request?.method ?? "GET",
215
+ initiator: p.initiator?.type ?? "other",
216
+ startTimestamp: p.timestamp ?? 0,
217
+ });
218
+ // H2: Enforce pending limit — evict oldest if exceeded
219
+ this._evictOldestPending();
220
+ }
221
+ _onResponseReceived(params) {
222
+ const p = params;
223
+ const requestId = p.requestId;
224
+ if (!requestId)
225
+ return;
226
+ const pending = this._pending.get(requestId);
227
+ if (!pending)
228
+ return; // Unknown requestId — ignore (data URIs, pre-flight)
229
+ pending.status = p.response?.status;
230
+ pending.mimeType = p.response?.mimeType;
231
+ }
232
+ _onLoadingFinished(params) {
233
+ const p = params;
234
+ const requestId = p.requestId;
235
+ if (!requestId)
236
+ return;
237
+ const pending = this._pending.get(requestId);
238
+ if (!pending)
239
+ return;
240
+ pending.endTimestamp = p.timestamp;
241
+ pending.encodedDataLength = p.encodedDataLength;
242
+ this._finishRequest(requestId);
243
+ }
244
+ _onLoadingFailed(params) {
245
+ const p = params;
246
+ const requestId = p.requestId;
247
+ if (!requestId)
248
+ return;
249
+ const pending = this._pending.get(requestId);
250
+ if (!pending)
251
+ return;
252
+ pending.endTimestamp = p.timestamp;
253
+ pending.failed = true;
254
+ pending.errorText = p.errorText;
255
+ this._finishRequest(requestId);
256
+ }
257
+ }
@@ -0,0 +1,20 @@
1
+ export interface CdpRequest {
2
+ id: number;
3
+ method: string;
4
+ params?: Record<string, unknown>;
5
+ sessionId?: string;
6
+ }
7
+ export interface CdpResponse {
8
+ id: number;
9
+ result?: unknown;
10
+ error?: CdpError;
11
+ }
12
+ export interface CdpEvent {
13
+ method: string;
14
+ params?: unknown;
15
+ sessionId?: string;
16
+ }
17
+ export interface CdpError {
18
+ code: number;
19
+ message: string;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ import type { CdpClient } from "./cdp-client.js";
2
+ export interface SessionInfo {
3
+ sessionId: string;
4
+ frameId: string;
5
+ url: string;
6
+ isMain: boolean;
7
+ }
8
+ export declare class SessionManager {
9
+ private _frameToSession;
10
+ private _sessionToFrame;
11
+ private _sessionToUrl;
12
+ private _nodeToSession;
13
+ private _mainSessionId;
14
+ private _cdpClient;
15
+ private _onOopifDetachCallback;
16
+ private _onAttachedBound;
17
+ private _onDetachedBound;
18
+ constructor(cdpClient: CdpClient, mainSessionId: string);
19
+ get mainSessionId(): string;
20
+ /**
21
+ * Register a callback to be invoked when an OOPIF session is detached.
22
+ * Used by A11yTreeProcessor to clean up ref-maps (H1).
23
+ */
24
+ onOopifDetach(callback: (sessionId: string) => void): void;
25
+ /**
26
+ * Returns the session for a given backendNodeId.
27
+ * Searches all registered composite keys for this backendNodeId.
28
+ * Falls back to main session if node is not registered (graceful degradation).
29
+ */
30
+ getSessionForNode(backendNodeId: number): string;
31
+ /**
32
+ * Register a node's session association using composite key.
33
+ * Key format: "${sessionId}:${backendNodeId}" to avoid collisions between OOPIF sessions.
34
+ * Called by A11yTreeProcessor when building the tree.
35
+ */
36
+ registerNode(backendNodeId: number, sessionId: string): void;
37
+ /**
38
+ * Returns all active sessions (main + OOPIFs).
39
+ */
40
+ getAllSessions(): SessionInfo[];
41
+ /**
42
+ * Initialize auto-attach and discover existing iFrames.
43
+ * H3: Also discovers already-existing OOPIF targets on startup.
44
+ */
45
+ init(): Promise<void>;
46
+ /**
47
+ * Handle Target.attachedToTarget event.
48
+ */
49
+ private _onAttached;
50
+ /**
51
+ * Handle Target.detachedFromTarget event.
52
+ */
53
+ private _onDetached;
54
+ /**
55
+ * Clean up event listeners and clear all state.
56
+ */
57
+ detach(): void;
58
+ /**
59
+ * Re-initialize after reconnect with new CDP client and session.
60
+ */
61
+ reinit(cdpClient: CdpClient, mainSessionId: string): Promise<void>;
62
+ }
@@ -0,0 +1,205 @@
1
+ import { debug } from "./debug.js";
2
+ import { wrapCdpError } from "../tools/error-utils.js";
3
+ // --- SessionManager ---
4
+ export class SessionManager {
5
+ _frameToSession = new Map(); // frameId → sessionId
6
+ _sessionToFrame = new Map(); // sessionId → frameId
7
+ _sessionToUrl = new Map(); // sessionId → url
8
+ _nodeToSession = new Map(); // "${sessionId}:${backendNodeId}" → sessionId
9
+ _mainSessionId;
10
+ _cdpClient;
11
+ _onOopifDetachCallback = null;
12
+ // Store bound callbacks for cleanup
13
+ _onAttachedBound = null;
14
+ _onDetachedBound = null;
15
+ constructor(cdpClient, mainSessionId) {
16
+ this._cdpClient = cdpClient;
17
+ this._mainSessionId = mainSessionId;
18
+ }
19
+ get mainSessionId() {
20
+ return this._mainSessionId;
21
+ }
22
+ /**
23
+ * Register a callback to be invoked when an OOPIF session is detached.
24
+ * Used by A11yTreeProcessor to clean up ref-maps (H1).
25
+ */
26
+ onOopifDetach(callback) {
27
+ this._onOopifDetachCallback = callback;
28
+ }
29
+ /**
30
+ * Returns the session for a given backendNodeId.
31
+ * Searches all registered composite keys for this backendNodeId.
32
+ * Falls back to main session if node is not registered (graceful degradation).
33
+ */
34
+ getSessionForNode(backendNodeId) {
35
+ // Search for any key ending with `:${backendNodeId}`
36
+ for (const [key, sid] of this._nodeToSession.entries()) {
37
+ const colonIdx = key.lastIndexOf(":");
38
+ if (colonIdx !== -1 && parseInt(key.slice(colonIdx + 1), 10) === backendNodeId) {
39
+ return sid;
40
+ }
41
+ }
42
+ return this._mainSessionId;
43
+ }
44
+ /**
45
+ * Register a node's session association using composite key.
46
+ * Key format: "${sessionId}:${backendNodeId}" to avoid collisions between OOPIF sessions.
47
+ * Called by A11yTreeProcessor when building the tree.
48
+ */
49
+ registerNode(backendNodeId, sessionId) {
50
+ this._nodeToSession.set(`${sessionId}:${backendNodeId}`, sessionId);
51
+ }
52
+ /**
53
+ * Returns all active sessions (main + OOPIFs).
54
+ */
55
+ getAllSessions() {
56
+ const sessions = [
57
+ {
58
+ sessionId: this._mainSessionId,
59
+ frameId: "main",
60
+ url: "",
61
+ isMain: true,
62
+ },
63
+ ];
64
+ for (const [sessionId, frameId] of this._sessionToFrame.entries()) {
65
+ sessions.push({
66
+ sessionId,
67
+ frameId,
68
+ url: this._sessionToUrl.get(sessionId) ?? "",
69
+ isMain: false,
70
+ });
71
+ }
72
+ return sessions;
73
+ }
74
+ /**
75
+ * Initialize auto-attach and discover existing iFrames.
76
+ * H3: Also discovers already-existing OOPIF targets on startup.
77
+ */
78
+ async init() {
79
+ // Register event listeners
80
+ this._onAttachedBound = (params) => this._onAttached(params);
81
+ this._onDetachedBound = (params) => this._onDetached(params);
82
+ this._cdpClient.on("Target.attachedToTarget", this._onAttachedBound);
83
+ this._cdpClient.on("Target.detachedFromTarget", this._onDetachedBound);
84
+ // Enable auto-attach on the page session — only iFrames of THIS page
85
+ try {
86
+ await this._cdpClient.send("Target.setAutoAttach", {
87
+ autoAttach: true,
88
+ waitForDebuggerOnStart: false,
89
+ flatten: true,
90
+ }, this._mainSessionId);
91
+ }
92
+ catch (err) {
93
+ debug("SessionManager: setAutoAttach failed: %s", wrapCdpError(err, "SessionManager.init"));
94
+ throw err;
95
+ }
96
+ // H3: Discover already-existing iframe targets and attach to them
97
+ try {
98
+ const { targetInfos } = await this._cdpClient.send("Target.getTargets");
99
+ const existingIframes = targetInfos.filter((t) => t.type === "iframe");
100
+ for (const iframe of existingIframes) {
101
+ // Check if we already have this target attached (auto-attach may have fired)
102
+ if (this._frameToSession.has(iframe.targetId))
103
+ continue;
104
+ try {
105
+ const { sessionId } = await this._cdpClient.send("Target.attachToTarget", { targetId: iframe.targetId, flatten: true });
106
+ // Process as if it were an auto-attach event
107
+ await this._onAttached({
108
+ sessionId,
109
+ targetInfo: iframe,
110
+ waitingForDebugger: false,
111
+ });
112
+ }
113
+ catch (err) {
114
+ debug("SessionManager: failed to attach existing iframe %s: %s", iframe.url, err);
115
+ }
116
+ }
117
+ }
118
+ catch (err) {
119
+ // Non-fatal: auto-attach will still catch new iframes
120
+ debug("SessionManager: Target.getTargets failed during init: %s", wrapCdpError(err, "SessionManager.init"));
121
+ }
122
+ debug("SessionManager initialized (auto-attach enabled on session %s, %d existing OOPIFs)", this._mainSessionId, this._sessionToFrame.size);
123
+ }
124
+ /**
125
+ * Handle Target.attachedToTarget event.
126
+ */
127
+ async _onAttached(params) {
128
+ const { sessionId, targetInfo } = params;
129
+ // Only handle iframe targets
130
+ if (targetInfo.type !== "iframe") {
131
+ debug("SessionManager ignoring non-iframe target: %s (type: %s)", targetInfo.url, targetInfo.type);
132
+ return;
133
+ }
134
+ // Store session mapping
135
+ this._sessionToFrame.set(sessionId, targetInfo.targetId);
136
+ this._frameToSession.set(targetInfo.targetId, sessionId);
137
+ this._sessionToUrl.set(sessionId, targetInfo.url);
138
+ // Enable required CDP domains on the OOPIF session
139
+ try {
140
+ await Promise.all([
141
+ this._cdpClient.send("Accessibility.enable", {}, sessionId),
142
+ this._cdpClient.send("DOM.enable", {}, sessionId),
143
+ this._cdpClient.send("Runtime.enable", {}, sessionId),
144
+ ]);
145
+ debug("OOPIF attached: %s (session: %s)", targetInfo.url, sessionId);
146
+ }
147
+ catch (err) {
148
+ debug("OOPIF domain enable failed for %s: %s", targetInfo.url, wrapCdpError(err, "SessionManager._onAttached"));
149
+ // Clean up on failure
150
+ this._sessionToFrame.delete(sessionId);
151
+ this._frameToSession.delete(targetInfo.targetId);
152
+ this._sessionToUrl.delete(sessionId);
153
+ }
154
+ }
155
+ /**
156
+ * Handle Target.detachedFromTarget event.
157
+ */
158
+ _onDetached(params) {
159
+ const { sessionId } = params;
160
+ // Remove all nodes associated with this session from nodeToSession map
161
+ for (const [key, sid] of this._nodeToSession.entries()) {
162
+ if (sid === sessionId) {
163
+ this._nodeToSession.delete(key);
164
+ }
165
+ }
166
+ // Notify cleanup callback (H1: allows A11yTreeProcessor to clean ref-maps)
167
+ if (this._onOopifDetachCallback) {
168
+ this._onOopifDetachCallback(sessionId);
169
+ }
170
+ // Remove session from frame maps
171
+ const frameId = this._sessionToFrame.get(sessionId);
172
+ if (frameId) {
173
+ this._frameToSession.delete(frameId);
174
+ }
175
+ this._sessionToFrame.delete(sessionId);
176
+ this._sessionToUrl.delete(sessionId);
177
+ debug("OOPIF detached: %s", sessionId);
178
+ }
179
+ /**
180
+ * Clean up event listeners and clear all state.
181
+ */
182
+ detach() {
183
+ if (this._onAttachedBound) {
184
+ this._cdpClient.off("Target.attachedToTarget", this._onAttachedBound);
185
+ this._onAttachedBound = null;
186
+ }
187
+ if (this._onDetachedBound) {
188
+ this._cdpClient.off("Target.detachedFromTarget", this._onDetachedBound);
189
+ this._onDetachedBound = null;
190
+ }
191
+ this._frameToSession.clear();
192
+ this._sessionToFrame.clear();
193
+ this._sessionToUrl.clear();
194
+ this._nodeToSession.clear();
195
+ }
196
+ /**
197
+ * Re-initialize after reconnect with new CDP client and session.
198
+ */
199
+ async reinit(cdpClient, mainSessionId) {
200
+ this.detach();
201
+ this._cdpClient = cdpClient;
202
+ this._mainSessionId = mainSessionId;
203
+ await this.init();
204
+ }
205
+ }
@@ -0,0 +1,16 @@
1
+ import type { CdpClient } from "./cdp-client.js";
2
+ export interface SettleOptions {
3
+ cdpClient: CdpClient;
4
+ sessionId: string;
5
+ frameId: string;
6
+ loaderId?: string;
7
+ spaNavigation?: boolean;
8
+ settleMs?: number;
9
+ timeoutMs?: number;
10
+ }
11
+ export interface SettleResult {
12
+ settled: boolean;
13
+ signal?: "networkIdle" | "networkAlmostIdle" | "timeout" | "spa";
14
+ elapsedMs: number;
15
+ }
16
+ export declare function settle(options: SettleOptions): Promise<SettleResult>;
@@ -0,0 +1,71 @@
1
+ const DEFAULT_SETTLE_MS = 500;
2
+ const DEFAULT_TIMEOUT_MS = 15_000;
3
+ export async function settle(options) {
4
+ const { cdpClient, sessionId, frameId, loaderId, spaNavigation = false, settleMs = DEFAULT_SETTLE_MS, timeoutMs = DEFAULT_TIMEOUT_MS, } = options;
5
+ const start = performance.now();
6
+ // SPA path: same-document navigation — just wait settle_ms, no lifecycle events expected
7
+ if (spaNavigation) {
8
+ await delay(Math.min(settleMs, timeoutMs));
9
+ return {
10
+ settled: true,
11
+ signal: "spa",
12
+ elapsedMs: Math.round(performance.now() - start),
13
+ };
14
+ }
15
+ // Cross-document navigation: wait for lifecycle events
16
+ return new Promise((resolve) => {
17
+ let resolved = false;
18
+ let settleTimer;
19
+ const cleanup = () => {
20
+ if (overallTimer !== undefined)
21
+ clearTimeout(overallTimer);
22
+ if (settleTimer !== undefined)
23
+ clearTimeout(settleTimer);
24
+ cdpClient.off("Page.lifecycleEvent", onLifecycleEvent);
25
+ };
26
+ const finish = (result) => {
27
+ if (resolved)
28
+ return;
29
+ resolved = true;
30
+ cleanup();
31
+ resolve(result);
32
+ };
33
+ const onLifecycleEvent = (params) => {
34
+ const event = params;
35
+ // Only listen to main frame events
36
+ if (event.frameId !== frameId)
37
+ return;
38
+ // Filter by loaderId to avoid stale events from previous navigations
39
+ if (loaderId && event.loaderId !== loaderId)
40
+ return;
41
+ if (event.name === "networkIdle" || event.name === "networkAlmostIdle") {
42
+ // Prefer networkIdle over networkAlmostIdle
43
+ const signal = event.name;
44
+ // If we already have a settleTimer running (from networkAlmostIdle)
45
+ // and now networkIdle arrives, upgrade the signal
46
+ if (settleTimer !== undefined) {
47
+ clearTimeout(settleTimer);
48
+ }
49
+ settleTimer = setTimeout(() => {
50
+ finish({
51
+ settled: true,
52
+ signal,
53
+ elapsedMs: Math.round(performance.now() - start),
54
+ });
55
+ }, settleMs);
56
+ }
57
+ };
58
+ cdpClient.on("Page.lifecycleEvent", onLifecycleEvent, sessionId);
59
+ // Overall timeout to prevent hanging
60
+ const overallTimer = setTimeout(() => {
61
+ finish({
62
+ settled: false,
63
+ signal: "timeout",
64
+ elapsedMs: Math.round(performance.now() - start),
65
+ });
66
+ }, timeoutMs);
67
+ });
68
+ }
69
+ function delay(ms) {
70
+ return new Promise((resolve) => setTimeout(resolve, ms));
71
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Parses CLI args and dispatches to the correct subcommand.
3
+ * Exported for testability — `src/index.ts` calls this.
4
+ */
5
+ export declare function runLicenseCommand(args: string[]): Promise<void>;
6
+ /**
7
+ * Parses CLI arguments and returns the parsed command.
8
+ * Pure function — exported for unit-testing the routing logic
9
+ * without side effects.
10
+ */
11
+ export declare function parseLicenseCommand(argv: string[]): {
12
+ subcommand: string | undefined;
13
+ key?: string;
14
+ };
15
+ /**
16
+ * Masks a license key for display: first 4 + "..." + last 4.
17
+ * Keys shorter than 10 characters are fully masked.
18
+ */
19
+ export declare function maskKey(key: string): string;