@xbrowser/cli 0.14.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 (55) hide show
  1. package/README.md +858 -0
  2. package/dist/admin-6UTU2RZ2.js +281 -0
  3. package/dist/admin-MDGF4CET.js +285 -0
  4. package/dist/admin-RPJJ5CAF.js +282 -0
  5. package/dist/browser-GWBH6OJK.js +46 -0
  6. package/dist/browser-I2HJZ7IP.js +48 -0
  7. package/dist/browser-R7B255ML.js +46 -0
  8. package/dist/chunk-2ONMTDLK.js +2050 -0
  9. package/dist/chunk-3RG5ZIWI.js +10 -0
  10. package/dist/chunk-43VX3TYN.js +83 -0
  11. package/dist/chunk-ATFTAKMN.js +267 -0
  12. package/dist/chunk-DESA2KMG.js +77 -0
  13. package/dist/chunk-DTJRVA76.js +206 -0
  14. package/dist/chunk-F3ZWFCJJ.js +2051 -0
  15. package/dist/chunk-FF5WHQHN.js +135 -0
  16. package/dist/chunk-HINTG75P.js +77 -0
  17. package/dist/chunk-KDYXFLAC.js +1503 -0
  18. package/dist/chunk-KTSQU4QT.js +29 -0
  19. package/dist/chunk-L53IDAWK.js +68 -0
  20. package/dist/chunk-M7CMBPCA.js +100 -0
  21. package/dist/chunk-NFGO7J2I.js +29 -0
  22. package/dist/chunk-OLB6UJ25.js +438 -0
  23. package/dist/chunk-OPRXFZVE.js +52 -0
  24. package/dist/chunk-RS6YYWTK.js +685 -0
  25. package/dist/chunk-VEDJ5XSQ.js +196 -0
  26. package/dist/chunk-VEKPHQBR.js +47 -0
  27. package/dist/chunk-VUJDJCIN.js +437 -0
  28. package/dist/chunk-YEN2ODUI.js +14 -0
  29. package/dist/chunk-ZZ2TFWIV.js +1382 -0
  30. package/dist/cli.js +11012 -0
  31. package/dist/convert-4DUWZIKH.js +205 -0
  32. package/dist/convert-EKQVHKB4.js +11 -0
  33. package/dist/daemon-client-3IJD6X4B.js +59 -0
  34. package/dist/daemon-client-GX2UYIW4.js +241 -0
  35. package/dist/daemon-client-XWSSQBEA.js +58 -0
  36. package/dist/daemon-main.js +9910 -0
  37. package/dist/extract-EGRXZSSK.js +67 -0
  38. package/dist/extract-JUOQQX4V.js +11 -0
  39. package/dist/filter-OLAE26HN.js +51 -0
  40. package/dist/filter-VID2GGZ7.js +9 -0
  41. package/dist/human-interaction-QPHNDD76.js +8 -0
  42. package/dist/index.d.ts +2313 -0
  43. package/dist/index.js +13839 -0
  44. package/dist/marketplace-FCVN5OTZ.js +706 -0
  45. package/dist/marketplace-FPT5YLKB.js +351 -0
  46. package/dist/marketplace-W545W4FR.js +706 -0
  47. package/dist/network-store-2S5HATEV.js +194 -0
  48. package/dist/network-store-BN6QEZ7R.js +196 -0
  49. package/dist/network-store-YAF5OIBH.js +12 -0
  50. package/dist/parse-action-dsl-DRSPBALP.js +72 -0
  51. package/dist/parse-action-dsl-T3DYC33D.js +74 -0
  52. package/dist/proxy-WKGUCH2C.js +7 -0
  53. package/dist/session-recorder-ILSSV2UC.js +6 -0
  54. package/dist/session-recorder-XET3DNML.js +7 -0
  55. package/package.json +111 -0
