@xbrowser/cli 1.11.0 → 1.13.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 (35) hide show
  1. package/dist/{browser-XNU7JQYC.js → browser-AQ2AT2YZ.js} +2 -2
  2. package/dist/{browser-Q5APBNF6.js → browser-OMGJMSBB.js} +1 -1
  3. package/dist/{browser-V3JIWSTR.js → browser-T7OVLAEC.js} +4 -3
  4. package/dist/{cdp-driver-XFTTZI5O.js → cdp-driver-ALDNOZYD.js} +1176 -13
  5. package/dist/{cdp-driver-GD6YBBGE.js → cdp-driver-HYCLE4YP.js} +2 -1
  6. package/dist/{cdp-driver-VEK6BNN6.js → cdp-driver-VS7Z7W7Y.js} +1 -1
  7. package/dist/chunk-A6LPGFAL.js +437 -0
  8. package/dist/{chunk-JEDP4PJW.js → chunk-BY5TT6WZ.js} +912 -688
  9. package/dist/{chunk-XKQVUFUS.js → chunk-CW7L53JE.js} +2 -2
  10. package/dist/{chunk-ABXMBNQ6.js → chunk-H2A5JUK5.js} +37 -466
  11. package/dist/{chunk-BNO7OKO4.js → chunk-HBMEFSTB.js} +39 -0
  12. package/dist/chunk-HD4FX2N5.js +1255 -0
  13. package/dist/{chunk-DWDXEGVK.js → chunk-NKW4A74J.js} +9 -3
  14. package/dist/{chunk-SQHSZENE.js → chunk-OMU63E6J.js} +3 -22
  15. package/dist/{chunk-7OFM755Z.js → chunk-R2POF2GP.js} +236 -13
  16. package/dist/{chunk-WHPBNUKB.js → chunk-SH6OTPXN.js} +42 -46
  17. package/dist/{chunk-XQ4HRPDJ.js → chunk-T2CNKWPV.js} +1177 -13
  18. package/dist/{chunk-MWFHZUIY.js → chunk-TEXCXIBW.js} +1 -1
  19. package/dist/{chunk-IIM5GOD7.js → chunk-ZTHE5RBZ.js} +7 -1
  20. package/dist/cli.js +1075 -569
  21. package/dist/{daemon-client-MMDRYCF5.js → daemon-client-7D6EZNOE.js} +42 -46
  22. package/dist/{daemon-client-Y2YSZCGK.js → daemon-client-HEGAXWGK.js} +1 -1
  23. package/dist/daemon-main.js +950 -499
  24. package/dist/{human-interaction-5AO42MBA.js → human-interaction-4YR2N6R6.js} +2 -2
  25. package/dist/{human-interaction-C5OEGXGO.js → human-interaction-Y5IDH6LD.js} +1 -1
  26. package/dist/{human-interaction-ISXZTAYY.js → human-interaction-ZUTR5AC2.js} +2 -2
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.js +1086 -578
  29. package/dist/{proxy-C6CK3UH5.js → proxy-LUR4U5YF.js} +2 -1
  30. package/dist/{recovery-J2ISVGUL.js → recovery-FTZGV6VY.js} +2 -2
  31. package/dist/{recovery-ZJVVHP7N.js → recovery-L5GLDX4O.js} +1 -1
  32. package/dist/{recovery-EF33LKRJ.js → recovery-Z3RDZENA.js} +2 -2
  33. package/dist/{session-replayer-WIUYVN5J.js → session-replayer-UVH5FGY2.js} +1 -1
  34. package/package.json +1 -1
  35. package/dist/chunk-HVPUVVSA.js +0 -2194
@@ -1,6 +1,3 @@
1
- // src/cdp-interceptor/proxy.ts
2
- import { WebSocketServer, WebSocket } from "ws";
3
-
4
1
  // src/cdp-interceptor/rules/shared.ts
