pi-lean-dimension 0.2.3 → 0.3.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 (176) hide show
  1. package/README.md +1 -1
  2. package/node_modules/pi-lean-portal/AGENTS.md +15 -23
  3. package/node_modules/pi-lean-portal/README.md +31 -1
  4. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +12 -1
  5. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +263 -1102
  6. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +6 -32
  7. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +1 -1
  8. package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +1 -2
  9. package/node_modules/pi-lean-portal/__tests__/helpers/setup-temp-dir.ts +26 -0
  10. package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +1 -1
  11. package/node_modules/pi-lean-portal/__tests__/plugin-contract.test.ts +1 -1
  12. package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +1 -1
  13. package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +1 -1
  14. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +65 -69
  15. package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +1 -14
  16. package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +31 -16
  17. package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +1 -1
  18. package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +146 -19
  19. package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
  20. package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
  21. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
  22. package/node_modules/pi-lean-portal/backends/python-adapter.ts +3 -3
  23. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +2 -4
  24. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
  25. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  26. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
  27. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  28. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  29. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +572 -81
  30. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  31. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313-pytest-9.1.1.pyc +0 -0
  32. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
  33. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
  34. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  35. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
  36. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +13 -43
  37. package/node_modules/pi-lean-portal/browser-profile.ts +2 -3
  38. package/node_modules/pi-lean-portal/browser-toggle.ts +168 -380
  39. package/node_modules/pi-lean-portal/contributed/CHOOSING.md +2 -2
  40. package/node_modules/pi-lean-portal/contributed/README.md +65 -6
  41. package/node_modules/pi-lean-portal/contributed/camoufox-py/__pycache__/bridge.cpython-312.pyc +0 -0
  42. package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +108 -14
  43. package/node_modules/pi-lean-portal/contributed/camoufox-py/pin.json +4 -0
  44. package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +1 -1
  45. package/node_modules/pi-lean-portal/core/fetch-backend.ts +10 -30
  46. package/node_modules/pi-lean-portal/core/guides.ts +14 -18
  47. package/node_modules/pi-lean-portal/core/router.ts +2 -20
  48. package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +19 -30
  49. package/node_modules/pi-lean-portal/core/shared/browser-data.ts +4 -4
  50. package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +2 -2
  51. package/node_modules/pi-lean-portal/core/shared/paths.ts +16 -2
  52. package/node_modules/pi-lean-portal/core/shared/settings-reader.ts +2 -7
  53. package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +1 -1
  54. package/node_modules/pi-lean-portal/index.ts +8 -1
  55. package/node_modules/pi-lean-portal/package.json +2 -1
  56. package/node_modules/pi-lean-portal/tools/web-learn.ts +1 -4
  57. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +87 -0
  58. package/node_modules/pi-lean-search/index.ts +134 -64
  59. package/node_modules/pi-lean-search/package.json +2 -1
  60. package/node_modules/playwright/README.md +3 -3
  61. package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +2 -2
  62. package/node_modules/playwright/lib/cli/reportActions.js +2 -2
  63. package/node_modules/playwright/lib/common/index.js +77 -52
  64. package/node_modules/playwright/lib/common/index.js.txt +10 -11
  65. package/node_modules/playwright/lib/errorContext.js +2 -3
  66. package/node_modules/playwright/lib/index.js +30 -8
  67. package/node_modules/playwright/lib/matchers/expect.js +4811 -4379
  68. package/node_modules/playwright/lib/matchers/expect.js.LICENSE +87 -140
  69. package/node_modules/playwright/lib/matchers/expect.js.txt +30 -35
  70. package/node_modules/playwright/lib/program.js +19 -0
  71. package/node_modules/playwright/lib/runner/index.js +309 -153
  72. package/node_modules/playwright/lib/runner/index.js.txt +13 -12
  73. package/node_modules/playwright/lib/transform/babelBundle.js +12899 -11654
  74. package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +379 -354
  75. package/node_modules/playwright/lib/transform/babelBundle.js.txt +39 -39
  76. package/node_modules/playwright/lib/transform/esmLoader.js +3129 -1116
  77. package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +81 -61
  78. package/node_modules/playwright/lib/transform/esmLoader.js.txt +21 -18
  79. package/node_modules/playwright/lib/util.js +2 -29
  80. package/node_modules/playwright/lib/worker/workerProcessEntry.js +14 -32
  81. package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +7 -8
  82. package/node_modules/playwright/package.json +3 -6
  83. package/node_modules/playwright/types/test.d.ts +391 -4
  84. package/node_modules/playwright/types/testReporter.d.ts +79 -4
  85. package/node_modules/playwright-core/browsers.json +12 -18
  86. package/node_modules/playwright-core/lib/bootstrap.js +1 -1
  87. package/node_modules/playwright-core/lib/coreBundle.js +11344 -9900
  88. package/node_modules/playwright-core/lib/server/electron/loader.js +5 -2
  89. package/node_modules/playwright-core/lib/serverRegistry.js +5 -1
  90. package/node_modules/playwright-core/lib/tools/cli-client/help.json +21 -6
  91. package/node_modules/playwright-core/lib/tools/cli-client/program.js +1 -1
  92. package/node_modules/playwright-core/lib/tools/cli-client/session.js +4 -0
  93. package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/SKILL.md +21 -5
  94. package/node_modules/playwright-core/lib/tools/{cli-client/skill/references/spec-driven-testing.md → skills/playwright-cli/references/test-generation.md} +135 -7
  95. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +143 -0
  96. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
  97. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +85 -0
  98. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
  99. package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
  100. package/node_modules/playwright-core/lib/tools/utils/extension.js +31 -8
  101. package/node_modules/playwright-core/lib/utilsBundle.js +37698 -33670
  102. package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +391 -268
  103. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
  104. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-DhC616m4.css +1 -0
  105. package/node_modules/playwright-core/lib/vite/dashboard/index.html +2 -2
  106. package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -1
  107. package/node_modules/playwright-core/lib/vite/htmlReport/report.js +15 -55
  108. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
  109. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +32 -0
  110. package/node_modules/playwright-core/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
  111. package/node_modules/playwright-core/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
  112. package/node_modules/playwright-core/lib/vite/recorder/index.html +2 -2
  113. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-By56iMx7.js +32 -0
  114. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B34OrIms.js +181 -0
  115. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
  116. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-COQkjINf.js +7 -0
  117. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
  118. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +1 -0
  119. package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
  120. package/node_modules/playwright-core/lib/vite/traceViewer/index.Dl36UVQT.js +1 -0
  121. package/node_modules/playwright-core/lib/vite/traceViewer/index.html +5 -5
  122. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
  123. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +2 -2
  124. package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +3 -4
  125. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.C7UW1sC9.css +1 -0
  126. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.D962mr9b.js +5 -0
  127. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +5 -5
  128. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
  129. package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
  130. package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
  131. package/node_modules/playwright-core/lib/xdg-open +338 -137
  132. package/node_modules/playwright-core/package.json +2 -2
  133. package/node_modules/playwright-core/types/protocol.d.ts +249 -36
  134. package/node_modules/playwright-core/types/structs.d.ts +3 -1
  135. package/node_modules/playwright-core/types/types.d.ts +2009 -266
  136. package/package.json +3 -3
  137. package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +0 -188
  138. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
  139. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
  140. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +0 -525
  141. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
  142. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
  143. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
  144. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc +0 -0
  145. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc +0 -0
  146. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  147. package/node_modules/pi-lean-portal/core/shared/url-safety.ts +0 -164
  148. package/node_modules/playwright-core/lib/server/deviceDescriptorsSource.json +0 -2739
  149. package/node_modules/playwright-core/lib/tools/cli-client/skill/references/test-generation.md +0 -134
  150. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-BY2S1tHT.css +0 -1
  151. package/node_modules/playwright-core/lib/vite/dashboard/assets/index-C_5TMfeg.js +0 -52
  152. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DYBRYzYX.css +0 -1
  153. package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DeBYQozu.js +0 -32
  154. package/node_modules/playwright-core/lib/vite/recorder/assets/index-4ZiSSCmn.css +0 -1
  155. package/node_modules/playwright-core/lib/vite/recorder/assets/index-Bq-mQf8S.js +0 -193
  156. package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-LEHpjmcn.js +0 -32
  157. package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-BNmKHKpQ.js +0 -264
  158. package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-BYQrIQwR.js +0 -1
  159. package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-CsJ4vdCR.js +0 -9
  160. package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.DYBRYzYX.css +0 -1
  161. package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.CjdS-WJx.css +0 -1
  162. package/node_modules/playwright-core/lib/vite/traceViewer/index.CzXZzn5A.css +0 -1
  163. package/node_modules/playwright-core/lib/vite/traceViewer/index.DMMX1gXU.js +0 -2
  164. package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.v8KI4P3m.js +0 -2
  165. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.BZQ54Kgt.css +0 -1
  166. package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Ut8wwJNp.js +0 -6
  167. package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.DYP7pi_n.css +0 -32
  168. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/element-attributes.md +0 -0
  169. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/playwright-tests.md +0 -0
  170. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/request-mocking.md +0 -0
  171. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/running-code.md +0 -0
  172. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/session-management.md +0 -0
  173. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/storage-state.md +0 -0
  174. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/tracing.md +0 -0
  175. /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/video-recording.md +0 -0
  176. /package/node_modules/playwright-core/lib/tools/{trace → skills/playwright-trace}/SKILL.md +0 -0