@@ -0,0 +1,2050 @@
1
+ // src/browser.ts
2
+ import { randomUUID } from "crypto";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { homedir } from "os";
6
+ import { chromium } from "playwright";
7
+
8
+ // src/cdp-interceptor/proxy.ts
9
+ import { WebSocketServer, WebSocket } from "ws";
10
+
11
+ // src/cdp-interceptor/rules/shared.ts
12
+ var PLAYWRIGHT_INTERNAL_MARKERS = [
13
+ "__commonJS",
14
+ "module.exports",
15
+ "__require",
16
+ "__toESM",
17
+ "inject_utils"
18
+ ];
19
+ function isPlaywrightInternal(code) {
20
+ return PLAYWRIGHT_INTERNAL_MARKERS.some((marker) => code.includes(marker));
21
+ }
22
+ function extractUserCode(ctx) {
23
+ if (ctx.method === "Runtime.evaluate") {
24
+ const expr = ctx.params.expression;
25
+ if (typeof expr === "string") {
26
+ if (isPlaywrightInternal(expr)) return null;
27
+ return expr;
28
+ }
29
+ }
30
+ if (ctx.method === "Runtime.callFunctionOn") {
31
+ const decl = ctx.params.functionDeclaration;
32
+ if (typeof decl === "string" && decl.includes("utilityScript.evaluate")) {
33
+ return extractAllStrings(ctx.params.arguments);
34
+ }
35
+ if (typeof decl === "string") return decl;
36
+ }
37
+ return null;
38
+ }
39
+ function extractAllStrings(rawArgs) {
40
+ if (!Array.isArray(rawArgs)) return null;
41
+ const strings = [];
42
+ for (const arg of rawArgs) {
43
+ if (arg && typeof arg === "object" && "value" in arg) {
44
+ const val = arg.value;
45
+ if (typeof val === "string" && val.length > 5 && !["true", "false"].includes(val)) {
46
+ strings.push(val);
47
+ }
48
+ }
49
+ }
50
+ return strings.length > 0 ? strings.join("\n") : null;
51
+ }
52
+
53
+ // src/cdp-interceptor/rules/dom-mutation.ts
54
+ var DOM_PATTERNS = [
55
+ // ── P0: Value/Checked (bypasses React onChange) ────────────
56
+ { pattern: /\.value\s*=\s*(?!["'\s]*$)/, name: ".value =", severity: "danger", action: "block", errorCode: -32001, suggestion: "Use page.fill(selector, value) which dispatches proper input/change events." },
57
+ { pattern: /\.checked\s*=\s*(?:true|false)/, name: ".checked =", severity: "danger", action: "block", errorCode: -32001, suggestion: "Use page.check(selector) or page.uncheck(selector)." },
58
+ { pattern: /\.indeterminate\s*=\s*true/, name: ".indeterminate =", severity: "warn", action: "block", errorCode: -32021, suggestion: "No human can set indeterminate state \u2014 remove this call." },
59
+ { pattern: /\.valueAsDate\s*=/, name: ".valueAsDate =", severity: "warn", action: "block", errorCode: -32022, suggestion: "Use page.fill() with formatted date string instead." },
60
+ { pattern: /\.valueAsNumber\s*=/, name: ".valueAsNumber =", severity: "warn", action: "block", errorCode: -32022, suggestion: "Use page.fill() with numeric string instead." },
61
+ // ── P1: Select/Option (bypasses React onChange on select) ─
62
+ { pattern: /\.selectedIndex\s*=\s*\d+/, name: ".selectedIndex =", severity: "danger", action: "block", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
63
+ { pattern: /(?:options|children)\s*\[[^\]]*\]\s*\.\s*selected\s*=\s*(?:true|false)/, name: "options[N].selected =", severity: "danger", action: "block", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
64
+ { pattern: /\.value\s*=\s*["'][^"']*["']\s*[;)]?\s*$/, name: "selectElement.value =", severity: "warn", action: "block", errorCode: -32023, suggestion: "For select elements, use page.selectOption(selector, value)." },
65
+ // ── P2: Content properties (bypasses virtual DOM diffing) ──
66
+ { pattern: /\.innerHTML\s*=/, name: ".innerHTML =", severity: "info", action: "pass", errorCode: -32007, suggestion: ".innerHTML bypasses React/Vue diffing. Use component state or page.setContent()." },
67
+ { pattern: /\.outerHTML\s*=/, name: ".outerHTML =", severity: "info", action: "pass", errorCode: -32007, suggestion: "outerHTML replacement destroys React fiber tree. Use page.setContent()." },
68
+ { pattern: /\.innerText\s*=/, name: ".innerText =", severity: "warn", action: "block", errorCode: -32024, suggestion: "Framework components should be updated via state, not innerText." },
69
+ { pattern: /\.textContent\s*=/, name: ".textContent =", severity: "warn", action: "block", errorCode: -32024, suggestion: "textContent bypasses React/Vue diffing. Use component state instead." },
70
+ { pattern: /\.outerText\s*=/, name: ".outerText =", severity: "warn", action: "block", errorCode: -32024, suggestion: "Non-standard property \u2014 use proper framework update methods." },
71
+ { pattern: /nodeValue\s*=/, name: "node.nodeValue =", severity: "info", action: "block", errorCode: -32025, suggestion: "Direct text node mutation. Use textContent instead if needed." },
72
+ // ── P3: Style properties ──────────────────────────────────
73
+ { pattern: /\.style\s*=\s*["']/, name: ".style = (string override)", severity: "info", action: "pass", errorCode: -32008, suggestion: 'Setting style as a string overwrites CSSStyleDeclaration. Use element.style.prop = "value".' },
74
+ { pattern: /\.style\.cssText\s*=/, name: ".style.cssText =", severity: "info", action: "pass", errorCode: -32008, suggestion: "style.cssText replacement destroys inline styles \u2014 use individual property set." },
75
+ { pattern: /\.style\.setProperty\s*\(/, name: ".style.setProperty()", severity: "info", action: "pass", errorCode: -32008, suggestion: "Direct style property set bypasses CSS transitions." },
76
+ { pattern: /\.style\.removeProperty\s*\(/, name: ".style.removeProperty()", severity: "info", action: "pass", errorCode: -32026, suggestion: "Style removal without user interaction is suspicious." },
77
+ { pattern: /\.style\.animation\s*=/, name: ".style.animation =", severity: "info", action: "pass", errorCode: -32027, suggestion: "Forcing animation state via CDP is detectable." },
78
+ { pattern: /\.style\.transition\s*=/, name: ".style.transition =", severity: "info", action: "pass", errorCode: -32027, suggestion: "Manipulating CSS transitions is rare in normal browsing." },
79
+ // ── P4: Class/Attribute properties ────────────────────────
80
+ { pattern: /\.className\s*=/, name: ".className =", severity: "info", action: "pass", errorCode: -32009, suggestion: "Use component state or a Playwright locator instead." },
81
+ { pattern: /\.classList\.add\s*\(/, name: ".classList.add()", severity: "info", action: "pass", errorCode: -32009, suggestion: "classList manipulation via CDP bypasses framework state tracking." },
82
+ { pattern: /\.classList\.remove\s*\(/, name: ".classList.remove()", severity: "info", action: "pass", errorCode: -32009, suggestion: "Use component state or attribute selectors instead." },
83
+ { pattern: /\.classList\.toggle\s*\(/, name: ".classList.toggle()", severity: "info", action: "pass", errorCode: -32009, suggestion: "Toggle without user interaction is detectable." },
84
+ { pattern: /\.classList\.replace\s*\(/, name: ".classList.replace()", severity: "info", action: "pass", errorCode: -32028, suggestion: "Rare operation \u2014 likely automated." },
85
+ // ── P5: Attribute manipulation ────────────────────────────
86
+ { pattern: /\.setAttribute\s*\(/, name: ".setAttribute()", severity: "info", action: "pass", errorCode: -32010, suggestion: "setAttribute bypasses framework attribute tracking. Use component state." },
87
+ { pattern: /\.removeAttribute\s*\(/, name: ".removeAttribute()", severity: "info", action: "pass", errorCode: -32010, suggestion: "Attribute removal without user interaction is suspicious." },
88
+ { pattern: /\.toggleAttribute\s*\(/, name: ".toggleAttribute()", severity: "info", action: "pass", errorCode: -32029, suggestion: "Attribute toggling via CDP is detectable." },
89
+ { pattern: /\.dataset\.\w+\s*=/, name: ".dataset.* =", severity: "info", action: "pass", errorCode: -32030, suggestion: "dataset mutations via evaluate bypass native mutation observers." },
90
+ // ── P6: Focus/Selection properties ────────────────────────
91
+ { pattern: /\.selectionStart\s*=/, name: ".selectionStart =", severity: "info", action: "pass", errorCode: -32011, suggestion: "Setting cursor position without focus is detectable." },
92
+ { pattern: /\.selectionEnd\s*=/, name: ".selectionEnd =", severity: "info", action: "pass", errorCode: -32011, suggestion: "Selection range manipulation without user input is suspicious." },
93
+ { pattern: /\.selectionDirection\s*=/, name: ".selectionDirection =", severity: "info", action: "pass", errorCode: -32031, suggestion: "Selection direction changes normally via mouse drag." },
94
+ // ── P7: Boolean properties ────────────────────────────────
95
+ { pattern: /\.disabled\s*=/, name: ".disabled =", severity: "info", action: "pass", errorCode: -32012, suggestion: "Disabling elements via CDP mid-interaction is detectable." },
96
+ { pattern: /\.readOnly\s*=/, name: ".readOnly =", severity: "info", action: "pass", errorCode: -32032, suggestion: "readOnly changes without user action." },
97
+ { pattern: /\.hidden\s*=/, name: ".hidden =", severity: "info", action: "pass", errorCode: -32012, suggestion: "Hiding elements is a common scraper tactic \u2014 detectable." },
98
+ { pattern: /\.required\s*=/, name: ".required =", severity: "info", action: "pass", errorCode: -32032, suggestion: "Validation constraint changes mid-session." },
99
+ { pattern: /\.multiple\s*=/, name: ".multiple =", severity: "info", action: "pass", errorCode: -32032, suggestion: "Multiple attribute toggle is rare in normal browsing." },
100
+ { pattern: /\.autofocus\s*=/, name: ".autofocus =", severity: "info", action: "pass", errorCode: -32032, suggestion: "autofocus changes mid-session are suspicious." },
101
+ // ── P8: Frame/Navigation properties ───────────────────────
102
+ { pattern: /\.src\s*=/, name: ".src = (on img/iframe/script)", severity: "info", action: "pass", errorCode: -32013, suggestion: "Changing src via CDP bypasses user interaction. Use page.click() on the element." },
103
+ { pattern: /\.href\s*=/, name: ".href =", severity: "info", action: "pass", errorCode: -32033, suggestion: "Changing anchor href via evaluate \u2014 use page.click() instead." },
104
+ { pattern: /\.action\s*=/, name: "form.action =", severity: "info", action: "pass", errorCode: -32034, suggestion: "Changing form action URL is highly suspicious." },
105
+ { pattern: /\.method\s*=/, name: "form.method =", severity: "info", action: "pass", errorCode: -32034, suggestion: "Form method changes without user interaction." },
106
+ { pattern: /\.target\s*=/, name: "link/area.target =", severity: "info", action: "pass", errorCode: -32034, suggestion: "Link target manipulation via CDP." },
107
+ // ── P9: Media properties ──────────────────────────────────
108
+ { pattern: /\.currentTime\s*=/, name: "media.currentTime = (seeking)", severity: "info", action: "pass", errorCode: -32014, suggestion: "Video seeking without user interaction \u2014 common scraper pattern." },
109
+ { pattern: /\.playbackRate\s*=/, name: "media.playbackRate =", severity: "info", action: "pass", errorCode: -32014, suggestion: "Changing playback speed is detectable automation signal." },
110
+ { pattern: /\.volume\s*=/, name: "media.volume =", severity: "info", action: "pass", errorCode: -32035, suggestion: "Volume setting via evaluate is suspicious." },
111
+ { pattern: /\.muted\s*=/, name: "media.muted =", severity: "info", action: "pass", errorCode: -32035, suggestion: "Muting media without user click is suspicious." },
112
+ // ── P10: Element geometry ─────────────────────────────────
113
+ { pattern: /\.scrollTop\s*=/, name: ".scrollTop =", severity: "info", action: "pass", errorCode: -32015, suggestion: "Programmatic scroll without user gesture. Use page.mouse.wheel()." },
114
+ { pattern: /\.scrollLeft\s*=/, name: ".scrollLeft =", severity: "info", action: "pass", errorCode: -32015, suggestion: "Horizontal scroll without user gesture." },
115
+ { pattern: /\.scrollTo\s*\(/, name: ".scrollTo()", severity: "info", action: "pass", errorCode: -32015, suggestion: "ScrollTo bypasses user scroll detection." },
116
+ { pattern: /\.scrollBy\s*\(/, name: ".scrollBy()", severity: "info", action: "pass", errorCode: -32015, suggestion: "ScrollBy without user gesture." },
117
+ { pattern: /\.scrollIntoView\s*\(/, name: ".scrollIntoView()", severity: "info", action: "pass", errorCode: -32015, suggestion: "scrollIntoView is a common bot pattern. Let Playwright handle scrolling." },
118
+ // ── P11: Shadow DOM ───────────────────────────────────────
119
+ { pattern: /\.shadowRoot\s*=/, name: ".shadowRoot = (override)", severity: "info", action: "block", errorCode: -32036, suggestion: "ShadowRoot is read-only \u2014 this set attempt is detectable." },
120
+ // ── P12: Force reflow / layout thrashing ──────────────────
121
+ { pattern: /\.offsetHeight\b(?!\s*===?\s*)/, name: ".offsetHeight read (forced reflow)", severity: "warn", action: "pass", errorCode: -32016, suggestion: "Reading offsetHeight triggers forced reflow \u2014 anti-crawlers detect this as layout probing." },
122
+ { pattern: /\.offsetWidth\b(?!\s*===?\s*)/, name: ".offsetWidth read (forced reflow)", severity: "warn", action: "pass", errorCode: -32016, suggestion: "Reading offsetWidth triggers forced reflow \u2014 detectable." },
123
+ { pattern: /getBoundingClientRect\s*\(/, name: "getBoundingClientRect()", severity: "warn", action: "pass", errorCode: -32037, suggestion: "getBoundingClientRect triggers reflow. Minimize calls." },
124
+ { pattern: /getComputedStyle\s*\(/, name: "getComputedStyle()", severity: "info", action: "pass", errorCode: -32038, suggestion: "getComputedStyle can trigger style recalculation." },
125
+ { pattern: /\.clientHeight\b/, name: ".clientHeight read", severity: "info", action: "pass", errorCode: -32038, suggestion: "clientHeight read triggers layout." },
126
+ { pattern: /\.clientWidth\b/, name: ".clientWidth read", severity: "info", action: "pass", errorCode: -32038, suggestion: "clientWidth read triggers layout." }
127
+ ];
128
+ var domMutationRule = {
129
+ id: "dom-mutation",
130
+ name: "DOM Property Mutation Detection (50+ setters)",
131
+ priority: 10,
132
+ canHandle(ctx) {
133
+ return ctx.method === "Runtime.evaluate" || ctx.method === "Runtime.callFunctionOn";
134
+ },
135
+ evaluate(ctx) {
136
+ const userCode = extractUserCode(ctx);
137
+ if (!userCode) return null;
138
+ for (const p of DOM_PATTERNS) {
139
+ if (p.pattern.test(userCode)) {
140
+ return {
141
+ ruleId: "dom-mutation",
142
+ action: p.action,
143
+ severity: p.severity,
144
+ reason: `Direct DOM property setter: "${p.name}". This bypasses framework event systems and is detectable as automation.`,
145
+ suggestion: p.suggestion,
146
+ errorCode: p.errorCode,
147
+ errorMessage: p.severity === "danger" ? `[CDP Firewall] ${p.name} blocked \u2014 bypasses framework reactivity` : `[CDP Firewall] ${p.name} detected \u2014 use proper interaction API`
148
+ };
149
+ }
150
+ }
151
+ return null;
152
+ }
153
+ };
154
+
155
+ // src/cdp-interceptor/rules/mouse-trajectory.ts
156
+ var TRACKER_KEY = "mouse-trajectory-tracker";
157
+ var MAX_SAMPLES = 200;
158
+ var MIN_SAMPLES_FOR_ANALYSIS = 5;
159
+ var mouseTrajectoryRule = {
160
+ id: "mouse-trajectory",
161
+ name: "Mouse Trajectory Analysis",
162
+ priority: 20,
163
+ canHandle(ctx) {
164
+ return ctx.method === "Input.dispatchMouseEvent";
165
+ },
166
+ evaluate(ctx) {
167
+ const type = ctx.params.type;
168
+ const x = ctx.params.x;
169
+ const y = ctx.params.y;
170
+ if (typeof x !== "number" || typeof y !== "number") return null;
171
+ let tracker = ctx.sessionState.get(TRACKER_KEY);
172
+ if (!tracker) {
173
+ tracker = { samples: [], lastMouseDownAt: 0, isDragging: false };
174
+ ctx.sessionState.set(TRACKER_KEY, tracker);
175
+ }
176
+ if (type === "mousePressed") {
177
+ const priorResult = analyzeTrajectory(tracker.samples);
178
+ tracker.lastMouseDownAt = Date.now();
179
+ tracker.isDragging = true;
180
+ tracker.samples = [];
181
+ return priorResult;
182
+ }
183
+ if (type === "mouseReleased") {
184
+ tracker.isDragging = false;
185
+ const result = analyzeTrajectory(tracker.samples);
186
+ tracker.samples = [];
187
+ return result;
188
+ }
189
+ if (type === "mouseMoved") {
190
+ const now = Date.now();
191
+ const prevSample = tracker.samples[tracker.samples.length - 1];
192
+ const distanceTraveled = prevSample ? prevSample.distanceTraveled + distance(prevSample.x, prevSample.y, x, y) : 0;
193
+ const sample = { x, y, timestamp: now, distanceTraveled };
194
+ tracker.samples.push(sample);
195
+ if (tracker.samples.length > MAX_SAMPLES) {
196
+ tracker.samples.shift();
197
+ }
198
+ return null;
199
+ }
200
+ if (type === "mouseReleased" && tracker.samples.length < MIN_SAMPLES_FOR_ANALYSIS) {
201
+ tracker.samples = [];
202
+ return null;
203
+ }
204
+ return null;
205
+ }
206
+ };
207
+ function analyzeTrajectory(samples) {
208
+ if (samples.length < MIN_SAMPLES_FOR_ANALYSIS) return null;
209
+ const issues = [];
210
+ const startX = samples[0].x;
211
+ const startY = samples[0].y;
212
+ const endX = samples[samples.length - 1].x;
213
+ const endY = samples[samples.length - 1].y;
214
+ const lineLength = distance(startX, startY, endX, endY);
215
+ const collinearityResult = checkCollinearity(samples, startX, startY, endX, endY, lineLength);
216
+ if (collinearityResult) {
217
+ issues.push(collinearityResult);
218
+ }
219
+ const velocityResult = checkConstantVelocity(samples);
220
+ if (velocityResult) {
221
+ issues.push(velocityResult);
222
+ }
223
+ const jitterResult = checkJitter(samples);
224
+ if (jitterResult) {
225
+ issues.push(jitterResult);
226
+ }
227
+ if (issues.length === 0) return null;
228
+ const stopX = samples[samples.length - 1].x;
229
+ const stopY = samples[samples.length - 1].y;
230
+ return {
231
+ ruleId: "mouse-trajectory",
232
+ action: "block",
233
+ severity: "danger",
234
+ reason: `Suspicious mouse trajectory: ${issues.join("; ")}`,
235
+ suggestion: `This mouse movement appears automated (straight line A\u2192B, no natural variation).
236
+ Use a humanized mouse API that generates:
237
+ - Bezier curves instead of straight lines
238
+ - Random acceleration/deceleration
239
+ - 1-3px micro-jitter per sample
240
+
241
+ Example: The 'faker' or 'ghost-cursor' libraries generate realistic mouse paths.
242
+ Target was: (${Math.round(startX)}, ${Math.round(startY)}) \u2192 (${Math.round(stopX)}, ${Math.round(stopY)})`,
243
+ errorCode: -32002,
244
+ errorMessage: "[CDP Firewall] Automated mouse trajectory blocked \u2014 appears non-human"
245
+ };
246
+ }
247
+ function distance(x1, y1, x2, y2) {
248
+ return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
249
+ }
250
+ function checkCollinearity(samples, x1, y1, x2, y2, lineLength) {
251
+ if (lineLength < 5) return null;
252
+ let maxDeviation = 0;
253
+ const dx = x2 - x1;
254
+ const dy = y2 - y1;
255
+ for (let i = 1; i < samples.length - 1; i++) {
256
+ const { x, y } = samples[i];
257
+ const cross = Math.abs(dy * x - dx * y + x2 * y1 - y2 * x1);
258
+ const dev = cross / lineLength;
259
+ if (dev > maxDeviation) maxDeviation = dev;
260
+ }
261
+ if (maxDeviation < 1.5 && lineLength > 20) {
262
+ return `Perfectly straight line (max deviation ${maxDeviation.toFixed(1)}px over ${lineLength.toFixed(0)}px)`;
263
+ }
264
+ if (maxDeviation < 0.5 && lineLength > 10) {
265
+ return `Near-perfect straight line (max deviation ${maxDeviation.toFixed(1)}px)`;
266
+ }
267
+ return null;
268
+ }
269
+ function checkConstantVelocity(samples) {
270
+ if (samples.length < 3) return null;
271
+ const speeds = [];
272
+ for (let i = 1; i < samples.length; i++) {
273
+ const d = distance(
274
+ samples[i - 1].x,
275
+ samples[i - 1].y,
276
+ samples[i].x,
277
+ samples[i].y
278
+ );
279
+ const dt = samples[i].timestamp - samples[i - 1].timestamp;
280
+ if (dt > 0) speeds.push(d / dt);
281
+ }
282
+ if (speeds.length < 2) return null;
283
+ const mean = speeds.reduce((a, b) => a + b, 0) / speeds.length;
284
+ if (mean === 0) return null;
285
+ const variance = speeds.reduce((sum, v) => sum + (v - mean) ** 2, 0) / speeds.length;
286
+ const stddev = Math.sqrt(variance);
287
+ const cv = stddev / mean;
288
+ if (cv < 0.05) {
289
+ return `Constant velocity (CV=${cv.toFixed(3)}, mean=${mean.toFixed(1)} px/ms)`;
290
+ }
291
+ return null;
292
+ }
293
+ function checkJitter(samples) {
294
+ if (samples.length < 4) return null;
295
+ let totalLateralChange = 0;
296
+ for (let i = 1; i < samples.length; i++) {
297
+ const d = distance(
298
+ samples[i - 1].x,
299
+ samples[i - 1].y,
300
+ samples[i].x,
301
+ samples[i].y
302
+ );
303
+ totalLateralChange += d;
304
+ }
305
+ const avgLateral = totalLateralChange / (samples.length - 1);
306
+ if (avgLateral < 0.3) {
307
+ return "No micro-jitter (sub-pixel precision, impossible for human)";
308
+ }
309
+ return null;
310
+ }
311
+
312
+ // src/cdp-interceptor/rules/input-keystroke.ts
313
+ var TRACKER_KEY2 = "keystroke-tracker";
314
+ var MIN_KEYS_FOR_ANALYSIS = 4;
315
+ var inputKeystrokeRule = {
316
+ id: "input-keystroke",
317
+ name: "Input Keystroke Timing Analysis",
318
+ priority: 40,
319
+ canHandle(ctx) {
320
+ return ctx.method === "Input.dispatchKeyEvent" || ctx.method === "Input.insertText";
321
+ },
322
+ evaluate(ctx) {
323
+ if (ctx.method === "Input.insertText") {
324
+ return {
325
+ ruleId: "input-keystroke",
326
+ action: "pass",
327
+ severity: "info",
328
+ reason: "Input.insertText bypasses native keyboard events. Playwright uses this internally for page.fill().",
329
+ suggestion: "Prefer page.type() with variable delay for human-like input.",
330
+ errorCode: -32004,
331
+ errorMessage: "[CDP Firewall] Input.insertText detected \u2014 note: Playwright uses this for fill()"
332
+ };
333
+ }
334
+ let tracker = ctx.sessionState.get(TRACKER_KEY2);
335
+ if (!tracker) {
336
+ tracker = { samples: [] };
337
+ ctx.sessionState.set(TRACKER_KEY2, tracker);
338
+ }
339
+ const type = ctx.params.type;
340
+ const code = ctx.params.code;
341
+ const key = ctx.params.key;
342
+ if (type === "keyDown" && key && key.length === 1) {
343
+ tracker.samples.push({
344
+ code,
345
+ key,
346
+ timestamp: Date.now(),
347
+ type
348
+ });
349
+ }
350
+ if (tracker.samples.length >= MIN_KEYS_FOR_ANALYSIS && (type === "keyUp" || type === "keyDown")) {
351
+ return analyzeKeyTiming(tracker.samples);
352
+ }
353
+ return null;
354
+ }
355
+ };
356
+ function analyzeKeyTiming(samples) {
357
+ if (samples.length < MIN_KEYS_FOR_ANALYSIS) return null;
358
+ const intervals = [];
359
+ for (let i = 1; i < samples.length; i++) {
360
+ const dt = samples[i].timestamp - samples[i - 1].timestamp;
361
+ if (dt > 0) intervals.push(dt);
362
+ }
363
+ if (intervals.length < 3) return null;
364
+ const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;
365
+ if (mean === 0) return null;
366
+ const variance = intervals.reduce((sum, v) => sum + (v - mean) ** 2, 0) / intervals.length;
367
+ const stddev = Math.sqrt(variance);
368
+ const cv = stddev / mean;
369
+ const uniqueIntervals = new Set(intervals);
370
+ const allIdentical = uniqueIntervals.size === 1;
371
+ if (allIdentical) {
372
+ return {
373
+ ruleId: "input-keystroke",
374
+ action: "block",
375
+ severity: "danger",
376
+ reason: `All ${intervals.length} keystroke intervals are exactly ${intervals[0]}ms \u2014 impossible for human typing.`,
377
+ suggestion: `Use page.fill(selector, text) instead of page.type() with delay.
378
+ Or add random variation: page.type(selector, text, {delay: 50 + Math.random() * 80}).`,
379
+ errorCode: -32004,
380
+ errorMessage: "[CDP Firewall] Constant keystroke timing detected \u2014 automated typing pattern"
381
+ };
382
+ }
383
+ if (cv < 0.08) {
384
+ return {
385
+ ruleId: "input-keystroke",
386
+ action: "block",
387
+ severity: "warn",
388
+ reason: `Unnatural keystroke timing (CV=${cv.toFixed(3)}). Human typing has CV > 0.2 on average.`,
389
+ suggestion: `Add random variation to your typing delay: page.type(selector, text, {delay: 50 + Math.random() * 80}).`,
390
+ errorCode: -32004,
391
+ errorMessage: "[CDP Firewall] Suspicious keystroke timing \u2014 likely automated"
392
+ };
393
+ }
394
+ return null;
395
+ }
396
+
397
+ // src/cdp-interceptor/rules/automation-signals.ts
398
+ var AUTOMATION_PATTERNS = [
399
+ // ── Playwright markers ─────────────────────────
400
+ { pattern: /window\s*\.\s*__playwright/, name: "window.__playwright", severity: "danger", errorCode: -32040 },
401
+ { pattern: /window\s*\.\s*__pw_[a-zA-Z]/, name: "window.__pw_*", severity: "danger", errorCode: -32040 },
402
+ { pattern: /window\s*\.\s*__pw_paused/, name: "window.__pw_paused", severity: "danger", errorCode: -32040 },
403
+ { pattern: /window\s*\.\s*playwright\b/, name: "window.playwright", severity: "danger", errorCode: -32040 },
404
+ { pattern: /window\s*\.\s*__pw_recorder/, name: "window.__pw_recorder", severity: "warn", errorCode: -32041 },
405
+ { pattern: /window\s*\.\s*__pw_trace/, name: "window.__pw_trace", severity: "warn", errorCode: -32041 },
406
+ // ── Puppeteer markers ──────────────────────────
407
+ { pattern: /window\s*\.\s*__puppeteer\b/, name: "window.__puppeteer", severity: "danger", errorCode: -32040 },
408
+ { pattern: /window\s*\.\s*__puppeteer_evaluation_script/, name: "window.__puppeteer_evaluation_script", severity: "danger", errorCode: -32040 },
409
+ { pattern: /window\s*\.\s*__puppeteer_testId/, name: "window.__puppeteer_testId", severity: "warn", errorCode: -32041 },
410
+ { pattern: /window\s*\.\s*__puppeteer_/, name: "window.__puppeteer_*", severity: "danger", errorCode: -32040 },
411
+ { pattern: /window\s*\.\s*_puppeteer\b/, name: "window._puppeteer", severity: "warn", errorCode: -32041 },
412
+ // ── Selenium / WebDriver markers ───────────────
413
+ { pattern: /window\s*\.\s*__webdriver_script_fn/, name: "window.__webdriver_script_fn", severity: "danger", errorCode: -32040 },
414
+ { pattern: /window\s*\.\s*__selenium\b/, name: "window.__selenium", severity: "danger", errorCode: -32040 },
415
+ { pattern: /window\s*\.\s*__selenium_evaluate/, name: "window.__selenium_evaluate", severity: "danger", errorCode: -32040 },
416
+ { pattern: /window\s*\.\s*__driver_evaluate/, name: "window.__driver_evaluate", severity: "warn", errorCode: -32041 },
417
+ { pattern: /window\s*\.\s*__webdriver_evaluate/, name: "window.__webdriver_evaluate", severity: "danger", errorCode: -32040 },
418
+ { pattern: /document\.\$cdc_/, name: "document.$cdc_* (Selenium marker)", severity: "danger", errorCode: -32040 },
419
+ { pattern: /document\.\$chrome_asyncScriptInfo/, name: "document.$chrome_asyncScriptInfo", severity: "danger", errorCode: -32040 },
420
+ // ── navigator.webdriver detection ─────────────
421
+ { pattern: /navigator\s*\.\s*webdriver/, name: "navigator.webdriver read", severity: "danger", errorCode: -32042 },
422
+ { pattern: /navigator\[["']webdriver["']\]/, name: 'navigator["webdriver"]', severity: "danger", errorCode: -32042 },
423
+ // ── Chrome headless API surface checks ────────
424
+ { pattern: /chrome\s*\.\s*app/, name: "chrome.app detection", severity: "danger", errorCode: -32043 },
425
+ { pattern: /chrome\s*\.\s*runtime/, name: "chrome.runtime detection", severity: "danger", errorCode: -32043 },
426
+ { pattern: /chrome\s*\.\s*loadTimes/, name: "chrome.loadTimes detection", severity: "warn", errorCode: -32044 },
427
+ { pattern: /chrome\s*\.\s*csi\b/, name: "chrome.csi detection", severity: "warn", errorCode: -32044 },
428
+ { pattern: /window\s*\.\s*chrome\b/, name: "window.chrome object probe", severity: "warn", errorCode: -32044 },
429
+ { pattern: /navigator\s*\.\s*plugins\b/, name: "navigator.plugins enumeration", severity: "warn", errorCode: -32044 },
430
+ { pattern: /navigator\s*\.\s*mimeTypes/, name: "navigator.mimeTypes enumeration", severity: "warn", errorCode: -32044 },
431
+ { pattern: /navigator\s*\.\s*hardwareConcurrency/, name: "navigator.hardwareConcurrency", severity: "warn", errorCode: -32044 },
432
+ { pattern: /navigator\s*\.\s*deviceMemory/, name: "navigator.deviceMemory", severity: "info", errorCode: -32045 },
433
+ { pattern: /navigator\s*\.\s*maxTouchPoints/, name: "navigator.maxTouchPoints", severity: "warn", errorCode: -32044 },
434
+ { pattern: /navigator\s*\.\s*languages/, name: "navigator.languages", severity: "info", errorCode: -32045 },
435
+ { pattern: /navigator\s*\.\s*platform/, name: "navigator.platform", severity: "info", errorCode: -32045 },
436
+ // ── Anti-detection injection attempts ─────────
437
+ { pattern: /navigator\.webdriver\s*=\s*(false|undefined|null)/, name: "navigator.webdriver override attempt", severity: "danger", errorCode: -32046, suggestionOverride: "Overriding navigator.webdriver is detectable. Use CDP Page.addScriptToEvaluateOnNewDocument instead." },
438
+ { pattern: /Object\.defineProperty\s*\([^)]*webdriver/, name: "Object.defineProperty navigator.webdriver", severity: "danger", errorCode: -32046, suggestionOverride: "Object.defineProperty patches are detectable via Function.prototype.toString checks." },
439
+ { pattern: /delete\s+navigator\s*\.\s*webdriver/, name: "delete navigator.webdriver", severity: "danger", errorCode: -32046, suggestionOverride: "Deleting webdriver flag is detectable \u2014 the delete itself is visible." },
440
+ // ── PhantomJS / Headless markers ──────────────
441
+ { pattern: /window\s*\.\s*callPhantom/, name: "window.callPhantom", severity: "danger", errorCode: -32040 },
442
+ { pattern: /window\s*\.\s*_phantom/, name: "window._phantom", severity: "danger", errorCode: -32040 },
443
+ { pattern: /window\s*\.\s*phantom\b/, name: "window.phantom", severity: "danger", errorCode: -32040 },
444
+ { pattern: /window\s*\.\s*Buffer\b/, name: "window.Buffer (Node.js leak)", severity: "danger", errorCode: -32040 },
445
+ { pattern: /window\s*\.\s*process\b/, name: "window.process (Node.js leak)", severity: "danger", errorCode: -32040 },
446
+ { pattern: /window\s*\.\s*global\b/, name: "window.global (Node.js leak)", severity: "danger", errorCode: -32040 },
447
+ { pattern: /window\s*\.\s*__dirname/, name: "window.__dirname (Node.js leak)", severity: "danger", errorCode: -32040 }
448
+ ];
449
+ var automationSignalsRule = {
450
+ id: "automation-signals",
451
+ name: "Browser Automation Signal Detection (35+ markers)",
452
+ priority: 20,
453
+ canHandle(ctx) {
454
+ return ctx.method === "Runtime.evaluate" || ctx.method === "Runtime.callFunctionOn" || ctx.method === "Page.addScriptToEvaluateOnNewDocument";
455
+ },
456
+ evaluate(ctx) {
457
+ if (ctx.method === "Page.addScriptToEvaluateOnNewDocument") {
458
+ const source = ctx.params.source;
459
+ if (typeof source === "string") {
460
+ for (const p of AUTOMATION_PATTERNS) {
461
+ if (p.pattern.test(source)) {
462
+ return makeDecision(p, source.substring(0, 60));
463
+ }
464
+ }
465
+ }
466
+ return null;
467
+ }
468
+ const userCode = extractUserCode(ctx);
469
+ if (!userCode) return null;
470
+ for (const p of AUTOMATION_PATTERNS) {
471
+ if (p.pattern.test(userCode)) {
472
+ return makeDecision(p, userCode.substring(0, 60));
473
+ }
474
+ }
475
+ return null;
476
+ }
477
+ };
478
+ function makeDecision(p, context) {
479
+ const suggestion = p.suggestionOverride ?? `Detected: "${p.name}" \u2014 an automation tool marker that anti-crawler systems immediately flag. Remove this pattern from your code.`;
480
+ return {
481
+ ruleId: "automation-signals",
482
+ action: "block",
483
+ severity: p.severity,
484
+ reason: `Automation marker detected: "${p.name}". Context: "${context}..."`,
485
+ suggestion,
486
+ errorCode: p.errorCode,
487
+ errorMessage: p.severity === "danger" ? `[CDP Firewall] ${p.name} blocked \u2014 automation tool marker detected` : `[CDP Firewall] ${p.name} detected \u2014 potential automation signal`
488
+ };
489
+ }
490
+
491
+ // src/cdp-interceptor/rules/fingerprinting.ts
492
+ var FP_PATTERNS = [
493
+ // ── Canvas Rendering fingerprinting ────────────────────
494
+ { pattern: /\.toDataURL\s*\(/, name: "canvas.toDataURL()", severity: "danger", errorCode: -32050, suggestion: "canvas.toDataURL() returns a unique hash that identifies the browser engine. Minimize calls." },
495
+ { pattern: /\.toBlob\s*\(/, name: "canvas.toBlob()", severity: "danger", errorCode: -32050, suggestion: "canvas.toBlob() is used for canvas fingerprinting. Avoid if possible." },
496
+ { pattern: /getImageData\s*\(/, name: "CanvasRenderingContext2D.getImageData()", severity: "danger", errorCode: -32050, suggestion: "getImageData reads pixel-level data used for fingerprinting." },
497
+ { pattern: /measureText\s*\(/, name: "CanvasRenderingContext2D.measureText()", severity: "warn", errorCode: -32051, suggestion: "Font metrics reveal installed fonts \u2014 a fingerprinting vector." },
498
+ { pattern: /OffscreenCanvas\s*\(/, name: "new OffscreenCanvas()", severity: "warn", errorCode: -32051, suggestion: "OffscreenCanvas is sometimes used to avoid visibility detection." },
499
+ { pattern: /convertToBlob\s*\(/, name: "OffscreenCanvas.convertToBlob()", severity: "warn", errorCode: -32051, suggestion: "Headless OffscreenCanvas rendering differs from real browser." },
500
+ // ── WebGL Fingerprinting ──────────────────────────────
501
+ { pattern: /getParameter\s*\([^)]*(?:VENDOR|RENDERER|VERSION)/, name: "WebGL getParameter(VENDOR/RENDERER)", severity: "danger", errorCode: -32052, suggestion: "WebGL VENDOR/RENDERER returns emulated values in headless. Cannot be reliably spoofed." },
502
+ { pattern: /getSupportedExtensions\s*\(/, name: "WebGL getSupportedExtensions()", severity: "warn", errorCode: -32053, suggestion: "WebGL extension list differs in headless mode." },
503
+ { pattern: /getShaderPrecisionFormat\s*\(/, name: "WebGL getShaderPrecisionFormat()", severity: "info", errorCode: -32054, suggestion: "Shader precision differs between headless and real GPU." },
504
+ { pattern: /UNMASKED_VENDOR_WEBGL/, name: "WEBGL_debug_renderer_info UNMASKED_VENDOR", severity: "warn", errorCode: -32053, suggestion: "Unmasked vendor info reveals the real GPU \u2014 blocked in many envs." },
505
+ { pattern: /UNMASKED_RENDERER_WEBGL/, name: "WEBGL_debug_renderer_info UNMASKED_RENDERER", severity: "warn", errorCode: -32053, suggestion: "Unmasked renderer string reveals the real GPU." },
506
+ // ── AudioContext Fingerprinting ────────────────────────
507
+ { pattern: /AnalyserNode\s*\(/, name: "new AnalyserNode()", severity: "warn", errorCode: -32055, suggestion: "Audio fingerprinting via AnalyserNode \u2014 produces silence in headless." },
508
+ { pattern: /getFloatFrequencyData\s*\(/, name: "AnalyserNode.getFloatFrequencyData()", severity: "danger", errorCode: -32050, suggestion: "Audio frequency data in headless returns silence (all zeros) \u2014 detectable." },
509
+ { pattern: /getByteFrequencyData\s*\(/, name: "AnalyserNode.getByteFrequencyData()", severity: "danger", errorCode: -32050, suggestion: "Audio byte frequency data in headless returns zeros." },
510
+ { pattern: /getByteTimeDomainData\s*\(/, name: "AnalyserNode.getByteTimeDomainData()", severity: "danger", errorCode: -32050, suggestion: "Time domain audio data in headless is a flat line \u2014 detectable." },
511
+ { pattern: /OfflineAudioContext\s*\(/, name: "new OfflineAudioContext()", severity: "warn", errorCode: -32055, suggestion: "Offline audio rendering is a known fingerprinting method." },
512
+ { pattern: /OscillatorNode\s*\(/, name: "new OscillatorNode()", severity: "info", errorCode: -32056, suggestion: "Audio oscillator used in fingerprinting probes." },
513
+ // ── Navigator property probing ───────────────────────
514
+ { pattern: /navigator\s*\.\s*connection\b/, name: "navigator.connection", severity: "info", errorCode: -32057, suggestion: 'Network connection info is used for fingerprinting (always "4g" in bots).' },
515
+ { pattern: /navigator\s*\.\s*getBattery\s*\(/, name: "navigator.getBattery()", severity: "warn", errorCode: -32058, suggestion: "Battery API is a fingerprinting vector. Returns fixed values in headless." },
516
+ { pattern: /navigator\s*\.\s*mediaDevices\s*\.\s*enumerateDevices/, name: "navigator.mediaDevices.enumerateDevices()", severity: "warn", errorCode: -32058, suggestion: "Media device enumeration returns empty/no devices in headless." },
517
+ { pattern: /navigator\s*\.\s*permissions\s*\.\s*query/, name: "navigator.permissions.query()", severity: "info", errorCode: -32059, suggestion: "Permission queries can reveal automation environment." },
518
+ // ── Screen / Window geometry probing ──────────────────
519
+ { pattern: /screen\.avail(Width|Height|Left|Top)/, name: "screen.avail*", severity: "warn", errorCode: -32060, suggestion: "screen.avail* values differ in headless (no OS chrome)." },
520
+ { pattern: /window\.outerWidth\s*-?\s*window\.innerWidth/, name: "window.outerWidth - window.innerWidth", severity: "danger", errorCode: -32050, suggestion: "This difference is 0 in headless (no browser chrome) \u2014 100% detection rate." },
521
+ { pattern: /window\.outerHeight\s*-?\s*window\.innerHeight/, name: "window.outerHeight - window.innerHeight", severity: "danger", errorCode: -32050, suggestion: "This difference is 0 in headless \u2014 immediate automation detection." },
522
+ { pattern: /screen\.width\b/, name: "screen.width", severity: "info", errorCode: -32061, suggestion: "Screen dimensions can be spoofed but inconsistencies with viewport are detectable." },
523
+ { pattern: /screen\.height\b/, name: "screen.height", severity: "info", errorCode: -32061, suggestion: "Screen dimension probes for viewport inconsistency detection." },
524
+ { pattern: /window\.devicePixelRatio/, name: "window.devicePixelRatio", severity: "warn", errorCode: -32060, suggestion: "devicePixelRatio is always 1 in headless \u2014 differs from real displays." },
525
+ { pattern: /matchMedia\s*\(/, name: "window.matchMedia()", severity: "warn", errorCode: -32060, suggestion: "matchMedia can detect CDP overridden viewport dimensions." },
526
+ // ── Performance / Timing API ──────────────────────────
527
+ { pattern: /performance\s*\.\s*now\s*\(/, name: "performance.now()", severity: "info", errorCode: -32062, suggestion: "High-resolution timer used for timing attacks and bot detection." },
528
+ { pattern: /performance\s*\.\s*memory/, name: "performance.memory", severity: "warn", errorCode: -32063, suggestion: "performance.memory shows VM memory limits in containers." },
529
+ { pattern: /performance\.getEntriesByType\s*\(\s*["']navigation["']\s*\)/, name: 'performance.getEntriesByType("navigation")', severity: "info", errorCode: -32062, suggestion: "Navigation timing reveals request pattern inconsistencies." },
530
+ { pattern: /performance\.getEntriesByType\s*\(\s*["']resource["']\s*\)/, name: 'performance.getEntriesByType("resource")', severity: "info", errorCode: -32062, suggestion: "Resource loading timing analysis for bot detection." },
531
+ // ── Font Detection ────────────────────────────────────
532
+ { pattern: /document\.fonts\.check\s*\(/, name: "document.fonts.check()", severity: "warn", errorCode: -32064, suggestion: "Font availability checks are used for fingerprinting. Installed font list is unique per user." },
533
+ { pattern: /document\.fonts\.ready/, name: "document.fonts.ready", severity: "info", errorCode: -32065, suggestion: "Font loading state probe for fingerprinting." },
534
+ // ── WebRTC / Connectivity ────────────────────────────
535
+ { pattern: /RTCPeerConnection\s*\(/, name: "new RTCPeerConnection()", severity: "warn", errorCode: -32066, suggestion: "WebRTC can leak internal IP and is used for connectivity fingerprinting." },
536
+ { pattern: /navigator\.mediaDevices\.getUserMedia/, name: "navigator.mediaDevices.getUserMedia()", severity: "info", errorCode: -32067, suggestion: "getUserMedia always fails in headless (no camera)." },
537
+ // ── Feature Consistency Checks ─────────────────────────
538
+ { pattern: /Intl\.DateTimeFormat.*resolvedOptions.*timeZone/, name: "Intl.DateTimeFormat timezone check", severity: "warn", errorCode: -32068, suggestion: "Timezone from Intl API vs Emulation.setTimezoneOverride will be inconsistent when mocked." },
539
+ { pattern: /new\s+Date\s*\(\s*\)\s*\.\s*getTimezoneOffset/, name: "Date.getTimezoneOffset()", severity: "info", errorCode: -32069, suggestion: "Timezone offset used for timing consistency cross-checks." },
540
+ { pattern: /Error\s*\(\s*\)\s*\.\s*stack/, name: "Error().stack format check", severity: "info", errorCode: -32070, suggestion: "Stack trace format differs between headless and full Chrome." },
541
+ { pattern: /Function\.prototype\.toString/, name: "Function.prototype.toString on native fn", severity: "info", errorCode: -32070, suggestion: "Native function toString() format can reveal patched APIs." }
542
+ ];
543
+ var fingerprintingRule = {
544
+ id: "fingerprinting",
545
+ name: "Browser Fingerprinting Access Detection (35+ APIs)",
546
+ priority: 30,
547
+ canHandle(ctx) {
548
+ return ctx.method === "Runtime.evaluate" || ctx.method === "Runtime.callFunctionOn";
549
+ },
550
+ evaluate(ctx) {
551
+ const userCode = extractUserCode(ctx);
552
+ if (!userCode) return null;
553
+ for (const p of FP_PATTERNS) {
554
+ if (p.pattern.test(userCode)) {
555
+ return {
556
+ ruleId: "fingerprinting",
557
+ action: "block",
558
+ severity: p.severity,
559
+ reason: `Browser fingerprinting API accessed: "${p.name}". Anti-crawler systems use this to identify your browser.`,
560
+ suggestion: p.suggestion,
561
+ errorCode: p.errorCode,
562
+ errorMessage: `[CDP Firewall] ${p.name} blocked \u2014 fingerprinting API access detected`
563
+ };
564
+ }
565
+ }
566
+ return null;
567
+ }
568
+ };
569
+
570
+ // src/cdp-interceptor/rules/event-simulation.ts
571
+ var EVENT_PATTERNS = [
572
+ // ── Direct method calls (all isTrusted=false) ─────────
573
+ { pattern: /\.click\s*\(\s*\)/, name: "el.click()", severity: "danger", errorCode: -32070, suggestion: "el.click() fires isTrusted=false events. Use page.click(selector) which uses Input.dispatchMouseEvent (isTrusted=true)." },
574
+ { pattern: /\.focus\s*\(\s*\)/, name: "el.focus()", severity: "danger", errorCode: -32071, suggestion: "el.focus() without user interaction is detectable. Use page.click(selector) which naturally focuses." },
575
+ { pattern: /\.blur\s*\(\s*\)/, name: "el.blur()", severity: "danger", errorCode: -32071, suggestion: "el.blur() without user interaction. Avoid in automation scripts." },
576
+ { pattern: /\.submit\s*\(\s*\)/, name: "el.submit()", severity: "danger", errorCode: -32072, suggestion: `form.submit() bypasses onSubmit handlers. Click the submit button: page.click('button[type="submit"]').` },
577
+ { pattern: /\.reset\s*\(\s*\)/, name: "el.reset()", severity: "danger", errorCode: -32072, suggestion: "form.reset() without user action. Let the user clear fields manually." },
578
+ { pattern: /\.select\s*\(\s*\)/, name: "el.select()", severity: "warn", errorCode: -32073, suggestion: "input.select() selects text without user interaction. Use page.click(selector) instead." },
579
+ { pattern: /dialog\.close\s*\(\s*\)|showModal\(\)/, name: "dialog.showModal()/close()", severity: "danger", errorCode: -32074, suggestion: "dialog.showModal() requires user gesture. Let the user open the dialog naturally." },
580
+ { pattern: /\.showPopover\s*\(\s*\)/, name: "el.showPopover()", severity: "warn", errorCode: -32073, suggestion: "Popover.show() requires user gesture. Simulate a click on the popover trigger." },
581
+ { pattern: /\.hidePopover\s*\(\s*\)/, name: "el.hidePopover()", severity: "warn", errorCode: -32073, suggestion: "Hiding popovers via CDP is detectable." },
582
+ { pattern: /\.requestFullscreen\s*\(\s*\)/, name: "el.requestFullscreen()", severity: "danger", errorCode: -32074, suggestion: "Fullscreen requests require user gesture. Cannot be triggered by automation." },
583
+ { pattern: /\.requestPointerLock\s*\(\s*\)/, name: "el.requestPointerLock()", severity: "danger", errorCode: -32074, suggestion: "Pointer lock requires user gesture. Blocked in automation." },
584
+ { pattern: /\.setSelectionRange\s*\(/, name: "el.setSelectionRange()", severity: "warn", errorCode: -32073, suggestion: "Setting selection range without user interaction is suspicious." },
585
+ { pattern: /\.setRangeText\s*\(/, name: "el.setRangeText()", severity: "info", errorCode: -32076, suggestion: "Range text replacement without user input is detectable." },
586
+ { pattern: /\.showPicker\s*\(\s*\)/, name: "HTMLInputElement.showPicker()", severity: "info", errorCode: -32076, suggestion: "Date/color picker shown without click \u2014 detectable." },
587
+ { pattern: /\.reportValidity\s*\(\s*\)/, name: "el.reportValidity()", severity: "info", errorCode: -32076, suggestion: "Validity reporting without form submission attempt." },
588
+ // ── dispatchEvent with synthetic events (isTrusted=false) ──
589
+ { pattern: /dispatchEvent\s*\(\s*new\s+(?:Event|CustomEvent)\s*\(/, name: "dispatchEvent(new Event/CustomEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic events have isTrusted=false. Use Input.dispatch* CDP methods for trusted events." },
590
+ { pattern: /dispatchEvent\s*\(\s*new\s+MouseEvent\s*\(/, name: "dispatchEvent(new MouseEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic mouse events (isTrusted=false). Use Input.dispatchMouseEvent CDP method." },
591
+ { pattern: /dispatchEvent\s*\(\s*new\s+KeyboardEvent\s*\(/, name: "dispatchEvent(new KeyboardEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic keyboard events (isTrusted=false). Use Input.dispatchKeyEvent CDP method." },
592
+ { pattern: /dispatchEvent\s*\(\s*new\s+FocusEvent\s*\(/, name: "dispatchEvent(new FocusEvent)", severity: "warn", errorCode: -32078, suggestion: "Synthetic focus events bypass user interaction." },
593
+ { pattern: /dispatchEvent\s*\(\s*new\s+InputEvent\s*\(/, name: "dispatchEvent(new InputEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic input events (isTrusted=false). Use page.fill() or page.type()." },
594
+ { pattern: /dispatchEvent\s*\(\s*new\s+(?:Event)\s*\(\s*["'](?:input|change|submit|reset)/, name: 'dispatchEvent("input"/"change"/"submit")', severity: "danger", errorCode: -32077, suggestion: "Synthetic input/change/submit events are trusted=false. Always detectable." },
595
+ { pattern: /dispatchEvent\s*\(\s*new\s+PointerEvent\s*\(/, name: "dispatchEvent(new PointerEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic pointer events bypass the trusted input pipeline." },
596
+ { pattern: /dispatchEvent\s*\(\s*new\s+TouchEvent\s*\(/, name: "dispatchEvent(new TouchEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic touch events on mobile are detectable." },
597
+ { pattern: /dispatchEvent\s*\(\s*new\s+WheelEvent\s*\(/, name: "dispatchEvent(new WheelEvent)", severity: "warn", errorCode: -32078, suggestion: "Synthetic scroll events (isTrusted=false). Use Input.dispatchMouseEvent with wheel type." },
598
+ { pattern: /dispatchEvent\s*\(\s*new\s+ClipboardEvent\s*\(/, name: "dispatchEvent(new ClipboardEvent)", severity: "warn", errorCode: -32078, suggestion: "Synthetic clipboard events are detectable." },
599
+ { pattern: /dispatchEvent\s*\(\s*new\s+DragEvent\s*\(/, name: "dispatchEvent(new DragEvent)", severity: "warn", errorCode: -32078, suggestion: "Synthetic drag events bypass real user interaction." },
600
+ { pattern: /dispatchEvent\s*\(\s*new\s+CompositionEvent\s*\(/, name: "dispatchEvent(new CompositionEvent)", severity: "info", errorCode: -32079, suggestion: "Synthetic IME composition events \u2014 detectable pattern." },
601
+ { pattern: /dispatchEvent\s*\(\s*new\s+AnimationEvent\s*\(/, name: "dispatchEvent(new AnimationEvent)", severity: "info", errorCode: -32079, suggestion: "Forcing animation state transitions via fake events." },
602
+ { pattern: /dispatchEvent\s*\(\s*new\s+TransitionEvent\s*\(/, name: "dispatchEvent(new TransitionEvent)", severity: "info", errorCode: -32079, suggestion: "Forcing CSS transition end via fake events." }
603
+ ];
604
+ var eventSimulationRule = {
605
+ id: "event-simulation",
606
+ name: "Event Simulation Detection (30+ patterns)",
607
+ priority: 40,
608
+ canHandle(ctx) {
609
+ return ctx.method === "Runtime.evaluate" || ctx.method === "Runtime.callFunctionOn";
610
+ },
611
+ evaluate(ctx) {
612
+ const userCode = extractUserCode(ctx);
613
+ if (!userCode) return null;
614
+ for (const p of EVENT_PATTERNS) {
615
+ if (p.pattern.test(userCode)) {
616
+ return {
617
+ ruleId: "event-simulation",
618
+ action: "block",
619
+ severity: p.severity,
620
+ reason: `Event simulation detected: "${p.name}". Synthetic events have isTrusted=false and are 100% detectable.`,
621
+ suggestion: p.suggestion,
622
+ errorCode: p.errorCode,
623
+ errorMessage: p.severity === "danger" ? `[CDP Firewall] ${p.name} blocked \u2014 synthetic event (isTrusted=false)` : `[CDP Firewall] ${p.name} detected \u2014 event simulation`
624
+ };
625
+ }
626
+ }
627
+ return null;
628
+ }
629
+ };
630
+
631
+ // src/cdp-interceptor/rules/emulation-override.ts
632
+ var OVERRIDE_PATTERNS = [
633
+ // ── Emulation overrides ──────────────────────────────
634
+ // NOTE: Emulation.setDeviceMetricsOverride is NOT included here because
635
+ // Playwright calls it internally for every new page (viewport setup).
636
+ // Blocking it would break page creation.
637
+ { method: "Emulation.setUserAgentOverride", name: "Emulation.setUserAgentOverride", severity: "danger", errorCode: -32080, suggestion: "navigator.userAgent override can be detected by checking consistency with navigator.plugins, WebGL vendor, etc." },
638
+ { method: "Emulation.setTouchEmulationEnabled", name: "Emulation.setTouchEmulationEnabled", severity: "warn", errorCode: -32081, suggestion: "Touch emulation creates inconsistent touch/mouse state. Windows touch events without real touch hardware." },
639
+ { method: "Emulation.setGeolocationOverride", name: "Emulation.setGeolocationOverride", severity: "danger", errorCode: -32082, suggestion: "Geolocation changing mid-session without user travel is impossible. Detectable via IP geo vs overridden geo." },
640
+ { method: "Emulation.setLocaleOverride", name: "Emulation.setLocaleOverride", severity: "warn", errorCode: -32081, suggestion: "Locale change without browser restart detectable via navigator.languages vs Accept-Language consistency." },
641
+ { method: "Emulation.setTimezoneOverride", name: "Emulation.setTimezoneOverride", severity: "danger", errorCode: -32082, suggestion: "Timezone mismatch vs IP geolocation + Date() is 100% detectable." },
642
+ { method: "Emulation.setDisabledImageTypes", name: "Emulation.setDisabledImageTypes", severity: "info", errorCode: -32083, suggestion: "Disabling image types prevents normal resource loading \u2014 visible to performance API." },
643
+ { method: "Emulation.setScriptExecutionDisabled", name: "Emulation.setScriptExecutionDisabled", severity: "info", errorCode: -32083, suggestion: "Disabling JS mid-session kills page interactivity \u2014 immediately obvious." },
644
+ { method: "Emulation.setCPUThrottlingRate", name: "Emulation.setCPUThrottlingRate", severity: "info", errorCode: -32083, suggestion: "CPU throttling creates unrealistic performance.now() profiles." },
645
+ { method: "Emulation.setVirtualTimePolicy", name: "Emulation.setVirtualTimePolicy", severity: "info", errorCode: -32083, suggestion: "Virtual time breaks Date.now() and performance.now() based detections \u2014 detectable via timer drift." },
646
+ // ── Network overrides ────────────────────────────────
647
+ { method: "Network.setUserAgentOverride", name: "Network.setUserAgentOverride (HTTP layer)", severity: "danger", errorCode: -32080, suggestion: "HTTP User-Agent vs navigator.userAgent inconsistency = immediate detection." },
648
+ { method: "Network.setExtraHTTPHeaders", name: "Network.setExtraHTTPHeaders", severity: "danger", errorCode: -32084, suggestion: "Custom headers can conflict with browser-generated headers. Missing client hints (Sec-CH-UA) are also detectable." },
649
+ { method: "Network.emulateNetworkConditions", name: "Network.emulateNetworkConditions", severity: "warn", errorCode: -32081, suggestion: "Network throttling creates unrealistic load timing patterns." },
650
+ { method: "Network.setCookie", name: "Network.setCookie", severity: "warn", errorCode: -32085, suggestion: "CDP-injected cookies are detectable via document.cookie vs Network.getCookies inconsistency." },
651
+ { method: "Network.deleteCookies", name: "Network.deleteCookies", severity: "warn", errorCode: -32085, suggestion: "Cookie deletion via CDP bypasses HTTP cookie expiration \u2014 detectable." },
652
+ // ── Security overrides ──────────────────────────────
653
+ { method: "Security.setIgnoreCertificateErrors", name: "Security.setIgnoreCertificateErrors", severity: "info", errorCode: -32086, suggestion: "Ignoring certificate errors creates unusual TLS behavior visible at network level." },
654
+ // ── Page overrides ──────────────────────────────────
655
+ { method: "Page.setDownloadBehavior", name: "Page.setDownloadBehavior", severity: "info", errorCode: -32087, suggestion: "Bypassing download dialogs \u2014 detectable via download event flow." },
656
+ { method: "Page.setWebLifecycleState", name: "Page.setWebLifecycleState", severity: "info", errorCode: -32087, suggestion: "Forcing page lifecycle transitions is unnatural." },
657
+ // ── Storage / Permissions ───────────────────────────
658
+ { method: "Storage.clearDataForOrigin", name: "Storage.clearDataForOrigin", severity: "info", errorCode: -32089, suggestion: "Clearing storage mid-session is unnatural for real users." },
659
+ { method: "Browser.grantPermissions", name: "Browser.grantPermissions", severity: "warn", errorCode: -32090, suggestion: "Granting permissions via CDP is detectable as the permission flow skips the user prompt." },
660
+ { method: "Browser.resetPermissions", name: "Browser.resetPermissions", severity: "info", errorCode: -32089, suggestion: "Permission resets without user action are unnatural." }
661
+ ];
662
+ var emulationOverrideRule = {
663
+ id: "emulation-override",
664
+ name: "CDP Emulation / Override Detection (20+ methods)",
665
+ priority: 60,
666
+ canHandle(ctx) {
667
+ for (const p of OVERRIDE_PATTERNS) {
668
+ if (ctx.method === p.method) return true;
669
+ }
670
+ return false;
671
+ },
672
+ evaluate(ctx) {
673
+ for (const p of OVERRIDE_PATTERNS) {
674
+ if (ctx.method !== p.method) continue;
675
+ return {
676
+ ruleId: "emulation-override",
677
+ action: "block",
678
+ severity: p.severity,
679
+ reason: `CDP emulation/override detected: "${p.name}". This creates detectable inconsistencies between the JS environment and real browser state.`,
680
+ suggestion: p.suggestion,
681
+ errorCode: p.errorCode,
682
+ errorMessage: `[CDP Firewall] ${p.name} blocked \u2014 creates detectable browser state inconsistency`
683
+ };
684
+ }
685
+ return null;
686
+ }
687
+ };
688
+
689
+ // src/cdp-interceptor/rules/network-anomaly.ts
690
+ var networkAnomalyRule = {
691
+ id: "network-anomaly",
692
+ name: "Network Anomaly Detection (8+ patterns)",
693
+ priority: 70,
694
+ canHandle(ctx) {
695
+ const m = ctx.method;
696
+ return m === "Network.setExtraHTTPHeaders" || m === "Network.clearBrowserCache" || m === "Network.clearBrowserCookies" || m === "Network.setBlockedURLs" || m === "Network.setBypassServiceWorker" || m === "Fetch.enable" || m === "Network.enable";
697
+ },
698
+ evaluate(ctx) {
699
+ switch (ctx.method) {
700
+ case "Network.setExtraHTTPHeaders": {
701
+ const headers = ctx.params.headers;
702
+ if (headers) {
703
+ const headerStr = JSON.stringify(headers).toLowerCase();
704
+ if (!headerStr.includes("sec-ch-ua")) {
705
+ return {
706
+ ruleId: "network-anomaly",
707
+ action: "block",
708
+ severity: "warn",
709
+ reason: "Network.setExtraHTTPHeaders called without Sec-CH-UA client hints \u2014 browser normally sends these.",
710
+ suggestion: "Modern browsers send Sec-CH-UA headers automatically. Adding custom headers without them creates detectable inconsistency.",
711
+ errorCode: -32110,
712
+ errorMessage: "[CDP Firewall] Missing client hints in custom headers"
713
+ };
714
+ }
715
+ }
716
+ return null;
717
+ }
718
+ case "Network.clearBrowserCache": {
719
+ return {
720
+ ruleId: "network-anomaly",
721
+ action: "block",
722
+ severity: "warn",
723
+ reason: "Network.clearBrowserCache called \u2014 cache clearing mid-session is unnatural for real users.",
724
+ suggestion: "Avoid cache clearing during sessions. Start with a fresh profile if needed.",
725
+ errorCode: -32111,
726
+ errorMessage: "[CDP Firewall] Cache clearing detected"
727
+ };
728
+ }
729
+ case "Network.clearBrowserCookies": {
730
+ return {
731
+ ruleId: "network-anomaly",
732
+ action: "block",
733
+ severity: "warn",
734
+ reason: "Network.clearBrowserCookies called \u2014 wiping cookies mid-session is a scraper optimization.",
735
+ suggestion: "Cookies should only be cleared via normal browser flow (expiration, user action).",
736
+ errorCode: -32112,
737
+ errorMessage: "[CDP Firewall] Cookie clearing detected"
738
+ };
739
+ }
740
+ case "Network.setBlockedURLs": {
741
+ return {
742
+ ruleId: "network-anomaly",
743
+ action: "block",
744
+ severity: "warn",
745
+ reason: "Network.setBlockedURLs blocks resource loading \u2014 this changes the page behavior and is detectable.",
746
+ suggestion: "Blocking images/fonts/etc creates measurable differences in performance and page rendering.",
747
+ errorCode: -32113,
748
+ errorMessage: "[CDP Firewall] URL blocking detected"
749
+ };
750
+ }
751
+ case "Network.setBypassServiceWorker": {
752
+ return {
753
+ ruleId: "network-anomaly",
754
+ action: "block",
755
+ severity: "info",
756
+ reason: "Network.setBypassServiceWorker called \u2014 bypassing service workers for content extraction.",
757
+ suggestion: "Service worker bypass changes fetch behavior and is detectable server-side.",
758
+ errorCode: -32114,
759
+ errorMessage: "[CDP Firewall] Service worker bypass detected"
760
+ };
761
+ }
762
+ case "Fetch.enable": {
763
+ return {
764
+ ruleId: "network-anomaly",
765
+ action: "block",
766
+ severity: "warn",
767
+ reason: "Fetch.enable intercepts all network requests \u2014 a man-in-the-middle approach used by scrapers.",
768
+ suggestion: "Do not use Fetch domain for network interception if avoiding detection.",
769
+ errorCode: -32115,
770
+ errorMessage: "[CDP Firewall] Fetch interception detected"
771
+ };
772
+ }
773
+ default:
774
+ return null;
775
+ }
776
+ }
777
+ };
778
+
779
+ // src/cdp-interceptor/rules/page-lifecycle.ts
780
+ var STAT_KEY = "lifecycle-tracker";
781
+ var pageLifecycleRule = {
782
+ id: "page-lifecycle",
783
+ name: "Page Lifecycle Anomaly Detection (10+ patterns)",
784
+ priority: 80,
785
+ canHandle(ctx) {
786
+ const m = ctx.method;
787
+ return m === "Page.navigate" || m === "Page.captureScreenshot" || m === "Page.printToPDF" || m === "Page.reload" || m === "Page.close" || m === "Runtime.evaluate" || m === "Runtime.callFunctionOn";
788
+ },
789
+ evaluate(ctx) {
790
+ let state = ctx.sessionState.get(STAT_KEY);
791
+ if (!state) {
792
+ state = { navigations: [], screenshots: 0, printToPDF: false, evaluateCount: 0, lastNavTime: 0, lastEvalTime: 0 };
793
+ ctx.sessionState.set(STAT_KEY, state);
794
+ }
795
+ const now = Date.now();
796
+ switch (ctx.method) {
797
+ case "Page.navigate": {
798
+ state.navigations.push(now);
799
+ if (state.navigations.length >= 2) {
800
+ const prev = state.navigations[state.navigations.length - 2];
801
+ const interval = now - prev;
802
+ if (interval < 100) {
803
+ return {
804
+ ruleId: "page-lifecycle",
805
+ action: "block",
806
+ severity: "warn",
807
+ reason: `Multiple Page.navigate calls within ${interval}ms of each other \u2014 unnatural rapid navigation.`,
808
+ suggestion: "Add proper waits between navigations: wait for page load before navigating again.",
809
+ errorCode: -32100,
810
+ errorMessage: "[CDP Firewall] Rapid navigation sequence detected"
811
+ };
812
+ }
813
+ }
814
+ state.lastNavTime = now;
815
+ return null;
816
+ }
817
+ case "Page.captureScreenshot": {
818
+ state.screenshots++;
819
+ if (state.lastNavTime > 0 && now - state.lastNavTime < 500) {
820
+ return {
821
+ ruleId: "page-lifecycle",
822
+ action: "block",
823
+ severity: "danger",
824
+ reason: "Page.captureScreenshot called within 500ms of navigation \u2014 content extraction pattern.",
825
+ suggestion: "Wait for the page to fully render before taking screenshots: wait for load/networkidle.",
826
+ errorCode: -32101,
827
+ errorMessage: "[CDP Firewall] Pre-render screenshot blocked \u2014 content extraction pattern"
828
+ };
829
+ }
830
+ if (state.screenshots > 3 && state.navigations.length < 2) {
831
+ return {
832
+ ruleId: "page-lifecycle",
833
+ action: "block",
834
+ severity: "warn",
835
+ reason: "Multiple screenshots on a single page without navigation \u2014 suspicious extraction behavior.",
836
+ suggestion: "Consider if all screenshots are necessary.",
837
+ errorCode: -32102,
838
+ errorMessage: "[CDP Firewall] Excessive screenshot detection"
839
+ };
840
+ }
841
+ return null;
842
+ }
843
+ case "Page.printToPDF": {
844
+ return {
845
+ ruleId: "page-lifecycle",
846
+ action: "block",
847
+ severity: "danger",
848
+ reason: "Page.printToPDF called \u2014 this is a telltale scraper pattern that gives away automation intent.",
849
+ suggestion: "Avoid PDF generation. If you must, add significant delays and user-like interaction first.",
850
+ errorCode: -32103,
851
+ errorMessage: "[CDP Firewall] printToPDF blocked \u2014 scraper intent detected"
852
+ };
853
+ }
854
+ case "Page.reload": {
855
+ if (state.lastNavTime > 0 && now - state.lastNavTime < 1e3) {
856
+ return {
857
+ ruleId: "page-lifecycle",
858
+ action: "block",
859
+ severity: "warn",
860
+ reason: "Page.reload called immediately after navigate \u2014 unnatural fast-reload pattern.",
861
+ suggestion: "Introduce delays between navigation and reload to simulate human behavior.",
862
+ errorCode: -32104,
863
+ errorMessage: "[CDP Firewall] Rapid reload detected"
864
+ };
865
+ }
866
+ return null;
867
+ }
868
+ case "Page.close": {
869
+ if (state.navigations.length < 2) {
870
+ return {
871
+ ruleId: "page-lifecycle",
872
+ action: "block",
873
+ severity: "info",
874
+ reason: "Page.close called after minimal interaction \u2014 zombie pages common in automation.",
875
+ suggestion: "Ensure meaningful interaction before closing pages.",
876
+ errorCode: -32105,
877
+ errorMessage: "[CDP Firewall] Page close after minimal interaction"
878
+ };
879
+ }
880
+ return null;
881
+ }
882
+ case "Runtime.evaluate":
883
+ case "Runtime.callFunctionOn": {
884
+ state.evaluateCount++;
885
+ if (state.evaluateCount > 50 && state.navigations.length === 0) {
886
+ return {
887
+ ruleId: "page-lifecycle",
888
+ action: "block",
889
+ severity: "info",
890
+ reason: "50+ evaluate calls without any Page.navigate \u2014 data extraction without real browsing.",
891
+ suggestion: "Navigate to a real page first. Evaluate on about:blank is suspicious.",
892
+ errorCode: -32106,
893
+ errorMessage: "[CDP Firewall] Excessive evaluate without navigation"
894
+ };
895
+ }
896
+ return null;
897
+ }
898
+ default:
899
+ return null;
900
+ }
901
+ }
902
+ };
903
+
904
+ // src/cdp-interceptor/rules-engine.ts
905
+ var BUILTIN_RULES = [
906
+ domMutationRule,
907
+ automationSignalsRule,
908
+ fingerprintingRule,
909
+ eventSimulationRule,
910
+ mouseTrajectoryRule,
911
+ inputKeystrokeRule,
912
+ emulationOverrideRule,
913
+ networkAnomalyRule,
914
+ pageLifecycleRule
915
+ ];
916
+ function createRuleEngine(customRules) {
917
+ const rules = [...BUILTIN_RULES, ...customRules ?? []].sort((a, b) => a.priority - b.priority);
918
+ const sessionStates = /* @__PURE__ */ new Map();
919
+ function getSessionState(sessionId) {
920
+ let state = sessionStates.get(sessionId);
921
+ if (!state) {
922
+ state = /* @__PURE__ */ new Map();
923
+ sessionStates.set(sessionId, state);
924
+ }
925
+ return state;
926
+ }
927
+ return {
928
+ start() {
929
+ sessionStates.clear();
930
+ },
931
+ stop() {
932
+ sessionStates.clear();
933
+ },
934
+ evaluate(ctx) {
935
+ const fullCtx = {
936
+ ...ctx,
937
+ sessionState: getSessionState(ctx.sessionId)
938
+ };
939
+ for (const rule of rules) {
940
+ if (rule.canHandle && !rule.canHandle(fullCtx)) continue;
941
+ const decision = rule.evaluate(fullCtx);
942
+ if (!decision) continue;
943
+ if (decision.action !== "pass") return decision;
944
+ }
945
+ return null;
946
+ }
947
+ };
948
+ }
949
+
950
+ // src/cdp-interceptor/logger.ts
951
+ function createLogger(config) {
952
+ const buffer = [];
953
+ const MAX_BUFFER = 2e3;
954
+ return {
955
+ info(message, meta) {
956
+ if (!config.enableLogging) return;
957
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
958
+ if (meta) {
959
+ console.log(`[CDPInterceptor ${ts}] ${message}`, JSON.stringify(meta));
960
+ } else {
961
+ console.log(`[CDPInterceptor ${ts}] ${message}`);
962
+ }
963
+ },
964
+ log(method, direction, sessionId, payload, decision) {
965
+ const entry = {
966
+ timestamp: Date.now(),
967
+ direction,
968
+ sessionId,
969
+ method,
970
+ payload: sanitizePayload(payload),
971
+ decision: decision ?? void 0
972
+ };
973
+ if (config.enableLogging) {
974
+ buffer.push(entry);
975
+ if (buffer.length > MAX_BUFFER) buffer.shift();
976
+ const tag = decision ? decision.action === "block" ? "\u{1F6AB}BLOCK" : decision.action === "transform" ? "\u{1F504}XFMR" : "\u2705" : " ";
977
+ const reason = decision ? ` [${decision.severity}] ${decision.reason}` : "";
978
+ console.log(`[CDP] ${tag} ${direction} ${method}${reason}`);
979
+ }
980
+ return entry;
981
+ },
982
+ getRecent(count) {
983
+ return buffer.slice(-count);
984
+ },
985
+ flush() {
986
+ buffer.length = 0;
987
+ }
988
+ };
989
+ }
990
+ function sanitizePayload(payload) {
991
+ if (typeof payload !== "object" || payload === null) return { raw: String(payload) };
992
+ const obj = payload;
993
+ const cleaned = {};
994
+ for (const [key, value] of Object.entries(obj)) {
995
+ if (key === "data" && typeof value === "string" && value.length > 200) {
996
+ cleaned[key] = `<binary: ${value.length} chars>`;
997
+ } else if (key === "expression" && typeof value === "string" && value.length > 500) {
998
+ cleaned[key] = value.substring(0, 500) + "...";
999
+ } else {
1000
+ cleaned[key] = value;
1001
+ }
1002
+ }
1003
+ return cleaned;
1004
+ }
1005
+
1006
+ // src/cdp-interceptor/advisor.ts
1007
+ function formatBlockMessage(decision, method) {
1008
+ const adv = advise(decision, method);
1009
+ const lines = [
1010
+ `[CDP-FIREWALL-BLOCK] rule=${decision.ruleId}`,
1011
+ `method=${method}`,
1012
+ `reason=${decision.reason}`,
1013
+ `suggestion=${decision.suggestion ?? adv.detail}`
1014
+ ];
1015
+ if (adv.codeExample) {
1016
+ lines.push(`code-example=`);
1017
+ lines.push(adv.codeExample);
1018
+ }
1019
+ return lines.join("\n");
1020
+ }
1021
+ function advise(decision, originalMethod) {
1022
+ const baseAdvice = getBaseAdvice(decision, originalMethod);
1023
+ return {
1024
+ ruleId: decision.ruleId,
1025
+ title: baseAdvice.title,
1026
+ detail: decision.suggestion ?? baseAdvice.detail,
1027
+ codeExample: baseAdvice.codeExample
1028
+ };
1029
+ }
1030
+ function getBaseAdvice(decision, method) {
1031
+ switch (decision.ruleId) {
1032
+ case "dom-mutation":
1033
+ return {
1034
+ title: "Direct DOM property mutation blocked",
1035
+ detail: `Your ${method} call tried to set a DOM property directly. In React/Vue/Angular, this bypasses the framework's virtual DOM completely meaning onChange/onInput never fires. The website CAN detect this mismatch as automation.`,
1036
+ codeExample: [
1037
+ "# \u274C BLOCKED \u2014 what you tried to do:",
1038
+ `page.evaluate(\`el.value = 'hello'\`) # triggers isTrusted=false`,
1039
+ "",
1040
+ "# \u2705 USE INSTEAD \u2014 proper CDP input dispatch:",
1041
+ "page.fill('#selector', 'hello') # Playwright: dispatches input+change events",
1042
+ "page.type('#selector', 'hello', {delay}) # Playwright: real keystrokes",
1043
+ "page.locator('#selector').fill('hello') # Playwright: recommended API"
1044
+ ].join("\n")
1045
+ };
1046
+ case "mouse-trajectory":
1047
+ return {
1048
+ title: "Unnatural mouse trajectory blocked",
1049
+ detail: `Your ${method} sequence formed a perfectly linear path. Human hands have micro-tremors (1-3px variation at any point along the arc), acceleration curves, and never draw straight lines between distant points.`,
1050
+ codeExample: [
1051
+ "# \u274C BLOCKED \u2014 linear interpolation:",
1052
+ "for i in range(20):",
1053
+ " page.mouse.move(x0 + (x1-x0)*(i/20), y0 + (y1-y0)*(i/20))",
1054
+ "",
1055
+ "# \u2705 USE INSTEAD \u2014 Bezier curves with overshoot:",
1056
+ '# Use "ghost-cursor" or similar library',
1057
+ "from ghost_cursor import path_to",
1058
+ "path_to(page, (x1, y1))"
1059
+ ].join("\n")
1060
+ };
1061
+ case "input-keystroke":
1062
+ if (method === "Input.insertText") {
1063
+ return {
1064
+ title: "Input.insertText detected (logged, not blocked)",
1065
+ detail: "Input.insertText bypasses native keyDown\u2192keyPress\u2192input\u2192keyUp events. Playwright uses this internally for page.fill(). Logged for observability."
1066
+ };
1067
+ }
1068
+ return {
1069
+ title: "Unnatural keystroke timing blocked",
1070
+ detail: `Your ${method} calls have unnaturally constant timing (e.g., exact 50ms intervals). Human typing always has variation (CV > 0.2).`,
1071
+ codeExample: [
1072
+ "# \u274C BLOCKED \u2014 exact constant delay:",
1073
+ "page.type('#input', 'hello', {delay: 50}) # every keystroke exactly 50ms apart",
1074
+ "",
1075
+ "# \u2705 USE INSTEAD \u2014 variable delay (human-like):",
1076
+ "page.fill('#input', 'hello') # recommended, dispatches events properly",
1077
+ "# OR: type with randomized delay",
1078
+ "page.type('#input', 'hello', {delay: 50 + Math.floor(Math.random() * 80)})"
1079
+ ].join("\n")
1080
+ };
1081
+ case "automation-signals":
1082
+ return {
1083
+ title: "Browser automation marker detected",
1084
+ detail: `Your ${method} call accessed a property/marker that anti-crawler systems check to detect automation. These markers (navigator.webdriver, window.__playwright, etc.) are the #1 detection vector.`,
1085
+ codeExample: [
1086
+ "# \u274C BLOCKED \u2014 don't check for automation markers:",
1087
+ "navigator.webdriver # NEVER check this",
1088
+ "window.__playwright # NEVER check this",
1089
+ "chrome.runtime # NEVER check this",
1090
+ "",
1091
+ "# \u2705 Just go about your business normally.",
1092
+ "# Anti-detection is handled by the CDP firewall automatically."
1093
+ ].join("\n")
1094
+ };
1095
+ case "fingerprinting":
1096
+ return {
1097
+ title: "Browser fingerprinting API access blocked",
1098
+ detail: `Your ${method} call accessed a known fingerprinting API. These APIs (canvas.toDataURL, WebGL getParameter, AudioContext, etc.) are used by anti-crawler systems to build a unique device fingerprint.`,
1099
+ codeExample: [
1100
+ "# \u274C BLOCKED \u2014 fingerprinting vector:",
1101
+ "canvas.toDataURL() # returns unique browser hash",
1102
+ 'gl.getParameter(gl.VENDOR) # returns "SwiftShader" in headless',
1103
+ "screen.availWidth - screen.availHeight # no OS chrome in headless",
1104
+ "",
1105
+ "# \u2705 Avoid accessing these APIs. They are only used for fingerprinting."
1106
+ ].join("\n")
1107
+ };
1108
+ case "event-simulation":
1109
+ return {
1110
+ title: "Synthetic event simulation blocked",
1111
+ detail: `Your ${method} call simulated user interaction via el.click() or dispatchEvent(new Event(...)). These produce isTrusted=false events, which are 100% detectable by any anti-crawler that checks isTrusted on critical events.`,
1112
+ codeExample: [
1113
+ "# \u274C BLOCKED \u2014 synthetic events (isTrusted=false):",
1114
+ "el.click() # isTrusted=false",
1115
+ 'el.dispatchEvent(new Event("click")) # isTrusted=false',
1116
+ "el.focus() # isTrusted=false",
1117
+ "",
1118
+ "# \u2705 USE INSTEAD \u2014 CDP-level input dispatch (isTrusted=true):",
1119
+ "page.click(selector) # uses Input.dispatchMouseEvent",
1120
+ "page.fill(selector, value) # dispatches real input events"
1121
+ ].join("\n")
1122
+ };
1123
+ case "emulation-override":
1124
+ return {
1125
+ title: "CDP emulation override blocked",
1126
+ detail: `Your ${method} call overrides browser behavior in a way that creates detectable inconsistencies. Anti-crawler systems cross-check multiple sources (e.g., navigator.userAgent vs HTTP User-Agent header) to catch these mismatches.`,
1127
+ codeExample: [
1128
+ "# \u274C BLOCKED \u2014 detectable emulation override:",
1129
+ "Emulation.setUserAgentOverride(...) # JS vs HTTP header mismatch",
1130
+ "Emulation.setGeolocationOverride(...) # IP geo vs overridden geo mismatch",
1131
+ "Emulation.setDeviceMetricsOverride(...) # matchMedia vs actual viewport",
1132
+ "",
1133
+ "# \u2705 These are handled automatically by the CDP firewall.",
1134
+ "# Do NOT call them manually."
1135
+ ].join("\n")
1136
+ };
1137
+ case "network-anomaly":
1138
+ return {
1139
+ title: "Network anomaly detected",
1140
+ detail: `Your ${method} call triggered a network pattern that is characteristic of scrapers: blocking URLs, clearing caches, or intercepting requests.`,
1141
+ codeExample: [
1142
+ "# \u274C BLOCKED \u2014 scraper optimization:",
1143
+ "Network.clearBrowserCache() # natural users never do this",
1144
+ 'Network.setBlockedURLs(["*fonts*"]) # blocking resources is detectable',
1145
+ "Fetch.enable() # MITM-style interception",
1146
+ "",
1147
+ "# \u2705 Let the browser manage its own cache and network normally."
1148
+ ].join("\n")
1149
+ };
1150
+ case "page-lifecycle":
1151
+ return {
1152
+ title: "Suspicious page lifecycle pattern blocked",
1153
+ detail: `Your ${method} call reveals an unnatural page interaction sequence: navigating too fast, taking screenshots before the page renders, or generating PDFs (a telltale scraper giveaway).`,
1154
+ codeExample: [
1155
+ "# \u274C BLOCKED \u2014 unnatural lifecycle:",
1156
+ "page.goto(url); page.pdf() # PDF = scraper giveaway",
1157
+ "page.goto(url); page.screenshot() <500ms # screenshot before render",
1158
+ "page.goto(url) 3x in <100ms # rapid navigation barrage",
1159
+ "",
1160
+ "# \u2705 Add proper waits between actions:",
1161
+ 'page.goto(url, {waitUntil: "networkidle"})',
1162
+ 'page.waitForSelector("body")',
1163
+ "page.screenshot() # after rendering"
1164
+ ].join("\n")
1165
+ };
1166
+ default:
1167
+ return {
1168
+ title: decision.reason,
1169
+ detail: decision.suggestion ?? `The CDP call "${method}" was blocked by rule "${decision.ruleId}".`
1170
+ };
1171
+ }
1172
+ }
1173
+
1174
+ // src/cdp-interceptor/proxy.ts
1175
+ function makeCompoundId(cdpSessionId, rawSessionId) {
1176
+ return `${cdpSessionId ?? "nil"}::${rawSessionId ?? "nil"}`;
1177
+ }
1178
+ var CDPInterceptorProxy = class {
1179
+ wss = null;
1180
+ engine;
1181
+ config;
1182
+ logger;
1183
+ started = false;
1184
+ stats = {
1185
+ totalMessages: 0,
1186
+ blockedMessages: 0,
1187
+ transformedMessages: 0,
1188
+ passedMessages: 0,
1189
+ byRule: {}
1190
+ };
1191
+ constructor(config) {
1192
+ this.config = config;
1193
+ this.engine = createRuleEngine(config.rules);
1194
+ this.logger = createLogger({
1195
+ enableLogging: config.enableLogging ?? true,
1196
+ logDir: config.logDir
1197
+ });
1198
+ }
1199
+ /** The port the proxy is listening on (only valid after start()) */
1200
+ get port() {
1201
+ const addr = this.wss?.address();
1202
+ if (addr && typeof addr === "object") return addr.port;
1203
+ return 0;
1204
+ }
1205
+ /** Start the proxy server */
1206
+ async start() {
1207
+ if (this.started) return this.port;
1208
+ return new Promise((resolve, reject) => {
1209
+ this.wss = new WebSocketServer({ port: this.config.listenPort ?? 0 }, () => {
1210
+ const port = this.port;
1211
+ this.engine.start();
1212
+ this.started = true;
1213
+ this.logger.info("CDP interceptor proxy started", { port, endpoint: this.config.cdpEndpoint });
1214
+ resolve(port);
1215
+ });
1216
+ this.wss.on("error", reject);
1217
+ this.wss.on("connection", (clientWs, _req) => {
1218
+ this.handleConnection(clientWs);
1219
+ });
1220
+ });
1221
+ }
1222
+ /** Stop the proxy server */
1223
+ async stop() {
1224
+ this.engine.stop();
1225
+ this.logger.flush();
1226
+ this.started = false;
1227
+ return new Promise((resolve) => {
1228
+ if (!this.wss) return resolve();
1229
+ this.wss.close(() => resolve());
1230
+ this.wss = null;
1231
+ });
1232
+ }
1233
+ /** Get accumulated statistics */
1234
+ getStats() {
1235
+ return { ...this.stats };
1236
+ }
1237
+ /** Get recent log entries (for inspection) */
1238
+ getRecentLogs(count = 50) {
1239
+ return this.logger.getRecent(count);
1240
+ }
1241
+ // ── Connection handling ──────────────────────────────────────
1242
+ handleConnection(clientWs) {
1243
+ let browserWs = null;
1244
+ let isAlive = true;
1245
+ const pendingMessages = [];
1246
+ browserWs = new WebSocket(this.config.cdpEndpoint);
1247
+ clientWs.on("message", (raw) => {
1248
+ if (browserWs && browserWs.readyState === WebSocket.OPEN) {
1249
+ this.handleClientMessage(clientWs, browserWs, raw);
1250
+ } else {
1251
+ pendingMessages.push(raw);
1252
+ }
1253
+ });
1254
+ browserWs.on("open", () => {
1255
+ for (const buf of pendingMessages) {
1256
+ this.handleClientMessage(clientWs, browserWs, buf);
1257
+ }
1258
+ pendingMessages.length = 0;
1259
+ });
1260
+ browserWs.on("error", (err) => {
1261
+ this.logger.info("Browser WebSocket error", { error: String(err) });
1262
+ });
1263
+ browserWs.on("close", (code, reason) => {
1264
+ if (isAlive && clientWs.readyState === WebSocket.OPEN) {
1265
+ this.logger.info("Browser WS closed, closing client", { code, reason: String(reason) });
1266
+ clientWs.close();
1267
+ }
1268
+ });
1269
+ browserWs.on("message", (raw) => {
1270
+ this.handleBrowserMessage(clientWs, browserWs, raw);
1271
+ });
1272
+ const cleanup = () => {
1273
+ isAlive = false;
1274
+ if (browserWs && browserWs.readyState === WebSocket.OPEN) {
1275
+ browserWs.close();
1276
+ }
1277
+ };
1278
+ clientWs.on("close", cleanup);
1279
+ clientWs.on("error", cleanup);
1280
+ browserWs.on("close", () => {
1281
+ if (isAlive && clientWs.readyState === WebSocket.OPEN) {
1282
+ clientWs.close();
1283
+ }
1284
+ });
1285
+ browserWs.on("error", cleanup);
1286
+ }
1287
+ // ── Message processing ───────────────────────────────────────
1288
+ handleClientMessage(clientWs, browserWs, raw) {
1289
+ const msg = this.parseMessage(raw);
1290
+ if (!msg) return;
1291
+ this.stats.totalMessages++;
1292
+ if (!("method" in msg)) {
1293
+ browserWs.send(raw.toString());
1294
+ return;
1295
+ }
1296
+ const request = msg;
1297
+ const ctx = {
1298
+ method: request.method,
1299
+ params: request.params ?? {},
1300
+ sessionId: makeCompoundId(browserWs._cdpSession, request.sessionId),
1301
+ direction: "client\u2192browser"
1302
+ };
1303
+ const decision = this.engine.evaluate(ctx);
1304
+ this.logger.log(ctx.method, "client\u2192browser", ctx.sessionId, { method: ctx.method, params: ctx.params }, decision);
1305
+ if (decision) {
1306
+ this.recordDecision(decision);
1307
+ }
1308
+ if (decision?.action === "block") {
1309
+ const blockMsg = formatBlockMessage(decision, ctx.method);
1310
+ const errorResponse = {
1311
+ id: request.id,
1312
+ error: {
1313
+ code: decision.errorCode ?? -32e3,
1314
+ message: blockMsg
1315
+ },
1316
+ sessionId: request.sessionId
1317
+ };
1318
+ this.stats.blockedMessages++;
1319
+ console.error(`
1320
+ ${blockMsg}
1321
+ `);
1322
+ clientWs.send(JSON.stringify(errorResponse));
1323
+ return;
1324
+ }
1325
+ if (decision?.action === "transform" && decision.transformedParams) {
1326
+ const transformed = { ...request, params: decision.transformedParams };
1327
+ this.stats.transformedMessages++;
1328
+ browserWs.send(JSON.stringify(transformed));
1329
+ return;
1330
+ }
1331
+ this.stats.passedMessages++;
1332
+ browserWs.send(raw.toString());
1333
+ }
1334
+ handleBrowserMessage(clientWs, _browserWs, raw) {
1335
+ const msg = this.parseMessage(raw);
1336
+ if (!msg) {
1337
+ clientWs.send(raw.toString());
1338
+ return;
1339
+ }
1340
+ if ("method" in msg) {
1341
+ const event = msg;
1342
+ const ctx = {
1343
+ method: event.method,
1344
+ params: event.params ?? {},
1345
+ sessionId: event.sessionId ?? "browser",
1346
+ direction: "browser\u2192client"
1347
+ };
1348
+ const decision = this.engine.evaluate(ctx);
1349
+ if (decision?.action === "block") {
1350
+ return;
1351
+ }
1352
+ }
1353
+ clientWs.send(raw.toString());
1354
+ }
1355
+ // ── Utilities ────────────────────────────────────────────────
1356
+ parseMessage(raw) {
1357
+ try {
1358
+ return JSON.parse(raw.toString());
1359
+ } catch {
1360
+ return null;
1361
+ }
1362
+ }
1363
+ recordDecision(decision) {
1364
+ if (!this.stats.byRule[decision.ruleId]) {
1365
+ this.stats.byRule[decision.ruleId] = { matched: 0, blocked: 0, transformed: 0 };
1366
+ }
1367
+ this.stats.byRule[decision.ruleId].matched++;
1368
+ if (decision.action === "block") {
1369
+ this.stats.byRule[decision.ruleId].blocked++;
1370
+ } else if (decision.action === "transform") {
1371
+ this.stats.byRule[decision.ruleId].transformed++;
1372
+ }
1373
+ }
1374
+ };
1375
+
1376
+ // src/utils/cdp.ts
1377
+ async function fetchNoProxy(url) {
1378
+ const savedProxy = {
1379
+ http_proxy: process.env.http_proxy,
1380
+ https_proxy: process.env.https_proxy,
1381
+ HTTP_PROXY: process.env.HTTP_PROXY,
1382
+ HTTPS_PROXY: process.env.HTTPS_PROXY,
1383
+ all_proxy: process.env.all_proxy,
1384
+ ALL_PROXY: process.env.ALL_PROXY
1385
+ };
1386
+ for (const key of Object.keys(savedProxy)) delete process.env[key];
1387
+ try {
1388
+ return await fetch(url);
1389
+ } finally {
1390
+ for (const [key, val] of Object.entries(savedProxy)) {
1391
+ if (val !== void 0) process.env[key] = val;
1392
+ }
1393
+ }
1394
+ }
1395
+ async function resolveCDPEndpoint(raw) {
1396
+ if (raw === "auto") {
1397
+ const httpResp = await fetchNoProxy("http://localhost:9222/json/version");
1398
+ const data = await httpResp.json();
1399
+ if (!data.webSocketDebuggerUrl) {
1400
+ throw new Error("Could not auto-discover CDP endpoint from localhost:9222");
1401
+ }
1402
+ return data.webSocketDebuggerUrl;
1403
+ }
1404
+ if (/^\d+$/.test(raw)) {
1405
+ const port = raw;
1406
+ const httpResp = await fetchNoProxy(`http://localhost:${port}/json/version`);
1407
+ const data = await httpResp.json();
1408
+ if (!data.webSocketDebuggerUrl) {
1409
+ throw new Error(`Could not discover CDP endpoint from localhost:${port}`);
1410
+ }
1411
+ return data.webSocketDebuggerUrl;
1412
+ }
1413
+ if (raw.startsWith("http://") || raw.startsWith("https://")) {
1414
+ try {
1415
+ const httpResp = await fetchNoProxy(`${raw}/json/version`);
1416
+ const data = await httpResp.json();
1417
+ if (!data.webSocketDebuggerUrl) {
1418
+ throw new Error(`Could not discover CDP endpoint from ${raw}`);
1419
+ }
1420
+ return data.webSocketDebuggerUrl;
1421
+ } catch (error) {
1422
+ console.warn(`Failed to fetch WebSocket URL from ${raw}, using endpoint directly: ${error instanceof Error ? error.message : String(error)}`);
1423
+ return raw;
1424
+ }
1425
+ }
1426
+ return raw;
1427
+ }
1428
+
1429
+ // src/browser.ts
1430
+ function logSessionEvent(event, details) {
1431
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").substring(0, 19);
1432
+ const pid = process.pid;
1433
+ console.error(`[SESSION] ${ts} [PID:${pid}] ${event} | ${details}`);
1434
+ }
1435
+ var SESSION_DIR = join(homedir(), ".xbrowser", "sessions");
1436
+ function sessionFile(name) {
1437
+ return join(SESSION_DIR, `${name}.json`);
1438
+ }
1439
+ function ensureSessionDir() {
1440
+ mkdirSync(SESSION_DIR, { recursive: true });
1441
+ }
1442
+ var sessions = /* @__PURE__ */ new Map();
1443
+ var _sharedBrowser = null;
1444
+ var _sharedCdpProxy = null;
1445
+ var IDLE_TIMEOUT_MS = (process.env.XBROWSER_IDLE_TIMEOUT ? parseInt(process.env.XBROWSER_IDLE_TIMEOUT, 10) : 30) * 60 * 1e3;
1446
+ var idleTimer = null;
1447
+ function resetIdleTimer() {
1448
+ if (idleTimer) clearTimeout(idleTimer);
1449
+ idleTimer = setTimeout(async () => {
1450
+ const now = Date.now();
1451
+ let allIdle = true;
1452
+ const idleSessions = [];
1453
+ for (const [, s] of sessions) {
1454
+ if (now - s.lastActivityAt < IDLE_TIMEOUT_MS) {
1455
+ allIdle = false;
1456
+ } else {
1457
+ idleSessions.push(`${s.name}(${(now - s.lastActivityAt) / 1e3}s idle)`);
1458
+ }
1459
+ }
1460
+ if (allIdle && (sessions.size > 0 || _sharedBrowser)) {
1461
+ logSessionEvent("idle_timeout", `Sessions idle for >${IDLE_TIMEOUT_MS / 6e4}min. Sessions: ${idleSessions.join(", ") || "all"}. Calling destroyBrowser()`);
1462
+ await destroyBrowser().catch(() => {
1463
+ });
1464
+ }
1465
+ }, IDLE_TIMEOUT_MS);
1466
+ if (idleTimer && typeof idleTimer.unref === "function") {
1467
+ idleTimer.unref();
1468
+ }
1469
+ }
1470
+ function touchSession(id) {
1471
+ const s = sessions.get(id);
1472
+ if (s) s.lastActivityAt = Date.now();
1473
+ resetIdleTimer();
1474
+ }
1475
+ process.on("exit", () => {
1476
+ for (const session of sessions.values()) {
1477
+ if (session.isCDP) {
1478
+ logSessionEvent("process_exit", `Session "${session.name}": CDP connection (not closing external browser).`);
1479
+ } else {
1480
+ logSessionEvent("process_exit", `Session "${session.name}": Closing browser.`);
1481
+ try {
1482
+ session.browser?.close();
1483
+ } catch {
1484
+ }
1485
+ }
1486
+ }
1487
+ if (_sharedBrowser) {
1488
+ logSessionEvent("process_exit", "Closing shared browser.");
1489
+ try {
1490
+ _sharedBrowser.close();
1491
+ } catch {
1492
+ }
1493
+ _sharedBrowser = null;
1494
+ }
1495
+ if (_sharedCdpProxy) {
1496
+ try {
1497
+ _sharedCdpProxy.stop();
1498
+ } catch {
1499
+ }
1500
+ _sharedCdpProxy = null;
1501
+ }
1502
+ sessions.clear();
1503
+ });
1504
+ async function getCDPTargets(cdpEndpoint) {
1505
+ try {
1506
+ const ep = String(cdpEndpoint);
1507
+ let host = "localhost";
1508
+ let port = "9222";
1509
+ if (ep.startsWith("http://") || ep.startsWith("https://")) {
1510
+ const u = new URL(ep);
1511
+ host = u.hostname;
1512
+ port = u.port || "9222";
1513
+ } else if (/^\d+$/.test(ep)) {
1514
+ port = ep;
1515
+ }
1516
+ const url = `http://${host}:${port}/json/list`;
1517
+ const resp = await fetch(url);
1518
+ return await resp.json();
1519
+ } catch {
1520
+ return [];
1521
+ }
1522
+ }
1523
+ async function findTargetPage(cdpEndpoint, target) {
1524
+ const targets = await getCDPTargets(cdpEndpoint);
1525
+ const pages = targets.filter((t) => t.url && !t.url.startsWith("about:blank") && !t.url.startsWith("chrome://"));
1526
+ const byId = pages.find((t) => t.id === target);
1527
+ if (byId) return { pageId: byId.id, wsUrl: byId.webSocketDebuggerUrl, title: byId.title, url: byId.url };
1528
+ const lowerTarget = target.toLowerCase();
1529
+ const byTitle = pages.find((t) => t.title && t.title.toLowerCase().includes(lowerTarget));
1530
+ if (byTitle) return { pageId: byTitle.id, wsUrl: byTitle.webSocketDebuggerUrl, title: byTitle.title, url: byTitle.url };
1531
+ const byUrl = pages.find((t) => t.url.toLowerCase().includes(lowerTarget));
1532
+ if (byUrl) return { pageId: byUrl.id, wsUrl: byUrl.webSocketDebuggerUrl, title: byUrl.title, url: byUrl.url };
1533
+ return null;
1534
+ }
1535
+ function resolveLaunchOpts(ctx) {
1536
+ if (ctx.cdpEndpoint) {
1537
+ return { cdpEndpoint: ctx.cdpEndpoint };
1538
+ }
1539
+ return { headless: true };
1540
+ }
1541
+ var CHROMIUM_CANDIDATES = [
1542
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
1543
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
1544
+ "/usr/bin/chromium-browser",
1545
+ "/usr/bin/chromium",
1546
+ "/usr/bin/google-chrome"
1547
+ ];
1548
+ function discoverChromiumPath() {
1549
+ for (const p of CHROMIUM_CANDIDATES) {
1550
+ if (existsSync(p)) return p;
1551
+ }
1552
+ return void 0;
1553
+ }
1554
+ async function createBrowser(options) {
1555
+ if (options?.cdpEndpoint) {
1556
+ const realEndpoint = await resolveCDPEndpoint(options.cdpEndpoint);
1557
+ if (options.intercept) {
1558
+ const config = typeof options.intercept === "object" ? { ...options.intercept, cdpEndpoint: realEndpoint } : { cdpEndpoint: realEndpoint };
1559
+ const proxy = new CDPInterceptorProxy(config);
1560
+ const proxyPort = await proxy.start();
1561
+ console.error(`[CDP Interceptor] Proxy running on ws://localhost:${proxyPort}, forwarding to ${realEndpoint}`);
1562
+ return await chromium.connectOverCDP(`ws://localhost:${proxyPort}`);
1563
+ }
1564
+ return await chromium.connectOverCDP(realEndpoint);
1565
+ }
1566
+ const executablePath = options?.executablePath || process.env.XBROWSER_CHROMIUM_PATH || discoverChromiumPath();
1567
+ return await chromium.launch({ executablePath, headless: options?.headless ?? true });
1568
+ }
1569
+ async function getBrowser(options) {
1570
+ if (_sharedBrowser) return _sharedBrowser;
1571
+ _sharedBrowser = await createBrowser(options);
1572
+ if (options?.cdpEndpoint && options.intercept) {
1573
+ }
1574
+ return _sharedBrowser;
1575
+ }
1576
+ function findSession(name) {
1577
+ for (const [, session] of sessions) {
1578
+ if (session.name === name) return session;
1579
+ }
1580
+ return void 0;
1581
+ }
1582
+ function getSessionById(id) {
1583
+ return sessions.get(id);
1584
+ }
1585
+ function setActivePage(session, page) {
1586
+ session.page = page;
1587
+ session.lastActivityAt = Date.now();
1588
+ }
1589
+ function saveSessionDiskMeta(name, data) {
1590
+ ensureSessionDir();
1591
+ const file = sessionFile(name);
1592
+ let existing = {};
1593
+ try {
1594
+ existing = JSON.parse(readFileSync(file, "utf8"));
1595
+ } catch {
1596
+ }
1597
+ Object.assign(existing, data, { name });
1598
+ writeFileSync(file, JSON.stringify(existing, null, 2));
1599
+ }
1600
+ function readSessionDiskMeta(name) {
1601
+ const file = sessionFile(name);
1602
+ try {
1603
+ return JSON.parse(readFileSync(file, "utf8"));
1604
+ } catch {
1605
+ return null;
1606
+ }
1607
+ }
1608
+ function deleteSessionDiskMeta(name) {
1609
+ const file = sessionFile(name);
1610
+ try {
1611
+ unlinkSync(file);
1612
+ } catch {
1613
+ }
1614
+ }
1615
+ async function findOrRestoreSession(name, cdpEndpoint) {
1616
+ const inMem = findSession(name);
1617
+ if (inMem) return inMem;
1618
+ const meta = readSessionDiskMeta(name);
1619
+ if (!meta) return void 0;
1620
+ const ep = cdpEndpoint || meta.cdpEndpoint;
1621
+ if (!ep) return void 0;
1622
+ try {
1623
+ const b = await createBrowser({ cdpEndpoint: ep });
1624
+ await new Promise((r) => setTimeout(r, 500));
1625
+ let contexts = b.contexts();
1626
+ if (contexts.length === 0) {
1627
+ await new Promise((r) => setTimeout(r, 500));
1628
+ contexts = b.contexts();
1629
+ }
1630
+ const context = contexts[0] || await b.newContext();
1631
+ const savedUrl = meta.conversationUrl || meta.url;
1632
+ const targetHostname = savedUrl ? (() => {
1633
+ try {
1634
+ return new URL(savedUrl).hostname;
1635
+ } catch {
1636
+ return "";
1637
+ }
1638
+ })() : "";
1639
+ let page = null;
1640
+ let fallbackPage = null;
1641
+ for (const ctx of contexts) {
1642
+ const pages = ctx.pages();
1643
+ for (const p of pages) {
1644
+ const pUrl = p.url();
1645
+ if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://")) {
1646
+ if (targetHostname && pUrl.includes(targetHostname)) {
1647
+ page = p;
1648
+ break;
1649
+ }
1650
+ if (!fallbackPage) {
1651
+ fallbackPage = p;
1652
+ }
1653
+ }
1654
+ }
1655
+ if (page) break;
1656
+ }
1657
+ page = page || fallbackPage;
1658
+ if (!page) {
1659
+ const targets = await getCDPTargets(ep);
1660
+ const matchTarget = targets.find(
1661
+ (t) => t.url && t.url !== "about:blank" && !t.url.startsWith("chrome://") && (targetHostname ? t.url.includes(targetHostname) : true)
1662
+ );
1663
+ if (matchTarget && matchTarget.url) {
1664
+ page = await context.newPage();
1665
+ await page.goto(matchTarget.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
1666
+ });
1667
+ }
1668
+ }
1669
+ if (!page) {
1670
+ const pages = context.pages();
1671
+ page = pages.length > 0 ? pages[0] : await context.newPage();
1672
+ }
1673
+ try {
1674
+ await Promise.race([
1675
+ page.evaluate(() => true),
1676
+ new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3e3))
1677
+ ]);
1678
+ } catch {
1679
+ console.log(`[Session] "${name}" restored page unresponsive, creating fresh session`);
1680
+ deleteSessionDiskMeta(name);
1681
+ return void 0;
1682
+ }
1683
+ const targetUrl = meta.conversationUrl || meta.url;
1684
+ if (targetUrl && page.url() !== targetUrl && !page.url().includes(new URL(targetUrl).hostname)) {
1685
+ await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
1686
+ });
1687
+ }
1688
+ const session = {
1689
+ id: meta.id || randomUUID(),
1690
+ name,
1691
+ context,
1692
+ page,
1693
+ browser: b,
1694
+ createdAt: meta.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
1695
+ lastActivityAt: Date.now(),
1696
+ isCDP: true,
1697
+ cdpEndpoint: ep
1698
+ };
1699
+ for (const [existingId, existingSession] of sessions) {
1700
+ if (existingSession.name === name) {
1701
+ logSessionEvent("remove_stale", `Removing stale session name="${name}" id="${existingId}" during restore`);
1702
+ sessions.delete(existingId);
1703
+ }
1704
+ }
1705
+ sessions.set(session.id, session);
1706
+ resetIdleTimer();
1707
+ await installNetworkCapture(page, name);
1708
+ return session;
1709
+ } catch (e) {
1710
+ console.error(`[Session Restore] Failed for "${name}":`, e.message);
1711
+ deleteSessionDiskMeta(name);
1712
+ return void 0;
1713
+ }
1714
+ }
1715
+ async function createEphemeralContext(options) {
1716
+ if (options?.cdpEndpoint) {
1717
+ const endpoint = await resolveCDPEndpoint(options.cdpEndpoint);
1718
+ const b2 = await chromium.connectOverCDP(endpoint);
1719
+ const contexts = b2.contexts();
1720
+ const ctx = contexts[0] || await b2.newContext();
1721
+ const page2 = await ctx.newPage();
1722
+ resetIdleTimer();
1723
+ ephemeralConnections.set(page2, b2);
1724
+ return { context: ctx, page: page2 };
1725
+ }
1726
+ const b = await getBrowser(options);
1727
+ const context = await b.newContext();
1728
+ const page = await context.newPage();
1729
+ resetIdleTimer();
1730
+ return { context, page };
1731
+ }
1732
+ var ephemeralConnections = /* @__PURE__ */ new WeakMap();
1733
+ async function closeEphemeralContext(context) {
1734
+ try {
1735
+ const pages = context.pages();
1736
+ for (const p of pages) {
1737
+ const conn = ephemeralConnections.get(p);
1738
+ if (conn) {
1739
+ ephemeralConnections.delete(p);
1740
+ await conn.close();
1741
+ break;
1742
+ }
1743
+ }
1744
+ await context.close();
1745
+ } catch {
1746
+ }
1747
+ if (sessions.size === 0 && idleTimer) {
1748
+ clearTimeout(idleTimer);
1749
+ idleTimer = null;
1750
+ }
1751
+ }
1752
+ function getAllSessions() {
1753
+ return Array.from(sessions.values());
1754
+ }
1755
+ async function installNetworkCapture(page, sessionName) {
1756
+ if (process.env.XBROWSER_DAEMON_WORKER !== "1") return;
1757
+ const { networkStore } = await import("./network-store-2S5HATEV.js");
1758
+ page.on("response", async (response) => {
1759
+ try {
1760
+ const request = response.request();
1761
+ const url = response.url();
1762
+ const contentType = response.headers()["content-type"] || "";
1763
+ const headers = {};
1764
+ for (const [k, v] of Object.entries(response.headers())) {
1765
+ headers[k] = v;
1766
+ }
1767
+ const requestHeaders = {};
1768
+ for (const [k, v] of Object.entries(request.headers())) {
1769
+ requestHeaders[k] = v;
1770
+ }
1771
+ let requestBody = void 0;
1772
+ const method = request.method();
1773
+ const isPostLike = ["POST", "PATCH", "PUT"].includes(method);
1774
+ if (isPostLike && requestHeaders["content-type"]?.includes("application/json")) {
1775
+ try {
1776
+ const postData = request.postData();
1777
+ if (postData) {
1778
+ try {
1779
+ requestBody = JSON.parse(postData);
1780
+ } catch {
1781
+ requestBody = postData;
1782
+ }
1783
+ }
1784
+ } catch {
1785
+ }
1786
+ }
1787
+ let responseBody = void 0;
1788
+ let size = 0;
1789
+ const isJsonish = contentType.includes("json") || contentType.includes("javascript") || contentType.includes("text/");
1790
+ if (isJsonish) {
1791
+ try {
1792
+ const text = await response.text();
1793
+ size = text.length;
1794
+ if (size <= 10240) {
1795
+ try {
1796
+ responseBody = JSON.parse(text);
1797
+ } catch {
1798
+ responseBody = text.slice(0, 200);
1799
+ }
1800
+ }
1801
+ } catch {
1802
+ }
1803
+ } else {
1804
+ try {
1805
+ const text = await response.text();
1806
+ size = text.length;
1807
+ } catch {
1808
+ size = 0;
1809
+ }
1810
+ }
1811
+ networkStore.add(sessionName, {
1812
+ timestamp: Date.now(),
1813
+ method,
1814
+ url,
1815
+ path: new URL(url).pathname,
1816
+ status: response.status(),
1817
+ contentType,
1818
+ size,
1819
+ headers,
1820
+ body: responseBody,
1821
+ requestHeaders,
1822
+ requestBody,
1823
+ resourceType: request.resourceType()
1824
+ });
1825
+ } catch {
1826
+ }
1827
+ });
1828
+ }
1829
+ async function createSession(name, url, options) {
1830
+ const existing = findSession(name);
1831
+ if (existing) {
1832
+ logSessionEvent("replace_session", `name="${name}" id="${existing.id}" \u2014 closing existing session before creating new one`);
1833
+ await closeSessionByName(name);
1834
+ }
1835
+ const b = await createBrowser(options);
1836
+ const isCDP = !!options?.cdpEndpoint;
1837
+ let context;
1838
+ let page;
1839
+ if (isCDP) {
1840
+ await new Promise((r) => setTimeout(r, 500));
1841
+ let contexts = b.contexts();
1842
+ if (contexts.length === 0) {
1843
+ await new Promise((r) => setTimeout(r, 500));
1844
+ contexts = b.contexts();
1845
+ }
1846
+ context = contexts[0] || await b.newContext();
1847
+ let targetPage = null;
1848
+ for (const ctx of contexts) {
1849
+ const pages = ctx.pages();
1850
+ for (const p of pages) {
1851
+ const pUrl = p.url();
1852
+ if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://")) {
1853
+ targetPage = p;
1854
+ break;
1855
+ }
1856
+ }
1857
+ if (targetPage) break;
1858
+ }
1859
+ if (!targetPage && options?.cdpEndpoint) {
1860
+ const targets = await getCDPTargets(options.cdpEndpoint);
1861
+ const matchTarget = targets.find(
1862
+ (t) => t.url && t.url !== "about:blank" && !t.url.startsWith("chrome://") && (url ? t.url.includes(new URL(url).hostname) : true)
1863
+ );
1864
+ if (matchTarget && matchTarget.url) {
1865
+ targetPage = await context.newPage();
1866
+ await targetPage.goto(matchTarget.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
1867
+ });
1868
+ }
1869
+ }
1870
+ if (!targetPage) {
1871
+ const pages = context.pages();
1872
+ if (pages.length > 0) {
1873
+ targetPage = pages[0];
1874
+ } else {
1875
+ targetPage = await context.newPage();
1876
+ }
1877
+ }
1878
+ page = targetPage;
1879
+ } else {
1880
+ context = await b.newContext({ viewport: { width: 1920, height: 1080 } });
1881
+ page = await context.newPage();
1882
+ }
1883
+ if (url && page.url() !== url) {
1884
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
1885
+ });
1886
+ }
1887
+ const session = {
1888
+ id: randomUUID(),
1889
+ name,
1890
+ context,
1891
+ page,
1892
+ browser: b,
1893
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1894
+ lastActivityAt: Date.now(),
1895
+ isCDP,
1896
+ cdpEndpoint: options?.cdpEndpoint
1897
+ };
1898
+ sessions.set(session.id, session);
1899
+ logSessionEvent("create_session", `name="${name}" id="${session.id}" url="${url || "(no url)"}" isCDP=${isCDP} cdpEndpoint=${options?.cdpEndpoint || "(none)"}`);
1900
+ resetIdleTimer();
1901
+ await installNetworkCapture(page, name);
1902
+ return session;
1903
+ }
1904
+ async function closeSessionByName(name) {
1905
+ for (const [id, session] of sessions) {
1906
+ if (session.name === name || session.id === name) {
1907
+ logSessionEvent("close_session", `name="${session.name}" id="${session.id}" url="${session.page.url()}"`);
1908
+ if (session.isCDP) {
1909
+ try {
1910
+ await session.page.close();
1911
+ } catch {
1912
+ }
1913
+ if (session.browser) {
1914
+ await session.browser.close().catch(() => {
1915
+ });
1916
+ }
1917
+ } else {
1918
+ await session.context.close();
1919
+ if (session.browser) {
1920
+ await session.browser.close().catch(() => {
1921
+ });
1922
+ }
1923
+ }
1924
+ sessions.delete(id);
1925
+ const file2 = sessionFile(session.name);
1926
+ try {
1927
+ unlinkSync(file2);
1928
+ } catch {
1929
+ }
1930
+ try {
1931
+ const { networkStore, commandLogStore } = await import("./network-store-2S5HATEV.js");
1932
+ networkStore.clear(session.name);
1933
+ commandLogStore.clear(session.name);
1934
+ } catch {
1935
+ }
1936
+ try {
1937
+ const { SessionRecorder } = await import("./session-recorder-ILSSV2UC.js");
1938
+ SessionRecorder.cleanup(session.name);
1939
+ } catch {
1940
+ }
1941
+ return true;
1942
+ }
1943
+ }
1944
+ const file = sessionFile(name);
1945
+ try {
1946
+ unlinkSync(file);
1947
+ } catch {
1948
+ }
1949
+ return false;
1950
+ }
1951
+ async function closeAllSessions() {
1952
+ const names = [...sessions.values()].map((s) => `${s.name}(${s.page.url()})`).join(", ");
1953
+ if (names) logSessionEvent("close_all_sessions", `Closing ${sessions.size} sessions: ${names}`);
1954
+ for (const [id, session] of sessions) {
1955
+ try {
1956
+ if (!session.isCDP) {
1957
+ await session.context.close();
1958
+ }
1959
+ if (session.browser) {
1960
+ await session.browser.close().catch(() => {
1961
+ });
1962
+ }
1963
+ sessions.delete(id);
1964
+ } catch {
1965
+ sessions.delete(id);
1966
+ }
1967
+ }
1968
+ }
1969
+ async function destroyBrowser() {
1970
+ logSessionEvent("destroy_browser", `Sessions count: ${sessions.size}. Clearing idle timer and closing all browsers.`);
1971
+ if (idleTimer) {
1972
+ clearTimeout(idleTimer);
1973
+ idleTimer = null;
1974
+ }
1975
+ await closeAllSessions();
1976
+ if (_sharedBrowser) {
1977
+ await _sharedBrowser.close().catch(() => {
1978
+ });
1979
+ _sharedBrowser = null;
1980
+ }
1981
+ if (_sharedCdpProxy) {
1982
+ await _sharedCdpProxy.stop().catch(() => {
1983
+ });
1984
+ _sharedCdpProxy = null;
1985
+ }
1986
+ }
1987
+ function resetForTesting() {
1988
+ sessions.clear();
1989
+ _sharedBrowser = null;
1990
+ _sharedCdpProxy = null;
1991
+ try {
1992
+ for (const f of readdirSync(SESSION_DIR)) {
1993
+ unlinkSync(join(SESSION_DIR, f));
1994
+ }
1995
+ } catch {
1996
+ }
1997
+ }
1998
+ async function ensureProcessCanExit() {
1999
+ if (idleTimer) {
2000
+ clearTimeout(idleTimer);
2001
+ idleTimer = null;
2002
+ }
2003
+ for (const session of sessions.values()) {
2004
+ if (session.browser) {
2005
+ if (session.isCDP) {
2006
+ await session.browser.close().catch(() => {
2007
+ });
2008
+ } else {
2009
+ await session.browser.close().catch(() => {
2010
+ });
2011
+ }
2012
+ }
2013
+ }
2014
+ sessions.clear();
2015
+ if (_sharedBrowser) {
2016
+ await _sharedBrowser.close().catch(() => {
2017
+ });
2018
+ _sharedBrowser = null;
2019
+ }
2020
+ if (_sharedCdpProxy) {
2021
+ await _sharedCdpProxy.stop().catch(() => {
2022
+ });
2023
+ _sharedCdpProxy = null;
2024
+ }
2025
+ }
2026
+
2027
+ export {
2028
+ createRuleEngine,
2029
+ touchSession,
2030
+ findTargetPage,
2031
+ resolveLaunchOpts,
2032
+ createBrowser,
2033
+ getBrowser,
2034
+ findSession,
2035
+ getSessionById,
2036
+ setActivePage,
2037
+ saveSessionDiskMeta,
2038
+ readSessionDiskMeta,
2039
+ deleteSessionDiskMeta,
2040
+ findOrRestoreSession,
2041
+ createEphemeralContext,
2042
+ closeEphemeralContext,
2043
+ getAllSessions,
2044
+ createSession,
2045
+ closeSessionByName,
2046
+ closeAllSessions,
2047
+ destroyBrowser,
2048
+ resetForTesting,
2049
+ ensureProcessCanExit
2050
+ };