@monoes/monobrowse 1.0.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 (208) hide show
  1. package/dist/src/browser/action-builder/analyzer.d.ts +11 -0
  2. package/dist/src/browser/action-builder/analyzer.d.ts.map +1 -0
  3. package/dist/src/browser/action-builder/analyzer.js +93 -0
  4. package/dist/src/browser/action-builder/analyzer.js.map +1 -0
  5. package/dist/src/browser/action-builder/types.d.ts +46 -0
  6. package/dist/src/browser/action-builder/types.d.ts.map +1 -0
  7. package/dist/src/browser/action-builder/types.js +2 -0
  8. package/dist/src/browser/action-builder/types.js.map +1 -0
  9. package/dist/src/browser/actions.d.ts +29 -0
  10. package/dist/src/browser/actions.d.ts.map +1 -0
  11. package/dist/src/browser/actions.js +398 -0
  12. package/dist/src/browser/actions.js.map +1 -0
  13. package/dist/src/browser/adapters/gemini.d.ts +3 -0
  14. package/dist/src/browser/adapters/gemini.d.ts.map +1 -0
  15. package/dist/src/browser/adapters/gemini.js +13 -0
  16. package/dist/src/browser/adapters/gemini.js.map +1 -0
  17. package/dist/src/browser/adapters/google.d.ts +3 -0
  18. package/dist/src/browser/adapters/google.d.ts.map +1 -0
  19. package/dist/src/browser/adapters/google.js +17 -0
  20. package/dist/src/browser/adapters/google.js.map +1 -0
  21. package/dist/src/browser/adapters/index.d.ts +15 -0
  22. package/dist/src/browser/adapters/index.d.ts.map +1 -0
  23. package/dist/src/browser/adapters/index.js +18 -0
  24. package/dist/src/browser/adapters/index.js.map +1 -0
  25. package/dist/src/browser/adapters/instagram.d.ts +3 -0
  26. package/dist/src/browser/adapters/instagram.d.ts.map +1 -0
  27. package/dist/src/browser/adapters/instagram.js +14 -0
  28. package/dist/src/browser/adapters/instagram.js.map +1 -0
  29. package/dist/src/browser/adapters/linkedin.d.ts +3 -0
  30. package/dist/src/browser/adapters/linkedin.d.ts.map +1 -0
  31. package/dist/src/browser/adapters/linkedin.js +13 -0
  32. package/dist/src/browser/adapters/linkedin.js.map +1 -0
  33. package/dist/src/browser/adapters/microsoft.d.ts +3 -0
  34. package/dist/src/browser/adapters/microsoft.d.ts.map +1 -0
  35. package/dist/src/browser/adapters/microsoft.js +16 -0
  36. package/dist/src/browser/adapters/microsoft.js.map +1 -0
  37. package/dist/src/browser/adapters/x.d.ts +3 -0
  38. package/dist/src/browser/adapters/x.d.ts.map +1 -0
  39. package/dist/src/browser/adapters/x.js +13 -0
  40. package/dist/src/browser/adapters/x.js.map +1 -0
  41. package/dist/src/browser/batch.d.ts +5 -0
  42. package/dist/src/browser/batch.d.ts.map +1 -0
  43. package/dist/src/browser/batch.js +2 -0
  44. package/dist/src/browser/batch.js.map +1 -0
  45. package/dist/src/browser/browser.d.ts +15 -0
  46. package/dist/src/browser/browser.d.ts.map +1 -0
  47. package/dist/src/browser/browser.js +256 -0
  48. package/dist/src/browser/browser.js.map +1 -0
  49. package/dist/src/browser/cdp.d.ts +18 -0
  50. package/dist/src/browser/cdp.d.ts.map +1 -0
  51. package/dist/src/browser/cdp.js +153 -0
  52. package/dist/src/browser/cdp.js.map +1 -0
  53. package/dist/src/browser/console-log.d.ts +23 -0
  54. package/dist/src/browser/console-log.d.ts.map +1 -0
  55. package/dist/src/browser/console-log.js +104 -0
  56. package/dist/src/browser/console-log.js.map +1 -0
  57. package/dist/src/browser/dashboard/api-types.d.ts +50 -0
  58. package/dist/src/browser/dashboard/api-types.d.ts.map +1 -0
  59. package/dist/src/browser/dashboard/api-types.js +14 -0
  60. package/dist/src/browser/dashboard/api-types.js.map +1 -0
  61. package/dist/src/browser/dashboard/server.d.ts +11 -0
  62. package/dist/src/browser/dashboard/server.d.ts.map +1 -0
  63. package/dist/src/browser/dashboard/server.js +191 -0
  64. package/dist/src/browser/dashboard/server.js.map +1 -0
  65. package/dist/src/browser/dialog.d.ts +12 -0
  66. package/dist/src/browser/dialog.d.ts.map +1 -0
  67. package/dist/src/browser/dialog.js +53 -0
  68. package/dist/src/browser/dialog.js.map +1 -0
  69. package/dist/src/browser/emulation.d.ts +15 -0
  70. package/dist/src/browser/emulation.d.ts.map +1 -0
  71. package/dist/src/browser/emulation.js +62 -0
  72. package/dist/src/browser/emulation.js.map +1 -0
  73. package/dist/src/browser/find.d.ts +22 -0
  74. package/dist/src/browser/find.d.ts.map +1 -0
  75. package/dist/src/browser/find.js +169 -0
  76. package/dist/src/browser/find.js.map +1 -0
  77. package/dist/src/browser/har.d.ts +27 -0
  78. package/dist/src/browser/har.d.ts.map +1 -0
  79. package/dist/src/browser/har.js +161 -0
  80. package/dist/src/browser/har.js.map +1 -0
  81. package/dist/src/browser/index.d.ts +23 -0
  82. package/dist/src/browser/index.d.ts.map +1 -0
  83. package/dist/src/browser/index.js +23 -0
  84. package/dist/src/browser/index.js.map +1 -0
  85. package/dist/src/browser/network.d.ts +32 -0
  86. package/dist/src/browser/network.d.ts.map +1 -0
  87. package/dist/src/browser/network.js +214 -0
  88. package/dist/src/browser/network.js.map +1 -0
  89. package/dist/src/browser/pdf.d.ts +15 -0
  90. package/dist/src/browser/pdf.d.ts.map +1 -0
  91. package/dist/src/browser/pdf.js +27 -0
  92. package/dist/src/browser/pdf.js.map +1 -0
  93. package/dist/src/browser/profiler.d.ts +10 -0
  94. package/dist/src/browser/profiler.d.ts.map +1 -0
  95. package/dist/src/browser/profiler.js +77 -0
  96. package/dist/src/browser/profiler.js.map +1 -0
  97. package/dist/src/browser/record.d.ts +21 -0
  98. package/dist/src/browser/record.d.ts.map +1 -0
  99. package/dist/src/browser/record.js +59 -0
  100. package/dist/src/browser/record.js.map +1 -0
  101. package/dist/src/browser/screenshot.d.ts +15 -0
  102. package/dist/src/browser/screenshot.d.ts.map +1 -0
  103. package/dist/src/browser/screenshot.js +38 -0
  104. package/dist/src/browser/screenshot.js.map +1 -0
  105. package/dist/src/browser/session.d.ts +8 -0
  106. package/dist/src/browser/session.d.ts.map +1 -0
  107. package/dist/src/browser/session.js +91 -0
  108. package/dist/src/browser/session.js.map +1 -0
  109. package/dist/src/browser/snapshot.d.ts +12 -0
  110. package/dist/src/browser/snapshot.d.ts.map +1 -0
  111. package/dist/src/browser/snapshot.js +179 -0
  112. package/dist/src/browser/snapshot.js.map +1 -0
  113. package/dist/src/browser/storage.d.ts +12 -0
  114. package/dist/src/browser/storage.d.ts.map +1 -0
  115. package/dist/src/browser/storage.js +46 -0
  116. package/dist/src/browser/storage.js.map +1 -0
  117. package/dist/src/browser/tabs.d.ts +8 -0
  118. package/dist/src/browser/tabs.d.ts.map +1 -0
  119. package/dist/src/browser/tabs.js +25 -0
  120. package/dist/src/browser/tabs.js.map +1 -0
  121. package/dist/src/browser/trace.d.ts +10 -0
  122. package/dist/src/browser/trace.d.ts.map +1 -0
  123. package/dist/src/browser/trace.js +87 -0
  124. package/dist/src/browser/trace.js.map +1 -0
  125. package/dist/src/browser/types.d.ts +111 -0
  126. package/dist/src/browser/types.d.ts.map +1 -0
  127. package/dist/src/browser/types.js +16 -0
  128. package/dist/src/browser/types.js.map +1 -0
  129. package/dist/src/browser/vitals.d.ts +15 -0
  130. package/dist/src/browser/vitals.d.ts.map +1 -0
  131. package/dist/src/browser/vitals.js +116 -0
  132. package/dist/src/browser/vitals.js.map +1 -0
  133. package/dist/src/browser/wait.d.ts +4 -0
  134. package/dist/src/browser/wait.d.ts.map +1 -0
  135. package/dist/src/browser/wait.js +146 -0
  136. package/dist/src/browser/wait.js.map +1 -0
  137. package/dist/src/browser/workflow/builtin-handlers.d.ts +3 -0
  138. package/dist/src/browser/workflow/builtin-handlers.d.ts.map +1 -0
  139. package/dist/src/browser/workflow/builtin-handlers.js +343 -0
  140. package/dist/src/browser/workflow/builtin-handlers.js.map +1 -0
  141. package/dist/src/browser/workflow/engine.d.ts +10 -0
  142. package/dist/src/browser/workflow/engine.d.ts.map +1 -0
  143. package/dist/src/browser/workflow/engine.js +174 -0
  144. package/dist/src/browser/workflow/engine.js.map +1 -0
  145. package/dist/src/browser/workflow/expression.d.ts +4 -0
  146. package/dist/src/browser/workflow/expression.d.ts.map +1 -0
  147. package/dist/src/browser/workflow/expression.js +89 -0
  148. package/dist/src/browser/workflow/expression.js.map +1 -0
  149. package/dist/src/browser/workflow/index.d.ts +6 -0
  150. package/dist/src/browser/workflow/index.d.ts.map +1 -0
  151. package/dist/src/browser/workflow/index.js +6 -0
  152. package/dist/src/browser/workflow/index.js.map +1 -0
  153. package/dist/src/browser/workflow/store.d.ts +8 -0
  154. package/dist/src/browser/workflow/store.d.ts.map +1 -0
  155. package/dist/src/browser/workflow/store.js +87 -0
  156. package/dist/src/browser/workflow/store.js.map +1 -0
  157. package/dist/src/browser/workflow/types.d.ts +62 -0
  158. package/dist/src/browser/workflow/types.d.ts.map +1 -0
  159. package/dist/src/browser/workflow/types.js +2 -0
  160. package/dist/src/browser/workflow/types.js.map +1 -0
  161. package/dist/src/index.d.ts +7 -0
  162. package/dist/src/index.d.ts.map +1 -0
  163. package/dist/src/index.js +7 -0
  164. package/dist/src/index.js.map +1 -0
  165. package/dist/tsconfig.tsbuildinfo +1 -0
  166. package/package.json +56 -0
  167. package/src/browser/action-builder/analyzer.ts +127 -0
  168. package/src/browser/action-builder/types.ts +17 -0
  169. package/src/browser/actions.ts +518 -0
  170. package/src/browser/adapters/gemini.ts +15 -0
  171. package/src/browser/adapters/google.ts +20 -0
  172. package/src/browser/adapters/index.ts +33 -0
  173. package/src/browser/adapters/instagram.ts +16 -0
  174. package/src/browser/adapters/linkedin.ts +15 -0
  175. package/src/browser/adapters/microsoft.ts +19 -0
  176. package/src/browser/adapters/x.ts +15 -0
  177. package/src/browser/batch.ts +4 -0
  178. package/src/browser/browser.ts +265 -0
  179. package/src/browser/cdp.ts +156 -0
  180. package/src/browser/console-log.ts +120 -0
  181. package/src/browser/dashboard/api-types.ts +71 -0
  182. package/src/browser/dashboard/server.ts +206 -0
  183. package/src/browser/dashboard/ui.html +444 -0
  184. package/src/browser/dialog.ts +63 -0
  185. package/src/browser/emulation.ts +94 -0
  186. package/src/browser/find.ts +225 -0
  187. package/src/browser/har.ts +202 -0
  188. package/src/browser/index.ts +22 -0
  189. package/src/browser/network.ts +244 -0
  190. package/src/browser/pdf.ts +41 -0
  191. package/src/browser/profiler.ts +96 -0
  192. package/src/browser/record.ts +92 -0
  193. package/src/browser/screenshot.ts +67 -0
  194. package/src/browser/session.ts +118 -0
  195. package/src/browser/snapshot.ts +248 -0
  196. package/src/browser/storage.ts +54 -0
  197. package/src/browser/tabs.ts +37 -0
  198. package/src/browser/trace.ts +106 -0
  199. package/src/browser/types.ts +135 -0
  200. package/src/browser/vitals.ts +122 -0
  201. package/src/browser/wait.ts +178 -0
  202. package/src/browser/workflow/builtin-handlers.ts +379 -0
  203. package/src/browser/workflow/engine.ts +202 -0
  204. package/src/browser/workflow/expression.ts +107 -0
  205. package/src/browser/workflow/index.ts +5 -0
  206. package/src/browser/workflow/store.ts +87 -0
  207. package/src/browser/workflow/types.ts +62 -0
  208. package/src/index.ts +6 -0