5
2
  var PLAYWRIGHT_INTERNAL_MARKERS = [
6
3
  "__commonJS",
@@ -46,22 +43,22 @@ function extractAllStrings(rawArgs) {
46
43
  // src/cdp-interceptor/rules/dom-mutation.ts
47
44
  var DOM_PATTERNS = [
48
45
  // ── P0: Value/Checked (bypasses React onChange) ────────────
49
- { 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." },
50
- { pattern: /\.checked\s*=\s*(?:true|false)/, name: ".checked =", severity: "danger", action: "block", errorCode: -32001, suggestion: "Use page.check(selector) or page.uncheck(selector)." },
51
- { pattern: /\.indeterminate\s*=\s*true/, name: ".indeterminate =", severity: "warn", action: "block", errorCode: -32021, suggestion: "No human can set indeterminate state \u2014 remove this call." },
52
- { pattern: /\.valueAsDate\s*=/, name: ".valueAsDate =", severity: "warn", action: "block", errorCode: -32022, suggestion: "Use page.fill() with formatted date string instead." },
53
- { pattern: /\.valueAsNumber\s*=/, name: ".valueAsNumber =", severity: "warn", action: "block", errorCode: -32022, suggestion: "Use page.fill() with numeric string instead." },
46
+ { pattern: /\.value\s*=\s*(?!["'\s]*$)/, name: ".value =", severity: "danger", action: "pass", errorCode: -32001, suggestion: "Use page.fill(selector, value) which dispatches proper input/change events." },
47
+ { pattern: /\.checked\s*=\s*(?:true|false)/, name: ".checked =", severity: "danger", action: "pass", errorCode: -32001, suggestion: "Use page.check(selector) or page.uncheck(selector)." },
48
+ { pattern: /\.indeterminate\s*=\s*true/, name: ".indeterminate =", severity: "warn", action: "pass", errorCode: -32021, suggestion: "No human can set indeterminate state \u2014 remove this call." },
49
+ { pattern: /\.valueAsDate\s*=/, name: ".valueAsDate =", severity: "warn", action: "pass", errorCode: -32022, suggestion: "Use page.fill() with formatted date string instead." },
50
+ { pattern: /\.valueAsNumber\s*=/, name: ".valueAsNumber =", severity: "warn", action: "pass", errorCode: -32022, suggestion: "Use page.fill() with numeric string instead." },
54
51
  // ── P1: Select/Option (bypasses React onChange on select) ─
55
- { pattern: /\.selectedIndex\s*=\s*\d+/, name: ".selectedIndex =", severity: "danger", action: "block", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
56
- { 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)." },
57
- { pattern: /\.value\s*=\s*["'][^"']*["']\s*[;)]?\s*$/, name: "selectElement.value =", severity: "warn", action: "block", errorCode: -32023, suggestion: "For select elements, use page.selectOption(selector, value)." },
52
+ { pattern: /\.selectedIndex\s*=\s*\d+/, name: ".selectedIndex =", severity: "danger", action: "pass", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
53
+ { pattern: /(?:options|children)\s*\[[^\]]*\]\s*\.\s*selected\s*=\s*(?:true|false)/, name: "options[N].selected =", severity: "danger", action: "pass", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
54
+ { pattern: /\.value\s*=\s*["'][^"']*["']\s*[;)]?\s*$/, name: "selectElement.value =", severity: "warn", action: "pass", errorCode: -32023, suggestion: "For select elements, use page.selectOption(selector, value)." },
58
55
  // ── P2: Content properties (bypasses virtual DOM diffing) ──
59
56
  { pattern: /\.innerHTML\s*=/, name: ".innerHTML =", severity: "info", action: "pass", errorCode: -32007, suggestion: ".innerHTML bypasses React/Vue diffing. Use component state or page.setContent()." },
60
57
  { pattern: /\.outerHTML\s*=/, name: ".outerHTML =", severity: "info", action: "pass", errorCode: -32007, suggestion: "outerHTML replacement destroys React fiber tree. Use page.setContent()." },
61
- { pattern: /\.innerText\s*=/, name: ".innerText =", severity: "warn", action: "block", errorCode: -32024, suggestion: "Framework components should be updated via state, not innerText." },
62
- { pattern: /\.textContent\s*=/, name: ".textContent =", severity: "warn", action: "block", errorCode: -32024, suggestion: "textContent bypasses React/Vue diffing. Use component state instead." },
63
- { pattern: /\.outerText\s*=/, name: ".outerText =", severity: "warn", action: "block", errorCode: -32024, suggestion: "Non-standard property \u2014 use proper framework update methods." },
64
- { pattern: /nodeValue\s*=/, name: "node.nodeValue =", severity: "info", action: "block", errorCode: -32025, suggestion: "Direct text node mutation. Use textContent instead if needed." },
58
+ { pattern: /\.innerText\s*=/, name: ".innerText =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "Framework components should be updated via state, not innerText." },
59
+ { pattern: /\.textContent\s*=/, name: ".textContent =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "textContent bypasses React/Vue diffing. Use component state instead." },
60
+ { pattern: /\.outerText\s*=/, name: ".outerText =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "Non-standard property \u2014 use proper framework update methods." },
61
+ { pattern: /nodeValue\s*=/, name: "node.nodeValue =", severity: "info", action: "pass", errorCode: -32025, suggestion: "Direct text node mutation. Use textContent instead if needed." },
65
62
  // ── P3: Style properties ──────────────────────────────────
66
63
  { 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".' },
67
64
  { 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." },
@@ -109,7 +106,7 @@ var DOM_PATTERNS = [
109
106
  { pattern: /\.scrollBy\s*\(/, name: ".scrollBy()", severity: "info", action: "pass", errorCode: -32015, suggestion: "ScrollBy without user gesture." },
110
107
  { pattern: /\.scrollIntoView\s*\(/, name: ".scrollIntoView()", severity: "info", action: "pass", errorCode: -32015, suggestion: "scrollIntoView is a common bot pattern. Let Playwright handle scrolling." },
111
108
  // ── P11: Shadow DOM ───────────────────────────────────────
112
- { pattern: /\.shadowRoot\s*=/, name: ".shadowRoot = (override)", severity: "info", action: "block", errorCode: -32036, suggestion: "ShadowRoot is read-only \u2014 this set attempt is detectable." },
109
+ { pattern: /\.shadowRoot\s*=/, name: ".shadowRoot = (override)", severity: "info", action: "pass", errorCode: -32036, suggestion: "ShadowRoot is read-only \u2014 this set attempt is detectable." },
113
110
  // ── P12: Force reflow / layout thrashing ──────────────────
114
111
  { 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." },
115
112
  { pattern: /\.offsetWidth\b(?!\s*===?\s*)/, name: ".offsetWidth read (forced reflow)", severity: "warn", action: "pass", errorCode: -32016, suggestion: "Reading offsetWidth triggers forced reflow \u2014 detectable." },
@@ -222,7 +219,7 @@ function analyzeTrajectory(samples) {
222
219
  const stopY = samples[samples.length - 1].y;
223
220
  return {
224
221
  ruleId: "mouse-trajectory",
225
- action: "block",
222
+ action: "pass",
226
223
  severity: "danger",
227
224
  reason: `Suspicious mouse trajectory: ${issues.join("; ")}`,
228
225
  suggestion: `This mouse movement appears automated (straight line A\u2192B, no natural variation).
@@ -364,7 +361,7 @@ function analyzeKeyTiming(samples) {
364
361
  if (allIdentical) {
365
362
  return {
366
363
  ruleId: "input-keystroke",
367
- action: "block",
364
+ action: "pass",
368
365
  severity: "danger",
369
366
  reason: `All ${intervals.length} keystroke intervals are exactly ${intervals[0]}ms \u2014 impossible for human typing.`,
370
367
  suggestion: `Use page.fill(selector, text) instead of page.type() with delay.
@@ -376,7 +373,7 @@ Or add random variation: page.type(selector, text, {delay: 50 + Math.random() *
376
373
  if (cv < 0.08) {
377
374
  return {
378
375
  ruleId: "input-keystroke",
379
- action: "block",
376
+ action: "pass",
380
377
  severity: "warn",
381
378
  reason: `Unnatural keystroke timing (CV=${cv.toFixed(3)}). Human typing has CV > 0.2 on average.`,
382
379
  suggestion: `Add random variation to your typing delay: page.type(selector, text, {delay: 50 + Math.random() * 80}).`,
@@ -472,7 +469,7 @@ function makeDecision(p, context) {
472
469
  const suggestion = p.suggestionOverride ?? `Detected: "${p.name}" \u2014 an automation tool marker that anti-crawler systems immediately flag. Remove this pattern from your code.`;
473
470
  return {
474
471
  ruleId: "automation-signals",
475
- action: "block",
472
+ action: "pass",
476
473
  severity: p.severity,
477
474
  reason: `Automation marker detected: "${p.name}". Context: "${context}..."`,
478
475
  suggestion,
@@ -547,7 +544,7 @@ var fingerprintingRule = {
547
544
  if (p.pattern.test(userCode)) {
548
545
  return {
549
546
  ruleId: "fingerprinting",
550
- action: "block",
547
+ action: "pass",
551
548
  severity: p.severity,
552
549
  reason: `Browser fingerprinting API accessed: "${p.name}". Anti-crawler systems use this to identify your browser.`,
553
550
  suggestion: p.suggestion,
@@ -564,7 +561,7 @@ var fingerprintingRule = {
564
561
  var EVENT_PATTERNS = [
565
562
  // ── Direct method calls (all isTrusted=false) ─────────
566
563
  { 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)." },
567
- { 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." },
564
+ { pattern: /\.focus\s*\(\s*\)/, name: "el.focus()", severity: "warn", errorCode: -32071, suggestion: "el.focus() without user interaction is detectable. Use page.click(selector) which naturally focuses." },
568
565
  { pattern: /\.blur\s*\(\s*\)/, name: "el.blur()", severity: "danger", errorCode: -32071, suggestion: "el.blur() without user interaction. Avoid in automation scripts." },
569
566
  { 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"]').` },
570
567
  { pattern: /\.reset\s*\(\s*\)/, name: "el.reset()", severity: "danger", errorCode: -32072, suggestion: "form.reset() without user action. Let the user clear fields manually." },
@@ -579,7 +576,7 @@ var EVENT_PATTERNS = [
579
576
  { pattern: /\.showPicker\s*\(\s*\)/, name: "HTMLInputElement.showPicker()", severity: "info", errorCode: -32076, suggestion: "Date/color picker shown without click \u2014 detectable." },
580
577
  { pattern: /\.reportValidity\s*\(\s*\)/, name: "el.reportValidity()", severity: "info", errorCode: -32076, suggestion: "Validity reporting without form submission attempt." },
581
578
  // ── dispatchEvent with synthetic events (isTrusted=false) ──
582
- { 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." },
579
+ { pattern: /dispatchEvent\s*\(\s*new\s+(?:Event|CustomEvent)\s*\(/, name: "dispatchEvent(new Event/CustomEvent)", severity: "warn", errorCode: -32077, suggestion: "Synthetic events have isTrusted=false. Use Input.dispatch* CDP methods for trusted events." },
583
580
  { 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." },
584
581
  { 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." },
585
582
  { pattern: /dispatchEvent\s*\(\s*new\s+FocusEvent\s*\(/, name: "dispatchEvent(new FocusEvent)", severity: "warn", errorCode: -32078, suggestion: "Synthetic focus events bypass user interaction." },
@@ -608,7 +605,9 @@ var eventSimulationRule = {
608
605
  if (p.pattern.test(userCode)) {
609
606
  return {
610
607
  ruleId: "event-simulation",
611
- action: "block",
608
+ // danger 级硬拦(el.click 等 100% 暴露的模式);
609
+ // warning 级放行+提示(dispatchEvent 在部分合法场景如 DataTransfer paste 使用)
610
+ action: p.severity === "danger" ? "block" : "pass",
612
611
  severity: p.severity,
613
612
  reason: `Event simulation detected: "${p.name}". Synthetic events have isTrusted=false and are 100% detectable.`,
614
613
  suggestion: p.suggestion,
@@ -667,7 +666,7 @@ var emulationOverrideRule = {
667
666
  if (ctx.method !== p.method) continue;
668
667
  return {
669
668
  ruleId: "emulation-override",
670
- action: "block",
669
+ action: "pass",
671
670
  severity: p.severity,
672
671
  reason: `CDP emulation/override detected: "${p.name}". This creates detectable inconsistencies between the JS environment and real browser state.`,
673
672
  suggestion: p.suggestion,
@@ -697,7 +696,7 @@ var networkAnomalyRule = {
697
696
  if (!headerStr.includes("sec-ch-ua")) {
698
697
  return {
699
698
  ruleId: "network-anomaly",
700
- action: "block",
699
+ action: "pass",
701
700
  severity: "warn",
702
701
  reason: "Network.setExtraHTTPHeaders called without Sec-CH-UA client hints \u2014 browser normally sends these.",
703
702
  suggestion: "Modern browsers send Sec-CH-UA headers automatically. Adding custom headers without them creates detectable inconsistency.",
@@ -711,7 +710,7 @@ var networkAnomalyRule = {
711
710
  case "Network.clearBrowserCache": {
712
711
  return {
713
712
  ruleId: "network-anomaly",
714
- action: "block",
713
+ action: "pass",
715
714
  severity: "warn",
716
715
  reason: "Network.clearBrowserCache called \u2014 cache clearing mid-session is unnatural for real users.",
717
716
  suggestion: "Avoid cache clearing during sessions. Start with a fresh profile if needed.",
@@ -722,7 +721,7 @@ var networkAnomalyRule = {
722
721
  case "Network.clearBrowserCookies": {
723
722
  return {
724
723
  ruleId: "network-anomaly",
725
- action: "block",
724
+ action: "pass",
726
725
  severity: "warn",
727
726
  reason: "Network.clearBrowserCookies called \u2014 wiping cookies mid-session is a scraper optimization.",
728
727
  suggestion: "Cookies should only be cleared via normal browser flow (expiration, user action).",
@@ -733,7 +732,7 @@ var networkAnomalyRule = {
733
732
  case "Network.setBlockedURLs": {
734
733
  return {
735
734
  ruleId: "network-anomaly",
736
- action: "block",
735
+ action: "pass",
737
736
  severity: "warn",
738
737
  reason: "Network.setBlockedURLs blocks resource loading \u2014 this changes the page behavior and is detectable.",
739
738
  suggestion: "Blocking images/fonts/etc creates measurable differences in performance and page rendering.",
@@ -755,7 +754,7 @@ var networkAnomalyRule = {
755
754
  case "Fetch.enable": {
756
755
  return {
757
756
  ruleId: "network-anomaly",
758
- action: "block",
757
+ action: "pass",
759
758
  severity: "warn",
760
759
  reason: "Fetch.enable intercepts all network requests \u2014 a man-in-the-middle approach used by scrapers.",
761
760
  suggestion: "Do not use Fetch domain for network interception if avoiding detection.",
@@ -795,7 +794,7 @@ var pageLifecycleRule = {
795
794
  if (interval < 100) {
796
795
  return {
797
796
  ruleId: "page-lifecycle",
798
- action: "block",
797
+ action: "pass",
799
798
  severity: "warn",
800
799
  reason: `Multiple Page.navigate calls within ${interval}ms of each other \u2014 unnatural rapid navigation.`,
801
800
  suggestion: "Add proper waits between navigations: wait for page load before navigating again.",
@@ -812,7 +811,7 @@ var pageLifecycleRule = {
812
811
  if (state.lastNavTime > 0 && now - state.lastNavTime < 500) {
813
812
  return {
814
813
  ruleId: "page-lifecycle",
815
- action: "block",
814
+ action: "pass",
816
815
  severity: "danger",
817
816
  reason: "Page.captureScreenshot called within 500ms of navigation \u2014 content extraction pattern.",
818
817
  suggestion: "Wait for the page to fully render before taking screenshots: wait for load/networkidle.",
@@ -823,7 +822,7 @@ var pageLifecycleRule = {
823
822
  if (state.screenshots > 3 && state.navigations.length < 2) {
824
823
  return {
825
824
  ruleId: "page-lifecycle",
826
- action: "block",
825
+ action: "pass",
827
826
  severity: "warn",
828
827
  reason: "Multiple screenshots on a single page without navigation \u2014 suspicious extraction behavior.",
829
828
  suggestion: "Consider if all screenshots are necessary.",
@@ -836,7 +835,7 @@ var pageLifecycleRule = {
836
835
  case "Page.printToPDF": {
837
836
  return {
838
837
  ruleId: "page-lifecycle",
839
- action: "block",
838
+ action: "pass",
840
839
  severity: "danger",
841
840
  reason: "Page.printToPDF called \u2014 this is a telltale scraper pattern that gives away automation intent.",
842
841
  suggestion: "Avoid PDF generation. If you must, add significant delays and user-like interaction first.",
@@ -848,7 +847,7 @@ var pageLifecycleRule = {
848
847
  if (state.lastNavTime > 0 && now - state.lastNavTime < 1e3) {
849
848
  return {
850
849
  ruleId: "page-lifecycle",
851
- action: "block",
850
+ action: "pass",
852
851
  severity: "warn",
853
852
  reason: "Page.reload called immediately after navigate \u2014 unnatural fast-reload pattern.",
854
853
  suggestion: "Introduce delays between navigation and reload to simulate human behavior.",
@@ -862,7 +861,7 @@ var pageLifecycleRule = {
862
861
  if (state.navigations.length < 2) {
863
862
  return {
864
863
  ruleId: "page-lifecycle",
865
- action: "block",
864
+ action: "pass",
866
865
  severity: "info",
867
866
  reason: "Page.close called after minimal interaction \u2014 zombie pages common in automation.",
868
867
  suggestion: "Ensure meaningful interaction before closing pages.",
@@ -878,7 +877,7 @@ var pageLifecycleRule = {
878
877
  if (state.evaluateCount > 50 && state.navigations.length === 0) {
879
878
  return {
880
879
  ruleId: "page-lifecycle",
881
- action: "block",
880
+ action: "pass",
882
881
  severity: "info",
883
882
  reason: "50+ evaluate calls without any Page.navigate \u2014 data extraction without real browsing.",
884
883
  suggestion: "Navigate to a real page first. Evaluate on about:blank is suspicious.",
@@ -940,432 +939,6 @@ function createRuleEngine(customRules) {
940
939
  };
941
940
  }
942
941
 
943
- // src/cdp-interceptor/logger.ts
944
- function createLogger(config) {
945
- const buffer = [];
946
- const MAX_BUFFER = 2e3;
947
- return {
948
- info(message, meta) {
949
- if (!config.enableLogging) return;
950
- const ts = (/* @__PURE__ */ new Date()).toISOString();
951
- if (meta) {
952
- console.log(`[CDPInterceptor ${ts}] ${message}`, JSON.stringify(meta));
953
- } else {
954
- console.log(`[CDPInterceptor ${ts}] ${message}`);
955
- }
956
- },
957
- log(method, direction, sessionId, payload, decision) {
958
- const entry = {
959
- timestamp: Date.now(),
960
- direction,
961
- sessionId,
962
- method,
963
- payload: sanitizePayload(payload),
964
- decision: decision ?? void 0
965
- };
966
- if (config.enableLogging) {
967
- buffer.push(entry);
968
- if (buffer.length > MAX_BUFFER) buffer.shift();
969
- const tag = decision ? decision.action === "block" ? "\u{1F6AB}BLOCK" : decision.action === "transform" ? "\u{1F504}XFMR" : "\u2705" : " ";
970
- const reason = decision ? ` [${decision.severity}] ${decision.reason}` : "";
971
- console.log(`[CDP] ${tag} ${direction} ${method}${reason}`);
972
- }
973
- return entry;
974
- },
975
- getRecent(count) {
976
- return buffer.slice(-count);
977
- },
978
- flush() {
979
- buffer.length = 0;
980
- }
981
- };
982
- }
983
- function sanitizePayload(payload) {
984
- if (typeof payload !== "object" || payload === null) return { raw: String(payload) };
985
- const obj = payload;
986
- const cleaned = {};
987
- for (const [key, value] of Object.entries(obj)) {
988
- if (key === "data" && typeof value === "string" && value.length > 200) {
989
- cleaned[key] = `<binary: ${value.length} chars>`;
990
- } else if (key === "expression" && typeof value === "string" && value.length > 500) {
991
- cleaned[key] = value.substring(0, 500) + "...";
992
- } else {
993
- cleaned[key] = value;
994
- }
995
- }
996
- return cleaned;
997
- }
998
-
999
- // src/cdp-interceptor/advisor.ts
1000
- function formatBlockMessage(decision, method) {
1001
- const adv = advise(decision, method);
1002
- const lines = [
1003
- `[CDP-FIREWALL-BLOCK] rule=${decision.ruleId}`,
1004
- `method=${method}`,
1005
- `reason=${decision.reason}`,
1006
- `suggestion=${decision.suggestion ?? adv.detail}`
1007
- ];
1008
- if (adv.codeExample) {
1009
- lines.push(`code-example=`);
1010
- lines.push(adv.codeExample);
1011
- }
1012
- return lines.join("\n");
1013
- }
1014
- function advise(decision, originalMethod) {
1015
- const baseAdvice = getBaseAdvice(decision, originalMethod);
1016
- return {
1017
- ruleId: decision.ruleId,
1018
- title: baseAdvice.title,
1019
- detail: decision.suggestion ?? baseAdvice.detail,
1020
- codeExample: baseAdvice.codeExample
1021
- };
1022
- }
1023
- function getBaseAdvice(decision, method) {
1024
- switch (decision.ruleId) {
1025
- case "dom-mutation":
1026
- return {
1027
- title: "Direct DOM property mutation blocked",
1028
- 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.`,
1029
- codeExample: [
1030
- "# \u274C BLOCKED \u2014 what you tried to do:",
1031
- `page.evaluate(\`el.value = 'hello'\`) # triggers isTrusted=false`,
1032
- "",
1033
- "# \u2705 USE INSTEAD \u2014 proper CDP input dispatch:",
1034
- "page.fill('#selector', 'hello') # Playwright: dispatches input+change events",
1035
- "page.type('#selector', 'hello', {delay}) # Playwright: real keystrokes",
1036
- "page.locator('#selector').fill('hello') # Playwright: recommended API"
1037
- ].join("\n")
1038
- };
1039
- case "mouse-trajectory":
1040
- return {
1041
- title: "Unnatural mouse trajectory blocked",
1042
- 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.`,
1043
- codeExample: [
1044
- "# \u274C BLOCKED \u2014 linear interpolation:",
1045
- "for i in range(20):",
1046
- " page.mouse.move(x0 + (x1-x0)*(i/20), y0 + (y1-y0)*(i/20))",
1047
- "",
1048
- "# \u2705 USE INSTEAD \u2014 Bezier curves with overshoot:",
1049
- '# Use "ghost-cursor" or similar library',
1050
- "from ghost_cursor import path_to",
1051
- "path_to(page, (x1, y1))"
1052
- ].join("\n")
1053
- };
1054
- case "input-keystroke":
1055
- if (method === "Input.insertText") {
1056
- return {
1057
- title: "Input.insertText detected (logged, not blocked)",
1058
- detail: "Input.insertText bypasses native keyDown\u2192keyPress\u2192input\u2192keyUp events. Playwright uses this internally for page.fill(). Logged for observability."
1059
- };
1060
- }
1061
- return {
1062
- title: "Unnatural keystroke timing blocked",
1063
- detail: `Your ${method} calls have unnaturally constant timing (e.g., exact 50ms intervals). Human typing always has variation (CV > 0.2).`,
1064
- codeExample: [
1065
- "# \u274C BLOCKED \u2014 exact constant delay:",
1066
- "page.type('#input', 'hello', {delay: 50}) # every keystroke exactly 50ms apart",
1067
- "",
1068
- "# \u2705 USE INSTEAD \u2014 variable delay (human-like):",
1069
- "page.fill('#input', 'hello') # recommended, dispatches events properly",
1070
- "# OR: type with randomized delay",
1071
- "page.type('#input', 'hello', {delay: 50 + Math.floor(Math.random() * 80)})"
1072
- ].join("\n")
1073
- };
1074
- case "automation-signals":
1075
- return {
1076
- title: "Browser automation marker detected",
1077
- 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.`,
1078
- codeExample: [
1079
- "# \u274C BLOCKED \u2014 don't check for automation markers:",
1080
- "navigator.webdriver # NEVER check this",
1081
- "window.__playwright # NEVER check this",
1082
- "chrome.runtime # NEVER check this",
1083
- "",
1084
- "# \u2705 Just go about your business normally.",
1085
- "# Anti-detection is handled by the CDP firewall automatically."
1086
- ].join("\n")
1087
- };
1088
- case "fingerprinting":
1089
- return {
1090
- title: "Browser fingerprinting API access blocked",
1091
- 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.`,
1092
- codeExample: [
1093
- "# \u274C BLOCKED \u2014 fingerprinting vector:",
1094
- "canvas.toDataURL() # returns unique browser hash",
1095
- 'gl.getParameter(gl.VENDOR) # returns "SwiftShader" in headless',
1096
- "screen.availWidth - screen.availHeight # no OS chrome in headless",
1097
- "",
1098
- "# \u2705 Avoid accessing these APIs. They are only used for fingerprinting."
1099
- ].join("\n")
1100
- };
1101
- case "event-simulation":
1102
- return {
1103
- title: "Synthetic event simulation blocked",
1104
- 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.`,
1105
- codeExample: [
1106
- "# \u274C BLOCKED \u2014 synthetic events (isTrusted=false):",
1107
- "el.click() # isTrusted=false",
1108
- 'el.dispatchEvent(new Event("click")) # isTrusted=false',
1109
- "el.focus() # isTrusted=false",
1110
- "",
1111
- "# \u2705 USE INSTEAD \u2014 CDP-level input dispatch (isTrusted=true):",
1112
- "page.click(selector) # uses Input.dispatchMouseEvent",
1113
- "page.fill(selector, value) # dispatches real input events"
1114
- ].join("\n")
1115
- };
1116
- case "emulation-override":
1117
- return {
1118
- title: "CDP emulation override blocked",
1119
- 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.`,
1120
- codeExample: [
1121
- "# \u274C BLOCKED \u2014 detectable emulation override:",
1122
- "Emulation.setUserAgentOverride(...) # JS vs HTTP header mismatch",
1123
- "Emulation.setGeolocationOverride(...) # IP geo vs overridden geo mismatch",
1124
- "Emulation.setDeviceMetricsOverride(...) # matchMedia vs actual viewport",
1125
- "",
1126
- "# \u2705 These are handled automatically by the CDP firewall.",
1127
- "# Do NOT call them manually."
1128
- ].join("\n")
1129
- };
1130
- case "network-anomaly":
1131
- return {
1132
- title: "Network anomaly detected",
1133
- detail: `Your ${method} call triggered a network pattern that is characteristic of scrapers: blocking URLs, clearing caches, or intercepting requests.`,
1134
- codeExample: [
1135
- "# \u274C BLOCKED \u2014 scraper optimization:",
1136
- "Network.clearBrowserCache() # natural users never do this",
1137
- 'Network.setBlockedURLs(["*fonts*"]) # blocking resources is detectable',
1138
- "Fetch.enable() # MITM-style interception",
1139
- "",
1140
- "# \u2705 Let the browser manage its own cache and network normally."
1141
- ].join("\n")
1142
- };
1143
- case "page-lifecycle":
1144
- return {
1145
- title: "Suspicious page lifecycle pattern blocked",
1146
- 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).`,
1147
- codeExample: [
1148
- "# \u274C BLOCKED \u2014 unnatural lifecycle:",
1149
- "page.goto(url); page.pdf() # PDF = scraper giveaway",
1150
- "page.goto(url); page.screenshot() <500ms # screenshot before render",
1151
- "page.goto(url) 3x in <100ms # rapid navigation barrage",
1152
- "",
1153
- "# \u2705 Add proper waits between actions:",
1154
- 'page.goto(url, {waitUntil: "networkidle"})',
1155
- 'page.waitForSelector("body")',
1156
- "page.screenshot() # after rendering"
1157
- ].join("\n")
1158
- };
1159
- default:
1160
- return {
1161
- title: decision.reason,
1162
- detail: decision.suggestion ?? `The CDP call "${method}" was blocked by rule "${decision.ruleId}".`
1163
- };
1164
- }
1165
- }
1166
-
1167
- // src/cdp-interceptor/proxy.ts
1168
- function makeCompoundId(cdpSessionId, rawSessionId) {
1169
- return `${cdpSessionId ?? "nil"}::${rawSessionId ?? "nil"}`;
1170
- }
1171
- var CDPInterceptorProxy = class {
1172
- wss = null;
1173
- engine;
1174
- config;
1175
- logger;
1176
- started = false;
1177
- stats = {
1178
- totalMessages: 0,
1179
- blockedMessages: 0,
1180
- transformedMessages: 0,
1181
- passedMessages: 0,
1182
- byRule: {}
1183
- };
1184
- constructor(config) {
1185
- this.config = config;
1186
- this.engine = createRuleEngine(config.rules);
1187
- this.logger = createLogger({
1188
- enableLogging: config.enableLogging ?? true,
1189
- logDir: config.logDir
1190
- });
1191
- }
1192
- /** The port the proxy is listening on (only valid after start()) */
1193
- get port() {
1194
- const addr = this.wss?.address();
1195
- if (addr && typeof addr === "object") return addr.port;
1196
- return 0;
1197
- }
1198
- /** Start the proxy server */
1199
- async start() {
1200
- if (this.started) return this.port;
1201
- return new Promise((resolve, reject) => {
1202
- this.wss = new WebSocketServer({ port: this.config.listenPort ?? 0 }, () => {
1203
- const port = this.port;
1204
- this.engine.start();
1205
- this.started = true;
1206
- this.logger.info("CDP interceptor proxy started", { port, endpoint: this.config.cdpEndpoint });
1207
- resolve(port);
1208
- });
1209
- this.wss.on("error", reject);
1210
- this.wss.on("connection", (clientWs, _req) => {
1211
- this.handleConnection(clientWs);
1212
- });
1213
- });
1214
- }
1215
- /** Stop the proxy server */
1216
- async stop() {
1217
- this.engine.stop();
1218
- this.logger.flush();
1219
- this.started = false;
1220
- return new Promise((resolve) => {
1221
- if (!this.wss) return resolve();
1222
- this.wss.close(() => resolve());
1223
- this.wss = null;
1224
- });
1225
- }
1226
- /** Get accumulated statistics */
1227
- getStats() {
1228
- return { ...this.stats };
1229
- }
1230
- /** Get recent log entries (for inspection) */
1231
- getRecentLogs(count = 50) {
1232
- return this.logger.getRecent(count);
1233
- }
1234
- // ── Connection handling ──────────────────────────────────────
1235
- handleConnection(clientWs) {
1236
- let browserWs = null;
1237
- let isAlive = true;
1238
- const pendingMessages = [];
1239
- browserWs = new WebSocket(this.config.cdpEndpoint);
1240
- clientWs.on("message", (raw) => {
1241
- if (browserWs && browserWs.readyState === WebSocket.OPEN) {
1242
- this.handleClientMessage(clientWs, browserWs, raw);
1243
- } else {
1244
- pendingMessages.push(raw);
1245
- }
1246
- });
1247
- browserWs.on("open", () => {
1248
- for (const buf of pendingMessages) {
1249
- this.handleClientMessage(clientWs, browserWs, buf);
1250
- }
1251
- pendingMessages.length = 0;
1252
- });
1253
- browserWs.on("error", (err) => {
1254
- this.logger.info("Browser WebSocket error", { error: String(err) });
1255
- });
1256
- browserWs.on("close", (code, reason) => {
1257
- if (isAlive && clientWs.readyState === WebSocket.OPEN) {
1258
- this.logger.info("Browser WS closed, closing client", { code, reason: String(reason) });
1259
- clientWs.close();
1260
- }
1261
- });
1262
- browserWs.on("message", (raw) => {
1263
- this.handleBrowserMessage(clientWs, browserWs, raw);
1264
- });
1265
- const cleanup = () => {
1266
- isAlive = false;
1267
- if (browserWs && browserWs.readyState === WebSocket.OPEN) {
1268
- browserWs.close();
1269
- }
1270
- };
1271
- clientWs.on("close", cleanup);
1272
- clientWs.on("error", cleanup);
1273
- browserWs.on("close", () => {
1274
- if (isAlive && clientWs.readyState === WebSocket.OPEN) {
1275
- clientWs.close();
1276
- }
1277
- });
1278
- browserWs.on("error", cleanup);
1279
- }
1280
- // ── Message processing ───────────────────────────────────────
1281
- handleClientMessage(clientWs, browserWs, raw) {
1282
- const msg = this.parseMessage(raw);
1283
- if (!msg) return;
1284
- this.stats.totalMessages++;
1285
- if (!("method" in msg)) {
1286
- browserWs.send(raw.toString());
1287
- return;
1288
- }
1289
- const request = msg;
1290
- const ctx = {
1291
- method: request.method,
1292
- params: request.params ?? {},
1293
- sessionId: makeCompoundId("_cdpSession" in browserWs ? browserWs._cdpSession : void 0, request.sessionId),
1294
- direction: "client\u2192browser"
1295
- };
1296
- const decision = this.engine.evaluate(ctx);
1297
- this.logger.log(ctx.method, "client\u2192browser", ctx.sessionId, { method: ctx.method, params: ctx.params }, decision);
1298
- if (decision) {
1299
- this.recordDecision(decision);
1300
- }
1301
- if (decision?.action === "block") {
1302
- const blockMsg = formatBlockMessage(decision, ctx.method);
1303
- const errorResponse = {
1304
- id: request.id,
1305
- error: {
1306
- code: decision.errorCode ?? -32e3,
1307
- message: blockMsg
1308
- },
1309
- sessionId: request.sessionId
1310
- };
1311
- this.stats.blockedMessages++;
1312
- console.error(`
1313
- ${blockMsg}
1314
- `);
1315
- clientWs.send(JSON.stringify(errorResponse));
1316
- return;
1317
- }
1318
- if (decision?.action === "transform" && decision.transformedParams) {
1319
- const transformed = { ...request, params: decision.transformedParams };
1320
- this.stats.transformedMessages++;
1321
- browserWs.send(JSON.stringify(transformed));
1322
- return;
1323
- }
1324
- this.stats.passedMessages++;
1325
- browserWs.send(raw.toString());
1326
- }
1327
- handleBrowserMessage(clientWs, _browserWs, raw) {
1328
- const msg = this.parseMessage(raw);
1329
- if (!msg) {
1330
- clientWs.send(raw.toString());
1331
- return;
1332
- }
1333
- if ("method" in msg) {
1334
- const event = msg;
1335
- const ctx = {
1336
- method: event.method,
1337
- params: event.params ?? {},
1338
- sessionId: event.sessionId ?? "browser",
1339
- direction: "browser\u2192client"
1340
- };
1341
- const decision = this.engine.evaluate(ctx);
1342
- if (decision?.action === "block") {
1343
- return;
1344
- }
1345
- }
1346
- clientWs.send(raw.toString());
1347
- }
1348
- // ── Utilities ────────────────────────────────────────────────
1349
- parseMessage(raw) {
1350
- try {
1351
- return JSON.parse(raw.toString());
1352
- } catch {
1353
- return null;
1354
- }
1355
- }
1356
- recordDecision(decision) {
1357
- if (!this.stats.byRule[decision.ruleId]) {
1358
- this.stats.byRule[decision.ruleId] = { matched: 0, blocked: 0, transformed: 0 };
1359
- }
1360
- this.stats.byRule[decision.ruleId].matched++;
1361
- if (decision.action === "block") {
1362
- this.stats.byRule[decision.ruleId].blocked++;
1363
- } else if (decision.action === "transform") {
1364
- this.stats.byRule[decision.ruleId].transformed++;
1365
- }
1366
- }
1367
- };
1368
-
1369
942
  export {
1370
943
  domMutationRule,
1371
944
  mouseTrajectoryRule,
@@ -1376,7 +949,5 @@ export {
1376
949
  emulationOverrideRule,
1377
950
  networkAnomalyRule,
1378
951
  pageLifecycleRule,
1379
- createRuleEngine,
1380
- advise,
1381
- CDPInterceptorProxy
952
+ createRuleEngine
1382
953
  };