pi-lean-dimension 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +96 -36
  2. package/node_modules/pi-lean-portal/AGENTS.md +146 -4
  3. package/node_modules/pi-lean-portal/README.md +112 -50
  4. package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
  5. package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
  6. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
  7. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
  8. package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
  9. package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
  10. package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
  11. package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
  12. package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
  13. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
  14. package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
  15. package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
  16. package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
  17. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
  18. package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
  19. package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
  20. package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
  21. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
  22. package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
  23. package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
  24. package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
  25. package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
  26. package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
  27. package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
  28. package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
  29. package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
  30. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
  31. package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
  32. package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
  33. package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
  34. package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
  35. package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
  36. package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
  37. package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
  38. package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
  39. package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
  40. package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
  41. package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
  42. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
  43. package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
  44. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
  45. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  47. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
  48. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
  49. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
  50. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
  51. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
  52. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
  53. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
  54. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
  55. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
  56. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
  57. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  58. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  59. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
  60. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
  61. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
  62. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
  63. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
  64. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
  65. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
  66. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
  67. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  68. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_accessibility.cpython-313-pytest-9.1.0.pyc → test_accessibility.cpython-313-pytest-9.1.1.pyc} +0 -0
  69. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
  70. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
  71. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
  72. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
  73. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
  74. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_chromium_py_bridge.cpython-313-pytest-9.1.0.pyc → test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
  75. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_firefox_py_bridge.cpython-313-pytest-9.1.0.pyc → test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
  76. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  77. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  78. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
  79. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_transport.cpython-313-pytest-9.1.0.pyc → test_transport.cpython-313-pytest-9.1.1.pyc} +0 -0
  80. package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
  81. package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
  82. package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
  83. package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
  84. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
  85. package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
  86. package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
  87. package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
  88. package/node_modules/pi-lean-portal/contributed/README.md +304 -0
  89. package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
  90. package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
  91. package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
  92. package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
  93. package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
  94. package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
  95. package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
  96. package/node_modules/pi-lean-portal/core/router.ts +59 -98
  97. package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
  98. package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
  99. package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
  100. package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
  101. package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
  102. package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
  103. package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
  104. package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
  105. package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
  106. package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
  107. package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
  108. package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
  109. package/node_modules/pi-lean-portal/index.ts +42 -7
  110. package/node_modules/pi-lean-portal/package.json +8 -3
  111. package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
  112. package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
  113. package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
  114. package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
  115. package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
  116. package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
  117. package/node_modules/pi-lean-search/README.md +6 -2
  118. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
  119. package/node_modules/pi-lean-search/index.ts +4 -21
  120. package/node_modules/pi-lean-search/package.json +2 -2
  121. package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
  122. package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
  123. package/package.json +4 -4
  124. package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
  125. package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
  126. package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
  127. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
  128. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
  129. package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
  130. package/node_modules/pi-lean-portal/backends/python-base/tests/test_firefox_py_bridge.py +0 -212
@@ -4,8 +4,13 @@
4
4
  * Parses Playwright's page.ariaSnapshot() YAML-like output into an
5
5
  * LLM-friendly text format with @e1, @e2 element references. Caches
6
6
  * parsed nodes so interactions (click, type) can map back via getByRole().
7
+ *
8
+ * Role sets and icons are loaded from the shared ``browser-data.json``
9
+ * — the same file used by the Python bridge so the two sides never drift.
7
10
  */
8
11
 
12
+ import { ACCESSIBILITY } from "./browser-data.js";
13
+
9
14
  /** A single parsed node from the aria snapshot, cached for interaction */
