capiumbrowser 1.1.7 → 1.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 +43 -25
- package/lib/channels.json +17 -16
- package/lib/config.js +52 -24
- package/lib/errors.js +1 -0
- package/lib/launch-common.js +10 -2
- package/lib/license-readiness.js +55 -0
- package/lib/version.js +5 -5
- package/package.json +1 -1
- package/playwright.js +23 -8
- package/puppeteer.js +26 -8
package/README.md
CHANGED
|
@@ -35,10 +35,10 @@ Not a patched config. Not a JavaScript injection. A real Chromium binary whose f
|
|
|
35
35
|
> Using Python? The same browser ships as a [PyPI package](https://pypi.org/project/capiumbrowser/) with the same options.
|
|
36
36
|
|
|
37
37
|
- **Source-level C++ stealth** — User-Agent & UA-CH, WebGL/GPU, canvas, audio, screen, hardware, fonts, voices, WebRTC, `navigator.webdriver`, DevTools, pointer, client-rects — all rewritten **in the browser's own source**, below the JavaScript layer.
|
|
38
|
-
- **
|
|
38
|
+
- **Repeatable per-seed identities** — one integer **seed** selects OS, GPU, cores, memory, screen, fonts and UA settings. Rendering still depends on the host; the same seed does not reproduce the same physical device across hosts.
|
|
39
39
|
- **`humanize: true`** — attaches `page.humanMove` / `humanClick` / `humanType` / `humanScroll`: curved, human-timed mouse, keyboard, and scroll input.
|
|
40
40
|
- **`geoip: true`** — the binary resolves its own exit IP through your proxy and pins timezone, WebRTC IP, geolocation, and `navigator.languages` to match. The clock, locale, and IP all tell one story.
|
|
41
|
-
- **Plain drivers over CDP** — no patched driver
|
|
41
|
+
- **Plain drivers over CDP** — no patched driver or fingerprint `Object.defineProperty` shim. Update the driver with `npm i -U playwright-core` (or `puppeteer-core`); select the Capium browser binary separately.
|
|
42
42
|
- **Always the latest binary** — one license key fetches the newest checksum-verified build for your platform and reuses it thereafter.
|
|
43
43
|
- **Free tier — 1 concurrent session.** Paid tiers raise the concurrent-session cap. [Start free →](https://capiumbrowser.com)
|
|
44
44
|
|
|
@@ -142,13 +142,14 @@ Puppeteer `Browser`:
|
|
|
142
142
|
- **Config-level stealth breaks.** `puppeteer-extra-plugin-stealth`, patched drivers, and
|
|
143
143
|
friends inject JavaScript or flip flags. Every Chrome update breaks them, and modern
|
|
144
144
|
anti-bot ML detects the patches themselves — the very act of hiding trips the alarm.
|
|
145
|
-
- **Capium patches Chromium source.**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
- **Coherence
|
|
149
|
-
|
|
150
|
-
and
|
|
151
|
-
- **
|
|
145
|
+
- **Capium patches Chromium source.** Fingerprint changes are compiled into the browser at the C++
|
|
146
|
+
level. SDK evaluation and exposed bindings can still reveal driver behavior; see the measured
|
|
147
|
+
[Chrome 155 audit](../../docs/SDK-GPU-AUDIT.md).
|
|
148
|
+
- **Coherence needs measurement.** A seed selects identity settings, and optional proxy geography
|
|
149
|
+
aligns network-related settings. Native rendering, fonts and server classifications still depend
|
|
150
|
+
on the host and launch policy; matching the persona to the host performs better in the current audit.
|
|
151
|
+
- **Common launch API.** Local, Docker, VPS and Lambda deployments need their own display, graphics
|
|
152
|
+
dependency and proxy checks.
|
|
152
153
|
- **Works with your stack.** Drop-in stealth for Crawlee, @playwright/test,
|
|
153
154
|
puppeteer-cluster, and anything that speaks CDP. See [integrations](#framework-integrations).
|
|
154
155
|
|
|
@@ -184,11 +185,11 @@ See **[capiumbrowser.com/#pricing](https://capiumbrowser.com/#pricing)** for cur
|
|
|
184
185
|
### Get a key
|
|
185
186
|
|
|
186
187
|
1. Create an account at **[capiumbrowser.com](https://capiumbrowser.com)** and create a license (start with the
|
|
187
|
-
free 1-session tier). Keys look like `
|
|
188
|
+
free 1-session tier). Keys look like `cap_your_license_key`.
|
|
188
189
|
2. Make it available to the SDK any one way (checked in this order):
|
|
189
190
|
|
|
190
191
|
```bash
|
|
191
|
-
export CAPIUM_LICENSE_KEY="
|
|
192
|
+
export CAPIUM_LICENSE_KEY="cap_your_license_key"
|
|
192
193
|
# optional — defaults to https://license.capzy.ai
|
|
193
194
|
export CAPIUM_LICENSE_SERVER="https://license.capzy.ai"
|
|
194
195
|
```
|
|
@@ -213,8 +214,17 @@ Handle license errors with typed error classes — `CapiumSeatLimitError`, `Capi
|
|
|
213
214
|
|
|
214
215
|
## What a persona reads
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
The table below retains historical Chrome 152 observations. These are examples,
|
|
218
|
+
not guarantees for every seed, SDK driver, host or detector revision.
|
|
219
|
+
The [Chrome 155 SDK/proxy audit](../../docs/reports/sdk-public-audit-155-2026-10-09.html)
|
|
220
|
+
records current BrowserScan and Fingerprint observations through Node Playwright/
|
|
221
|
+
Puppeteer and Python sync/async on all three hosts. Matched Windows/Mac personas
|
|
222
|
+
have clean tampering observations; Linux and cross-OS cases remain flagged.
|
|
223
|
+
Local API checks do not establish a public verdict or certify a seeded GPU as
|
|
224
|
+
physical hardware.
|
|
225
|
+
The active Rebrowser checks also detect driver stack labels, main-world DOM calls
|
|
226
|
+
and exposed bindings. A clean launch observation does not cover every script's
|
|
227
|
+
behavior or make `page.evaluate()` / `exposeFunction()` invisible to the page.
|
|
218
228
|
|
|
219
229
|
| Signal | Stock driver Chromium | Capium |
|
|
220
230
|
|---|---|---|
|
|
@@ -228,11 +238,13 @@ clean against a modern fingerprinting suite — with the correct GPU, fonts, and
|
|
|
228
238
|
| JA4 TLS fingerprint | differs from Chrome | **matches stock Chrome** |
|
|
229
239
|
| WebRTC IP behind a proxy | real IP leaks | **pinned to the proxy exit** |
|
|
230
240
|
|
|
231
|
-
>
|
|
232
|
-
>
|
|
233
|
-
>
|
|
234
|
-
>
|
|
235
|
-
>
|
|
241
|
+
> Software rendering can initialize WebGL without reproducing a hardware GPU.
|
|
242
|
+
> A seeded renderer name does not change the actual device or establish a clean
|
|
243
|
+
> detector verdict. On the tested 155 Linux/Xvfb stack, defaults provide headless
|
|
244
|
+
> WebGL but headed contexts are absent; explicit Mesa flags initialize both modes.
|
|
245
|
+
> Use matched stock/native controls and verify the assigned browser proxy exit
|
|
246
|
+
> before and after each public target. Keep unavailable contexts and unmeasured
|
|
247
|
+
> server verdicts explicit.
|
|
236
248
|
|
|
237
249
|
---
|
|
238
250
|
|
|
@@ -652,16 +664,21 @@ launches **headed**, which reads cleaner than headless.
|
|
|
652
664
|
|
|
653
665
|
| Platform | Distro tag | Notes |
|
|
654
666
|
|---|---|---|
|
|
655
|
-
| Windows x86_64 | `windows-x64` | Native GPU/fonts
|
|
656
|
-
| macOS arm64 (Apple Silicon) | `macos-arm64` |
|
|
657
|
-
| Linux x86_64 | `linux-x64` | Defaults to a Windows persona;
|
|
658
|
-
| Linux arm64 | `linux-arm64` |
|
|
667
|
+
| Windows x86_64 | `windows-x64` | Native Windows GPU/fonts support a matching persona; seeded hardware fidelity still requires measurement |
|
|
668
|
+
| macOS arm64 (Apple Silicon) | `macos-arm64` | Native Apple graphics support a matching persona; cross-OS identities need separate checks |
|
|
669
|
+
| Linux x86_64 | `linux-x64` | Defaults to a Windows persona; select a matching Linux persona and verify the deployment's graphics stack |
|
|
670
|
+
| Linux arm64 | `linux-arm64` | Declared in the manifest; not yet published or covered by the three-host audit |
|
|
659
671
|
|
|
660
672
|
The SDK auto-detects your host and downloads the matching tag; macOS Intel and Windows ARM are **not**
|
|
661
|
-
published and throw a clear error.
|
|
673
|
+
published and throw a clear error. The **1.2.0** manifest selects **Chromium 155.0.8059.39, revision 1** on the three
|
|
674
|
+
supported hosts. This release is prepared as a draft; downloads become available when the browser
|
|
675
|
+
artifacts are published. Linux arm64 remains unpublished.
|
|
662
676
|
|
|
663
677
|
---
|
|
664
678
|
|
|
679
|
+
The 1.2.0 launch APIs wait for the packaged browser's licensing decision before
|
|
680
|
+
returning a handle. Invalid keys raise a typed error during launch.
|
|
681
|
+
|
|
665
682
|
## CLI & environment
|
|
666
683
|
|
|
667
684
|
```bash
|
|
@@ -695,12 +712,13 @@ license key; it prints a fix for anything missing and exits non-zero if the envi
|
|
|
695
712
|
| `no license configured` | Set `CAPIUM_LICENSE_KEY`, pass `licenseKey: '…'`, or create `~/.capium/license` (`KEY=cap_…`). |
|
|
696
713
|
| `playwright-core is not installed` | `npm install playwright-core` (or `puppeteer-core`) — the driver is a peer dependency you choose. |
|
|
697
714
|
| `CapiumSeatLimitError` | Free tier is 1 concurrent session — close the other Capium browser (or upgrade the tier). |
|
|
698
|
-
| Still blocked on aggressive sites |
|
|
715
|
+
| Still blocked on aggressive sites | Compare a matching persona with stock Chrome through assigned proxies. Check route, timezone, graphics availability and detector fields separately; headed mode under Xvfb also needs a working graphics backend. |
|
|
699
716
|
| Intermittent 403s on reCAPTCHA Enterprise sites | Prefer the **Playwright** front-end — Puppeteer's CDP protocol usage leaks more automation signals (a Puppeteer limitation, not a Capium one). |
|
|
700
|
-
| FPJS `tampering: true` |
|
|
717
|
+
| FPJS `tampering: true` | Prefer a matching-OS host and keep the default `--fingerprint-noise=false`. The current Linux audit remains flagged even with hardware Vulkan; ML zero alone does not clear tampering or anomaly. See the [audit](../../docs/SDK-GPU-AUDIT.md). |
|
|
701
718
|
| "Windows persona but missing fonts" | Supply real `C:\Windows\Fonts\*.ttf` (see [Fonts](#fonts)) or set `CAPIUM_SUPPRESS_FONT_WARNING=1`. |
|
|
702
719
|
| `os_mismatch: true` | A network/transport signal (genuine Chrome shows it too behind some proxies) — a routing concern, not the browser. |
|
|
703
720
|
| Headed browser won't start on a server | Run under `Xvfb` and export `DISPLAY=:99`. |
|
|
721
|
+
| Chrome 155 dev: intermittent headed Puppeteer startup on macOS | The owned-origin matrix reproduced launch timeouts with both WebSocket and pipe transport. An active process sample waited in the licensing startup gate before page probes; the exact cause remains unresolved. Python and Node Playwright headed launches, and Puppeteer headless launches, completed in the matrix. |
|
|
704
722
|
|
|
705
723
|
More: [docs.capiumbrowser.com](https://docs.capiumbrowser.com).
|
|
706
724
|
|
package/lib/channels.json
CHANGED
|
@@ -1,31 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "SINGLE SOURCE OF TRUTH for the current stable Capium browser build per OS/arch. Each OS reaches stable independently -- edit one entry without touching the others. This file ships inside the wheel (the SDK reads it to pick each host's stable build) AND is published to the download server by the release pipeline. `version` is the Chromium version (what the browser reports to the customer -- unchanged by the revision); `revision` is our packaging counter -- the served R2 folder is chromium-v<version>.<revision>/, ALWAYS dotted-suffixed starting at 1 (chromium-v<version>.1/ is the first build, .2/ a same-engine re-spin), so publishing a re-spin never overwrites the artifact older SDKs still fetch. `published: false` means the tag is declared but no artifact exists yet -- the SDK raises a clear 'not published yet' error instead of a raw 404.",
|
|
3
3
|
"schema": 1,
|
|
4
|
-
"updated": "2026-10-
|
|
4
|
+
"updated": "2026-10-10",
|
|
5
5
|
"channels": {
|
|
6
6
|
"stable": {
|
|
7
7
|
"windows-x64": {
|
|
8
|
-
"version": "
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
8
|
+
"version": "155.0.8059.39",
|
|
9
|
+
"revision": 1,
|
|
10
|
+
"sha256": "d973e2460ce05e99f7e5c1dddc319ae96733434d9986dadc4cec5bac1c2597fb",
|
|
11
|
+
"published": true,
|
|
12
|
+
"license_status_protocol": 1
|
|
13
13
|
},
|
|
14
14
|
"macos-arm64": {
|
|
15
|
-
"version": "
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
15
|
+
"version": "155.0.8059.39",
|
|
16
|
+
"revision": 1,
|
|
17
|
+
"sha256": "f07d54b88feb6644388196d6b91c9787592c1f6e07a8122344f47faed76b9262",
|
|
18
|
+
"published": true,
|
|
19
|
+
"license_status_protocol": 1
|
|
20
20
|
},
|
|
21
21
|
"linux-x64": {
|
|
22
|
-
"version": "
|
|
23
|
-
"revision":
|
|
24
|
-
"sha256": "
|
|
25
|
-
"published": true
|
|
22
|
+
"version": "155.0.8059.39",
|
|
23
|
+
"revision": 1,
|
|
24
|
+
"sha256": "a15cd395e1f8b792d9539c8284edcd589c6b91ef77f8b1fa5ea408f1f220b540",
|
|
25
|
+
"published": true,
|
|
26
|
+
"license_status_protocol": 1
|
|
26
27
|
},
|
|
27
28
|
"linux-arm64": {
|
|
28
|
-
"version": "
|
|
29
|
+
"version": "155.0.8059.39",
|
|
29
30
|
"revision": 1,
|
|
30
31
|
"sha256": null,
|
|
31
32
|
"published": false
|
package/lib/config.js
CHANGED
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
* config -- default stealth arguments and binary discovery.
|
|
3
3
|
*
|
|
4
4
|
* The capium binary is driven through its `capium` bash wrapper (executablePath), which
|
|
5
|
-
*
|
|
5
|
+
* supports explicit backend selection and platform flags; geoip remains opt-in.
|
|
6
6
|
* This module only builds the *fingerprint-level* Chrome flags and locates that wrapper.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* 001 (disable Runtime.enable console-reporting) -> FingerprintJS developer_tools = false
|
|
11
|
-
* 009 (webdriver) -> navigator.webdriver = false
|
|
12
|
-
* so we launch with plain Playwright / Puppeteer and let the binary do the CDP hardening.
|
|
8
|
+
* The binary handles CDP isolation and console masking, plus the webdriver surface.
|
|
9
|
+
* We launch with plain drivers; detector outcomes still require native host tests.
|
|
13
10
|
*/
|
|
14
11
|
'use strict';
|
|
15
12
|
|
|
@@ -17,17 +14,45 @@ const fs = require('fs');
|
|
|
17
14
|
const os = require('os');
|
|
18
15
|
const path = require('path');
|
|
19
16
|
|
|
17
|
+
// Playwright/Puppeteer default args to SUPPRESS (ignoreDefaultArgs): these leak automation
|
|
18
|
+
// or force a software render.
|
|
19
|
+
// --enable-automation : exposes navigator.webdriver=true + an automation banner.
|
|
20
|
+
// --enable-unsafe-swiftshader : permits a software fallback. Keep that choice
|
|
21
|
+
// explicit instead of inheriting a driver's default.
|
|
22
|
+
// The historical 153 package lacked its ICD manifest;
|
|
23
|
+
// the 155 packager includes it. Validate drawing in
|
|
24
|
+
// the deployed environment for either backend.
|
|
25
|
+
const IGNORE_DEFAULT_ARGS = ['--enable-automation', '--enable-unsafe-swiftshader'];
|
|
26
|
+
|
|
27
|
+
// The 155 engine selects a seeded screen within these default bounds. Start
|
|
28
|
+
// headless windows small enough to fit its modes without replacing the screen.
|
|
29
|
+
// Explicit screen/window flags take
|
|
30
|
+
// precedence. HEADLESS_SCREEN remains the upper bound for existing consumers.
|
|
31
|
+
const HEADLESS_SCREEN = [1920, 1080];
|
|
32
|
+
const HEADLESS_VIEWPORT = [800, 600];
|
|
33
|
+
|
|
34
|
+
function getHeadlessDefaultArgs() {
|
|
35
|
+
const [vw, vh] = HEADLESS_VIEWPORT;
|
|
36
|
+
return [
|
|
37
|
+
`--window-size=${vw},${vh}`,
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasScreenOverride(extra) {
|
|
42
|
+
if (!Array.isArray(extra)) return false;
|
|
43
|
+
const keys = ['--window-size', '--fingerprint-screen-width', '--fingerprint-screen-height'];
|
|
44
|
+
return extra.some((a) => typeof a === 'string' && keys.some((k) => a.startsWith(k)));
|
|
45
|
+
}
|
|
46
|
+
|
|
20
47
|
/**
|
|
21
48
|
* Return the default per-launch fingerprint flags.
|
|
22
49
|
*
|
|
23
50
|
* seed: int identity seed (a stable, coherent device per seed). Random if null.
|
|
24
51
|
* platform: "windows" | "macos" | "linux" -- the spoofed OS persona.
|
|
25
|
-
* screen:
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* only for a fixed-kiosk scenario. (Previously defaulted to [1440,900], a cross-instance
|
|
30
|
-
* correlation + GPU-incoherence tell -- now matches the python SDK's screen=None.)
|
|
52
|
+
* screen: null uses the binary's seeded screen and DPR. Development 155 defaults
|
|
53
|
+
* vary within 1920x1080. Headless starts with an 800x600 window; its screen
|
|
54
|
+
* remains independent of the viewport. An explicit [w,h] overrides the screen
|
|
55
|
+
* and startup window, including sizes above the default cap.
|
|
31
56
|
*/
|
|
32
57
|
function getDefaultStealthArgs(seed = null, platform = 'windows', screen = null) {
|
|
33
58
|
const args = [];
|
|
@@ -55,17 +80,15 @@ function getDefaultStealthArgs(seed = null, platform = 'windows', screen = null)
|
|
|
55
80
|
'primaryPointerType=4,availablePointerTypes=4',
|
|
56
81
|
'--no-first-run',
|
|
57
82
|
'--no-default-browser-check',
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
83
|
+
// Engine 155 honors the requested execution world by default. Its console
|
|
84
|
+
// event policy is independent of page evaluation. Engine 153 retains the
|
|
85
|
+
// older default isolation behavior; --capium-isolate-cdp-eval explicitly
|
|
86
|
+
// requests that legacy mode on 155. See docs/SDK-GPU-AUDIT.md for measured
|
|
87
|
+
// page-global, object-handle and binding compatibility.
|
|
63
88
|
);
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
// Trade-off: personas on the same host share its real canvas (no per-seed canvas
|
|
68
|
-
// uniqueness) -- pass --fingerprint-noise=true to restore it (accepts tampering).
|
|
89
|
+
// Keep injected canvas/audio/rectangle noise opt-in. Device fonts and the
|
|
90
|
+
// actual renderer can still affect pixels; noise=false does not guarantee
|
|
91
|
+
// identical canvases across personas or a particular detector classification.
|
|
69
92
|
args.push('--fingerprint-noise=false');
|
|
70
93
|
// A Windows persona's font metrics must match real Windows (needs Windows fonts
|
|
71
94
|
// present on the host); harmless elsewhere, so gate it to the windows persona.
|
|
@@ -74,8 +97,8 @@ function getDefaultStealthArgs(seed = null, platform = 'windows', screen = null)
|
|
|
74
97
|
// screen (e.g. 2560x1080) mismatches the actual small browser viewport (e.g.
|
|
75
98
|
// 945x939) -- browserscan flags "screen dimensions don't match viewport" as a
|
|
76
99
|
// virtual machine. Pin the spoofed screen to a window we also size, so
|
|
77
|
-
//
|
|
78
|
-
//
|
|
100
|
+
// --window-size requests the outer window; the inner viewport excludes Chrome's
|
|
101
|
+
// frame/UI and must be measured rather than assumed equal to screen dimensions.
|
|
79
102
|
if (screen) {
|
|
80
103
|
const sw = Math.trunc(Number(screen[0]));
|
|
81
104
|
const sh = Math.trunc(Number(screen[1]));
|
|
@@ -285,6 +308,11 @@ function findBinary(binary = null) {
|
|
|
285
308
|
|
|
286
309
|
module.exports = {
|
|
287
310
|
getDefaultStealthArgs,
|
|
311
|
+
IGNORE_DEFAULT_ARGS,
|
|
312
|
+
HEADLESS_SCREEN,
|
|
313
|
+
HEADLESS_VIEWPORT,
|
|
314
|
+
getHeadlessDefaultArgs,
|
|
315
|
+
hasScreenOverride,
|
|
288
316
|
newSeed,
|
|
289
317
|
findBinary,
|
|
290
318
|
searchBases,
|
package/lib/errors.js
CHANGED
|
@@ -96,6 +96,7 @@ function readLaunchStatus(statusPath) {
|
|
|
96
96
|
if (!raw.trim()) return null;
|
|
97
97
|
const nl = raw.indexOf('\n');
|
|
98
98
|
const code = (nl === -1 ? raw : raw.slice(0, nl)).trim();
|
|
99
|
+
if (code === '0') return null;
|
|
99
100
|
const msg = (nl === -1 ? '' : raw.slice(nl + 1)).trim() ||
|
|
100
101
|
'the browser refused to start for a licensing reason';
|
|
101
102
|
const Cls = STATUS_CODES[code] || CapiumLicenseError;
|
package/lib/launch-common.js
CHANGED
|
@@ -39,9 +39,17 @@ function proxyAndGeoArgs(spec, geoipOn) {
|
|
|
39
39
|
return { launchOptions, args: out };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function buildArgs({ seed, platform, stealthArgs, timezone, locale, extensionPaths, extra
|
|
42
|
+
function buildArgs({ seed, platform, stealthArgs, timezone, locale, extensionPaths, extra,
|
|
43
|
+
headless = false }) {
|
|
43
44
|
let args = [];
|
|
44
|
-
if (stealthArgs)
|
|
45
|
+
if (stealthArgs) {
|
|
46
|
+
args = args.concat(config.getDefaultStealthArgs(seed, platform));
|
|
47
|
+
// Keep the engine's seeded screen. Use initial window bounds only when
|
|
48
|
+
// the caller has not provided screen/window flags.
|
|
49
|
+
if (headless && !config.hasScreenOverride(extra)) {
|
|
50
|
+
args = args.concat(config.getHeadlessDefaultArgs());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
45
53
|
if (timezone) args.push(`--timezone=${timezone}`); // capium in-binary tz spoof
|
|
46
54
|
if (locale) {
|
|
47
55
|
args.push(`--lang=${locale}`);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { readLaunchStatus, CapiumServerDownError } = require('./errors');
|
|
5
|
+
|
|
6
|
+
function required(binary) {
|
|
7
|
+
let directory = path.dirname(path.resolve(binary));
|
|
8
|
+
for (let i = 0; i < 8; i++) {
|
|
9
|
+
try {
|
|
10
|
+
const info = fs.readFileSync(path.join(directory, 'CAPIUM_BUILD_INFO'), 'utf8');
|
|
11
|
+
return /^license_status_protocol\s*:\s*1\s*$/m.test(info);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error.code !== 'ENOENT') throw error;
|
|
14
|
+
const parent = path.dirname(directory);
|
|
15
|
+
if (parent === directory) break;
|
|
16
|
+
directory = parent;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function wait(binary, statusPath, timeoutMs = 30000, signal = null) {
|
|
23
|
+
if (!required(binary)) return;
|
|
24
|
+
const deadline = Date.now() + (timeoutMs || 30000);
|
|
25
|
+
for (;;) {
|
|
26
|
+
if (signal?.aborted) return;
|
|
27
|
+
const error = readLaunchStatus(statusPath);
|
|
28
|
+
if (error) throw error;
|
|
29
|
+
try {
|
|
30
|
+
if (fs.readFileSync(statusPath, 'utf8').trim() === '0\nCAPIUM_LICENSE_READY') return;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code !== 'ENOENT') throw error;
|
|
33
|
+
}
|
|
34
|
+
if (Date.now() >= deadline) {
|
|
35
|
+
throw new CapiumServerDownError('Timed out waiting for the browser license startup decision');
|
|
36
|
+
}
|
|
37
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Drivers may leave startup pending after the native process refuses the license.
|
|
41
|
+
// Observe the native decision while the driver connects, so rejection cannot hang.
|
|
42
|
+
async function withLaunch(pending, binary, statusPath, timeoutMs) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
try {
|
|
45
|
+
const [handle] = await Promise.all([
|
|
46
|
+
pending, wait(binary, statusPath, timeoutMs, controller.signal),
|
|
47
|
+
]);
|
|
48
|
+
return handle;
|
|
49
|
+
} finally {
|
|
50
|
+
// A driver can reject before the native decision is written. Do not keep
|
|
51
|
+
// polling a removed status file, or keep Node alive until the deadline.
|
|
52
|
+
controller.abort();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
module.exports = { required, wait, withLaunch };
|
package/lib/version.js
CHANGED
|
@@ -25,12 +25,12 @@ const SDK_VERSION = require('../package.json').version;
|
|
|
25
25
|
// Used only if channels.json can't be read (corrupt/removed install). ensureBinary() raises a
|
|
26
26
|
// clear error long before the "unsupported platform" case matters; this keeps lookups total.
|
|
27
27
|
const FALLBACK_VERSIONS = {
|
|
28
|
-
'windows-x64': '
|
|
29
|
-
'macos-arm64': '
|
|
30
|
-
'linux-x64': '
|
|
31
|
-
'linux-arm64': '
|
|
28
|
+
'windows-x64': '155.0.8059.39', // Chromium 155 release candidate on all supported hosts
|
|
29
|
+
'macos-arm64': '155.0.8059.39',
|
|
30
|
+
'linux-x64': '155.0.8059.39',
|
|
31
|
+
'linux-arm64': '155.0.8059.39', // declared; not yet published (see channels.json)
|
|
32
32
|
};
|
|
33
|
-
const DEFAULT_BINARY_VERSION = '
|
|
33
|
+
const DEFAULT_BINARY_VERSION = '155.0.8059.39';
|
|
34
34
|
|
|
35
35
|
function loadStable() {
|
|
36
36
|
// The `channels.stable` map from the bundled manifest, or null if unreadable.
|
package/package.json
CHANGED
package/playwright.js
CHANGED
|
@@ -25,6 +25,7 @@ const config = require('./lib/config');
|
|
|
25
25
|
const download = require('./lib/download');
|
|
26
26
|
const human = require('./lib/human');
|
|
27
27
|
const license = require('./lib/license');
|
|
28
|
+
const readiness = require('./lib/license-readiness');
|
|
28
29
|
const { CapiumError, translateLaunchError, readLaunchStatus } = require('./lib/errors');
|
|
29
30
|
const { proxyAndGeoArgs, buildArgs, newStatusFile, clearStatusFile } =
|
|
30
31
|
require('./lib/launch-common');
|
|
@@ -78,7 +79,7 @@ async function prepare(opts) {
|
|
|
78
79
|
seed = null, platform = 'windows', proxy = null, geoip = null, args = null,
|
|
79
80
|
stealthArgs = true, timezone = null, locale = null, extensionPaths = null,
|
|
80
81
|
binary = null, licenseKey = null, licenseServer = null,
|
|
81
|
-
licenseThroughProxy = false, licensePreflight = true,
|
|
82
|
+
licenseThroughProxy = false, licensePreflight = true, headless = false,
|
|
82
83
|
} = opts;
|
|
83
84
|
if (licensePreflight) license.preflight(licenseKey, licenseServer);
|
|
84
85
|
const { key } = license.effective(licenseKey, licenseServer);
|
|
@@ -87,6 +88,7 @@ async function prepare(opts) {
|
|
|
87
88
|
const { launchOptions, args: proxyArgs } = proxyAndGeoArgs(proxy, geoip);
|
|
88
89
|
const launchArgs = proxyArgs.concat(buildArgs({
|
|
89
90
|
seed: finalSeed, platform, stealthArgs, timezone, locale, extensionPaths, extra: args,
|
|
91
|
+
headless,
|
|
90
92
|
}));
|
|
91
93
|
if (licenseThroughProxy) launchArgs.push('--license-through-proxy');
|
|
92
94
|
const env = license.childEnv(licenseKey, licenseServer);
|
|
@@ -121,10 +123,11 @@ async function buildLaunchOptions(opts = {}) {
|
|
|
121
123
|
} = opts;
|
|
122
124
|
const prep = await prepare({
|
|
123
125
|
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
124
|
-
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
126
|
+
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
125
127
|
});
|
|
126
128
|
return {
|
|
127
129
|
executablePath: prep.binPath, headless, args: prep.launchArgs, env: prep.env,
|
|
130
|
+
ignoreDefaultArgs: config.IGNORE_DEFAULT_ARGS, // strip automation + forced-software WebGL defaults
|
|
128
131
|
...prep.launchOptions, ...rest,
|
|
129
132
|
};
|
|
130
133
|
}
|
|
@@ -174,16 +177,22 @@ async function launch(opts = {}) {
|
|
|
174
177
|
} = opts;
|
|
175
178
|
const prep = await prepare({
|
|
176
179
|
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
177
|
-
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
180
|
+
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
178
181
|
});
|
|
179
182
|
const pw = requireDriver();
|
|
180
183
|
let browser;
|
|
181
184
|
try {
|
|
182
|
-
|
|
185
|
+
const pending = pw.chromium.launch({
|
|
183
186
|
executablePath: prep.binPath, headless, args: prep.launchArgs, env: prep.env,
|
|
187
|
+
ignoreDefaultArgs: config.IGNORE_DEFAULT_ARGS, // strip automation + forced-software WebGL defaults
|
|
184
188
|
...prep.launchOptions, ...rest,
|
|
185
|
-
});
|
|
189
|
+
}).then(handle => { browser = handle; return handle; });
|
|
190
|
+
await readiness.withLaunch(pending, prep.binPath, prep.statusPath, rest.timeout);
|
|
186
191
|
} catch (e) {
|
|
192
|
+
// A native refusal terminates the process; closing its dead CDP connection can hang.
|
|
193
|
+
if (!readLaunchStatus(prep.statusPath) && browser) {
|
|
194
|
+
try { await browser.close(); } catch {}
|
|
195
|
+
}
|
|
187
196
|
await throwTranslated(e, prep.statusPath);
|
|
188
197
|
}
|
|
189
198
|
clearStatusFile(prep.statusPath);
|
|
@@ -204,7 +213,7 @@ async function launchPersistentContext(userDataDir, opts = {}) {
|
|
|
204
213
|
} = opts;
|
|
205
214
|
const prep = await prepare({
|
|
206
215
|
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
207
|
-
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
216
|
+
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
208
217
|
});
|
|
209
218
|
// Default to the page filling the window (see launchContext) unless the caller pinned a
|
|
210
219
|
// viewport themselves. viewport: null is Playwright-JS for "no fixed viewport".
|
|
@@ -212,11 +221,17 @@ async function launchPersistentContext(userDataDir, opts = {}) {
|
|
|
212
221
|
const pw = requireDriver();
|
|
213
222
|
let ctx;
|
|
214
223
|
try {
|
|
215
|
-
|
|
224
|
+
const pending = pw.chromium.launchPersistentContext(userDataDir, {
|
|
216
225
|
executablePath: prep.binPath, headless, args: prep.launchArgs, env: prep.env,
|
|
226
|
+
ignoreDefaultArgs: config.IGNORE_DEFAULT_ARGS, // strip automation + forced-software WebGL defaults
|
|
217
227
|
...prep.launchOptions, ...rest,
|
|
218
|
-
});
|
|
228
|
+
}).then(handle => { ctx = handle; return handle; });
|
|
229
|
+
await readiness.withLaunch(pending, prep.binPath, prep.statusPath, rest.timeout);
|
|
219
230
|
} catch (e) {
|
|
231
|
+
// A native refusal terminates the process; closing its dead CDP connection can hang.
|
|
232
|
+
if (!readLaunchStatus(prep.statusPath) && ctx) {
|
|
233
|
+
try { await ctx.close(); } catch {}
|
|
234
|
+
}
|
|
220
235
|
await throwTranslated(e, prep.statusPath);
|
|
221
236
|
}
|
|
222
237
|
clearStatusFile(prep.statusPath);
|
package/puppeteer.js
CHANGED
|
@@ -29,6 +29,7 @@ const config = require('./lib/config');
|
|
|
29
29
|
const download = require('./lib/download');
|
|
30
30
|
const human = require('./lib/human');
|
|
31
31
|
const license = require('./lib/license');
|
|
32
|
+
const readiness = require('./lib/license-readiness');
|
|
32
33
|
const { CapiumError, translateLaunchError, readLaunchStatus } = require('./lib/errors');
|
|
33
34
|
const { proxyAndGeoArgs, buildArgs, newStatusFile, clearStatusFile } =
|
|
34
35
|
require('./lib/launch-common');
|
|
@@ -78,7 +79,7 @@ async function prepare(opts) {
|
|
|
78
79
|
seed = null, platform = 'windows', proxy = null, geoip = null, args = null,
|
|
79
80
|
stealthArgs = true, timezone = null, locale = null, extensionPaths = null,
|
|
80
81
|
binary = null, licenseKey = null, licenseServer = null,
|
|
81
|
-
licenseThroughProxy = false, licensePreflight = true,
|
|
82
|
+
licenseThroughProxy = false, licensePreflight = true, headless = false,
|
|
82
83
|
} = opts;
|
|
83
84
|
if (licensePreflight) license.preflight(licenseKey, licenseServer);
|
|
84
85
|
const { key } = license.effective(licenseKey, licenseServer);
|
|
@@ -87,6 +88,7 @@ async function prepare(opts) {
|
|
|
87
88
|
const { launchOptions, args: proxyArgs } = proxyAndGeoArgs(proxy, geoip);
|
|
88
89
|
const launchArgs = proxyArgs.concat(buildArgs({
|
|
89
90
|
seed: finalSeed, platform, stealthArgs, timezone, locale, extensionPaths, extra: args,
|
|
91
|
+
headless,
|
|
90
92
|
}));
|
|
91
93
|
if (licenseThroughProxy) launchArgs.push('--license-through-proxy');
|
|
92
94
|
const env = license.childEnv(licenseKey, licenseServer);
|
|
@@ -135,7 +137,7 @@ async function buildLaunchOptions(opts = {}) {
|
|
|
135
137
|
} = opts;
|
|
136
138
|
const prep = await prepare({
|
|
137
139
|
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
138
|
-
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
140
|
+
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
139
141
|
});
|
|
140
142
|
const proxyOption = prep.launchOptions.proxy || null;
|
|
141
143
|
const launchArgs = [...prep.launchArgs];
|
|
@@ -149,7 +151,7 @@ async function buildLaunchOptions(opts = {}) {
|
|
|
149
151
|
args: launchArgs,
|
|
150
152
|
env: prep.env,
|
|
151
153
|
defaultViewport: 'defaultViewport' in rest ? rest.defaultViewport : null,
|
|
152
|
-
ignoreDefaultArgs: ignoreDefaultArgs === undefined ?
|
|
154
|
+
ignoreDefaultArgs: ignoreDefaultArgs === undefined ? config.IGNORE_DEFAULT_ARGS : ignoreDefaultArgs,
|
|
153
155
|
...rest,
|
|
154
156
|
};
|
|
155
157
|
}
|
|
@@ -167,7 +169,7 @@ async function launch(opts = {}) {
|
|
|
167
169
|
} = opts;
|
|
168
170
|
const prep = await prepare({
|
|
169
171
|
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
170
|
-
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
172
|
+
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
171
173
|
});
|
|
172
174
|
const puppeteer = requireDriver();
|
|
173
175
|
// The CDP escape hatch puts {proxy: {server, username?, ...}} in launchOptions; Puppeteer
|
|
@@ -178,11 +180,13 @@ async function launch(opts = {}) {
|
|
|
178
180
|
launchArgs.push(`--proxy-server=${proxyOption.server}`);
|
|
179
181
|
if (proxyOption.bypass) launchArgs.push(`--proxy-bypass-list=${proxyOption.bypass}`);
|
|
180
182
|
}
|
|
181
|
-
// Strip
|
|
182
|
-
const ida = ignoreDefaultArgs === undefined ?
|
|
183
|
+
// Strip automation + forced-software WebGL defaults unless the caller took over ignoreDefaultArgs.
|
|
184
|
+
const ida = ignoreDefaultArgs === undefined ? config.IGNORE_DEFAULT_ARGS : ignoreDefaultArgs;
|
|
185
|
+
const nativeGate = readiness.required(prep.binPath);
|
|
186
|
+
const waitForInitialPage = rest.waitForInitialPage !== false;
|
|
183
187
|
let browser;
|
|
184
188
|
try {
|
|
185
|
-
|
|
189
|
+
const pending = puppeteer.launch({
|
|
186
190
|
executablePath: prep.binPath,
|
|
187
191
|
headless,
|
|
188
192
|
args: launchArgs,
|
|
@@ -190,8 +194,22 @@ async function launch(opts = {}) {
|
|
|
190
194
|
defaultViewport: 'defaultViewport' in rest ? rest.defaultViewport : null,
|
|
191
195
|
ignoreDefaultArgs: ida,
|
|
192
196
|
...rest,
|
|
193
|
-
|
|
197
|
+
// A rejected native gate never creates a page. Obtain the transport first,
|
|
198
|
+
// so Puppeteer does not leave its initial-page timer running after refusal.
|
|
199
|
+
waitForInitialPage: nativeGate ? false : waitForInitialPage,
|
|
200
|
+
}).then(handle => { browser = handle; return handle; });
|
|
201
|
+
await readiness.withLaunch(pending, prep.binPath, prep.statusPath, rest.timeout);
|
|
202
|
+
if (nativeGate && waitForInitialPage) {
|
|
203
|
+
await browser.waitForTarget(target => target.type() === 'page', {
|
|
204
|
+
timeout: rest.timeout === undefined ? 30000 : rest.timeout,
|
|
205
|
+
signal: rest.signal,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
194
208
|
} catch (e) {
|
|
209
|
+
// A native refusal terminates the process; closing its dead CDP connection can hang.
|
|
210
|
+
if (!readLaunchStatus(prep.statusPath) && browser) {
|
|
211
|
+
try { await browser.close(); } catch {}
|
|
212
|
+
}
|
|
195
213
|
await throwTranslated(e, prep.statusPath);
|
|
196
214
|
}
|
|
197
215
|
clearStatusFile(prep.statusPath);
|