@@ -16,8 +16,8 @@ documented in `playwright_base.py`.
16
16
  > **You need the git repo, not just `npm install pi-lean-portal`.**
17
17
  > The template bridge and this README live under
18
18
  > `packages/pi-lean-portal/contributed/` in the source
19
- > repository. `docs/` is excluded from the portal `package.json`
20
- > `files`, so the templates are not in the published tarball. Clone the
19
+ > repository. `contributed/` is not included in the portal `package.json`
20
+ > `files` allow-list, so the templates are not in the published tarball. Clone the
21
21
  > repo (or obtain a copy of these files) before starting.
22
22
 
23
23
  For *whether* you should reach for a stealth backend at all, see
@@ -26,6 +26,52 @@ discovery, `browser.init` RPC, `PYTHONPATH` injection), see the
26
26
  "Stealth backends (user-managed)" section of
27
27
  [`packages/pi-lean-portal/AGENTS.md`](../../AGENTS.md).
28
28
 
29
+ ## Pinned CI stack
30
+
31
+ The `contributed` GitHub Actions job pins both pieces of the Camoufox
32
+ stack for reproducibility — the patched-Firefox **binary** and the
33
+ **`cloverlabs-camoufox`** PyPI package. The canonical pin values live in
34
+ the sidecar manifest `contributed/camoufox-py/pin.json` (the CI workflow
35
+ reads them from there via `jq`). Unpinned, `camoufox fetch` pulls the
36
+ latest stable binary on every run, and a new release silently changed
37
+ wheel-scroll and humanized-click semantics — the quirks in
38
+ `playwright_base.py` were written against the older binary and the
39
+ `contributed` job went red.
40
+
41
+ | Pin | Value | Pinned since | Source |
42
+ |-----|-------|--------------|--------|
43
+ | PyPI package | `cloverlabs-camoufox==0.6.0` | Jul 14 2026 | `pin.json` `.package` |
44
+ | Patched binary | `official/152.0.4-beta.28` | Jul 16 2026 | `pin.json` `.binary` |
45
+
46
+ `152.0.4-beta.27` shipped on Jul 16 2026 and broke the unpinned job:
47
+ `page.mouse.wheel` became a no-op and `humanize=True`'s bezier motion
48
+ made `locator.click(timeout=5s)` flake/timeout and blew MiniWoB task
49
+ budgets. The quirks were adapted to the new binary —
50
+ `_scroll_via_wheel` flipped off (eval `window.scrollBy` works again)
51
+ and the contributed test suites force `launch.humanize=false` (they
52
+ exercise the backend contract, not human-emulation stealth). The pin
53
+ now tracks `152.0.4-beta.28`.
54
+
55
+ ### Upgrade procedure
56
+
57
+ 1. Reproduce the failure locally with `BROWSER_DEBUG=1` against the
58
+ **latest** Camoufox stack (bump both pins or drop them temporarily):
59
+ `npm run setup:miniwob` then
60
+ `CONTRIB_RUN=1 npx vitest run packages/pi-lean-portal/__tests__/run-contributed-suites.test.ts bench/miniwob/suites/miniwob-user-backends.test.ts`.
61
+ 2. Adapt the affected quirks in
62
+ `contributed/camoufox-py/bridge.py` and/or
63
+ `backends/python-base/pi_browser_bridge/playwright_base.py`. Keep the
64
+ `ponytail:` ceiling-notes on each quirk accurate to the new binary.
65
+ 3. Update `pin.json` to the newest passing version and re-run the
66
+ `contributed` job via `workflow_dispatch` to confirm green.
67
+ 4. The `pin.json` file drives both CI and the per-backend binary drift
68
+ check (see below), so bumping it here is the single source of truth
69
+ — no separate CI edit is needed.
70
+
71
+ The `camoufox fetch` version format is `<repo>/<version>` (e.g.
72
+ `official/152.0.4-beta.28`); run `camoufox sync` then `camoufox list` to
73
+ discover available versions.
74
+
29
75
  ## Where things live