10
15
  export interface AriaCachedNode {
11
16
  ref: string;
@@ -31,84 +36,16 @@ export interface AriaParseResult {
31
36
 
32
37
  /**
33
38
  * Roles that get @e refs and can be used for interaction.
39
+ * Loaded from shared browser-data.json.
34
40
  */
35
- export const INTERACTIVE_ROLES = new Set([
36
- "button",
37
- "link",
38
- "textbox",
39
- "searchbox",
40
- "combobox",
41
- "checkbox",
42
- "radio",
43
- "heading",
44
- "listbox",
45
- "option",
46
- "menuitem",
47
- "menuitemcheckbox",
48
- "menuitemradio",
49
- "tab",
50
- "treeitem",
51
- "switch",
52
- "slider",
53
- "spinbutton",
54
- "progressbar",
55
- "meter",
56
- "scrollbar",
57
- "gridcell",
58
- "cell",
59
- "columnheader",
60
- "rowheader",
61
- "tabpanel",
62
- "img",
63
- "figure",
64
- "listitem",
65
- "dialog",
66
- "alertdialog",
67
- "tooltip",
68
- "navigation",
69
- "banner",
70
- "form",
71
- "search",
72
- "toolbar",
73
- "menu",
74
- "menubar",
75
- "note",
76
- "alert",
77
- "status",
78
- "list",
79
- "table",
80
- "grid",
81
- "treegrid",
82
- "article",
83
- "section",
84
- "blockquote",
85
- "code",
86
- ]);
41
+ const INTERACTIVE_ROLES = new Set(ACCESSIBILITY.interactiveRoles);
87
42
 
88
43
  /**
89
44
  * Roles that are shown in the tree but DON'T get @e refs
90
45
  * (informational only, not useful click targets).
46
+ * Loaded from shared browser-data.json.
91
47
  */
92
- export const INFORMATIONAL_ROLES = new Set([
93
- "paragraph",
94
- "text",
95
- "group",
96
- "region",
97
- "main",
98
- "complementary",
99
- "contentinfo",
100
- "definition",
101
- "term",
102
- "math",
103
- "marquee",
104
- "timer",
105
- "log",
106
- "deletion",
107
- "insertion",
108
- "mark",
109
- "suggestion",
110
- "comment",
111
- ]);
48
+ const INFORMATIONAL_ROLES = new Set(ACCESSIBILITY.informationalRoles);
112
49
 
113
50
  /**
114
51
  * Parse the YAML-like output of page.ariaSnapshot().
@@ -306,77 +243,7 @@ function countLeadingSpaces(s: string): number {
306
243
  // ─── Helpers ──────────────────────────────────────────────────────────
307
244
 
308
245
  export function roleIcon(role: string): string {
309
- const icons: Record<string, string> = {
310
- alert: "🔔 ",
311
- alertdialog: "⚠ ",
312
- article: "📰 ",
313
- banner: "📰 ",
314
- blockquote: "💬 ",
315
- button: "🔘 ",
316
- cell: "▫ ",
317
- checkbox: "☑ ",
318
- code: "💻 ",
319
- columnheader: "📊 ",
320
- combobox: "📋 ",
321
- comment: "💬 ",
322
- complementary: "📎 ",
323
- contentinfo: "ℹ ",
324
- definition: "📖 ",
325
- deletion: "❌ ",
326
- dialog: "💬 ",
327
- figure: "🖼 ",
328
- form: "📝 ",
329
- grid: "📊 ",
330
- gridcell: "▫ ",
331
- group: "📦 ",
332
- heading: "📌 ",
333
- img: "🖼 ",
334
- insertion: "➕ ",
335
- link: "🔗 ",
336
- list: "📋 ",
337
- listbox: "📋 ",
338
- listitem: "• ",
339
- log: "📋 ",
340
- main: "📄 ",
341
- mark: "🖍️ ",
342
- marquee: "📜 ",
343
- math: "🧮 ",
344
- menu: "📋 ",
345
- menubar: "📋 ",
346
- menuitem: "📋 ",
347
- menuitemcheckbox: "☑ ",
348
- menuitemradio: "○ ",
349
- meter: "📊 ",
350
- navigation: "🧭 ",
351
- note: "📝 ",
352
- option: "• ",
353
- paragraph: "📃 ",
354
- progressbar: "⏳ ",
355
- radio: "○ ",
356
- region: "📦 ",
357
- rowheader: "📊 ",
358
- search: "🔍 ",
359
- searchbox: "🔍 ",
360
- scrollbar: "📜 ",
361
- section: "📄 ",
362
- slider: "🔧 ",
363
- spinbutton: "🔢 ",
364
- status: "📊 ",
365
- suggestion: "💡 ",
366
- switch: "🔀 ",
367
- tab: "📑 ",
368
- table: "📊 ",
369
- tabpanel: "📑 ",
370
- term: "📖 ",
371
- text: "📝 ",
372
- textbox: "📝 ",
373
- timer: "⏱️ ",
374
- toolbar: "🔧 ",
375
- tooltip: "💡 ",
376
- treegrid: "📊 ",
377
- treeitem: "• ",
378
- };
379
- return icons[role] || "";
246
+ return ACCESSIBILITY.roleIcons[role] ?? "";
380
247
  }
381
248
 
382
249
  function truncate(s: string, max: number): string {
@@ -6,93 +6,54 @@
6
6
  * the router flags the navigation as bot-blocked so the agent
7
7
  * can decide how to proceed — try web-fetch, try a different URL,
8
8
  * or switch to a stealth browser backend if one is configured.
9
+ *
10
+ * Signal lists are loaded from the shared ``browser-data.json`` —
11
+ * the same file used by the Python bridge so the two sides never drift.
9
12
  */
10
13
 
14
+ import { BOT_SIGNALS } from "./browser-data.js";
15
+
11
16
  export interface BotDetectionResult {
12
17
  /** True if the page appears to be a bot block/challenge page */
13
18
  isBlocked: boolean;
14
- /** Confidence score 0-1 */
15
- confidence: number;
16
- /** Signal that triggered the detection */
17
- signal?: string;
18
19
  }
19
20
 
20
21
  /**
21
- * Content patterns that indicate a bot block.
22
- *
23
- * Only specific challenge phrases are included — generic single words
24
- * like "cloudflare", "captcha", "recaptcha", "hcaptcha", "enable javascript"
25
- * etc. are excluded because they cause false positives on legitimate
26
- * pages (Cloudflare's own site, web scraping articles, CAPTCHA service pages).
27
- * Real challenge pages always use these exact phrases.
22
+ * Content patterns that indicate a bot block — loaded from shared data.
28
23
  */
29
- const BLOCK_SIGNALS = [
30
- "please verify you are human",
31
- "attention required!",
32
- "just a moment...",
33
- "checking your browser",
34
- "you have been blocked",
35
- "sorry, you have been blocked",
36
- "verify you are human",
37
- "your request has been blocked",
38
- "we are checking your browser",
39
- "cf-challenge",
40
- "_cf_chl_opt",
41
- "cdn-cgi/challenge",
42
- ];
24
+ const BLOCK_SIGNALS = BOT_SIGNALS.blockSignals;
43
25
 
44
26
  /**
45
- * Body-only string patterns — checked in body text (not the title) to
46
- * avoid false matches on legitimate content. Catches CDN-specific
47
- * block pages (Akamai "Access Denied", generic 403s).
27
+ * Body-only string patterns — loaded from shared data.
48
28
  */
49
- const BODY_ONLY_SIGNALS = [
50
- "errors.edgesuite.net",
51
- "you don't have permission to access",
52
- ];
29
+ const BODY_ONLY_SIGNALS = BOT_SIGNALS.bodyOnlySignals;
53
30
 
54
31
  /**
55
- * Body-only regex patterns — more specific than string inclusion.
56
- * These match the exact format of CDN error reference codes to
57
- * avoid false positives from generic "reference #123" in normal content.
32
+ * Body-only regex patterns — compiled from shared data at module load.
58
33
  */
59
- const BODY_ONLY_PATTERNS: RegExp[] = [
60
- /reference\s*#[a-f0-9]+(?:\.[a-f0-9]+)+/i,
61
- ];
34
+ const BODY_ONLY_PATTERNS: RegExp[] = BOT_SIGNALS.bodyOnlyPatterns.map(
35
+ (src) => new RegExp(src, "i"),
36
+ );
62
37
 
63
38
  /**
64
- * HTML-level signals — checked against ``document.documentElement.innerHTML``
65
- * rather than visible body text. These look for CAPTCHA widget embed codes
66
- * in the raw HTML source, which is a stronger signal than visible text
67
- * (generic mentions of "captcha" in body copy are excluded to avoid false
68
- * positives, but embed codes in HTML attributes are unambiguous).
39
+ * HTML-level signals — loaded from shared data.
69
40
  */
70
- const HTML_SIGNALS = [
71
- "recaptcha",
72
- "hcaptcha",
73
- "turnstile",
74
- "g-recaptcha",
75
- "data-sitekey",
76
- ];
41
+ const HTML_SIGNALS = BOT_SIGNALS.htmlSignals;
77
42
 
78
43
  /**
79
44
  * Check if page text content suggests a bot block.
80
45
  */
81
- export function checkBodyText(bodyText: string): BotDetectionResult {
82
- if (!bodyText) return { isBlocked: false, confidence: 0 };
46
+ function checkBodyText(bodyText: string): BotDetectionResult {
47
+ if (!bodyText) return { isBlocked: false };
83
48
 
84
49
  const lower = bodyText.toLowerCase();
85
50
  for (const signal of BLOCK_SIGNALS) {
86
51
  if (lower.includes(signal)) {
87
- return {
88
- isBlocked: true,
89
- confidence: signal.length > 20 ? 0.9 : 0.7,
90
- signal,
91
- };
52
+ return { isBlocked: true };
92
53
  }
93
54
  }
94
55
 
95
- return { isBlocked: false, confidence: 0 };
56
+ return { isBlocked: false };
96
57
  }
97
58
 
98
59
  /**
@@ -102,14 +63,14 @@ export function checkBodyText(bodyText: string): BotDetectionResult {
102
63
  * (for Akamai reference codes, etc.).
103
64
  */
104
65
  function checkBodyOnlyText(bodyText: string): BotDetectionResult {
105
- if (!bodyText) return { isBlocked: false, confidence: 0 };
66
+ if (!bodyText) return { isBlocked: false };
106
67
 
107
68
  const lower = bodyText.toLowerCase();
108
69
 
109
70
  // Check string signals first
110
71
  for (const signal of BODY_ONLY_SIGNALS) {
111
72
  if (lower.includes(signal)) {
112
- return { isBlocked: true, confidence: 0.85, signal };
73
+ return { isBlocked: true };
113
74
  }
114
75
  }
115
76
 
@@ -117,39 +78,32 @@ function checkBodyOnlyText(bodyText: string): BotDetectionResult {
117
78
  for (const pattern of BODY_ONLY_PATTERNS) {
118
79
  const match = bodyText.match(pattern);
119
80
  if (match) {
120
- return {
121
- isBlocked: true,
122
- confidence: 0.9,
123
- signal: `regex: ${match[0].slice(0, 50)}`,
124
- };
81
+ return { isBlocked: true };
125
82
  }
126
83
  }
127
84
 
128
- return { isBlocked: false, confidence: 0 };
85
+ return { isBlocked: false };
129
86
  }
130
87
 
131
88
  /**
132
89
  * Check raw HTML content for CAPTCHA widget embed codes.
133
90
  *
134
91
  * Looks for specific widget identifiers in the HTML source (not visible text),
135
- * so false positive risk from generic mentions is low. Mirrors the Python
136
- * bridge's ``_HTML_SIGNALS`` in ``backends/chromium-py/bridge.py``.
92
+ * so false positive risk from generic mentions is low. The signal list is
93
+ * loaded from the shared ``browser-data.json`` so TypeScript and Python
94
+ * bridge implementations share a single source of truth.
137
95
  */
138
- export function checkHtmlContent(html: string): BotDetectionResult {
139
- if (!html) return { isBlocked: false, confidence: 0 };
96
+ function checkHtmlContent(html: string): BotDetectionResult {
97
+ if (!html) return { isBlocked: false };
140
98
 
141
99
  const lower = html.toLowerCase();
142
100
  for (const signal of HTML_SIGNALS) {
143
101
  if (lower.includes(signal)) {
144
- return {
145
- isBlocked: true,
146
- confidence: 0.85,
147
- signal: `html:${signal}`,
148
- };
102
+ return { isBlocked: true };
149
103
  }
150
104
  }
151
105
 
152
- return { isBlocked: false, confidence: 0 };
106
+ return { isBlocked: false };
153
107
  }
154
108
 
155
109
  /**
@@ -183,5 +137,5 @@ export function checkPage(
183
137
  if (htmlResult.isBlocked) return htmlResult;
184
138
  }
185
139
 
186
- return { isBlocked: false, confidence: 0 };
140
+ return { isBlocked: false };
187
141
  }
@@ -0,0 +1,183 @@
1
+ {
2
+ "version": 1,
3
+ "botSignals": {
4
+ "blockSignals": [
5
+ "please verify you are human",
6
+ "attention required!",
7
+ "just a moment...",
8
+ "checking your browser",
9
+ "you have been blocked",
10
+ "sorry, you have been blocked",
11
+ "verify you are human",
12
+ "your request has been blocked",
13
+ "we are checking your browser",
14
+ "cf-challenge",
15
+ "_cf_chl_opt",
16
+ "cdn-cgi/challenge"
17
+ ],
18
+ "bodyOnlySignals": [
19
+ "errors.edgesuite.net",
20
+ "you don't have permission to access"
21
+ ],
22
+ "bodyOnlyPatterns": [
23
+ "reference\\s*#[a-f0-9]+(?:\\.[a-f0-9]+)+"
24
+ ],
25
+ "htmlSignals": [
26
+ "recaptcha",
27
+ "hcaptcha",
28
+ "turnstile",
29
+ "g-recaptcha",
30
+ "data-sitekey"
31
+ ]
32
+ },
33
+ "accessibility": {
34
+ "interactiveRoles": [
35
+ "button",
36
+ "link",
37
+ "textbox",
38
+ "searchbox",
39
+ "combobox",
40
+ "checkbox",
41
+ "radio",
42
+ "heading",
43
+ "listbox",
44
+ "option",
45
+ "menuitem",
46
+ "menuitemcheckbox",
47
+ "menuitemradio",
48
+ "tab",
49
+ "treeitem",
50
+ "switch",
51
+ "slider",
52
+ "spinbutton",
53
+ "progressbar",
54
+ "meter",
55
+ "scrollbar",
56
+ "gridcell",
57
+ "cell",
58
+ "columnheader",
59
+ "rowheader",
60
+ "tabpanel",
61
+ "img",
62
+ "figure",
63
+ "listitem",
64
+ "dialog",
65
+ "alertdialog",
66
+ "tooltip",
67
+ "navigation",
68
+ "banner",
69
+ "form",
70
+ "search",
71
+ "toolbar",
72
+ "menu",
73
+ "menubar",
74
+ "note",
75
+ "alert",
76
+ "status",
77
+ "list",
78
+ "table",
79
+ "grid",
80
+ "treegrid",
81
+ "article",
82
+ "section",
83
+ "blockquote",
84
+ "code"
85
+ ],
86
+ "informationalRoles": [
87
+ "paragraph",
88
+ "text",
89
+ "group",
90
+ "region",
91
+ "main",
92
+ "complementary",
93
+ "contentinfo",
94
+ "definition",
95
+ "term",
96
+ "math",
97
+ "marquee",
98
+ "timer",
99
+ "log",
100
+ "deletion",
101
+ "insertion",
102
+ "mark",
103
+ "suggestion",
104
+ "comment"
105
+ ],
106
+ "roleIcons": {
107
+ "alert": "🔔 ",
108
+ "alertdialog": "⚠ ",
109
+ "article": "📰 ",
110
+ "banner": "📰 ",
111
+ "blockquote": "💬 ",
112
+ "button": "🔘 ",
113
+ "cell": "▫ ",
114
+ "checkbox": "☑ ",
115
+ "code": "💻 ",
116
+ "columnheader": "📊 ",
117
+ "combobox": "📋 ",
118
+ "comment": "💬 ",
119
+ "complementary": "📎 ",
120
+ "contentinfo": "ℹ ",
121
+ "definition": "📖 ",
122
+ "deletion": "❌ ",
123
+ "dialog": "💬 ",
124
+ "figure": "🖼 ",
125
+ "form": "📝 ",
126
+ "grid": "📊 ",
127
+ "gridcell": "▫ ",
128
+ "group": "📦 ",
129
+ "heading": "📌 ",
130
+ "img": "🖼 ",
131
+ "insertion": "➕ ",
132
+ "link": "🔗 ",
133
+ "list": "📋 ",
134
+ "listbox": "📋 ",
135
+ "listitem": "• ",
136
+ "log": "📋 ",
137
+ "main": "📄 ",
138
+ "mark": "🖍️ ",
139
+ "marquee": "📜 ",
140
+ "math": "🧮 ",
141
+ "menu": "📋 ",
142
+ "menubar": "📋 ",
143
+ "menuitem": "📋 ",
144
+ "menuitemcheckbox": "☑ ",
145
+ "menuitemradio": "○ ",
146
+ "meter": "📊 ",
147
+ "navigation": "🧭 ",
148
+ "note": "📝 ",
149
+ "option": "• ",
150
+ "paragraph": "📃 ",
151
+ "progressbar": "⏳ ",
152
+ "radio": "○ ",
153
+ "region": "📦 ",
154
+ "rowheader": "📊 ",
155
+ "search": "🔍 ",
156
+ "searchbox": "🔍 ",
157
+ "scrollbar": "📜 ",
158
+ "section": "📄 ",
159
+ "slider": "🔧 ",
160
+ "spinbutton": "🔢 ",
161
+ "status": "📊 ",
162
+ "suggestion": "💡 ",
163
+ "switch": "🔀 ",
164
+ "tab": "📑 ",
165
+ "table": "📊 ",
166
+ "tabpanel": "📑 ",
167
+ "term": "📖 ",
168
+ "text": "📝 ",
169
+ "textbox": "📝 ",
170
+ "timer": "⏱️ ",
171
+ "toolbar": "🔧 ",
172
+ "tooltip": "💡 ",
173
+ "treegrid": "📊 ",
174
+ "treeitem": "• "
175
+ }
176
+ },
177
+ "navSettle": {
178
+ "navTimeoutMs": 5000,
179
+ "settleTimeoutMs": 400,
180
+ "settleRaceMs": 150,
181
+ "domStabilizationJs": "new Promise((resolve) => {\n const count = document.querySelectorAll(\"*\").length;\n setTimeout(() => {\n resolve(document.querySelectorAll(\"*\").length === count || count > 5000);\n }, 400);\n})"
182
+ }
183
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Typed loader for shared browser data (bot-detection signals,
3
+ * accessibility roles/icons, nav-settle constants).
4
+ *
5
+ * Imports from the sibling JSON file so the same source of truth is
6
+ * available to both TypeScript and Python consumers. The import
7
+ * assertion ensures the JSON is loaded directly — no runtime path
8
+ * resolution needed, which keeps vitest and the pi runtime happy.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import _raw from "./browser-data.json" with { type: "json" };
14
+
15
+ // ─── Types ─────────────────────────────────────────────────────────────
16
+
17
+ export interface BotSignals {
18
+ blockSignals: string[];
19
+ bodyOnlySignals: string[];
20
+ bodyOnlyPatterns: string[];
21
+ htmlSignals: string[];
22
+ }
23
+
24
+ export interface AccessibilityData {
25
+ interactiveRoles: string[];
26
+ informationalRoles: string[];
27
+ roleIcons: Record<string, string>;
28
+ }
29
+
30
+ export interface NavSettleData {
31
+ navTimeoutMs: number;
32
+ settleTimeoutMs: number;
33
+ settleRaceMs: number;
34
+ domStabilizationJs: string;
35
+ }
36
+
37
+ export interface BrowserDataFile {
38
+ version: number;
39
+ botSignals: BotSignals;
40
+ accessibility: AccessibilityData;
41
+ navSettle: NavSettleData;
42
+ }
43
+
44
+ const _data = _raw as BrowserDataFile;
45
+
46
+ // ─── Exports ───────────────────────────────────────────────────────────
47
+
48
+ export const BOT_SIGNALS: BotSignals = _data.botSignals;
49
+ export const ACCESSIBILITY: AccessibilityData = _data.accessibility;
50
+ export const NAV_SETTLE: NavSettleData = _data.navSettle;
@@ -10,7 +10,7 @@
10
10
 
11
11
  import type { Page } from "playwright";
12
12
 
13
- export interface DialogEvent {
13
+ export interface DialogLogEntry {
14
14
  type: "alert" | "confirm" | "prompt" | "beforeunload";
15
15
  message: string;
16
16
  /** Default value for prompt dialogs */
@@ -35,13 +35,13 @@ export interface ConsoleEvent {
35
35
  }
36
36
 
37
37
  /** Dialogs logged per task, for reporting to user */
38
- const _dialogLog = new Map<string, DialogEvent[]>();
38
+ const _dialogLog = new Map<string, DialogLogEntry[]>();
39
39
 
40
40
  /** Console messages logged per task */
41
41
  const _consoleLog = new Map<string, ConsoleEvent[]>();
42
42
 
43
43
  /** Get logged dialogs for a task */
44
- export function getDialogLog(taskId: string): DialogEvent[] {
44
+ export function getDialogLog(taskId: string): DialogLogEntry[] {
45
45
  return _dialogLog.get(taskId) ?? [];
46
46
  }
47
47
 
@@ -61,7 +61,7 @@ export function clearConsoleLog(taskId: string): void {
61
61
  * Captures console messages (log, warn, error, info, debug) for retrieval.
62
62
  */
63
63
  export function installDialogHandlers(taskId: string, page: Page): void {
64
- const dialogLog: DialogEvent[] = [];
64
+ const dialogLog: DialogLogEntry[] = [];
65
65
  _dialogLog.set(taskId, dialogLog);
66
66
 
67
67
  const consoleLog: ConsoleEvent[] = [];
@@ -69,8 +69,8 @@ export function installDialogHandlers(taskId: string, page: Page): void {
69
69
 
70
70
  // Auto-accept JavaScript dialogs
71
71
  page.on("dialog", async (dialog) => {
72
- const entry: DialogEvent = {
73
- type: dialog.type() as DialogEvent["type"],
72
+ const entry: DialogLogEntry = {
73
+ type: dialog.type() as DialogLogEntry["type"],
74
74
  message: dialog.message(),
75
75
  defaultValue: dialog.defaultValue(),
76
76
  handledAs: "accepted",