pi-lean-dimension 0.1.0 → 0.2.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 +96 -36
- package/node_modules/pi-lean-portal/AGENTS.md +146 -4
- package/node_modules/pi-lean-portal/README.md +112 -50
- package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
- package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
- package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
- package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
- package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
- package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
- package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
- package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
- package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
- package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
- package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
- package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
- package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
- package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
- package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
- package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
- package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
- package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
- 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__/accessibility.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.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/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- 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
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
- 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
- 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
- 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/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
- 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
- package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
- package/node_modules/pi-lean-portal/contributed/README.md +304 -0
- package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
- package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
- package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
- package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
- package/node_modules/pi-lean-portal/core/router.ts +59 -98
- package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
- package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
- package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
- package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
- package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
- package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
- package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
- package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
- package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
- package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
- package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
- package/node_modules/pi-lean-portal/index.ts +42 -7
- package/node_modules/pi-lean-portal/package.json +8 -3
- package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
- package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
- package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
- package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
- package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
- package/node_modules/pi-lean-search/README.md +6 -2
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
- package/node_modules/pi-lean-search/index.ts +4 -21
- package/node_modules/pi-lean-search/package.json +2 -2
- package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
- package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
- package/package.json +4 -4
- package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
- package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
- package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_firefox_py_bridge.py +0 -212
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared data loader for ``browser-data.json``.
|
|
3
|
+
|
|
4
|
+
Single source of truth that mirrors the TypeScript ``browser-data.ts``
|
|
5
|
+
loader. Opened and validated once at import time, then re-exported
|
|
6
|
+
as module-level dicts that the three consumers
|
|
7
|
+
(``bot_detection``, ``accessibility``, ``playwright_base``) import.
|
|
8
|
+
|
|
9
|
+
Loud failure: missing/corrupt file or missing keys raises RuntimeError
|
|
10
|
+
at import time, so both the TS and Python sides fail identically.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
_SHARED_DATA_PATH = os.path.join(
|
|
18
|
+
os.path.dirname(__file__),
|
|
19
|
+
"..", "..", "..",
|
|
20
|
+
"core", "shared", "browser-data.json",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load() -> dict[str, Any]:
|
|
25
|
+
"""Open, parse, and validate every expected key in browser-data.json."""
|
|
26
|
+
try:
|
|
27
|
+
with open(_SHARED_DATA_PATH, "r") as f:
|
|
28
|
+
data = json.load(f)
|
|
29
|
+
except Exception as exc:
|
|
30
|
+
raise RuntimeError(
|
|
31
|
+
f"[pi-lean-portal] Failed to load shared browser data from "
|
|
32
|
+
f"{_SHARED_DATA_PATH}: {exc}"
|
|
33
|
+
) from exc
|
|
34
|
+
|
|
35
|
+
if not isinstance(data.get("version"), int) or data["version"] < 1:
|
|
36
|
+
raise RuntimeError(
|
|
37
|
+
f"[pi-lean-portal] browser-data.json has invalid or missing version "
|
|
38
|
+
f"field in {_SHARED_DATA_PATH}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# ── botSignals ──────────────────────────────────────────────
|
|
42
|
+
bot = data.get("botSignals")
|
|
43
|
+
if not isinstance(bot, dict):
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
f"[pi-lean-portal] browser-data.json: missing 'botSignals' section "
|
|
46
|
+
f"in {_SHARED_DATA_PATH}"
|
|
47
|
+
)
|
|
48
|
+
for key in ("blockSignals", "bodyOnlySignals", "bodyOnlyPatterns", "htmlSignals"):
|
|
49
|
+
if key not in bot:
|
|
50
|
+
raise RuntimeError(
|
|
51
|
+
f"[pi-lean-portal] browser-data.json: missing "
|
|
52
|
+
f"'botSignals.{key}' in {_SHARED_DATA_PATH}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# ── accessibility ───────────────────────────────────────────
|
|
56
|
+
a11y = data.get("accessibility")
|
|
57
|
+
if not isinstance(a11y, dict):
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
f"[pi-lean-portal] browser-data.json: missing 'accessibility' section "
|
|
60
|
+
f"in {_SHARED_DATA_PATH}"
|
|
61
|
+
)
|
|
62
|
+
for key in ("interactiveRoles", "informationalRoles", "roleIcons"):
|
|
63
|
+
if key not in a11y:
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
f"[pi-lean-portal] browser-data.json: missing "
|
|
66
|
+
f"'accessibility.{key}' in {_SHARED_DATA_PATH}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# ── navSettle ───────────────────────────────────────────────
|
|
70
|
+
ns = data.get("navSettle")
|
|
71
|
+
if not isinstance(ns, dict):
|
|
72
|
+
raise RuntimeError(
|
|
73
|
+
f"[pi-lean-portal] browser-data.json: missing 'navSettle' section "
|
|
74
|
+
f"in {_SHARED_DATA_PATH}"
|
|
75
|
+
)
|
|
76
|
+
for key in ("navTimeoutMs", "settleTimeoutMs", "settleRaceMs", "domStabilizationJs"):
|
|
77
|
+
if key not in ns:
|
|
78
|
+
raise RuntimeError(
|
|
79
|
+
f"[pi-lean-portal] browser-data.json: missing "
|
|
80
|
+
f"'navSettle.{key}' in {_SHARED_DATA_PATH}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_RAW = _load()
|
|
87
|
+
|
|
88
|
+
# ─── Section exports (dicts) ─────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
BOT_SIGNALS: dict[str, Any] = _RAW["botSignals"]
|
|
91
|
+
ACCESSIBILITY: dict[str, Any] = _RAW["accessibility"]
|
|
92
|
+
NAV_SETTLE: dict[str, Any] = _RAW["navSettle"]
|
package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Idempotent Playwright coreBundle.js patcher — guards pageError.location for Camoufox.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import sysconfig
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ── Pattern constants ────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
# The exact JavaScript snippet that crashes (two identical occurrences).
|
|
14
|
+
_VULNERABLE_SNIPPET = (
|
|
15
|
+
"location: {\n"
|
|
16
|
+
" url: pageError.location.url,\n"
|
|
17
|
+
" line: pageError.location.lineNumber,\n"
|
|
18
|
+
" column: pageError.location.columnNumber\n"
|
|
19
|
+
" }"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# The guard replacement. Uses a truthy check so that undefined, null,
|
|
23
|
+
# and missing all produce the fallback object. The fallback's values are
|
|
24
|
+
# safe empty-string / zero — never undefined — because the wire
|
|
25
|
+
# serializer validates ``location`` must be an object.
|
|
26
|
+
_GUARDED_SNIPPET = (
|
|
27
|
+
"location: pageError.location ? {\n"
|
|
28
|
+
" url: pageError.location.url,\n"
|
|
29
|
+
" line: pageError.location.lineNumber,\n"
|
|
30
|
+
" column: pageError.location.columnNumber\n"
|
|
31
|
+
" } : { url: \"\", line: 0, column: 0 }"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Marker comment added after a successful patch so we can cheaply detect
|
|
35
|
+
# it on subsequent calls without re-reading the whole file.
|
|
36
|
+
_PATCH_MARKER = "/* pi-bridge: guarded pageError.location */"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── Locate the driver file ───────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
def _default_driver_path() -> Path | None:
|
|
42
|
+
"""Return the path to ``coreBundle.js`` for the active Python venv.
|
|
43
|
+
|
|
44
|
+
Resolves via ``playwright.__file__`` when playwright is importable,
|
|
45
|
+
or by guessing the path from ``sys.prefix`` on the standard venv
|
|
46
|
+
layout. Returns ``None`` when neither approach finds a file.
|
|
47
|
+
"""
|
|
48
|
+
# Strategy 1: follow the installed playwright package's __file__
|
|
49
|
+
try:
|
|
50
|
+
import playwright # type: ignore[import-unresolved] # noqa: F401
|
|
51
|
+
|
|
52
|
+
pw_dir = Path(playwright.__file__).resolve().parent
|
|
53
|
+
candidate = pw_dir / "driver" / "package" / "lib" / "coreBundle.js"
|
|
54
|
+
if candidate.is_file():
|
|
55
|
+
return candidate
|
|
56
|
+
except (ImportError, AttributeError, OSError):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
# Strategy 2: guess from sys.prefix + known venv layout
|
|
60
|
+
site_packages = sysconfig.get_paths().get("purelib")
|
|
61
|
+
if site_packages:
|
|
62
|
+
candidate = (
|
|
63
|
+
Path(site_packages)
|
|
64
|
+
/ "playwright"
|
|
65
|
+
/ "driver"
|
|
66
|
+
/ "package"
|
|
67
|
+
/ "lib"
|
|
68
|
+
/ "coreBundle.js"
|
|
69
|
+
)
|
|
70
|
+
if candidate.is_file():
|
|
71
|
+
return candidate
|
|
72
|
+
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ── Core patch logic ─────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
def _check_already_patched(content: str) -> bool:
|
|
79
|
+
"""Cheap idempotency check — the marker comment we add after a patch."""
|
|
80
|
+
return _PATCH_MARKER in content
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _check_has_unpatched_pattern(content: str) -> bool:
|
|
84
|
+
"""True if the vulnerable snippet exists in **unpatched** form."""
|
|
85
|
+
return _VULNERABLE_SNIPPET in content
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _apply_patch(
|
|
89
|
+
driver_path: Path, *, loud: bool = False, fail_on_readonly: bool = True
|
|
90
|
+
) -> bool:
|
|
91
|
+
"""Apply the guard to ``coreBundle.js``.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
driver_path: Absolute path to the Playwright driver file.
|
|
95
|
+
loud: Print a one-time stderr notice on first patch.
|
|
96
|
+
fail_on_readonly: When ``True`` (default), raise ``RuntimeError``
|
|
97
|
+
if the file is read-only. When ``False``, return ``False``.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
``True`` if a patch was applied (first time); ``False`` if already
|
|
101
|
+
patched, the vulnerable pattern was not found, or (when
|
|
102
|
+
``fail_on_readonly=False``) the file was read-only.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
RuntimeError: If ``fail_on_readonly=True`` and the file is
|
|
106
|
+
read-only, or the pattern match fails unexpectedly.
|
|
107
|
+
"""
|
|
108
|
+
content = driver_path.read_text(encoding="utf-8")
|
|
109
|
+
|
|
110
|
+
# --- Idempotency check ---
|
|
111
|
+
if _check_already_patched(content):
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
# --- Check for unguarded snippet ---
|
|
115
|
+
if not _check_has_unpatched_pattern(content):
|
|
116
|
+
# The vulnerable snippet isn't present in unguarded form. This could
|
|
117
|
+
# mean Playwright already fixed it upstream (newer version) or
|
|
118
|
+
# restructured the bundle. Warn and skip.
|
|
119
|
+
if loud:
|
|
120
|
+
print(
|
|
121
|
+
(
|
|
122
|
+
"[camoufox-py] WARNING: Playwright coreBundle.js does not contain "
|
|
123
|
+
"the expected vulnerable pattern (pageError.location.url without "
|
|
124
|
+
"a guard). This Playwright version may already have the fix, or "
|
|
125
|
+
"the bundle structure changed. No patch applied."
|
|
126
|
+
),
|
|
127
|
+
file=sys.stderr,
|
|
128
|
+
flush=True,
|
|
129
|
+
)
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
# --- Apply the replacement ---
|
|
133
|
+
new_content = content.replace(_VULNERABLE_SNIPPET, _GUARDED_SNIPPET)
|
|
134
|
+
|
|
135
|
+
# Sanity check: the replace should have changed the file
|
|
136
|
+
if new_content == content:
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
"BUG: pattern matched but replace produced identical content. "
|
|
139
|
+
"Cannot patch Playwright coreBundle.js."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Verify the replace didn't introduce a regression (count occurrences)
|
|
143
|
+
patched_count = new_content.count(
|
|
144
|
+
"pageError.location ? {"
|
|
145
|
+
)
|
|
146
|
+
expected_count = 2 # two identical occurrences in the bundle
|
|
147
|
+
if patched_count != expected_count:
|
|
148
|
+
raise RuntimeError(
|
|
149
|
+
f"BUG: expected {expected_count} patched occurrences, got {patched_count}. "
|
|
150
|
+
"Playwright bundle structure may have changed. "
|
|
151
|
+
"Cannot safely patch coreBundle.js."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Add the marker comment (appended to end for cheap subsequent check)
|
|
155
|
+
new_content += "\n" + _PATCH_MARKER + "\n"
|
|
156
|
+
|
|
157
|
+
# --- Write back ---
|
|
158
|
+
try:
|
|
159
|
+
driver_path.write_text(new_content, encoding="utf-8")
|
|
160
|
+
except PermissionError as exc:
|
|
161
|
+
if fail_on_readonly:
|
|
162
|
+
raise RuntimeError(
|
|
163
|
+
"Cannot write to Playwright coreBundle.js — file is read-only.\n"
|
|
164
|
+
"To patch manually as a one-time command:\n"
|
|
165
|
+
f" python -m pi_browser_bridge.patch_playwright\n"
|
|
166
|
+
f" (from the camoufox-py virtual environment: {sys.prefix})"
|
|
167
|
+
) from exc
|
|
168
|
+
return False
|
|
169
|
+
except OSError as exc:
|
|
170
|
+
raise RuntimeError(
|
|
171
|
+
"Cannot write to Playwright coreBundle.js.\n"
|
|
172
|
+
f" Path: {driver_path}\n"
|
|
173
|
+
f" Error: {exc}\n"
|
|
174
|
+
"To patch manually, copy the file, edit it, and re-run:\n"
|
|
175
|
+
" python -m pi_browser_bridge.patch_playwright"
|
|
176
|
+
) from exc
|
|
177
|
+
|
|
178
|
+
# --- Loud one-time notice ---
|
|
179
|
+
if loud:
|
|
180
|
+
print(
|
|
181
|
+
(
|
|
182
|
+
"[camoufox-py] Patched Playwright coreBundle.js "
|
|
183
|
+
f"({driver_path})\n"
|
|
184
|
+
" Guarded pageError.location access — required for "
|
|
185
|
+
"Camoufox binary v135.0.1-beta.24 which emits "
|
|
186
|
+
"uncaught-error events without location metadata."
|
|
187
|
+
),
|
|
188
|
+
file=sys.stderr,
|
|
189
|
+
flush=True,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ── Public API ───────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
def patch_playwright(
|
|
198
|
+
*,
|
|
199
|
+
driver_path: str | None = None,
|
|
200
|
+
loud: bool = False,
|
|
201
|
+
fail_on_readonly: bool = True,
|
|
202
|
+
) -> bool:
|
|
203
|
+
"""Idempotently guard ``pageError.location`` in Playwright's coreBundle.js.
|
|
204
|
+
|
|
205
|
+
Designed to be called automatically at bridge launch (from
|
|
206
|
+
``_launch_browser()``) or as a standalone ``python -m`` command.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
driver_path: Explicit path to ``coreBundle.js``. When ``None``,
|
|
210
|
+
auto-detect from the running venv's Playwright installation.
|
|
211
|
+
loud: Print a one-time stderr notice when a patch is applied.
|
|
212
|
+
fail_on_readonly: When ``True`` (default), raise ``RuntimeError``
|
|
213
|
+
if the file exists but is read-only. When ``False``, just
|
|
214
|
+
return ``False`` and let the caller decide.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
``True`` if a patch was applied (first-time). ``False`` if already
|
|
218
|
+
patched, the pattern was not found (Playwright version may not need
|
|
219
|
+
the fix), or (when ``fail_on_readonly=False``) the file was read-only.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
RuntimeError: If the driver file cannot be located, or
|
|
223
|
+
``fail_on_readonly=True`` and the file is read-only.
|
|
224
|
+
"""
|
|
225
|
+
# --- Resolve driver path ---
|
|
226
|
+
if driver_path is not None:
|
|
227
|
+
resolved = Path(driver_path).resolve()
|
|
228
|
+
else:
|
|
229
|
+
resolved = _default_driver_path()
|
|
230
|
+
if resolved is None:
|
|
231
|
+
raise RuntimeError(
|
|
232
|
+
"Cannot locate Playwright coreBundle.js — is Playwright "
|
|
233
|
+
"installed in the current Python environment?\n"
|
|
234
|
+
f" sys.prefix: {sys.prefix}\n"
|
|
235
|
+
f" sys.executable: {sys.executable}\n"
|
|
236
|
+
"If Playwright is installed, try the manual command:\n"
|
|
237
|
+
" python -m pi_browser_bridge.patch_playwright --help"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if not resolved.is_file():
|
|
241
|
+
raise RuntimeError(
|
|
242
|
+
f"Playwright coreBundle.js not found at: {resolved}\n"
|
|
243
|
+
"Is Playwright properly installed in the current "
|
|
244
|
+
"virtual environment?"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# --- Idempotent patch ---
|
|
248
|
+
return _apply_patch(resolved, loud=loud, fail_on_readonly=fail_on_readonly)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ── CLI entry point ──────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
def main() -> None:
|
|
254
|
+
"""CLI entry point for ``python -m pi_browser_bridge.patch_playwright``.
|
|
255
|
+
|
|
256
|
+
Accepts:
|
|
257
|
+
``--driver PATH`` — explicit path to coreBundle.js.
|
|
258
|
+
``--quiet`` — suppress the one-time patch notice.
|
|
259
|
+
``--check`` — only check whether a patch is needed (exit 0 = patched
|
|
260
|
+
or not-needed, exit 1 = unpatched).
|
|
261
|
+
"""
|
|
262
|
+
import argparse
|
|
263
|
+
|
|
264
|
+
parser = argparse.ArgumentParser(
|
|
265
|
+
description="Guard pageError.location in Playwright's coreBundle.js",
|
|
266
|
+
)
|
|
267
|
+
parser.add_argument(
|
|
268
|
+
"--driver",
|
|
269
|
+
type=str,
|
|
270
|
+
default=None,
|
|
271
|
+
help="Explicit path to coreBundle.js (auto-detected by default)",
|
|
272
|
+
)
|
|
273
|
+
parser.add_argument(
|
|
274
|
+
"--quiet",
|
|
275
|
+
action="store_true",
|
|
276
|
+
help="Suppress the one-time patch notice",
|
|
277
|
+
)
|
|
278
|
+
parser.add_argument(
|
|
279
|
+
"--check",
|
|
280
|
+
action="store_true",
|
|
281
|
+
help="Check-only: exit 0 if already patched / not needed, 1 if "
|
|
282
|
+
"the vulnerable pattern is still present",
|
|
283
|
+
)
|
|
284
|
+
args = parser.parse_args()
|
|
285
|
+
|
|
286
|
+
if args.check:
|
|
287
|
+
driver: Path | None
|
|
288
|
+
if args.driver:
|
|
289
|
+
driver = Path(args.driver).resolve()
|
|
290
|
+
else:
|
|
291
|
+
driver = _default_driver_path()
|
|
292
|
+
if driver is None or not driver.is_file():
|
|
293
|
+
print("coreBundle.js not found — cannot check", file=sys.stderr)
|
|
294
|
+
sys.exit(1)
|
|
295
|
+
content = driver.read_text(encoding="utf-8")
|
|
296
|
+
if _check_already_patched(content):
|
|
297
|
+
print("PATCHED")
|
|
298
|
+
sys.exit(0)
|
|
299
|
+
if _check_has_unpatched_pattern(content):
|
|
300
|
+
print("UNPATCHED")
|
|
301
|
+
sys.exit(1)
|
|
302
|
+
print("NOT_NEEDED")
|
|
303
|
+
sys.exit(0)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
patched = patch_playwright(
|
|
307
|
+
driver_path=args.driver,
|
|
308
|
+
loud=not args.quiet,
|
|
309
|
+
fail_on_readonly=True,
|
|
310
|
+
)
|
|
311
|
+
if patched:
|
|
312
|
+
print("Patch applied.", file=sys.stderr)
|
|
313
|
+
else:
|
|
314
|
+
print("Already patched or not needed.", file=sys.stderr)
|
|
315
|
+
except RuntimeError as exc:
|
|
316
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
317
|
+
sys.exit(1)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
if __name__ == "__main__":
|
|
321
|
+
main()
|