30
76
 
31
77
  ```
@@ -35,6 +81,8 @@ discovery, `browser.init` RPC, `PYTHONPATH` injection), see the
35
81
  └── user-backends/ ← stealth backends go here
36
82
  └── camoufox-py/
37
83
  ├── bridge.py ← you copy this from the source repo
84
+ ├── pin.json ← pinned versions this bridge was tested
85
+ │ against; copy alongside bridge.py
38
86
  └── .venv/ ← you create this (engine pip pkg + playwright)
39
87
  ```
40
88
 
@@ -56,7 +104,7 @@ The convention is `<name>-py/` (mirrors the shipped `chromium-py` /
56
104
  `firefox-py` naming). The directory name is the plugin `name` you will
57
105
  register in `settings.json`.
58
106
 
59
- ### 2. Copy the template bridge
107
+ ### 2. Copy the template bridge and pin manifest
60
108
 
61
109
  Copy the source-repo template into your user-backends tree:
62
110
 
@@ -64,11 +112,19 @@ Copy the source-repo template into your user-backends tree:
64
112
  mkdir -p ~/.pi/agent/pi-lean-portal/user-backends/camoufox-py
65
113
  cp packages/pi-lean-portal/contributed/camoufox-py/bridge.py \
66
114
  ~/.pi/agent/pi-lean-portal/user-backends/camoufox-py/bridge.py
