pi-lean-dimension 0.2.4 → 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.
- package/README.md +1 -1
- package/node_modules/pi-lean-portal/AGENTS.md +15 -23
- package/node_modules/pi-lean-portal/README.md +31 -1
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +12 -1
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +263 -1126
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +6 -32
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +1 -1
- package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +1 -2
- package/node_modules/pi-lean-portal/__tests__/helpers/setup-temp-dir.ts +26 -0
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/plugin-contract.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +65 -69
- package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +1 -14
- package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +31 -16
- package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +146 -19
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +3 -3
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +2 -4
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +572 -81
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +13 -43
- package/node_modules/pi-lean-portal/browser-profile.ts +2 -3
- package/node_modules/pi-lean-portal/browser-toggle.ts +168 -385
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +2 -2
- package/node_modules/pi-lean-portal/contributed/README.md +65 -6
- package/node_modules/pi-lean-portal/contributed/camoufox-py/__pycache__/bridge.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +108 -14
- package/node_modules/pi-lean-portal/contributed/camoufox-py/pin.json +4 -0
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +10 -30
- package/node_modules/pi-lean-portal/core/guides.ts +14 -18
- package/node_modules/pi-lean-portal/core/router.ts +2 -20
- package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +19 -30
- package/node_modules/pi-lean-portal/core/shared/browser-data.ts +4 -4
- package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +2 -2
- package/node_modules/pi-lean-portal/core/shared/paths.ts +16 -2
- package/node_modules/pi-lean-portal/core/shared/settings-reader.ts +2 -7
- package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +1 -1
- package/node_modules/pi-lean-portal/index.ts +8 -1
- package/node_modules/pi-lean-portal/package.json +2 -1
- package/node_modules/pi-lean-portal/tools/web-learn.ts +1 -4
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +87 -0
- package/node_modules/pi-lean-search/index.ts +134 -64
- package/node_modules/pi-lean-search/package.json +2 -1
- package/node_modules/playwright/README.md +3 -3
- package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +2 -2
- package/node_modules/playwright/lib/cli/reportActions.js +2 -2
- package/node_modules/playwright/lib/common/index.js +77 -52
- package/node_modules/playwright/lib/common/index.js.txt +10 -11
- package/node_modules/playwright/lib/errorContext.js +2 -3
- package/node_modules/playwright/lib/index.js +30 -8
- package/node_modules/playwright/lib/matchers/expect.js +4811 -4379
- package/node_modules/playwright/lib/matchers/expect.js.LICENSE +87 -140
- package/node_modules/playwright/lib/matchers/expect.js.txt +30 -35
- package/node_modules/playwright/lib/program.js +19 -0
- package/node_modules/playwright/lib/runner/index.js +309 -153
- package/node_modules/playwright/lib/runner/index.js.txt +13 -12
- package/node_modules/playwright/lib/transform/babelBundle.js +12899 -11654
- package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +379 -354
- package/node_modules/playwright/lib/transform/babelBundle.js.txt +39 -39
- package/node_modules/playwright/lib/transform/esmLoader.js +3129 -1116
- package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +81 -61
- package/node_modules/playwright/lib/transform/esmLoader.js.txt +21 -18
- package/node_modules/playwright/lib/util.js +2 -29
- package/node_modules/playwright/lib/worker/workerProcessEntry.js +14 -32
- package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +7 -8
- package/node_modules/playwright/package.json +3 -6
- package/node_modules/playwright/types/test.d.ts +391 -4
- package/node_modules/playwright/types/testReporter.d.ts +79 -4
- package/node_modules/playwright-core/browsers.json +12 -18
- package/node_modules/playwright-core/lib/bootstrap.js +1 -1
- package/node_modules/playwright-core/lib/coreBundle.js +11344 -9900
- package/node_modules/playwright-core/lib/server/electron/loader.js +5 -2
- package/node_modules/playwright-core/lib/serverRegistry.js +5 -1
- package/node_modules/playwright-core/lib/tools/cli-client/help.json +21 -6
- package/node_modules/playwright-core/lib/tools/cli-client/program.js +1 -1
- package/node_modules/playwright-core/lib/tools/cli-client/session.js +4 -0
- package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/SKILL.md +21 -5
- package/node_modules/playwright-core/lib/tools/{cli-client/skill/references/spec-driven-testing.md → skills/playwright-cli/references/test-generation.md} +135 -7
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +143 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +85 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
- package/node_modules/playwright-core/lib/tools/utils/extension.js +31 -8
- package/node_modules/playwright-core/lib/utilsBundle.js +37698 -33670
- package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +391 -268
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-DhC616m4.css +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/index.html +2 -2
- package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -1
- package/node_modules/playwright-core/lib/vite/htmlReport/report.js +15 -55
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +32 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/index.html +2 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-By56iMx7.js +32 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B34OrIms.js +181 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-COQkjINf.js +7 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.Dl36UVQT.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.html +5 -5
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +2 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +3 -4
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.C7UW1sC9.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.D962mr9b.js +5 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +5 -5
- package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
- package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
- package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
- package/node_modules/playwright-core/lib/xdg-open +338 -137
- package/node_modules/playwright-core/package.json +2 -2
- package/node_modules/playwright-core/types/protocol.d.ts +249 -36
- package/node_modules/playwright-core/types/structs.d.ts +3 -1
- package/node_modules/playwright-core/types/types.d.ts +2009 -266
- package/package.json +3 -3
- package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +0 -188
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +0 -525
- package/node_modules/pi-lean-portal/core/shared/url-safety.ts +0 -164
- package/node_modules/playwright-core/lib/server/deviceDescriptorsSource.json +0 -2739
- package/node_modules/playwright-core/lib/tools/cli-client/skill/references/test-generation.md +0 -134
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-BY2S1tHT.css +0 -1
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-C_5TMfeg.js +0 -52
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DYBRYzYX.css +0 -1
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DeBYQozu.js +0 -32
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-4ZiSSCmn.css +0 -1
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-Bq-mQf8S.js +0 -193
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-LEHpjmcn.js +0 -32
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-BNmKHKpQ.js +0 -264
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-BYQrIQwR.js +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-CsJ4vdCR.js +0 -9
- package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.DYBRYzYX.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.CjdS-WJx.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/index.CzXZzn5A.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/index.DMMX1gXU.js +0 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.v8KI4P3m.js +0 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.BZQ54Kgt.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Ut8wwJNp.js +0 -6
- package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.DYP7pi_n.css +0 -32
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/element-attributes.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/playwright-tests.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/request-mocking.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/running-code.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/session-management.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/storage-state.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/tracing.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/video-recording.md +0 -0
- /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. `
|
|
20
|
-
> `files
|
|
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 (`
|
|
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")
|
|
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
|
package/node_modules/pi-lean-portal/contributed/camoufox-py/__pycache__/bridge.cpython-312.pyc
ADDED
|
Binary file
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
"
|
|
204
|
-
|
|
205
|
-
"
|
|
206
|
-
|
|
207
|
-
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
283
|
+
// ── Merged Guide Content (always fresh) ──────────────────────
|
|
284
284
|
|
|
285
|
-
let
|
|
285
|
+
let _testGuideOverrides: Record<string, Guide> | null = null;
|
|
286
286
|
|
|
287
287
|
/**
|
|
288
288
|
* Get the merged guide content (builtin + user-authored).
|
|
289
|
-
*
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
...loadUserGuides(),
|
|
297
|
-
};
|
|
293
|
+
const base = { ...BUILTIN_GUIDES, ...loadUserGuides() };
|
|
294
|
+
if (_testGuideOverrides) {
|
|
295
|
+
return { ...base, ..._testGuideOverrides };
|
|
298
296
|
}
|
|
299
|
-
return
|
|
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
|
-
*
|
|
309
|
-
* Pass undefined/null to
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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):
|
|
47
|
-
if (!bodyText) return
|
|
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
|
|
47
|
+
return true;
|
|
53
48
|
}
|
|
54
49
|
}
|
|
55
50
|
|
|
56
|
-
return
|
|
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):
|
|
66
|
-
if (!bodyText) return
|
|
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
|
|
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
|
|
76
|
+
return true;
|
|
82
77
|
}
|
|
83
78
|
}
|
|
84
79
|
|
|
85
|
-
return
|
|
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):
|
|
97
|
-
if (!html) return
|
|
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
|
|
97
|
+
return true;
|
|
103
98
|
}
|
|
104
99
|
}
|
|
105
100
|
|
|
106
|
-
return
|
|
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
|
-
):
|
|
116
|
+
): boolean {
|
|
122
117
|
// Check title first (often contains "Attention Required!" etc.)
|
|
123
|
-
|
|
124
|
-
if (titleResult.isBlocked) return titleResult;
|
|
118
|
+
if (checkBodyText(title)) return true;
|
|
125
119
|
|
|
126
120
|
// Check body against challenge phrases
|
|
127
|
-
|
|
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
|
-
|
|
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
|
|
129
|
+
return false;
|
|
141
130
|
}
|
|
@@ -14,27 +14,27 @@ import _raw from "./browser-data.json" with { type: "json" };
|
|
|
14
14
|
|
|
15
15
|
// ─── Types ─────────────────────────────────────────────────────────────
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
interface BotSignals {
|
|
18
18
|
blockSignals: string[];
|
|
19
19
|
bodyOnlySignals: string[];
|
|
20
20
|
bodyOnlyPatterns: string[];
|
|
21
21
|
htmlSignals: string[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
interface AccessibilityData {
|
|
25
25
|
interactiveRoles: string[];
|
|
26
26
|
informationalRoles: string[];
|
|
27
27
|
roleIcons: Record<string, string>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
interface NavSettleData {
|
|
31
31
|
navTimeoutMs: number;
|
|
32
32
|
settleTimeoutMs: number;
|
|
33
33
|
settleRaceMs: number;
|
|
34
34
|
domStabilizationJs: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
interface BrowserDataFile {
|
|
38
38
|
version: number;
|
|
39
39
|
botSignals: BotSignals;
|
|
40
40
|
accessibility: AccessibilityData;
|