@@ -0,0 +1,225 @@
1
+ import type { CdpClient } from './cdp.js';
2
+ import type { ElementRef } from './types.js';
3
+ import { evaluateJs } from './actions.js';
4
+ import { getObjectIdForRef } from './snapshot.js';
5
+
6
+ export type FindAction = 'click' | 'fill' | 'type' | 'hover' | 'focus' | 'check' | 'uncheck' | 'text';
7
+
8
+ export interface FindOptions {
9
+ name?: string;
10
+ exact?: boolean;
11
+ nth?: number;
12
+ first?: boolean;
13
+ last?: boolean;
14
+ }
15
+
16
+ export async function findBySelector(
17
+ client: CdpClient,
18
+ sessionId: string,
19
+ refs: Map<string, ElementRef>,
20
+ selector: string,
21
+ options: FindOptions = {}
22
+ ): Promise<ElementRef | null> {
23
+ if (options.nth !== undefined && options.nth < 1) {
24
+ throw new Error(`nth must be >= 1 (received ${options.nth})`);
25
+ }
26
+ try {
27
+ const doc = await client.send<{ root: { nodeId: number } }>('DOM.getDocument', {}, sessionId);
28
+
29
+ let targetNodeId: number;
30
+ if (options.nth !== undefined || options.last) {
31
+ const result = await client.send<{ nodeIds: number[] }>(
32
+ 'DOM.querySelectorAll', { nodeId: doc.root.nodeId, selector }, sessionId
33
+ );
34
+ const nodeIds = result.nodeIds ?? [];
35
+ if (nodeIds.length === 0) return null;
36
+ targetNodeId = options.last ? nodeIds[nodeIds.length - 1] : (nodeIds[(options.nth ?? 1) - 1] ?? 0);
37
+ } else {
38
+ const result = await client.send<{ nodeId: number }>(
39
+ 'DOM.querySelector', { nodeId: doc.root.nodeId, selector }, sessionId
40
+ );
41
+ targetNodeId = result.nodeId ?? 0;
42
+ }
43
+
44
+ if (!targetNodeId) return null;
45
+
46
+ const desc = await client.send<{ node: { backendNodeId: number } }>(
47
+ 'DOM.describeNode', { nodeId: targetNodeId }, sessionId
48
+ );
49
+ const backendDOMNodeId = desc.node?.backendNodeId;
50
+ if (!backendDOMNodeId) return null;
51
+
52
+ // Return existing ref from snapshot if this node is already indexed
53
+ const existing = [...refs.values()].find((r) => r.backendDOMNodeId === backendDOMNodeId);
54
+ if (existing) return existing;
55
+
56
+ // Synthetic ref for elements not represented in the AX tree — insert into refs so it can be used in subsequent commands
57
+ const syntheticRef: ElementRef = { ref: `sel-${backendDOMNodeId}`, role: 'generic', name: selector, nodeId: targetNodeId, backendDOMNodeId };
58
+ refs.set(syntheticRef.ref, syntheticRef);
59
+ return syntheticRef;
60
+ } catch (err: unknown) {
61
+ const msg = err instanceof Error ? err.message : String(err);
62
+ if (/invalid|illegal|SyntaxError/i.test(msg)) throw err;
63
+ return null;
64
+ }
65
+ }
66
+
67
+ export async function findByRole(
68
+ client: CdpClient,
69
+ sessionId: string,
70
+ refs: Map<string, ElementRef>,
71
+ role: string,
72
+ options: FindOptions = {}
73
+ ): Promise<ElementRef | null> {
74
+ if (options.nth !== undefined && options.nth < 1) {
75
+ throw new Error(`nth must be >= 1 (received ${options.nth})`);
76
+ }
77
+ const lowerRole = role.toLowerCase();
78
+ const candidates = [...refs.values()].filter((r) => r.role.toLowerCase() === lowerRole);
79
+
80
+ let matches = candidates;
81
+ if (options.name) {
82
+ const nameLower = options.name.toLowerCase();
83
+ matches = options.exact
84
+ ? candidates.filter((r) => r.name.toLowerCase() === nameLower)
85
+ : candidates.filter((r) => r.name.toLowerCase().includes(nameLower));
86
+ }
87
+
88
+ if (matches.length === 0) return null;
89
+ if (options.nth !== undefined) return matches[options.nth - 1] ?? null;
90
+ if (options.last) return matches[matches.length - 1];
91
+ return matches[0];
92
+ }
93
+
94
+ export async function findByText(
95
+ client: CdpClient,
96
+ sessionId: string,
97
+ refs: Map<string, ElementRef>,
98
+ text: string,
99
+ options: FindOptions = {}
100
+ ): Promise<ElementRef | null> {
101
+ if (options.nth !== undefined && options.nth < 1) {
102
+ throw new Error(`nth must be >= 1 (received ${options.nth})`);
103
+ }
104
+ const lower = text.toLowerCase();
105
+ const candidates = [...refs.values()].filter((r) =>
106
+ options.exact ? r.name.toLowerCase() === lower : r.name.toLowerCase().includes(lower)
107
+ );
108
+ if (options.nth !== undefined) return candidates[options.nth - 1] ?? null;
109
+ if (options.last) return candidates[candidates.length - 1] ?? null;
110
+ return candidates[0] ?? null;
111
+ }
112
+
113
+ export async function findByLabel(
114
+ client: CdpClient,
115
+ sessionId: string,
116
+ refs: Map<string, ElementRef>,
117
+ label: string,
118
+ options: FindOptions = {}
119
+ ): Promise<ElementRef | null> {
120
+ return findByText(client, sessionId, refs, label, options);
121
+ }
122
+
123
+ export async function findByPlaceholder(
124
+ client: CdpClient,
125
+ sessionId: string,
126
+ refs: Map<string, ElementRef>,
127
+ placeholder: string,
128
+ options: FindOptions = {}
129
+ ): Promise<ElementRef | null> {
130
+ if (options.nth !== undefined && options.nth < 1) {
131
+ throw new Error(`nth must be >= 1 (received ${options.nth})`);
132
+ }
133
+ const lower = placeholder.toLowerCase();
134
+ const candidates = [...refs.values()].filter((r) => {
135
+ const ph = (r.placeholder ?? '').toLowerCase();
136
+ return options.exact ? ph === lower : ph.includes(lower);
137
+ });
138
+ if (options.nth !== undefined) return candidates[options.nth - 1] ?? null;
139
+ if (options.last) return candidates[candidates.length - 1] ?? null;
140
+ return candidates[0] ?? null;
141
+ }
142
+
143
+ export async function findByTestId(
144
+ client: CdpClient,
145
+ sessionId: string,
146
+ testId: string
147
+ ): Promise<string | null> {
148
+ const escapedId = testId.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
149
+ const selectors = [
150
+ `[data-testid="${escapedId}"]`,
151
+ `[data-test-id="${escapedId}"]`,
152
+ `[data-test="${escapedId}"]`,
153
+ ];
154
+ for (const sel of selectors) {
155
+ const result = await evaluateJs(client, sessionId, `!!document.querySelector(${JSON.stringify(sel)})`);
156
+ if (result) return sel;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ export async function isVisible(client: CdpClient, sessionId: string, ref: ElementRef): Promise<boolean> {
162
+ const objectId = await getObjectIdForRef(client, sessionId, ref);
163
+ if (!objectId) return false;
164
+
165
+ const result = await client.send<{ result: { value?: boolean } }>('Runtime.callFunctionOn', {
166
+ functionDeclaration: `function() {
167
+ const rect = this.getBoundingClientRect();
168
+ const style = window.getComputedStyle(this);
169
+ return rect.width > 0 && rect.height > 0 &&
170
+ style.display !== 'none' &&
171
+ style.visibility !== 'hidden' &&
172
+ style.opacity !== '0';
173
+ }`,
174
+ objectId,
175
+ returnByValue: true,
176
+ }, sessionId);
177
+ return result.result?.value ?? false;
178
+ }
179
+
180
+ export async function isEnabled(client: CdpClient, sessionId: string, ref: ElementRef): Promise<boolean> {
181
+ return !ref.disabled;
182
+ }
183
+
184
+ export async function isChecked(client: CdpClient, sessionId: string, ref: ElementRef): Promise<boolean> {
185
+ const objectId = await getObjectIdForRef(client, sessionId, ref);
186
+ if (!objectId) return false;
187
+
188
+ const result = await client.send<{ result: { value?: boolean } }>('Runtime.callFunctionOn', {
189
+ functionDeclaration: 'function() { return !!this.checked; }',
190
+ objectId,
191
+ returnByValue: true,
192
+ }, sessionId);
193
+ return result.result?.value ?? false;
194
+ }
195
+
196
+ export async function scrollIntoView(client: CdpClient, sessionId: string, ref: ElementRef): Promise<void> {
197
+ const objectId = await getObjectIdForRef(client, sessionId, ref);
198
+ if (!objectId) throw new Error(`Cannot scroll: ref @${ref.ref} not found in DOM`);
199
+
200
+ await client.send('Runtime.callFunctionOn', {
201
+ functionDeclaration: 'function() { this.scrollIntoView({ behavior: "smooth", block: "center" }); }',
202
+ objectId,
203
+ returnByValue: true,
204
+ }, sessionId);
205
+ }
206
+
207
+ export async function highlightElement(client: CdpClient, sessionId: string, ref: ElementRef): Promise<void> {
208
+ const objectId = await getObjectIdForRef(client, sessionId, ref);
209
+ if (!objectId) return;
210
+
211
+ await client.send('Runtime.callFunctionOn', {
212
+ functionDeclaration: `function() {
213
+ const prev = this.style.outline;
214
+ const prevBg = this.style.backgroundColor;
215
+ this.style.outline = '3px solid #ff5722';
216
+ this.style.backgroundColor = 'rgba(255, 87, 34, 0.1)';
217
+ setTimeout(() => {
218
+ this.style.outline = prev;
219
+ this.style.backgroundColor = prevBg;
220
+ }, 2000);
221
+ }`,
222
+ objectId,
223
+ returnByValue: true,
224
+ }, sessionId);
225
+ }
@@ -0,0 +1,202 @@
1
+ import type { CdpClient } from './cdp.js';
2
+ import { writeFile } from 'fs/promises';
3
+ import { join, resolve, relative, isAbsolute } from 'path';
4
+ import { tmpdir, homedir } from 'os';
5
+
6
+ interface HarRequest {
7
+ id: string;
8
+ url: string;
9
+ method: string;
10
+ status: number;
11
+ statusText: string;
12
+ mimeType: string;
13
+ requestHeaders: Record<string, string>;
14
+ responseHeaders: Record<string, string>;
15
+ startTime: number;
16
+ endTime: number;
17
+ size: number;
18
+ encodedSize: number;
19
+ fromCache: boolean;
20
+ responseBody?: string;
21
+ bodyEncoding?: 'base64';
22
+ }
23
+
24
+ const MAX_CONCURRENT_SESSIONS = 100;
25
+ const MAX_REQUESTS_PER_SESSION = 5_000;
26
+
27
+ const _sessions = new Map<string, {
28
+ requests: Map<string, Partial<HarRequest>>;
29
+ offReq: () => void;
30
+ offResp: () => void;
31
+ offFinished: () => void;
32
+ startTime: number;
33
+ startWallMs: number;
34
+ }>();
35
+
36
+ /** Validate output path is within cwd or home dir to prevent path traversal. */
37
+ function safeOutputPath(p: string): string {
38
+ const resolved = resolve(p);
39
+ const cwd = process.cwd();
40
+ const home = homedir();
41
+ const relCwd = relative(cwd, resolved);
42
+ const relHome = relative(home, resolved);
43
+ if ((!relCwd.startsWith('..') && !isAbsolute(relCwd)) ||
44
+ (!relHome.startsWith('..') && !isAbsolute(relHome))) {
45
+ return resolved;
46
+ }
47
+ // Reject out-of-scope paths — fall back to tmpdir
48
+ return join(tmpdir(), `monomind-har-${Date.now()}.har`);
49
+ }
50
+
51
+ export async function startHarRecording(client: CdpClient, sessionId: string): Promise<void> {
52
+ if (_sessions.has(sessionId)) throw new Error('HAR recording already in progress');
53
+ if (_sessions.size >= MAX_CONCURRENT_SESSIONS) {
54
+ throw new Error(`HAR recording limit reached (max ${MAX_CONCURRENT_SESSIONS} concurrent sessions)`);
55
+ }
56
+
57
+ const requests = new Map<string, Partial<HarRequest>>();
58
+ const startTime = Date.now();
59
+ const startWallMs = startTime;
60
+ let refCdpTs = 0;
61
+
62
+ const cdpToWall = (cdpTs: number) => {
63
+ if (refCdpTs === 0) refCdpTs = cdpTs;
64
+ return startWallMs + (cdpTs - refCdpTs) * 1000;
65
+ };
66
+
67
+ const offReq = client.on('Network.requestWillBeSent', (params, sid) => {
68
+ if (sid !== sessionId) return;
69
+ // Cap to prevent unbounded memory growth on high-traffic pages
70
+ if (requests.size >= MAX_REQUESTS_PER_SESSION) return;
71
+ const p = params as { requestId: string; request: { url: string; method: string; headers: Record<string, string> }; timestamp: number };
72
+ requests.set(p.requestId, {
73
+ id: p.requestId,
74
+ url: p.request.url,
75
+ method: p.request.method,
76
+ requestHeaders: p.request.headers,
77
+ startTime: cdpToWall(p.timestamp),
78
+ fromCache: false,
79
+ });
80
+ });
81
+
82
+ const offResp = client.on('Network.responseReceived', (params, sid) => {
83
+ if (sid !== sessionId) return;
84
+ const p = params as { requestId: string; response: { url: string; status: number; statusText: string; mimeType: string; headers: Record<string, string>; fromDiskCache: boolean; fromServiceWorker: boolean }; timestamp: number };
85
+ const entry = requests.get(p.requestId);
86
+ if (entry) {
87
+ entry.status = p.response.status;
88
+ entry.statusText = p.response.statusText;
89
+ entry.mimeType = p.response.mimeType;
90
+ entry.responseHeaders = p.response.headers;
91
+ entry.fromCache = p.response.fromDiskCache || p.response.fromServiceWorker;
92
+ entry.endTime = cdpToWall(p.timestamp);
93
+ }
94
+ });
95
+
96
+ const offFinished = client.on('Network.loadingFinished', (params, sid) => {
97
+ if (sid !== sessionId) return;
98
+ const p = params as { requestId: string; encodedDataLength: number; timestamp: number };
99
+ const entry = requests.get(p.requestId);
100
+ if (entry) {
101
+ entry.encodedSize = p.encodedDataLength;
102
+ entry.endTime = cdpToWall(p.timestamp);
103
+ }
104
+ });
105
+
106
+ _sessions.set(sessionId, { requests, offReq, offResp, offFinished, startTime, startWallMs });
107
+ }
108
+
109
+ export async function stopHarRecording(
110
+ client: CdpClient,
111
+ sessionId: string,
112
+ outputPath?: string,
113
+ captureResponseBodies = false
114
+ ): Promise<string> {
115
+ const state = _sessions.get(sessionId);
116
+ if (!state) throw new Error('No active HAR recording for this session');
117
+
118
+ state.offReq();
119
+ state.offResp();
120
+ state.offFinished();
121
+ _sessions.delete(sessionId); // remove immediately after detaching listeners to prevent duplicate invocations
122
+
123
+ // Optionally fetch response bodies
124
+ if (captureResponseBodies) {
125
+ for (const [reqId, entry] of state.requests.entries()) {
126
+ try {
127
+ const body = await client.send<{ body: string; base64Encoded: boolean }>(
128
+ 'Network.getResponseBody', { requestId: reqId }, sessionId
129
+ );
130
+ entry.responseBody = body.body;
131
+ entry.bodyEncoding = body.base64Encoded ? 'base64' : undefined;
132
+ entry.size = body.base64Encoded
133
+ ? Buffer.byteLength(body.body, 'base64')
134
+ : Buffer.byteLength(body.body, 'utf8');
135
+ } catch {
136
+ // body may not be available for cached/redirected responses
137
+ }
138
+ }
139
+ }
140
+
141
+ const har = buildHar(Array.from(state.requests.values()), state.startTime);
142
+ const safePath = outputPath ? safeOutputPath(outputPath) : join(tmpdir(), `monomind-har-${Date.now()}.har`);
143
+ await writeFile(safePath, JSON.stringify(har, null, 2));
144
+ return safePath;
145
+ }
146
+
147
+ export function getHarStatus(sessionId: string): { recording: boolean; requestCount: number } {
148
+ const state = _sessions.get(sessionId);
149
+ return { recording: !!state, requestCount: state?.requests.size ?? 0 };
150
+ }
151
+
152
+ export function getRequests(sessionId: string): Partial<HarRequest>[] {
153
+ const state = _sessions.get(sessionId);
154
+ return state ? Array.from(state.requests.values()) : [];
155
+ }
156
+
157
+ function buildHar(entries: Partial<HarRequest>[], startTime: number) {
158
+ return {
159
+ log: {
160
+ version: '1.2',
161
+ creator: { name: 'monomind browse', version: '1.0.0' },
162
+ pages: [{
163
+ startedDateTime: new Date(startTime).toISOString(),
164
+ id: 'page_1',
165
+ title: '',
166
+ pageTimings: {},
167
+ }],
168
+ entries: entries.map((e) => ({
169
+ startedDateTime: new Date(e.startTime ?? startTime).toISOString(),
170
+ time: (e.endTime ?? e.startTime ?? startTime) - (e.startTime ?? startTime),
171
+ request: {
172
+ method: e.method ?? 'GET',
173
+ url: e.url ?? '',
174
+ httpVersion: 'HTTP/1.1',
175
+ cookies: [],
176
+ headers: Object.entries(e.requestHeaders ?? {}).map(([name, value]) => ({ name, value })),
177
+ queryString: [],
178
+ headersSize: -1,
179
+ bodySize: -1,
180
+ },
181
+ response: {
182
+ status: e.status ?? 0,
183
+ statusText: e.statusText ?? '',
184
+ httpVersion: 'HTTP/1.1',
185
+ cookies: [],
186
+ headers: Object.entries(e.responseHeaders ?? {}).map(([name, value]) => ({ name, value })),
187
+ content: {
188
+ size: e.size ?? -1,
189
+ mimeType: e.mimeType ?? 'application/octet-stream',
190
+ text: e.responseBody,
191
+ encoding: e.bodyEncoding,
192
+ },
193
+ redirectURL: '',
194
+ headersSize: -1,
195
+ bodySize: e.encodedSize ?? -1,
196
+ },
197
+ cache: {},
198
+ timings: { send: 0, wait: 0, receive: 0 },
199
+ })),
200
+ },
201
+ };
202
+ }
@@ -0,0 +1,22 @@
1
+ export * from './types.js';
2
+ export * from './cdp.js';
3
+ export * from './browser.js';
4
+ export * from './snapshot.js';
5
+ export * from './actions.js';
6
+ export * from './wait.js';
7
+ export * from './network.js';
8
+ export * from './screenshot.js';
9
+ export * from './session.js';
10
+ export * from './tabs.js';
11
+ export * from './dialog.js';
12
+ export * from './console-log.js';
13
+ export * from './storage.js';
14
+ export * from './find.js';
15
+ export * from './pdf.js';
16
+ export * from './emulation.js';
17
+ export * from './batch.js';
18
+ export * from './record.js';
19
+ export * from './trace.js';
20
+ export * from './profiler.js';
21
+ export * from './vitals.js';
22
+ export * from './har.js';
@@ -0,0 +1,244 @@
1
+ import type { CdpClient } from './cdp.js';
2
+ import type { CdpCookie, NetworkRoute } from './types.js';
3
+
4
+ export async function getCookies(client: CdpClient, sessionId: string): Promise<CdpCookie[]> {
5
+ const result = await client.send<{ cookies: CdpCookie[] }>('Network.getAllCookies', {}, sessionId);
6
+ return result.cookies ?? [];
7
+ }
8
+
9
+ export async function setCookies(client: CdpClient, sessionId: string, cookies: CdpCookie[]): Promise<void> {
10
+ await client.send('Network.setCookies', { cookies }, sessionId);
11
+ }
12
+
13
+ export async function clearCookies(client: CdpClient, sessionId: string): Promise<void> {
14
+ await client.send('Network.clearBrowserCookies', {}, sessionId);
15
+ }
16
+
17
+ export async function setExtraHeaders(
18
+ client: CdpClient,
19
+ sessionId: string,
20
+ headers: Record<string, string>
21
+ ): Promise<void> {
22
+ await client.send('Network.setExtraHTTPHeaders', { headers }, sessionId);
23
+ }
24
+
25
+ export async function enableInterception(client: CdpClient, sessionId: string): Promise<void> {
26
+ await client.send('Fetch.enable', {
27
+ patterns: [{ requestStage: 'Request' }],
28
+ }, sessionId);
29
+ }
30
+
31
+ const _routeListeners = new Map<string, () => void>();
32
+
33
+ export async function setupRoutes(
34
+ client: CdpClient,
35
+ sessionId: string,
36
+ routes: NetworkRoute[]
37
+ ): Promise<void> {
38
+ // Remove previous route listener for this session before registering a new one
39
+ const prevOff = _routeListeners.get(sessionId);
40
+ if (prevOff) { prevOff(); _routeListeners.delete(sessionId); }
41
+
42
+ if (routes.length === 0) {
43
+ await client.send('Fetch.disable', {}, sessionId).catch(() => {});
44
+ return;
45
+ }
46
+
47
+ // Pre-compile route regexes once — avoids creating new RegExp on every paused request
48
+ const compiledRoutes = routes.map((r) => ({ route: r, regex: globToRegex(r.pattern) }));
49
+
50
+ // Register listener BEFORE Fetch.enable to avoid race where Chrome emits
51
+ // Fetch.requestPaused before the handler is in place, leaving requests hung
52
+ const off = client.on('Fetch.requestPaused', async (params, sid) => {
53
+ if (sid !== sessionId) return;
54
+
55
+ const { requestId, request } = params as { requestId: string; request: { url: string } };
56
+ try {
57
+ const matchedRoute = compiledRoutes.find((e) => e.regex.test(request.url))?.route;
58
+
59
+ if (!matchedRoute) {
60
+ await client.send('Fetch.continueRequest', { requestId }, sessionId);
61
+ return;
62
+ }
63
+
64
+ switch (matchedRoute.action) {
65
+ case 'abort':
66
+ await client.send('Fetch.failRequest', { requestId, errorReason: 'Failed' }, sessionId);
67
+ break;
68
+ case 'fulfill':
69
+ await client.send('Fetch.fulfillRequest', {
70
+ requestId,
71
+ responseCode: matchedRoute.response?.status ?? 200,
72
+ responseHeaders: Object.entries(matchedRoute.response?.headers ?? {}).map(([name, value]) => ({ name, value })),
73
+ body: matchedRoute.response?.body ? Buffer.from(matchedRoute.response.body).toString('base64') : '',
74
+ }, sessionId);
75
+ break;
76
+ case 'continue':
77
+ default:
78
+ await client.send('Fetch.continueRequest', { requestId }, sessionId);
79
+ break;
80
+ }
81
+ } catch {
82
+ // Best-effort: resume the request so it doesn't hang in Chrome
83
+ await client.send('Fetch.continueRequest', { requestId }, sessionId).catch(() => {});
84
+ }
85
+ });
86
+ _routeListeners.set(sessionId, off);
87
+
88
+ await client.send('Fetch.enable', {
89
+ patterns: routes.map((r) => ({ urlPattern: globToFetchPattern(r.pattern), requestStage: 'Request' })),
90
+ }, sessionId);
91
+ }
92
+
93
+ type CapturedRequest = {
94
+ id: string; url: string; method: string; status?: number; mimeType?: string;
95
+ requestHeaders?: Record<string, string>; responseHeaders?: Record<string, string>;
96
+ startTime: number; endTime?: number; encodedSize?: number;
97
+ };
98
+
99
+ const _capturedRequests = new Map<string, Map<string, CapturedRequest>>();
100
+
101
+ const _captureListeners = new Map<string, Array<() => void>>();
102
+
103
+ export function startRequestCapture(client: CdpClient, sessionId: string): void {
104
+ stopRequestCapture(sessionId);
105
+ _capturedRequests.set(sessionId, new Map<string, CapturedRequest>());
106
+ client.send('Network.enable', {}, sessionId).catch(() => {});
107
+
108
+ // Use indirection so clearCapturedRequests (which replaces the Map) affects live listeners
109
+ const idx = () => _capturedRequests.get(sessionId)!;
110
+
111
+ const offReq = client.on('Network.requestWillBeSent', (params, sid) => {
112
+ if (sid !== sessionId) return;
113
+ const p = params as { requestId: string; request: { url: string; method: string; headers: Record<string, string> }; timestamp: number };
114
+ idx().set(p.requestId, { id: p.requestId, url: p.request.url, method: p.request.method, requestHeaders: p.request.headers, startTime: p.timestamp * 1000 });
115
+ });
116
+
117
+ const offResp = client.on('Network.responseReceived', (params, sid) => {
118
+ if (sid !== sessionId) return;
119
+ const p = params as { requestId: string; response: { status: number; mimeType: string; headers: Record<string, string> }; timestamp: number };
120
+ const entry = idx().get(p.requestId);
121
+ if (entry) { entry.status = p.response.status; entry.mimeType = p.response.mimeType; entry.responseHeaders = p.response.headers; entry.endTime = p.timestamp * 1000; }
122
+ });
123
+
124
+ const offFinished = client.on('Network.loadingFinished', (params, sid) => {
125
+ if (sid !== sessionId) return;
126
+ const p = params as { requestId: string; encodedDataLength: number; timestamp: number };
127
+ const entry = idx().get(p.requestId);
128
+ if (entry) { entry.encodedSize = p.encodedDataLength; entry.endTime = p.timestamp * 1000; }
129
+ });
130
+
131
+ const offFailed = client.on('Network.loadingFailed', (params, sid) => {
132
+ if (sid !== sessionId) return;
133
+ const p = params as { requestId: string; timestamp: number };
134
+ const entry = idx().get(p.requestId);
135
+ if (entry) { entry.endTime = p.timestamp * 1000; }
136
+ });
137
+
138
+ _captureListeners.set(sessionId, [offReq, offResp, offFinished, offFailed]);
139
+ }
140
+
141
+ export function stopRequestCapture(sessionId: string): void {
142
+ const offs = _captureListeners.get(sessionId);
143
+ if (offs) { for (const off of offs) off(); _captureListeners.delete(sessionId); }
144
+ _capturedRequests.delete(sessionId);
145
+ }
146
+
147
+ export function getCapturedRequests(sessionId: string): CapturedRequest[] {
148
+ return [...(_capturedRequests.get(sessionId)?.values() ?? [])];
149
+ }
150
+
151
+ export function clearCapturedRequests(sessionId: string): void {
152
+ _capturedRequests.set(sessionId, new Map());
153
+ }
154
+
155
+ export async function disableInterception(client: CdpClient, sessionId: string): Promise<void> {
156
+ const prevOff = _routeListeners.get(sessionId);
157
+ if (prevOff) { prevOff(); _routeListeners.delete(sessionId); }
158
+ await client.send('Fetch.disable', {}, sessionId);
159
+ }
160
+
161
+ export function teardownRouteInterception(sessionId: string): void {
162
+ const prevOff = _routeListeners.get(sessionId);
163
+ if (prevOff) { prevOff(); _routeListeners.delete(sessionId); }
164
+ }
165
+
166
+ export async function getLocalStorage(client: CdpClient, sessionId: string): Promise<Record<string, string>> {
167
+ const result = await client.send<{ result: { value?: string }; exceptionDetails?: { text: string; exception?: { description?: string } } }>('Runtime.evaluate', {
168
+ expression: 'JSON.stringify(Object.fromEntries(Object.entries(localStorage)))',
169
+ returnByValue: true,
170
+ }, sessionId);
171
+ if (result.exceptionDetails) return {};
172
+ try {
173
+ return JSON.parse(result.result?.value ?? '{}');
174
+ } catch {
175
+ return {};
176
+ }
177
+ }
178
+
179
+ export async function setLocalStorage(
180
+ client: CdpClient,
181
+ sessionId: string,
182
+ data: Record<string, string>
183
+ ): Promise<void> {
184
+ const script = Object.entries(data)
185
+ .map(([k, v]) => `localStorage.setItem(${JSON.stringify(k)}, ${JSON.stringify(v)})`)
186
+ .join('; ');
187
+ if (script) {
188
+ const result = await client.send<{
189
+ result: unknown;
190
+ exceptionDetails?: { text: string; exception?: { description?: string } };
191
+ }>('Runtime.evaluate', { expression: script, returnByValue: true }, sessionId);
192
+ if (result.exceptionDetails) {
193
+ throw new Error(`setLocalStorage failed: ${result.exceptionDetails.exception?.description ?? result.exceptionDetails.text}`);
194
+ }
195
+ }
196
+ }
197
+
198
+ export async function getSessionStorage(client: CdpClient, sessionId: string): Promise<Record<string, string>> {
199
+ const result = await client.send<{ result: { value?: string }; exceptionDetails?: { text: string; exception?: { description?: string } } }>('Runtime.evaluate', {
200
+ expression: 'JSON.stringify(Object.fromEntries(Object.entries(sessionStorage)))',
201
+ returnByValue: true,
202
+ }, sessionId);
203
+ if (result.exceptionDetails) return {};
204
+ try {
205
+ return JSON.parse(result.result?.value ?? '{}');
206
+ } catch {
207
+ return {};
208
+ }
209
+ }
210
+
211
+ export async function setSessionStorage(
212
+ client: CdpClient,
213
+ sessionId: string,
214
+ data: Record<string, string>
215
+ ): Promise<void> {
216
+ const script = Object.entries(data)
217
+ .map(([k, v]) => `sessionStorage.setItem(${JSON.stringify(k)}, ${JSON.stringify(v)})`)
218
+ .join('; ');
219
+ if (script) {
220
+ const result = await client.send<{
221
+ result: unknown;
222
+ exceptionDetails?: { text: string; exception?: { description?: string } };
223
+ }>('Runtime.evaluate', { expression: script, returnByValue: true }, sessionId);
224
+ if (result.exceptionDetails) {
225
+ throw new Error(`setSessionStorage failed: ${result.exceptionDetails.exception?.description ?? result.exceptionDetails.text}`);
226
+ }
227
+ }
228
+ }
229
+
230
+ function globToRegex(pattern: string): RegExp {
231
+ // Chrome's Fetch urlPattern treats '*' as matching everything including '/'
232
+ // and '?' as any single character — match both behaviors here to avoid divergence
233
+ const escaped = pattern
234
+ .replace(/[-[\]{}()+.,\\^$|#\s]/g, '\\$&')
235
+ .replace(/\*\*/g, '\x00')
236
+ .replace(/\*/g, '[^/]*')
237
+ .replace(/\x00/g, '.*')
238
+ .replace(/\?/g, '[^/]');
239
+ return new RegExp(`^${escaped}$`);
240
+ }
241
+
242
+ function globToFetchPattern(pattern: string): string {
243
+ return pattern.replace(/\*\*/g, '*');
244
+ }