115
+ cp packages/pi-lean-portal/contributed/camoufox-py/pin.json \
116
+ ~/.pi/agent/pi-lean-portal/user-backends/camoufox-py/pin.json
67
117
  ```
68
118
 
119
+ The `pin.json` sidecar is what the CI reads and what the bridge's
120
+ built-in drift check uses at launch. Copying it alongside the bridge
121
+ lets the binary self-verify that the locally installed package
122
+ matches the tested pin — a warning is emitted to stderr if the
123
+ versions diverge.
124
+
69
125
  You need the **git repo** for this step — the template is not in the
70
- `npm install pi-lean-portal` tarball (`docs/` is excluded from
71
- `package.json` `files` by design). Alternatively, write your own
126
+ `npm install pi-lean-portal` tarball (`contributed/` is not included in the
127
+ `package.json` `files` allow-list by design). Alternatively, write your own
72
128
  subclass using the [quirks schema](#quirks-schema-reference) documented
73
129
  in `backends/python-base/pi_browser_bridge/playwright_base.py`. Either
74
130
  way, audit what you copy — this is trusted user code that the extension
@@ -226,10 +282,13 @@ as class attributes on your subclass:
226
282
  |------|---------|-----------------|
227
283
  | `_fingerprint_managed_context` | `False` | `create_browser_context()` skips hardcoded `viewport`/`user_agent`; lets the fingerprint package set them. |
228
284
  | `_eval_prefix` | `""` | Prepended to every `page.evaluate` expression in `do_evaluate` (e.g. Camoufox's `"mw:"` routes writes to the main world). |
229
- | `_scroll_via_wheel` | `False` | `do_scroll` uses `page.mouse.wheel` instead of `page.evaluate("window.scrollBy")` (avoids eval-write under isolated-world stealth). |
285
+ | `_scroll_via_wheel` | `False` | `do_scroll` uses `page.mouse.wheel` instead of `page.evaluate("window.scrollBy")`. Legacy Camoufox (``135.0.1-beta.24``) needed this — eval-write scrollBy no-op'd under the isolated world. Current binary (``152.0.4-beta.28``) is the reverse: wheel no-ops, eval scrollBy works, so Camoufox now leaves the default `False`. |
230
286
  | `_skip_default_viewport` | `False` | Skips Playwright's `Browser.setDefaultViewport` CDP call (Camoufox binary rejects its `isMobile` prop). |
231
287
  | `_skip_networkidle` | `False` | Nav-settle uses `load` instead of `networkidle` (patched binaries don't fire `networkidle` reliably). |
232
288
  | `_wrap_mw_eval_in_eval` | `False` | `do_evaluate` rewrites the expression as `eval(<JSON-string of expression>)` before prepending `_eval_prefix`, so multi-statement scripts survive Camoufox's `let _s = (${script})` main-world wrapper (which only accepts a single expression). Camoufox-only; flip back to `False` when a future driver fixes the wrapper. |
289
+ | `_settle_budget_ms` | `400` | Poll budget (ms) for `_wait_for_navigation_settle` when no `framenavigated` event fires. Stealth backends whose patched Juggler fires events with higher latency (e.g. Camoufox) set a larger value (2000) to avoid settling before the navigation commit is observable. |
290
+ | `_url_stability_settle` | `False` | When True, the no-nav poll waits for the URL to stabilise at a new value for 150 ms before exiting, instead of a fixed budget. Complements a wider `_settle_budget_ms` by confirming the URL has finished changing. |
291
+ | `_csp_safe_readonly_via_init_script` | `False` | `create_browser_context` registers an init script (isolated world, CSP-free) that wraps the EXTRACTOR_SCRIPT in a `DOMContentLoaded`-deferred IIFE writing JSON to `<meta id="__pi-extract">`. `do_evaluate(read_only=True)` reads from that meta via `query_selector` + `get_attribute` instead of `page.evaluate` (which is CSP-blocked on some patched-Firefox stealth binaries). |
233
292
 
234
293
  All flags default off, so the shipped `chromium-py` / `firefox-py`
235
294
  behavior is bit-identical to a pre-stealth install. For the two
@@ -6,6 +6,7 @@ User-installed backend for fingerprint-managed browsing via cloverlabs-camoufox.
6
6
 
7
7
  import json
8
8
  import sys
9
+ from pathlib import Path
9
10
 
10
11
  # Lazy-patch Playwright's coreBundle.js if needed. The patcher is idempotent
11
12
  # and guarded: it only runs once per file (marker comment), warns on pattern
@@ -34,7 +35,7 @@ class CamoufoxPyBridge(PlaywrightBridge):
34
35
  """Stealth bridge using Camoufox (Firefox-based).
35
36
 
36
37
  The base's quirks dispatch handles eval-world routing (``mw:`` prefix)
37
- and wheel-based scrolling (``_scroll_via_wheel``). This subclass sets
38
+ and eval-based scrolling (``window.scrollBy``). This subclass sets
38
39
  the quirk flags and overrides ``_launch_browser`` to call
39
40
  ``camoufox.NewBrowser`` with the plugin config's launch options.
40
41
 
@@ -60,7 +61,13 @@ class CamoufoxPyBridge(PlaywrightBridge):
60
61
  # crashes on this binary (same ``isMobile`` rejection). Keep using
61
62
  # the standard ``browser.new_context()``.
62
63
  _eval_prefix: str = "mw:"
63
- _scroll_via_wheel: bool = True
64
+ # Camoufox ``152.0.4-beta.27+`` no-ops ``page.mouse.wheel`` (the ``wheel``
65
+ # listener never fires), so the base's eval-based ``window.scrollBy`` is
66
+ # the only path that moves the page on the current binary. Leave the
67
+ # base default (``_scroll_via_wheel = False``). The legacy
68
+ # ``135.0.1-beta.24`` binary needed ``True`` (eval-write scrollBy
69
+ # no-op'd under the isolated world); flipped off when the CI pin moved
70
+ # to ``152.0.4-beta.28``. See ``playwright_base._scroll_via_wheel``.
64
71
  # The Camoufox patched Firefox binary rejects the ``isMobile`` property that
65
72
  # Playwright includes in ``Browser.setDefaultViewport``. Skip the call.
66
73
  _skip_default_viewport: bool = True
@@ -75,6 +82,15 @@ class CamoufoxPyBridge(PlaywrightBridge):
75
82
  # `mw:eval(<json>)` so it is a single expression that `eval` runs
76
83
  # verbatim. See the `_wrap_mw_eval_in_eval` quirk docstring.
77
84
  _wrap_mw_eval_in_eval: bool = True
85
+ # The patched Camoufox Juggler fires ``framenavigated`` events and
86
+ # updates ``page.url`` with higher latency than a standard Playwright
87
+ # browser. Widen the settle poll budget so a ``setTimeout``-delayed
88
+ # redirect (e.g. ``location.href`` after 280 ms) is still captured
89
+ # under CI load. URL stability mode confirms the URL has finished
90
+ # changing before declaring "no navigation" — the 150 ms hold is
91
+ # negligible against the wider budget.
92
+ _settle_budget_ms: int = 2000
93
+ _url_stability_settle: bool = True
78
94
  _install_hint: str = (
79
95
  "Camoufox browser not installed.\n"
80
96
  "Run the following commands in your camoufox-py virtual environment:\n"
@@ -82,6 +98,80 @@ class CamoufoxPyBridge(PlaywrightBridge):
82
98
  " python -m camoufox fetch"
83
99
  )
84
100
 
101
+ def _check_pinned_version(self) -> None:
102
+ """Best-effort runtime check that the installed Camoufox stack matches pin.json.
103
+
104
+ Reads ``pin.json`` (same directory as this module) and
105
+ compares the pinned ``cloverlabs-camoufox`` package version
106
+ against what's actually installed via pip metadata.
107
+ Warns to stderr on any mismatch or if the check cannot be
108
+ performed. Never raises — this is advisory only.
109
+ """
110
+ try:
111
+ pin_path = Path(__file__).parent / "pin.json"
112
+ if not pin_path.exists():
113
+ return
114
+ pinned = json.loads(pin_path.read_text())
115
+ except Exception:
116
+ return
117
+
118
+ expected_pkg = pinned.get("package", "")
119
+ if expected_pkg:
120
+ try:
121
+ from importlib.metadata import version as _get_version
122
+
123
+ installed = _get_version("cloverlabs-camoufox")
124
+ expected_ver = expected_pkg.split("==")[-1].split("[")[0]
125
+ if installed != expected_ver:
126
+ print(
127
+ "pi-bridge camoufox-py: pin.json pins "
128
+ f"cloverlabs-camoufox=={expected_ver} but "
129
+ f"{installed} is installed — browser binary "
130
+ "drift risk.",
131
+ file=sys.stderr,
132
+ )
133
+ except Exception:
134
+ pass # package not installed or metadata unavailable
135
+
136
+ expected_binary = pinned.get("binary", "")
137
+ if expected_binary:
138
+ try:
139
+ import camoufox # type: ignore[import-unresolved]
140
+ _binary_version = None
141
+ for _attr in (
142
+ "__binary_version__",
143
+ "_binary_version",
144
+ "binary_version",
145
+ ):
146
+ _candidate = getattr(camoufox, _attr, None)
147
+ if _candidate is not None:
148
+ _binary_version = _candidate
149
+ break
150
+ if _binary_version is None:
151
+ _sync = getattr(camoufox, "sync", None)
152
+ if _sync is not None:
153
+ for _attr in (
154
+ "__binary_version__",
155
+ "_binary_version",
156
+ "binary_version",
157
+ ):
158
+ _candidate = getattr(_sync, _attr, None)
159
+ if _candidate is not None:
160
+ _binary_version = _candidate
161
+ break
162
+ if _binary_version is None:
163
+ return
164
+ expected_ref = expected_binary.split("/")[-1]
165
+ if _binary_version != expected_ref:
166
+ print(
167
+ "pi-bridge camoufox-py: pin.json expects binary "
168
+ f"{expected_ref} but {_binary_version} is "
169
+ "installed — binary drift risk.",
170
+ file=sys.stderr,
171
+ )
172
+ except Exception:
173
+ pass # camoufox not importable yet — skip silently
174
+
85
175
  def _launch_browser(self): # type: ignore[override]
86
176
  """Launch Camoufox via ``camoufox.NewBrowser``.
87
177
 
@@ -103,6 +193,8 @@ class CamoufoxPyBridge(PlaywrightBridge):
103
193
 
104
194
  Returns a standard Playwright ``Browser`` patched by Camoufox.
105
195
  """
196
+ self._check_pinned_version()
197
+
106
198
  # Lazy-import so non-Camoufox bridges don't pay the import cost
107
199
  # and to give a clear error when the package is missing.
108
200
  try:
@@ -196,18 +288,20 @@ if __name__ == "__main__":
196
288
  import camoufox # type: ignore[import-unresolved] # noqa: F401
197
289
  except ImportError:
198
290
  print(
199
- json.dumps({
200
- "jsonrpc": "2.0",
201
- "id": None,
202
- "error": {
203
- "code": -32000,
204
- "message": (
205
- "cloverlabs-camoufox is not installed.\n"
206
- "Run: pip install cloverlabs-camoufox[geoip] && "
207
- "python -m camoufox fetch"
208
- ),
209
- },
210
- })
291
+ json.dumps(
292
+ {
293
+ "jsonrpc": "2.0",
294
+ "id": None,
295
+ "error": {
296
+ "code": -32000,
297
+ "message": (
298
+ "cloverlabs-camoufox is not installed.\n"
299
+ "Run: pip install cloverlabs-camoufox[geoip] && "
300
+ "python -m camoufox fetch"
301
+ ),
302
+ },
303
+ }
304
+ )
211
305
  )
212
306
  sys.stdout.flush()
213
307
  sys.exit(1)
@@ -0,0 +1,4 @@
1
+ {
2
+ "package": "cloverlabs-camoufox[geoip]==0.6.0",
3
+ "binary": "official/152.0.4-beta.28"
4
+ }
@@ -85,7 +85,7 @@ import time
85
85
  from typing import Any
86
86
 
87
87
  from pi_browser_bridge.playwright_base import PlaywrightBridge
88
- from pi_browser_bridge.bridge import SessionNotFoundError
88
+ from pi_browser_bridge.playwright_base import SessionNotFoundError
89
89
 
90
90
 
91
91
  class InvisiblePyBridge(PlaywrightBridge):
@@ -6,7 +6,6 @@
6
6
  * No JavaScript execution — fastest path for static content.
7
7
  *
8
8
  * When called directly via the `webFetch()` entry point, this backend:
9
- * - Validates the URL (blocks localhost, private IPs, dangerous schemes)
10
9
  * - Fetches and converts HTML → Markdown
11
10
  * - Runs JS-shell detection inline
12
11
  * - Runs bot-detection heuristics inline
@@ -20,10 +19,10 @@ import {
20
19
  BROWSER_TEMP_DIR,
21
20
  safeTaskId,
22
21
  ensureBrowserTempDir,
22
+ formatBytes,
23
23
  } from "./shared/paths.js";
24
24
  import TurndownService from "turndown";
25
25
  import { parse as parseHtml } from "node-html-parser";
26
- import { validateUrl } from "./shared/url-safety.js";
27
26
  import { checkPage } from "./shared/bot-detection.js";
28
27
 
29
28
  const DEFAULT_USER_AGENT =
@@ -194,7 +193,7 @@ function htmlToMarkdown(root: ReturnType<typeof parseHtml>): string {
194
193
 
195
194
  // ─── Decoupled entry point: webFetch() ────────────────────────────────
196
195
 
197
- export interface WebFetchOptions {
196
+ interface WebFetchOptions {
198
197
  url: string;
199
198
  timeout?: number; // seconds, default 30, max 120
200
199
  signal?: AbortSignal;
@@ -202,7 +201,7 @@ export interface WebFetchOptions {
202
201
  taskId?: string;
203
202
  }
204
203
 
205
- export interface WebFetchResult {
204
+ interface WebFetchResult {
206
205
  success: boolean;
207
206
  url: string;
208
207
  title: string;
@@ -231,12 +230,6 @@ const activeFetchFiles = new Map<string, string[]>();
231
230
 
232
231
  // ─── Temp file management ──────────────────────────────────────────────
233
232
 
234
- function formatBytes(bytes: number): string {
235
- if (bytes < 1024) return `${bytes}B`;
236
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`;
237
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
238
- }
239
-
240
233
  function writeFetchTempFile(content: string, taskId: string): string {
241
234
  ensureBrowserTempDir();
242
235
 
@@ -312,7 +305,7 @@ export function cleanupFetchTempFiles(taskId?: string): void {
312
305
  /**
313
306
  * Decoupled web fetch entry point.
314
307
  *
315
- * Pipeline: URL safety → fetch → JS detection → bot detection → content capping
308
+ * Pipeline: fetch → JS detection → bot detection → content capping
316
309
  *
317
310
  * This function is the new recommended way to perform a stateless HTTP fetch.
318
311
  * It replaces the router-level fetch dispatch that used to live in `router.ts navigate()`.
@@ -322,7 +315,7 @@ export async function webFetch(
322
315
  ): Promise<WebFetchResult> {
323
316
  const timeout = (options.timeout ?? 30) * 1000;
324
317
 
325
- // Step 1: URL safety validation
318
+ // Parse URL
326
319
  let url: string;
327
320
  try {
328
321
  url = new URL(options.url).href;
@@ -337,19 +330,7 @@ export async function webFetch(
337
330
  };
338
331
  }
339
332
 
340
- const safety = validateUrl(url);
341
- if (!safety.safe) {
342
- return {
343
- success: false,
344
- url,
345
- title: "",
346
- content: safety.reason || "URL blocked",
347
- backendUsed: "fetch",
348
- error: `URL blocked: ${safety.reason}`,
349
- };
350
- }
351
-
352
- // Step 2: Perform fetch
333
+ // Step 1: Perform fetch
353
334
  let result: {
354
335
  html: string;
355
336
  title: string;
@@ -401,21 +382,20 @@ export async function webFetch(
401
382
  };
402
383
  }
403
384
 
404
- // Step 3: Bot detection via shared utility (uses un-mutated parsed root)
385
+ // Step 2: Bot detection via shared utility (uses un-mutated parsed root)
405
386
  let botDetected: boolean | undefined;
406
387
  try {
407
388
  const bodyText = result.root.textContent?.trim() || "";
408
- const detection = checkPage(result.title, bodyText);
409
- if (detection.isBlocked) botDetected = true;
389
+ if (checkPage(result.title, bodyText)) botDetected = true;
410
390
  } catch {
411
391
  /* best-effort — don't fail on bot detection errors */
412
392
  }
413
393
 
414
- // Step 4: Convert to Markdown (uses pre-parsed root from performFetch)
394
+ // Step 3: Convert to Markdown (uses pre-parsed root from performFetch)
415
395
  // NOTE: htmlToMarkdown mutates the tree — run after bot detection
416
396
  const markdown = htmlToMarkdown(result.root);
417
397
 
418
- // Step 5: Cap content
398
+ // Step 4: Cap content
419
399
  const tid = options.taskId ?? "web-fetch-default";
420
400
  const { inline, filePath, totalChars } = capFetchContent(markdown, tid);
421
401
 
@@ -280,37 +280,31 @@ export function loadUserGuides(): Record<string, Guide> {
280
280
  return result;
281
281
  }
282
282
 
283
- // ── Merged Guide Content (lazy) ────────────────────────────────
283
+ // ── Merged Guide Content (always fresh) ──────────────────────
284
284
 
285
- let _guideContentCache: Record<string, Guide> | null = null;
285
+ let _testGuideOverrides: Record<string, Guide> | null = null;
286
286
 
287
287
  /**
288
288
  * Get the merged guide content (builtin + user-authored).
289
- * Lazily built on first call; invalidate via invalidateGuideContent().
289
+ * Always reads user guides from disk — no stale cache.
290
290
  * User-authored guides override builtin guides on name collision.
291
291
  */
292
292
  export function getGuideContent(): Record<string, Guide> {
293
- if (!_guideContentCache) {
294
- _guideContentCache = {
295
- ...BUILTIN_GUIDES,
296
- ...loadUserGuides(),
297
- };
293
+ const base = { ...BUILTIN_GUIDES, ...loadUserGuides() };
294
+ if (_testGuideOverrides) {
295
+ return { ...base, ..._testGuideOverrides };
298
296
  }
299
- return _guideContentCache;
300
- }
301
-
302
- /** Invalidate the guide content cache so the next getGuideContent() call rescans guides/. */
303
- export function invalidateGuideContent(): void {
304
- _guideContentCache = null;
297
+ return base;
305
298
  }
306
299
 
307
300
  /**
308
- * Override the guide content cache for testing.
309
- * Pass undefined/null to reset to default (same as invalidateGuideContent).
301
+ * Inject test-only guide overrides.
302
+ * Pass undefined/null to clear. These are layered on top of the real
303
+ * builtin+user guides — no filesystem writes needed in tests.
310
304
  * @internal
311
305
  */
312
306
  export function _setGuideContentForTest(content?: Record<string, Guide>): void {
313
- _guideContentCache = content ?? null;
307
+ _testGuideOverrides = content ?? null;
314
308
  }
315
309
 
316
310
  /** Format guide listing grouped by category, with icon/shortName and trigger info. */
@@ -434,7 +428,9 @@ export function formatGuideFooter(guides: ApplicableGuide[]): string {
434
428
  lines.push(" Site:");
435
429
  addedSiteHeader = true;
436
430
  }
437
- lines.push(` • ${g.icon} ${g.shortName} — ${g.reason}`);
431
+ lines.push(
432
+ ` • ${g.icon} ${g.shortName} — ${g.reason} (web-guide guide="${g.name}")`,
433
+ );
438
434
  }
439
435
 
440
436
  return lines.join("\n");
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Plugin Router — registry-based dispatch for interactive browser operations.
3
- * Handles snapshot truncation, URL safety, session lifecycle, and crash recovery.
3
+ * Handles snapshot truncation, session lifecycle, and crash recovery.
4
4
  */
5
5
 
6
6
  import { writeFileSync } from "node:fs";
@@ -8,7 +8,6 @@ import { BROWSER_TEMP_DIR, ensureBrowserTempDir } from "./shared/paths.js";
8
8
  import { pluginRegistry } from "./plugin-registry.js";
9
9
  import { sessionManager } from "./shared/session-manager.js";
10
10
  import type { BrowserSession } from "./shared/session-manager.js";
11
- import { validateUrl } from "./shared/url-safety.js";
12
11
  import { snapshotFingerprint } from "./shared/accessibility-tree.js";
13
12
  import {
14
13
  cacheSnapshot,
@@ -425,23 +424,6 @@ export async function navigate(
425
424
  };
426
425
  }
427
426
 
428
- // URL safety check
429
- const safety = validateUrl(normalizedUrl);
430
- if (!safety.safe) {
431
- return {
432
- success: false,
433
- url: normalizedUrl,
434
- title: "",
435
- snapshot: "",
436
- elementCount: 0,
437
- error: `URL blocked: ${safety.reason}`,
438
- backendUsed: plugin.name,
439
- } as NavigateResult & {
440
- backendUsed: string;
441
- botDetectionWarning?: boolean;
442
- };
443
- }
444
-
445
427
  // ── Resolve profile mode ───────────────────────────────────
446
428
  let resolvedProfileName: string | undefined;
447
429
  let profileMode: "none" | "session" | "named" | undefined;
@@ -866,7 +848,7 @@ export async function clearConsole(
866
848
 
867
849
  // ─── browser-inspect public types ──────────────────────────────────
868
850
 
869
- export interface InspectParams {
851
+ interface InspectParams {
870
852
  role?: string;
871
853
  name?: string;
872
854
  ref?: string;
@@ -13,11 +13,6 @@
13
13
 
14
14
  import { BOT_SIGNALS } from "./browser-data.js";
15
15
 
16
- export interface BotDetectionResult {
17
- /** True if the page appears to be a bot block/challenge page */
18
- isBlocked: boolean;
19
- }
20
-
21
16
  /**
22
17
  * Content patterns that indicate a bot block — loaded from shared data.
23
18
  */
@@ -43,17 +38,17 @@ const HTML_SIGNALS = BOT_SIGNALS.htmlSignals;
43
38
  /**
44
39
  * Check if page text content suggests a bot block.
45
40
  */
46
- function checkBodyText(bodyText: string): BotDetectionResult {
47
- if (!bodyText) return { isBlocked: false };
41
+ function checkBodyText(bodyText: string): boolean {
42
+ if (!bodyText) return false;
48
43
 
49
44
  const lower = bodyText.toLowerCase();
50
45
  for (const signal of BLOCK_SIGNALS) {
51
46
  if (lower.includes(signal)) {
52
- return { isBlocked: true };
47
+ return true;
53
48
  }
54
49
  }
55
50
 
56
- return { isBlocked: false };
51
+ return false;
57
52
  }
58
53
 
59
54
  /**
@@ -62,15 +57,15 @@ function checkBodyText(bodyText: string): BotDetectionResult {
62
57
  * inclusion (for CDN domains, generic 403 messages) and regex patterns
63
58
  * (for Akamai reference codes, etc.).
64
59
  */
65
- function checkBodyOnlyText(bodyText: string): BotDetectionResult {
66
- if (!bodyText) return { isBlocked: false };
60
+ function checkBodyOnlyText(bodyText: string): boolean {
61
+ if (!bodyText) return false;
67
62
 
68
63
  const lower = bodyText.toLowerCase();
69
64
 
70
65
  // Check string signals first
71
66
  for (const signal of BODY_ONLY_SIGNALS) {
72
67
  if (lower.includes(signal)) {
73
- return { isBlocked: true };
68
+ return true;
74
69
  }
75
70
  }
76
71
 
@@ -78,11 +73,11 @@ function checkBodyOnlyText(bodyText: string): BotDetectionResult {
78
73
  for (const pattern of BODY_ONLY_PATTERNS) {
79
74
  const match = bodyText.match(pattern);
80
75
  if (match) {
81
- return { isBlocked: true };
76
+ return true;
82
77
  }
83
78
  }
84
79
 
85
- return { isBlocked: false };
80
+ return false;
86
81
  }
87
82
 
88
83
  /**
@@ -93,17 +88,17 @@ function checkBodyOnlyText(bodyText: string): BotDetectionResult {
93
88
  * loaded from the shared ``browser-data.json`` so TypeScript and Python
94
89
  * bridge implementations share a single source of truth.
95
90
  */
96
- function checkHtmlContent(html: string): BotDetectionResult {
97
- if (!html) return { isBlocked: false };
91
+ function checkHtmlContent(html: string): boolean {
92
+ if (!html) return false;
98
93
 
99
94
  const lower = html.toLowerCase();
100
95
  for (const signal of HTML_SIGNALS) {
101
96
  if (lower.includes(signal)) {
102
- return { isBlocked: true };
97
+ return true;
103
98
  }
104
99
  }
105
100
 
106
- return { isBlocked: false };
101
+ return false;
107
102
  }
108
103
 
109
104
  /**
@@ -118,24 +113,18 @@ export function checkPage(
118
113
  title: string,
119
114
  bodyText: string,
120
115
  html?: string,
121
- ): BotDetectionResult {
116
+ ): boolean {
122
117
  // Check title first (often contains "Attention Required!" etc.)
123
- const titleResult = checkBodyText(title);
124
- if (titleResult.isBlocked) return titleResult;
118
+ if (checkBodyText(title)) return true;
125
119
 
126
120
  // Check body against challenge phrases
127
- const bodyResult = checkBodyText(bodyText);
128
- if (bodyResult.isBlocked) return bodyResult;
121
+ if (checkBodyText(bodyText)) return true;
129
122
 
130
123
  // Check body against CDN-specific patterns (reference #, etc.)
131
- const bodyOnlyResult = checkBodyOnlyText(bodyText);
132
- if (bodyOnlyResult.isBlocked) return bodyOnlyResult;
124
+ if (checkBodyOnlyText(bodyText)) return true;
133
125
 
134
126
  // Check HTML source for CAPTCHA widget embed codes
135
- if (html !== undefined) {
136
- const htmlResult = checkHtmlContent(html);
137
- if (htmlResult.isBlocked) return htmlResult;
138
- }
127
+ if (html !== undefined && checkHtmlContent(html)) return true;
139
128
 
140
- return { isBlocked: false };
129
+ return false;
141
130
  }