capiumbrowser 1.1.8 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -26
- package/index.d.ts +11 -2
- package/lib/android-phones.json +99 -0
- package/lib/browser-brands.json +47 -0
- package/lib/channels.json +18 -17
- package/lib/config.js +52 -43
- package/lib/desktop.js +68 -0
- package/lib/errors.js +1 -0
- package/lib/ios-phones.json +19 -0
- package/lib/launch-common.js +5 -5
- package/lib/license-readiness.js +58 -0
- package/lib/mobile.js +104 -0
- package/lib/version.js +5 -5
- package/package.json +1 -1
- package/playwright.js +66 -19
- package/puppeteer.js +51 -12
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
|
|
|
@@ -66,6 +66,7 @@ await browser.close();
|
|
|
66
66
|
- [Install](#install) · [Why Capium](#why-capium) · [Licensing & tiers](#licensing)
|
|
67
67
|
- [What a persona reads](#what-a-persona-reads) · [How it works](#how-it-works)
|
|
68
68
|
- [API](#api) · [Options](#options) · [Personas & seeds](#personas--seeds) · [Fingerprint switches](#fingerprint-switches)
|
|
69
|
+
- [Browser and mobile profiles](#browser-and-mobile-profiles)
|
|
69
70
|
- [Human behavior](#human-behavior) · [Proxies & geo](#proxies--geo) · [Fonts](#fonts)
|
|
70
71
|
- [Framework integrations](#framework-integrations) · [Deployment](#deployment) · [Platforms](#platforms)
|
|
71
72
|
- [CLI](#cli--environment) · [Troubleshooting](#troubleshooting) · [FAQ](#faq) · [Security](#security) · [License](#license)
|
|
@@ -142,13 +143,14 @@ Puppeteer `Browser`:
|
|
|
142
143
|
- **Config-level stealth breaks.** `puppeteer-extra-plugin-stealth`, patched drivers, and
|
|
143
144
|
friends inject JavaScript or flip flags. Every Chrome update breaks them, and modern
|
|
144
145
|
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
|
-
- **
|
|
146
|
+
- **Capium patches Chromium source.** Fingerprint changes are compiled into the browser at the C++
|
|
147
|
+
level. SDK evaluation and exposed bindings can still reveal driver behavior; see the measured
|
|
148
|
+
[Chrome 155 audit](../../docs/SDK-GPU-AUDIT.md).
|
|
149
|
+
- **Coherence needs measurement.** A seed selects identity settings, and optional proxy geography
|
|
150
|
+
aligns network-related settings. Native rendering, fonts and server classifications still depend
|
|
151
|
+
on the host and launch policy; matching the persona to the host performs better in the current audit.
|
|
152
|
+
- **Common launch API.** Local, Docker, VPS and Lambda deployments need their own display, graphics
|
|
153
|
+
dependency and proxy checks.
|
|
152
154
|
- **Works with your stack.** Drop-in stealth for Crawlee, @playwright/test,
|
|
153
155
|
puppeteer-cluster, and anything that speaks CDP. See [integrations](#framework-integrations).
|
|
154
156
|
|
|
@@ -184,11 +186,11 @@ See **[capiumbrowser.com/#pricing](https://capiumbrowser.com/#pricing)** for cur
|
|
|
184
186
|
### Get a key
|
|
185
187
|
|
|
186
188
|
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 `
|
|
189
|
+
free 1-session tier). Keys look like `cap_your_license_key`.
|
|
188
190
|
2. Make it available to the SDK any one way (checked in this order):
|
|
189
191
|
|
|
190
192
|
```bash
|
|
191
|
-
export CAPIUM_LICENSE_KEY="
|
|
193
|
+
export CAPIUM_LICENSE_KEY="cap_your_license_key"
|
|
192
194
|
# optional — defaults to https://license.capzy.ai
|
|
193
195
|
export CAPIUM_LICENSE_SERVER="https://license.capzy.ai"
|
|
194
196
|
```
|
|
@@ -213,8 +215,17 @@ Handle license errors with typed error classes — `CapiumSeatLimitError`, `Capi
|
|
|
213
215
|
|
|
214
216
|
## What a persona reads
|
|
215
217
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
The table below retains historical Chrome 152 observations. These are examples,
|
|
219
|
+
not guarantees for every seed, SDK driver, host or detector revision.
|
|
220
|
+
The [Chrome 155 SDK/proxy audit](../../docs/reports/sdk-public-audit-155-2026-10-09.html)
|
|
221
|
+
records current BrowserScan and Fingerprint observations through Node Playwright/
|
|
222
|
+
Puppeteer and Python sync/async on all three hosts. Matched Windows/Mac personas
|
|
223
|
+
have clean tampering observations; Linux and cross-OS cases remain flagged.
|
|
224
|
+
Local API checks do not establish a public verdict or certify a seeded GPU as
|
|
225
|
+
physical hardware.
|
|
226
|
+
The active Rebrowser checks also detect driver stack labels, main-world DOM calls
|
|
227
|
+
and exposed bindings. A clean launch observation does not cover every script's
|
|
228
|
+
behavior or make `page.evaluate()` / `exposeFunction()` invisible to the page.
|
|
218
229
|
|
|
219
230
|
| Signal | Stock driver Chromium | Capium |
|
|
220
231
|
|---|---|---|
|
|
@@ -228,11 +239,13 @@ clean against a modern fingerprinting suite — with the correct GPU, fonts, and
|
|
|
228
239
|
| JA4 TLS fingerprint | differs from Chrome | **matches stock Chrome** |
|
|
229
240
|
| WebRTC IP behind a proxy | real IP leaks | **pinned to the proxy exit** |
|
|
230
241
|
|
|
231
|
-
>
|
|
232
|
-
>
|
|
233
|
-
>
|
|
234
|
-
>
|
|
235
|
-
>
|
|
242
|
+
> Software rendering can initialize WebGL without reproducing a hardware GPU.
|
|
243
|
+
> A seeded renderer name does not change the actual device or establish a clean
|
|
244
|
+
> detector verdict. On the tested 155 Linux/Xvfb stack, defaults provide headless
|
|
245
|
+
> WebGL but headed contexts are absent; explicit Mesa flags initialize both modes.
|
|
246
|
+
> Use matched stock/native controls and verify the assigned browser proxy exit
|
|
247
|
+
> before and after each public target. Keep unavailable contexts and unmeasured
|
|
248
|
+
> server verdicts explicit.
|
|
236
249
|
|
|
237
250
|
---
|
|
238
251
|
|
|
@@ -328,7 +341,9 @@ Python SDK, in camelCase.
|
|
|
328
341
|
| Option | Type | Default | Description |
|
|
329
342
|
|---|---|---|---|
|
|
330
343
|
| `seed` | `number` \| `null` | `null` | Persona seed → a complete coherent device. `null` = fresh random seed each launch. The range selects the OS family (see [Personas & seeds](#personas--seeds)). |
|
|
331
|
-
| `platform` | `string` | `"windows"` |
|
|
344
|
+
| `platform` | `string` | `"windows"` | OS profile: `"windows"` \| `"macos"` \| `"linux"` \| `"android"` \| `"ios"`. Mobile profiles are opt-in. |
|
|
345
|
+
| `browserBrand` | `string` | `"chrome"` | Desktop profile: `chrome`, `edge`, `opera`, `vivaldi`. |
|
|
346
|
+
| `mobileDevice` | `string` | seed/default | Phone catalog ID; requires `android` or `ios`. |
|
|
332
347
|
| `headless` | `boolean` | `false` | Headed is recommended (reads cleaner). On a server, run headed under Xvfb. |
|
|
333
348
|
| `stealthArgs` | `boolean` | `true` | Apply the default coherent fingerprint flags. Set `false` to build the whole command line yourself via `args`. |
|
|
334
349
|
| `args` | `string[]` | — | Extra raw Chrome flags appended verbatim (see [Fingerprint switches](#fingerprint-switches)). |
|
|
@@ -430,6 +445,43 @@ Full runnable version (personas, persistent profiles, a fleet, proxy+geo, full c
|
|
|
430
445
|
|
|
431
446
|
---
|
|
432
447
|
|
|
448
|
+
## Browser and mobile profiles
|
|
449
|
+
|
|
450
|
+
CapiumBrowser 1.2.1 supports desktop Chrome, Edge, Opera and Vivaldi identity
|
|
451
|
+
profiles, plus opt-in Android and iPhone profiles on Windows x64, macOS arm64
|
|
452
|
+
and Linux x64. Chrome remains the default.
|
|
453
|
+
|
|
454
|
+
| Profile | Python | Node.js |
|
|
455
|
+
| --- | --- | --- |
|
|
456
|
+
| Desktop browser | `browser_brand="edge"` (also `chrome`, `opera`, `vivaldi`) | `browserBrand: 'edge'` |
|
|
457
|
+
| Android phone | `platform="android", mobile_device="pixel-9"` | `platform: 'android', mobileDevice: 'pixel-9'` |
|
|
458
|
+
| iPhone 16 | `platform="ios", mobile_device="iphone-16"` | `platform: 'ios', mobileDevice: 'iphone-16'` |
|
|
459
|
+
|
|
460
|
+
Android includes Pixel 7/8/9 families and Samsung Galaxy A55. Omit the Android
|
|
461
|
+
device to retain the original seed selection; iOS defaults to iPhone 16. Mobile
|
|
462
|
+
profiles set the mobile layout and identity while leaving desktop defaults intact.
|
|
463
|
+
These are experimental desktop emulation profiles: they do not install Android
|
|
464
|
+
or iOS, reproduce WebKit, or certify physical-phone fingerprints. Android Chrome
|
|
465
|
+
and Chrome iOS are the supported mobile browser profiles; arbitrary app/WebView
|
|
466
|
+
UAs are outside these presets.
|
|
467
|
+
|
|
468
|
+
Desktop profiles use verified browser/Chromium version pairs: Edge 155/155,
|
|
469
|
+
Opera 137/153 and Vivaldi 8.2/152. Vivaldi follows its usual Chrome branding.
|
|
470
|
+
Capium's engine remains Chromium 155; vendor-specific features are not emulated.
|
|
471
|
+
A version catalog is not an older browser engine. Native compatibility checks
|
|
472
|
+
reject binaries that do not implement the selected profile.
|
|
473
|
+
|
|
474
|
+
```js
|
|
475
|
+
const { launchContext } = require('capiumbrowser');
|
|
476
|
+
|
|
477
|
+
const { browser, page } = await launchContext({
|
|
478
|
+
seed: 200123, platform: 'ios', mobileDevice: 'iphone-16',
|
|
479
|
+
});
|
|
480
|
+
// Android: platform: 'android', mobileDevice: 'pixel-9'
|
|
481
|
+
// Desktop Edge: platform: 'windows', browserBrand: 'edge'
|
|
482
|
+
await browser.close();
|
|
483
|
+
```
|
|
484
|
+
|
|
433
485
|
## Fingerprint switches
|
|
434
486
|
|
|
435
487
|
The SDK sets the essentials from `seed`/`platform`. Everything below is available for precise control —
|
|
@@ -652,16 +704,20 @@ launches **headed**, which reads cleaner than headless.
|
|
|
652
704
|
|
|
653
705
|
| Platform | Distro tag | Notes |
|
|
654
706
|
|---|---|---|
|
|
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` |
|
|
707
|
+
| Windows x86_64 | `windows-x64` | Native Windows GPU/fonts support a matching persona; seeded hardware fidelity still requires measurement |
|
|
708
|
+
| macOS arm64 (Apple Silicon) | `macos-arm64` | Native Apple graphics support a matching persona; cross-OS identities need separate checks |
|
|
709
|
+
| Linux x86_64 | `linux-x64` | Defaults to a Windows persona; select a matching Linux persona and verify the deployment's graphics stack |
|
|
710
|
+
| Linux arm64 | `linux-arm64` | Declared in the manifest; not yet published or covered by the three-host audit |
|
|
659
711
|
|
|
660
712
|
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.
|
|
713
|
+
published and throw a clear error. The **1.2.1** manifest selects **Chromium 155.0.8059.39, revision 2** on the three
|
|
714
|
+
supported hosts. Browser downloads are available with a valid license. Linux arm64 remains unpublished.
|
|
662
715
|
|
|
663
716
|
---
|
|
664
717
|
|
|
718
|
+
The launch APIs wait for the packaged browser's licensing decision before
|
|
719
|
+
returning a handle. Invalid keys raise a typed error during launch.
|
|
720
|
+
|
|
665
721
|
## CLI & environment
|
|
666
722
|
|
|
667
723
|
```bash
|
|
@@ -695,12 +751,13 @@ license key; it prints a fix for anything missing and exits non-zero if the envi
|
|
|
695
751
|
| `no license configured` | Set `CAPIUM_LICENSE_KEY`, pass `licenseKey: '…'`, or create `~/.capium/license` (`KEY=cap_…`). |
|
|
696
752
|
| `playwright-core is not installed` | `npm install playwright-core` (or `puppeteer-core`) — the driver is a peer dependency you choose. |
|
|
697
753
|
| `CapiumSeatLimitError` | Free tier is 1 concurrent session — close the other Capium browser (or upgrade the tier). |
|
|
698
|
-
| Still blocked on aggressive sites |
|
|
754
|
+
| 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
755
|
| 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` |
|
|
756
|
+
| 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
757
|
| "Windows persona but missing fonts" | Supply real `C:\Windows\Fonts\*.ttf` (see [Fonts](#fonts)) or set `CAPIUM_SUPPRESS_FONT_WARNING=1`. |
|
|
702
758
|
| `os_mismatch: true` | A network/transport signal (genuine Chrome shows it too behind some proxies) — a routing concern, not the browser. |
|
|
703
759
|
| Headed browser won't start on a server | Run under `Xvfb` and export `DISPLAY=:99`. |
|
|
760
|
+
| 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
761
|
|
|
705
762
|
More: [docs.capiumbrowser.com](https://docs.capiumbrowser.com).
|
|
706
763
|
|
package/index.d.ts
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
* const browser = (await launch({ seed: 42 })) as unknown as Browser;
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/** Mobile profiles emulate identity/layout on the supported desktop hosts. */
|
|
13
|
+
export type Platform = 'windows' | 'macos' | 'linux' | 'android' | 'ios';
|
|
14
|
+
export type AndroidDevice = 'pixel-7' | 'pixel-8' | 'pixel-8-pro' | 'pixel-8a' | 'pixel-9' |
|
|
15
|
+
'samsung-galaxy-a55' | 'pixel-9-pro' | 'pixel-9-pro-xl';
|
|
16
|
+
export type IOSDevice = 'iphone-16';
|
|
13
17
|
|
|
14
18
|
export interface ProxySpec {
|
|
15
19
|
/** "scheme://host:port" (http, https, socks5). */
|
|
@@ -21,10 +25,14 @@ export interface ProxySpec {
|
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export interface LaunchOptions {
|
|
28
|
+
/** Desktop identity profile; vendor features are not emulated. */
|
|
29
|
+
browserBrand?: 'chrome' | 'edge' | 'opera' | 'vivaldi' | null;
|
|
24
30
|
/** Identity seed (a stable, coherent device per seed). Random if omitted. */
|
|
25
31
|
seed?: number | null;
|
|
26
32
|
/** The spoofed OS persona. Default "windows". */
|
|
27
33
|
platform?: Platform;
|
|
34
|
+
/** Phone catalog ID. Android uses the original seed mapping; iOS defaults to iPhone 16. */
|
|
35
|
+
mobileDevice?: AndroidDevice | IOSDevice | null;
|
|
28
36
|
/** Default false (a windowed browser is the coherent persona default). */
|
|
29
37
|
headless?: boolean;
|
|
30
38
|
/** "http://user:pass@host:port" | "socks5://host:port" | ProxySpec. */
|
|
@@ -102,7 +110,8 @@ export function status(licenseKey?: string | null, licenseServer?: string | null
|
|
|
102
110
|
|
|
103
111
|
export namespace config {
|
|
104
112
|
function getDefaultStealthArgs(seed?: number | null, platform?: Platform,
|
|
105
|
-
screen?: [number, number] | null
|
|
113
|
+
screen?: [number, number] | null, mobileDevice?: AndroidDevice | null,
|
|
114
|
+
browserBrand?: 'chrome' | 'edge' | 'opera' | 'vivaldi' | null): string[];
|
|
106
115
|
function newSeed(): number;
|
|
107
116
|
function findBinary(binary?: string | null): string;
|
|
108
117
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 2,
|
|
3
|
+
"minimum_release_year": 2022,
|
|
4
|
+
"browser": "chrome",
|
|
5
|
+
"status": "experimental-emulation",
|
|
6
|
+
"gpu_policy": "host",
|
|
7
|
+
"hardware_policy": "host",
|
|
8
|
+
"physical_device_validation": "pending",
|
|
9
|
+
"default_profile_ids": [
|
|
10
|
+
"pixel-7",
|
|
11
|
+
"pixel-8",
|
|
12
|
+
"pixel-8-pro",
|
|
13
|
+
"pixel-8a",
|
|
14
|
+
"pixel-9"
|
|
15
|
+
],
|
|
16
|
+
"sources": [
|
|
17
|
+
"https://support.google.com/pixelphone/answer/7158570?hl=en",
|
|
18
|
+
"https://news.samsung.com/global/samsung-galaxy-a55-5g-and-galaxy-a35-5g-awesome-innovations-and-security-engineered-for-everyone",
|
|
19
|
+
"https://www.samsung.com/ie/business/smartphones/galaxy-a/galaxy-a55-5g-awesome-navy-256gb-sm-a556bzkceub/",
|
|
20
|
+
"https://chromium.googlesource.com/devtools/devtools-frontend/+/cd7bfad9bba968ac23ba67e538f9e1d499991d25/front_end/models/emulation/EmulatedDevices.ts"
|
|
21
|
+
],
|
|
22
|
+
"profiles": [
|
|
23
|
+
{
|
|
24
|
+
"id": "pixel-7",
|
|
25
|
+
"model": "Pixel 7",
|
|
26
|
+
"release_year": 2022,
|
|
27
|
+
"platform_version": "13",
|
|
28
|
+
"width": 412,
|
|
29
|
+
"height": 915,
|
|
30
|
+
"dpr": 2.625
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "pixel-8",
|
|
34
|
+
"model": "Pixel 8",
|
|
35
|
+
"release_year": 2023,
|
|
36
|
+
"platform_version": "14",
|
|
37
|
+
"width": 412,
|
|
38
|
+
"height": 915,
|
|
39
|
+
"dpr": 2.625
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "pixel-8-pro",
|
|
43
|
+
"model": "Pixel 8 Pro",
|
|
44
|
+
"release_year": 2023,
|
|
45
|
+
"platform_version": "14",
|
|
46
|
+
"width": 448,
|
|
47
|
+
"height": 997,
|
|
48
|
+
"dpr": 3.0
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "pixel-8a",
|
|
52
|
+
"model": "Pixel 8a",
|
|
53
|
+
"release_year": 2024,
|
|
54
|
+
"platform_version": "14",
|
|
55
|
+
"width": 412,
|
|
56
|
+
"height": 915,
|
|
57
|
+
"dpr": 2.625
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "pixel-9",
|
|
61
|
+
"model": "Pixel 9",
|
|
62
|
+
"release_year": 2024,
|
|
63
|
+
"platform_version": "14",
|
|
64
|
+
"width": 412,
|
|
65
|
+
"height": 924,
|
|
66
|
+
"dpr": 2.625
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"id": "samsung-galaxy-a55",
|
|
70
|
+
"model": "SM-A556B",
|
|
71
|
+
"display_name": "Samsung Galaxy A55",
|
|
72
|
+
"release_year": 2024,
|
|
73
|
+
"platform_version": "14",
|
|
74
|
+
"width": 360,
|
|
75
|
+
"height": 800,
|
|
76
|
+
"dpr": 2.25,
|
|
77
|
+
"geometry_status": "devtools-emulation-only",
|
|
78
|
+
"geometry_note": "These Chromium preset dimensions/DPR do not directly map to the published 1080x2340 panel; physical Chrome capture required."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "pixel-9-pro",
|
|
82
|
+
"model": "Pixel 9 Pro",
|
|
83
|
+
"release_year": 2024,
|
|
84
|
+
"platform_version": "14",
|
|
85
|
+
"width": 427,
|
|
86
|
+
"height": 952,
|
|
87
|
+
"dpr": 3.0
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"id": "pixel-9-pro-xl",
|
|
91
|
+
"model": "Pixel 9 Pro XL",
|
|
92
|
+
"release_year": 2024,
|
|
93
|
+
"platform_version": "14",
|
|
94
|
+
"width": 448,
|
|
95
|
+
"height": 997,
|
|
96
|
+
"dpr": 3.0
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"verified_on": "2026-10-10",
|
|
4
|
+
"engine_version": "155.0.8059.39",
|
|
5
|
+
"scope": "Desktop UA and Client Hints profiles; does not install vendor engines or features.",
|
|
6
|
+
"profiles": [
|
|
7
|
+
{
|
|
8
|
+
"brand": "edge",
|
|
9
|
+
"product_version": "155.0.4283.45",
|
|
10
|
+
"chromium_version": "155.0.8059.40",
|
|
11
|
+
"hint_brand": "Microsoft Edge",
|
|
12
|
+
"ua_suffix": " Edg/",
|
|
13
|
+
"ua_version": "155.0.0.0",
|
|
14
|
+
"sources": [
|
|
15
|
+
"https://edgeupdates.microsoft.com/api/products?view=enterprise"
|
|
16
|
+
],
|
|
17
|
+
"measurement": "Stock Edge 155 Windows and Linux owned-origin captures"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"brand": "opera",
|
|
21
|
+
"product_version": "137.0.6036.39",
|
|
22
|
+
"chromium_version": "153.0.8010.55",
|
|
23
|
+
"hint_brand": "Opera",
|
|
24
|
+
"ua_suffix": " OPR/",
|
|
25
|
+
"ua_version": "137.0.0.0",
|
|
26
|
+
"sources": [
|
|
27
|
+
"https://blogs.opera.com/desktop/changelog-for-137/",
|
|
28
|
+
"https://blogs.opera.com/desktop/2026/10/opera-137-stable/"
|
|
29
|
+
],
|
|
30
|
+
"measurement": "Stock Opera 137 Linux owned-origin capture"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"brand": "vivaldi",
|
|
34
|
+
"product_version": "8.2.4133.84",
|
|
35
|
+
"chromium_version": "152.0.7977.160",
|
|
36
|
+
"hint_brand": "Google Chrome",
|
|
37
|
+
"ua_suffix": "",
|
|
38
|
+
"ua_version": "",
|
|
39
|
+
"sources": [
|
|
40
|
+
"https://vivaldi.com/blog/minor-update-nine-8-2/",
|
|
41
|
+
"https://vivaldi.com/blog/desktop/",
|
|
42
|
+
"https://help.vivaldi.com/developers/web/vivaldi-user-agent-and-client-hints-user-agent/"
|
|
43
|
+
],
|
|
44
|
+
"measurement": "Stock Vivaldi 8.2 Linux owned-origin capture; default branding only, partner-site exceptions excluded"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
package/lib/channels.json
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
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": 2,
|
|
10
|
+
"sha256": "25ef98b8713c2c9fb62615b4c9aa0d7a3bd20c76320ec3d99e848775b1d4fdd6",
|
|
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": 2,
|
|
17
|
+
"sha256": "2c39538576a8c5f63b39ac1f09cdb1cc5f7d2b6899ed7625752c6c4c2737a90f",
|
|
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": 2,
|
|
24
|
+
"sha256": "87889050d6a1e46a8aabe4800f8fff5e474a07cd23e22a66f79b830f5544487c",
|
|
25
|
+
"published": true,
|
|
26
|
+
"license_status_protocol": 1
|
|
26
27
|
},
|
|
27
28
|
"linux-arm64": {
|
|
28
|
-
"version": "
|
|
29
|
-
"revision":
|
|
29
|
+
"version": "155.0.8059.39",
|
|
30
|
+
"revision": 2,
|
|
30
31
|
"sha256": null,
|
|
31
32
|
"published": false
|
|
32
33
|
}
|
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
|
|
|
@@ -20,29 +17,23 @@ const path = require('path');
|
|
|
20
17
|
// Playwright/Puppeteer default args to SUPPRESS (ignoreDefaultArgs): these leak automation
|
|
21
18
|
// or force a software render.
|
|
22
19
|
// --enable-automation : exposes navigator.webdriver=true + an automation banner.
|
|
23
|
-
// --enable-unsafe-swiftshader :
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
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.
|
|
27
25
|
const IGNORE_DEFAULT_ARGS = ['--enable-automation', '--enable-unsafe-swiftshader'];
|
|
28
26
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
// window on a 1080p Windows monitor -- screen 1920x1080, viewport 1920x947 (1040 avail height
|
|
34
|
-
// minus ~93px of tab/omnibox/bookmark chrome). Dominant real-world viewport, so a headless
|
|
35
|
-
// instance blends in, and screen > viewport stays coherent. Applied only in headless and only
|
|
36
|
-
// when the caller pins no screen (an explicit --fingerprint-screen-* / --window-size opts out).
|
|
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.
|
|
37
31
|
const HEADLESS_SCREEN = [1920, 1080];
|
|
38
|
-
const HEADLESS_VIEWPORT = [
|
|
32
|
+
const HEADLESS_VIEWPORT = [800, 600];
|
|
39
33
|
|
|
40
34
|
function getHeadlessDefaultArgs() {
|
|
41
|
-
const [sw, sh] = HEADLESS_SCREEN;
|
|
42
35
|
const [vw, vh] = HEADLESS_VIEWPORT;
|
|
43
36
|
return [
|
|
44
|
-
`--fingerprint-screen-width=${sw}`,
|
|
45
|
-
`--fingerprint-screen-height=${sh}`,
|
|
46
37
|
`--window-size=${vw},${vh}`,
|
|
47
38
|
];
|
|
48
39
|
}
|
|
@@ -57,15 +48,35 @@ function hasScreenOverride(extra) {
|
|
|
57
48
|
* Return the default per-launch fingerprint flags.
|
|
58
49
|
*
|
|
59
50
|
* seed: int identity seed (a stable, coherent device per seed). Random if null.
|
|
60
|
-
* platform: "windows" | "macos" | "linux" -- the
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* correlation + GPU-incoherence tell -- now matches the python SDK's screen=None.)
|
|
51
|
+
* platform: "windows" | "macos" | "linux" | "android" | "ios" -- the OS persona.
|
|
52
|
+
* mobileDevice: optional Android catalog ID; omitted keeps the seed modulo 5 mapping.
|
|
53
|
+
* screen: null uses the binary's seeded screen and DPR. Development 155 defaults
|
|
54
|
+
* vary within 1920x1080. Headless starts with an 800x600 window; its screen
|
|
55
|
+
* remains independent of the viewport. An explicit [w,h] overrides the screen
|
|
56
|
+
* and startup window, including sizes above the default cap.
|
|
67
57
|
*/
|
|
68
|
-
function getDefaultStealthArgs(seed = null, platform = 'windows', screen = null) {
|
|
58
|
+
function getDefaultStealthArgs(seed = null, platform = 'windows', screen = null, mobileDevice = null, browserBrand = null) {
|
|
59
|
+
const desktopArgs = require('./desktop').brandArgs(browserBrand, platform);
|
|
60
|
+
if (mobileDevice !== null && mobileDevice !== undefined && !['android', 'ios'].includes(platform)) {
|
|
61
|
+
throw new Error("mobileDevice requires platform: 'android' or 'ios'");
|
|
62
|
+
}
|
|
63
|
+
if (['android', 'ios'].includes(platform)) {
|
|
64
|
+
const p = require('./mobile').mobileProfile(seed, mobileDevice, platform);
|
|
65
|
+
const [sw, sh] = screen ? screen.map(v => Math.trunc(Number(v))) : [p.width, p.height];
|
|
66
|
+
return [
|
|
67
|
+
`--fingerprint=${seed}`, `--fingerprint-platform=${platform}`,
|
|
68
|
+
...(mobileDevice !== null && mobileDevice !== undefined ? [`--fingerprint-mobile-device=${mobileDevice}`] : []),
|
|
69
|
+
'--fingerprint-allow-3p-cookies', '--disable-features=TrackingProtection3pcd',
|
|
70
|
+
'--blink-settings=primaryHoverType=0,availableHoverTypes=0,' +
|
|
71
|
+
'primaryPointerType=2,availablePointerTypes=2,maxTouchPoints=5,' +
|
|
72
|
+
'viewportEnabled=true,viewportMetaEnabled=true,shrinksViewportContentToFit=true',
|
|
73
|
+
'--touch-events=enabled', '--enable-viewport',
|
|
74
|
+
'--no-first-run', '--no-default-browser-check', '--fingerprint-noise=false',
|
|
75
|
+
`--window-size=${sw},${sh}`,
|
|
76
|
+
...(platform === 'ios' ? [`--user-agent=${p.user_agent}`, '--capium-preserve-user-agent'] : []),
|
|
77
|
+
...(screen ? [`--fingerprint-screen-width=${sw}`, `--fingerprint-screen-height=${sh}`] : []),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
69
80
|
const args = [];
|
|
70
81
|
if (seed !== null && seed !== undefined) {
|
|
71
82
|
const s = Math.trunc(Number(seed));
|
|
@@ -91,17 +102,15 @@ function getDefaultStealthArgs(seed = null, platform = 'windows', screen = null)
|
|
|
91
102
|
'primaryPointerType=4,availablePointerTypes=4',
|
|
92
103
|
'--no-first-run',
|
|
93
104
|
'--no-default-browser-check',
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
105
|
+
// Engine 155 honors the requested execution world by default. Its console
|
|
106
|
+
// event policy is independent of page evaluation. Engine 153 retains the
|
|
107
|
+
// older default isolation behavior; --capium-isolate-cdp-eval explicitly
|
|
108
|
+
// requests that legacy mode on 155. See docs/SDK-GPU-AUDIT.md for measured
|
|
109
|
+
// page-global, object-handle and binding compatibility.
|
|
99
110
|
);
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
// Trade-off: personas on the same host share its real canvas (no per-seed canvas
|
|
104
|
-
// uniqueness) -- pass --fingerprint-noise=true to restore it (accepts tampering).
|
|
111
|
+
// Keep injected canvas/audio/rectangle noise opt-in. Device fonts and the
|
|
112
|
+
// actual renderer can still affect pixels; noise=false does not guarantee
|
|
113
|
+
// identical canvases across personas or a particular detector classification.
|
|
105
114
|
args.push('--fingerprint-noise=false');
|
|
106
115
|
// A Windows persona's font metrics must match real Windows (needs Windows fonts
|
|
107
116
|
// present on the host); harmless elsewhere, so gate it to the windows persona.
|
|
@@ -110,8 +119,8 @@ function getDefaultStealthArgs(seed = null, platform = 'windows', screen = null)
|
|
|
110
119
|
// screen (e.g. 2560x1080) mismatches the actual small browser viewport (e.g.
|
|
111
120
|
// 945x939) -- browserscan flags "screen dimensions don't match viewport" as a
|
|
112
121
|
// virtual machine. Pin the spoofed screen to a window we also size, so
|
|
113
|
-
//
|
|
114
|
-
//
|
|
122
|
+
// --window-size requests the outer window; the inner viewport excludes Chrome's
|
|
123
|
+
// frame/UI and must be measured rather than assumed equal to screen dimensions.
|
|
115
124
|
if (screen) {
|
|
116
125
|
const sw = Math.trunc(Number(screen[0]));
|
|
117
126
|
const sh = Math.trunc(Number(screen[1]));
|
|
@@ -119,7 +128,7 @@ function getDefaultStealthArgs(seed = null, platform = 'windows', screen = null)
|
|
|
119
128
|
args.push(`--fingerprint-screen-width=${sw}`);
|
|
120
129
|
args.push(`--fingerprint-screen-height=${sh}`);
|
|
121
130
|
}
|
|
122
|
-
return args;
|
|
131
|
+
return args.concat(desktopArgs);
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
/** A fresh random identity seed. */
|
package/lib/desktop.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const catalog = require('./browser-brands.json');
|
|
3
|
+
const profiles = new Map(catalog.profiles.map(p => [p.brand, p]));
|
|
4
|
+
|
|
5
|
+
function normalizeBrand(brand) {
|
|
6
|
+
if (brand === null || brand === undefined) return 'chrome';
|
|
7
|
+
if (typeof brand !== 'string' || !['chrome', ...profiles.keys()].includes(brand.toLowerCase())) {
|
|
8
|
+
throw new Error('browserBrand must be chrome, edge, opera, or vivaldi');
|
|
9
|
+
}
|
|
10
|
+
return brand.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function brandArgs(brand, platform) {
|
|
14
|
+
brand = normalizeBrand(brand);
|
|
15
|
+
if (brand === 'chrome') return [];
|
|
16
|
+
if (!['windows', 'macos', 'linux'].includes(platform)) throw new Error('browserBrand requires a desktop platform');
|
|
17
|
+
return [`--fingerprint-brand=${brand}`];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateLaunch(brand, platform, binary, extra, stealthArgs) {
|
|
21
|
+
if (!brandArgs(brand, platform).length) return;
|
|
22
|
+
if (!stealthArgs) throw new Error('browserBrand requires stealthArgs: true');
|
|
23
|
+
const conflicts = ['--user-agent', '--fingerprint-brand', '--fingerprint-brand-version', '--fingerprint-platform'];
|
|
24
|
+
if ((extra || []).some(arg => typeof arg === 'string' && conflicts.some(key => arg === key || arg.startsWith(key + '=')))) {
|
|
25
|
+
throw new Error('browserBrand uses the verified catalog; conflicting identity overrides are not supported');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function engineProbe(expected) {
|
|
30
|
+
const h = await navigator.userAgentData?.getHighEntropyValues(['fullVersionList', 'uaFullVersion']);
|
|
31
|
+
if (!h) return false;
|
|
32
|
+
const chromium = expected.chromium_version;
|
|
33
|
+
const product = expected.hint_brand === 'Google Chrome' ? chromium : expected.product_version;
|
|
34
|
+
const expectedBrands = { Chromium: chromium, [expected.hint_brand]: product };
|
|
35
|
+
const full = Object.fromEntries(h.fullVersionList.map(b => [b.brand, b.version]));
|
|
36
|
+
const low = Object.fromEntries(h.brands.map(b => [b.brand, b.version]));
|
|
37
|
+
return h.uaFullVersion === product && h.mobile === false &&
|
|
38
|
+
Object.entries(expectedBrands).every(([brand, version]) => full[brand] === version && low[brand] === version.split('.')[0]) &&
|
|
39
|
+
['Google Chrome', 'Microsoft Edge', 'Opera', 'Vivaldi', 'Chromium'].every(brand => !(brand in full) || brand in expectedBrands) &&
|
|
40
|
+
navigator.userAgent.includes('Chrome/' + chromium.split('.')[0] + '.0.0.0') &&
|
|
41
|
+
(!expected.ua_suffix || navigator.userAgent.endsWith(expected.ua_suffix + expected.ua_version)) &&
|
|
42
|
+
(expected.brand !== 'vivaldi' || !navigator.userAgent.includes('Vivaldi/'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function profile(brand) { return { ...profiles.get(normalizeBrand(brand)) }; }
|
|
46
|
+
|
|
47
|
+
async function requireEngine(context, brand) {
|
|
48
|
+
if (normalizeBrand(brand) === 'chrome') return;
|
|
49
|
+
const page = await context.newPage();
|
|
50
|
+
try {
|
|
51
|
+
await page.route('**/*', route => route.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html>' }));
|
|
52
|
+
await page.goto('https://capium-desktop-preview.invalid/', { waitUntil: 'domcontentloaded' });
|
|
53
|
+
if (!await page.evaluate(engineProbe, profile(brand))) throw new Error('This binary does not support the verified desktop browser-brand mappings; use Capium 1.2.1 revision 2 or later');
|
|
54
|
+
} finally { await page.close(); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function requirePuppeteerEngine(browser, brand) {
|
|
58
|
+
if (normalizeBrand(brand) === 'chrome') return;
|
|
59
|
+
const page = await browser.newPage();
|
|
60
|
+
try {
|
|
61
|
+
await page.setRequestInterception(true);
|
|
62
|
+
page.on('request', request => request.respond({ status: 200, contentType: 'text/html', body: '<!doctype html>' }));
|
|
63
|
+
await page.goto('https://capium-desktop-preview.invalid/', { waitUntil: 'domcontentloaded' });
|
|
64
|
+
if (!await page.evaluate(engineProbe, profile(brand))) throw new Error('This binary does not support the verified desktop browser-brand mappings; use Capium 1.2.1 revision 2 or later');
|
|
65
|
+
} finally { await page.close(); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { normalizeBrand, brandArgs, validateLaunch, profile, engineProbe, requireEngine, requirePuppeteerEngine };
|
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;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"status": "experimental-emulation",
|
|
4
|
+
"minimum_release_year": 2022,
|
|
5
|
+
"physical_device_validation": "pending",
|
|
6
|
+
"profiles": [
|
|
7
|
+
{
|
|
8
|
+
"id": "iphone-16",
|
|
9
|
+
"model": "iPhone 16",
|
|
10
|
+
"release_year": 2024,
|
|
11
|
+
"os_version": "26.6.1",
|
|
12
|
+
"browser_version": "155.0.8059.37",
|
|
13
|
+
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 26_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/155.0.8059.37 Mobile/15E148 Safari/604.1",
|
|
14
|
+
"width": 393,
|
|
15
|
+
"height": 852,
|
|
16
|
+
"dpr": 3
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
package/lib/launch-common.js
CHANGED
|
@@ -40,13 +40,13 @@ function proxyAndGeoArgs(spec, geoipOn) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function buildArgs({ seed, platform, stealthArgs, timezone, locale, extensionPaths, extra,
|
|
43
|
-
headless = false }) {
|
|
43
|
+
headless = false, mobileDevice = null, browserBrand = null }) {
|
|
44
44
|
let args = [];
|
|
45
45
|
if (stealthArgs) {
|
|
46
|
-
args = args.concat(config.getDefaultStealthArgs(seed, platform));
|
|
47
|
-
//
|
|
48
|
-
// caller
|
|
49
|
-
if (headless && !config.hasScreenOverride(extra)) {
|
|
46
|
+
args = args.concat(config.getDefaultStealthArgs(seed, platform, null, mobileDevice, browserBrand));
|
|
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 && !['android', 'ios'].includes(platform) && !config.hasScreenOverride(extra)) {
|
|
50
50
|
args = args.concat(config.getHeadlessDefaultArgs());
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
// The native writer briefly locks this owned file on Windows. Keep
|
|
33
|
+
// waiting for a real decision; unreadable status is never readiness.
|
|
34
|
+
if (!['ENOENT', 'EBUSY'].includes(error.code) &&
|
|
35
|
+
!(process.platform === 'win32' && ['EACCES', 'EPERM'].includes(error.code))) throw error;
|
|
36
|
+
}
|
|
37
|
+
if (Date.now() >= deadline) {
|
|
38
|
+
throw new CapiumServerDownError('Timed out waiting for the browser license startup decision');
|
|
39
|
+
}
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Drivers may leave startup pending after the native process refuses the license.
|
|
44
|
+
// Observe the native decision while the driver connects, so rejection cannot hang.
|
|
45
|
+
async function withLaunch(pending, binary, statusPath, timeoutMs) {
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
try {
|
|
48
|
+
const [handle] = await Promise.all([
|
|
49
|
+
pending, wait(binary, statusPath, timeoutMs, controller.signal),
|
|
50
|
+
]);
|
|
51
|
+
return handle;
|
|
52
|
+
} finally {
|
|
53
|
+
// A driver can reject before the native decision is written. Do not keep
|
|
54
|
+
// polling a removed status file, or keep Node alive until the deadline.
|
|
55
|
+
controller.abort();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
module.exports = { required, wait, withLaunch };
|
package/lib/mobile.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared with the native engine. These are emulation presets, not physical
|
|
4
|
+
// GPU/font/CPU/memory certifications. Desktop defaults never read this catalog.
|
|
5
|
+
const catalog = require('./android-phones.json');
|
|
6
|
+
const profiles = new Map(catalog.profiles.map(p => [p.id, p]));
|
|
7
|
+
const iosProfiles = new Map(require('./ios-phones.json').profiles.map(p => [p.id, p]));
|
|
8
|
+
|
|
9
|
+
function androidProfile(seed, device = null) {
|
|
10
|
+
if (!Number.isInteger(seed) || seed < 0 || seed > 0xffffffff) {
|
|
11
|
+
throw new RangeError('Android seed must be an integer from 0 to 4294967295');
|
|
12
|
+
}
|
|
13
|
+
if (device === null || device === undefined) {
|
|
14
|
+
device = catalog.default_profile_ids[seed % catalog.default_profile_ids.length];
|
|
15
|
+
}
|
|
16
|
+
if (typeof device !== 'string' || !profiles.has(device)) {
|
|
17
|
+
throw new RangeError('Unknown Android mobileDevice; choose a catalog ID: ' + [...profiles.keys()].join(', '));
|
|
18
|
+
}
|
|
19
|
+
return { ...profiles.get(device) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mobileProfile(seed, device = null, platform = 'android') {
|
|
23
|
+
if (platform === 'android') return androidProfile(seed, device);
|
|
24
|
+
if (platform !== 'ios') throw new Error("Mobile profiles require platform: 'android' or 'ios'");
|
|
25
|
+
if (!Number.isInteger(seed) || seed < 0 || seed > 0xffffffff) {
|
|
26
|
+
throw new RangeError('iPhone seed must be an integer from 0 to 4294967295');
|
|
27
|
+
}
|
|
28
|
+
device = device === null || device === undefined ? 'iphone-16' : device;
|
|
29
|
+
if (typeof device !== 'string' || !iosProfiles.has(device)) {
|
|
30
|
+
throw new RangeError('Unknown iPhone mobileDevice; choose a catalog ID: ' + [...iosProfiles.keys()].join(', '));
|
|
31
|
+
}
|
|
32
|
+
return { ...iosProfiles.get(device) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function contextOptions(seed, options = {}, device = null, platform = 'android') {
|
|
36
|
+
if (options.userAgent !== undefined && options.userAgent !== null) {
|
|
37
|
+
throw new Error((platform === 'ios' ? 'iPhone uses its catalog UA; ' :
|
|
38
|
+
'Android Chrome uses the native engine UA and matching Client Hints; ') +
|
|
39
|
+
'context userAgent overrides are not supported');
|
|
40
|
+
}
|
|
41
|
+
if ('viewport' in options && options.viewport === null) return { ...options };
|
|
42
|
+
const p = mobileProfile(seed, device, platform);
|
|
43
|
+
return {
|
|
44
|
+
viewport: { width: p.width, height: p.height },
|
|
45
|
+
screen: { width: p.width, height: p.height },
|
|
46
|
+
deviceScaleFactor: p.dpr, isMobile: true, hasTouch: true,
|
|
47
|
+
...options,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function puppeteerViewport(seed, device = null, platform = 'android') {
|
|
52
|
+
const p = mobileProfile(seed, device, platform);
|
|
53
|
+
return { width: p.width, height: p.height, deviceScaleFactor: p.dpr,
|
|
54
|
+
isMobile: true, hasTouch: true };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateLaunch(platform, binary, extra, stealthArgs = true, mobileDevice = null) {
|
|
58
|
+
if (typeof platform === 'string' && ['android', 'ios'].includes(platform.toLowerCase()) && platform !== platform.toLowerCase()) {
|
|
59
|
+
throw new Error(`Use platform: '${platform.toLowerCase()}' for the mobile profile`);
|
|
60
|
+
}
|
|
61
|
+
if (!['android', 'ios'].includes(platform)) {
|
|
62
|
+
if (mobileDevice !== null && mobileDevice !== undefined) throw new Error("mobileDevice requires platform: 'android' or 'ios'");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (mobileDevice !== null && mobileDevice !== undefined) mobileProfile(0, mobileDevice, platform);
|
|
66
|
+
if (!stealthArgs) throw new Error('Mobile profiles require stealthArgs: true to enable their native identity');
|
|
67
|
+
const conflicts = ['--user-agent=', '--fingerprint-brand=', '--fingerprint-brand-version=',
|
|
68
|
+
'--fingerprint-platform-version=', '--fingerprint-platform=', '--fingerprint=', '--fingerprint-mobile-device='];
|
|
69
|
+
if ((extra || []).some(arg => typeof arg === 'string' &&
|
|
70
|
+
conflicts.some(key => arg.startsWith(key) || arg === key.slice(0, -1)))) {
|
|
71
|
+
throw new Error('Android preview uses the catalog model/OS and current Chrome; ' +
|
|
72
|
+
'conflicting identity overrides are not supported');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Runs on a fulfilled empty .invalid page for every mobile launch; no network is sent.
|
|
77
|
+
async function engineProbe(expected) {
|
|
78
|
+
if (expected?.user_agent) {
|
|
79
|
+
if (navigator.platform !== 'iPhone' || navigator.userAgent !== expected.user_agent ||
|
|
80
|
+
'userAgentData' in navigator || 'NavigatorUAData' in globalThis || navigator.maxTouchPoints < 1) return false;
|
|
81
|
+
const gl = document.createElement('canvas').getContext('webgl');
|
|
82
|
+
const ext = gl?.getExtension('WEBGL_debug_renderer_info');
|
|
83
|
+
return !!ext && gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) === 'Apple Inc.' &&
|
|
84
|
+
gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) === 'Apple GPU';
|
|
85
|
+
}
|
|
86
|
+
const match = navigator.userAgent.match(/Chrome\/(\d+)\.0\.0\.0 Mobile Safari\//);
|
|
87
|
+
if (!expected || navigator.platform !== 'Linux armv81' || !match ||
|
|
88
|
+
!navigator.userAgent.includes('(Linux; Android 10; K)') ||
|
|
89
|
+
navigator.userAgent.includes('Version/4.0') || !navigator.userAgentData) return false;
|
|
90
|
+
const ch = await navigator.userAgentData.getHighEntropyValues([
|
|
91
|
+
'model', 'platformVersion', 'uaFullVersion', 'fullVersionList', 'formFactors',
|
|
92
|
+
'architecture', 'bitness', 'wow64']);
|
|
93
|
+
const full = ch.uaFullVersion;
|
|
94
|
+
const versionsMatch = typeof full === 'string' && full.split('.')[0] === match[1] &&
|
|
95
|
+
['Chromium', 'Google Chrome'].every(brand =>
|
|
96
|
+
ch.brands?.some(item => item.brand === brand && item.version === match[1]) &&
|
|
97
|
+
ch.fullVersionList?.some(item => item.brand === brand && item.version === full));
|
|
98
|
+
return versionsMatch && ch.platform === 'Android' && ch.mobile === true &&
|
|
99
|
+
ch.model === expected.model && ch.platformVersion === expected.platform_version &&
|
|
100
|
+
ch.architecture === '' && ch.bitness === '' && ch.wow64 === false &&
|
|
101
|
+
ch.formFactors?.length === 1 && ch.formFactors[0] === 'Mobile';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { androidProfile, mobileProfile, contextOptions, puppeteerViewport, validateLaunch, engineProbe };
|
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');
|
|
@@ -75,24 +76,27 @@ async function resolveBinary(binary, licenseKey) {
|
|
|
75
76
|
*/
|
|
76
77
|
async function prepare(opts) {
|
|
77
78
|
const {
|
|
78
|
-
seed = null, platform = 'windows', proxy = null, geoip = null, args = null,
|
|
79
|
+
seed = null, platform = 'windows', mobileDevice = null, browserBrand = null, 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
82
|
licenseThroughProxy = false, licensePreflight = true, headless = false,
|
|
82
83
|
} = opts;
|
|
84
|
+
require('./lib/desktop').validateLaunch(browserBrand, platform, binary, args, stealthArgs);
|
|
85
|
+
require('./lib/mobile').validateLaunch(platform, binary, args, stealthArgs, mobileDevice);
|
|
83
86
|
if (licensePreflight) license.preflight(licenseKey, licenseServer);
|
|
84
87
|
const { key } = license.effective(licenseKey, licenseServer);
|
|
85
88
|
const binPath = await resolveBinary(binary, key);
|
|
86
89
|
const finalSeed = seed === null || seed === undefined ? config.newSeed() : seed;
|
|
87
90
|
const { launchOptions, args: proxyArgs } = proxyAndGeoArgs(proxy, geoip);
|
|
88
91
|
const launchArgs = proxyArgs.concat(buildArgs({
|
|
89
|
-
seed: finalSeed, platform, stealthArgs, timezone, locale, extensionPaths, extra: args,
|
|
92
|
+
seed: finalSeed, platform, mobileDevice, browserBrand, stealthArgs, timezone, locale, extensionPaths, extra: args,
|
|
90
93
|
headless,
|
|
91
94
|
}));
|
|
92
95
|
if (licenseThroughProxy) launchArgs.push('--license-through-proxy');
|
|
93
96
|
const env = license.childEnv(licenseKey, licenseServer);
|
|
94
97
|
const statusPath = newStatusFile(env);
|
|
95
|
-
return { binPath, finalSeed, launchOptions, launchArgs, env, statusPath
|
|
98
|
+
return { binPath, finalSeed, launchOptions, launchArgs, env, statusPath,
|
|
99
|
+
browserBrand, mobileSeed: ['android', 'ios'].includes(platform) && stealthArgs ? finalSeed : null, mobileDevice, mobilePlatform: platform };
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
async function throwTranslated(e, statusPath) {
|
|
@@ -116,12 +120,12 @@ async function throwTranslated(e, statusPath) {
|
|
|
116
120
|
*/
|
|
117
121
|
async function buildLaunchOptions(opts = {}) {
|
|
118
122
|
const {
|
|
119
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
123
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
120
124
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
121
125
|
headless = false, ...rest
|
|
122
126
|
} = opts;
|
|
123
127
|
const prep = await prepare({
|
|
124
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
128
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
125
129
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
126
130
|
});
|
|
127
131
|
return {
|
|
@@ -139,12 +143,13 @@ async function buildLaunchOptions(opts = {}) {
|
|
|
139
143
|
* avoid; this closes the plain-launch() path too). An explicit viewport (including a
|
|
140
144
|
* user-passed null) is always honored.
|
|
141
145
|
*/
|
|
142
|
-
function applyViewportDefaults(browser) {
|
|
146
|
+
function applyViewportDefaults(browser, mobileSeed = null, mobileDevice = null, mobilePlatform = 'android') {
|
|
143
147
|
for (const method of ['newContext', 'newPage']) {
|
|
144
148
|
const orig = browser[method];
|
|
145
149
|
if (typeof orig !== 'function') continue;
|
|
146
150
|
browser[method] = function (options = {}, ...more) {
|
|
147
|
-
if (
|
|
151
|
+
if (mobileSeed !== null) options = require('./lib/mobile').contextOptions(mobileSeed, options, mobileDevice, mobilePlatform);
|
|
152
|
+
else if (!('viewport' in options)) options = { ...options, viewport: null };
|
|
148
153
|
return orig.call(this, options, ...more);
|
|
149
154
|
};
|
|
150
155
|
}
|
|
@@ -170,28 +175,59 @@ function applyViewportDefaults(browser) {
|
|
|
170
175
|
*/
|
|
171
176
|
async function launch(opts = {}) {
|
|
172
177
|
const {
|
|
173
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
178
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
174
179
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
175
180
|
headless = false, ...rest
|
|
176
181
|
} = opts;
|
|
177
182
|
const prep = await prepare({
|
|
178
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
183
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
179
184
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
180
185
|
});
|
|
181
186
|
const pw = requireDriver();
|
|
182
187
|
let browser;
|
|
183
188
|
try {
|
|
184
|
-
|
|
189
|
+
const pending = pw.chromium.launch({
|
|
185
190
|
executablePath: prep.binPath, headless, args: prep.launchArgs, env: prep.env,
|
|
186
191
|
ignoreDefaultArgs: config.IGNORE_DEFAULT_ARGS, // strip automation + forced-software WebGL defaults
|
|
187
192
|
...prep.launchOptions, ...rest,
|
|
188
|
-
});
|
|
193
|
+
}).then(handle => { browser = handle; return handle; });
|
|
194
|
+
await readiness.withLaunch(pending, prep.binPath, prep.statusPath, rest.timeout);
|
|
189
195
|
} catch (e) {
|
|
196
|
+
// A native refusal terminates the process; closing its dead CDP connection can hang.
|
|
197
|
+
if (!readLaunchStatus(prep.statusPath) && browser) {
|
|
198
|
+
try { await browser.close(); } catch {}
|
|
199
|
+
}
|
|
190
200
|
await throwTranslated(e, prep.statusPath);
|
|
191
201
|
}
|
|
192
202
|
clearStatusFile(prep.statusPath);
|
|
193
203
|
browser._capiumSeed = prep.finalSeed;
|
|
194
|
-
|
|
204
|
+
browser._capiumMobile = prep.mobileSeed !== null;
|
|
205
|
+
applyViewportDefaults(browser, prep.mobileSeed, prep.mobileDevice, prep.mobilePlatform);
|
|
206
|
+
if (browser._capiumMobile) {
|
|
207
|
+
try {
|
|
208
|
+
const context = await browser.newContext();
|
|
209
|
+
try { await requireMobileEngine(context, prep.mobileDevice, prep.finalSeed, prep.mobilePlatform); } finally { await context.close(); }
|
|
210
|
+
} catch (e) { await browser.close(); throw e; }
|
|
211
|
+
}
|
|
212
|
+
if (require('./lib/desktop').normalizeBrand(prep.browserBrand) !== 'chrome') {
|
|
213
|
+
try {
|
|
214
|
+
const context = await browser.newContext();
|
|
215
|
+
try { await require('./lib/desktop').requireEngine(context, prep.browserBrand); } finally { await context.close(); }
|
|
216
|
+
} catch (e) { await browser.close(); throw e; }
|
|
217
|
+
}
|
|
218
|
+
return browser;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function requireMobileEngine(context, mobileDevice = null, seed = 0, platform = 'android') {
|
|
222
|
+
const page = await context.newPage();
|
|
223
|
+
try {
|
|
224
|
+
const mobile = require('./lib/mobile');
|
|
225
|
+
const expected = mobile.mobileProfile(seed, mobileDevice, platform);
|
|
226
|
+
await page.route('**/*', route => route.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html>' }));
|
|
227
|
+
await page.goto('https://capium-mobile-preview.invalid/', { waitUntil: 'domcontentloaded' });
|
|
228
|
+
const supported = await page.evaluate(mobile.engineProbe, expected);
|
|
229
|
+
if (!supported) throw new Error('This binary does not support the requested mobile profile/device; use Capium 1.2.1 revision 2 or later');
|
|
230
|
+
} finally { await page.close(); }
|
|
195
231
|
}
|
|
196
232
|
|
|
197
233
|
/**
|
|
@@ -201,30 +237,41 @@ async function launch(opts = {}) {
|
|
|
201
237
|
*/
|
|
202
238
|
async function launchPersistentContext(userDataDir, opts = {}) {
|
|
203
239
|
const {
|
|
204
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
240
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
205
241
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
206
242
|
headless = false, ...rest
|
|
207
243
|
} = opts;
|
|
208
244
|
const prep = await prepare({
|
|
209
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
245
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
210
246
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
211
247
|
});
|
|
212
248
|
// Default to the page filling the window (see launchContext) unless the caller pinned a
|
|
213
249
|
// viewport themselves. viewport: null is Playwright-JS for "no fixed viewport".
|
|
214
|
-
if (
|
|
250
|
+
if (prep.mobileSeed !== null) Object.assign(rest, require('./lib/mobile').contextOptions(prep.mobileSeed, rest, prep.mobileDevice, prep.mobilePlatform));
|
|
251
|
+
else if (!('viewport' in rest)) rest.viewport = null;
|
|
215
252
|
const pw = requireDriver();
|
|
216
253
|
let ctx;
|
|
217
254
|
try {
|
|
218
|
-
|
|
255
|
+
const pending = pw.chromium.launchPersistentContext(userDataDir, {
|
|
219
256
|
executablePath: prep.binPath, headless, args: prep.launchArgs, env: prep.env,
|
|
220
257
|
ignoreDefaultArgs: config.IGNORE_DEFAULT_ARGS, // strip automation + forced-software WebGL defaults
|
|
221
258
|
...prep.launchOptions, ...rest,
|
|
222
|
-
});
|
|
259
|
+
}).then(handle => { ctx = handle; return handle; });
|
|
260
|
+
await readiness.withLaunch(pending, prep.binPath, prep.statusPath, rest.timeout);
|
|
223
261
|
} catch (e) {
|
|
262
|
+
// A native refusal terminates the process; closing its dead CDP connection can hang.
|
|
263
|
+
if (!readLaunchStatus(prep.statusPath) && ctx) {
|
|
264
|
+
try { await ctx.close(); } catch {}
|
|
265
|
+
}
|
|
224
266
|
await throwTranslated(e, prep.statusPath);
|
|
225
267
|
}
|
|
226
268
|
clearStatusFile(prep.statusPath);
|
|
227
269
|
ctx._capiumSeed = prep.finalSeed;
|
|
270
|
+
if (prep.mobileSeed !== null) {
|
|
271
|
+
try { await requireMobileEngine(ctx, prep.mobileDevice, prep.finalSeed, prep.mobilePlatform); } catch (e) { await ctx.close(); throw e; }
|
|
272
|
+
}
|
|
273
|
+
try { await require('./lib/desktop').requireEngine(ctx, prep.browserBrand); }
|
|
274
|
+
catch (e) { await ctx.close(); throw e; }
|
|
228
275
|
return ctx;
|
|
229
276
|
}
|
|
230
277
|
|
|
@@ -272,10 +319,10 @@ async function launchContext(opts = {}) {
|
|
|
272
319
|
// viewport: null -- let the page fill the real window (fitWindow sizes the window to the
|
|
273
320
|
// persona's screen). A fixed 1280x720 emulated viewport would leave the page smaller than
|
|
274
321
|
// the window AND mismatch window.screen -- an incoherent fingerprint.
|
|
275
|
-
const context = browser.contexts()[0] || (await browser.newContext({ viewport: null }));
|
|
322
|
+
const context = browser.contexts()[0] || (await browser.newContext(browser._capiumMobile ? {} : { viewport: null }));
|
|
276
323
|
if (humanize) human.humanize(context, humanPreset);
|
|
277
324
|
const page = context.pages()[0] || (await context.newPage());
|
|
278
|
-
if (!headless) await fitWindow(browser, context, page); // modest window <= spoofed screen
|
|
325
|
+
if (!headless && !browser._capiumMobile) await fitWindow(browser, context, page); // modest window <= spoofed screen
|
|
279
326
|
if (url) await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
280
327
|
return { browser, context, page };
|
|
281
328
|
}
|
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');
|
|
@@ -75,24 +76,27 @@ async function resolveBinary(binary, licenseKey) {
|
|
|
75
76
|
|
|
76
77
|
async function prepare(opts) {
|
|
77
78
|
const {
|
|
78
|
-
seed = null, platform = 'windows', proxy = null, geoip = null, args = null,
|
|
79
|
+
seed = null, platform = 'windows', mobileDevice = null, browserBrand = null, 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
82
|
licenseThroughProxy = false, licensePreflight = true, headless = false,
|
|
82
83
|
} = opts;
|
|
84
|
+
require('./lib/desktop').validateLaunch(browserBrand, platform, binary, args, stealthArgs);
|
|
85
|
+
require('./lib/mobile').validateLaunch(platform, binary, args, stealthArgs, mobileDevice);
|
|
83
86
|
if (licensePreflight) license.preflight(licenseKey, licenseServer);
|
|
84
87
|
const { key } = license.effective(licenseKey, licenseServer);
|
|
85
88
|
const binPath = await resolveBinary(binary, key);
|
|
86
89
|
const finalSeed = seed === null || seed === undefined ? config.newSeed() : seed;
|
|
87
90
|
const { launchOptions, args: proxyArgs } = proxyAndGeoArgs(proxy, geoip);
|
|
88
91
|
const launchArgs = proxyArgs.concat(buildArgs({
|
|
89
|
-
seed: finalSeed, platform, stealthArgs, timezone, locale, extensionPaths, extra: args,
|
|
92
|
+
seed: finalSeed, platform, mobileDevice, browserBrand, stealthArgs, timezone, locale, extensionPaths, extra: args,
|
|
90
93
|
headless,
|
|
91
94
|
}));
|
|
92
95
|
if (licenseThroughProxy) launchArgs.push('--license-through-proxy');
|
|
93
96
|
const env = license.childEnv(licenseKey, licenseServer);
|
|
94
97
|
const statusPath = newStatusFile(env);
|
|
95
|
-
return { binPath, finalSeed, launchOptions, launchArgs, env, statusPath
|
|
98
|
+
return { binPath, finalSeed, launchOptions, launchArgs, env, statusPath,
|
|
99
|
+
browserBrand, mobileSeed: ['android', 'ios'].includes(platform) && stealthArgs ? finalSeed : null, mobileDevice, mobilePlatform: platform };
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
/**
|
|
@@ -130,12 +134,12 @@ async function throwTranslated(e, statusPath) {
|
|
|
130
134
|
*/
|
|
131
135
|
async function buildLaunchOptions(opts = {}) {
|
|
132
136
|
const {
|
|
133
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
137
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
134
138
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
135
139
|
headless = false, ignoreDefaultArgs, ...rest
|
|
136
140
|
} = opts;
|
|
137
141
|
const prep = await prepare({
|
|
138
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
142
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
139
143
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
140
144
|
});
|
|
141
145
|
const proxyOption = prep.launchOptions.proxy || null;
|
|
@@ -149,7 +153,8 @@ async function buildLaunchOptions(opts = {}) {
|
|
|
149
153
|
headless,
|
|
150
154
|
args: launchArgs,
|
|
151
155
|
env: prep.env,
|
|
152
|
-
defaultViewport: 'defaultViewport' in rest ? rest.defaultViewport :
|
|
156
|
+
defaultViewport: 'defaultViewport' in rest ? rest.defaultViewport :
|
|
157
|
+
(prep.mobileSeed !== null ? require('./lib/mobile').puppeteerViewport(prep.mobileSeed, prep.mobileDevice, prep.mobilePlatform) : null),
|
|
153
158
|
ignoreDefaultArgs: ignoreDefaultArgs === undefined ? config.IGNORE_DEFAULT_ARGS : ignoreDefaultArgs,
|
|
154
159
|
...rest,
|
|
155
160
|
};
|
|
@@ -162,12 +167,12 @@ async function buildLaunchOptions(opts = {}) {
|
|
|
162
167
|
*/
|
|
163
168
|
async function launch(opts = {}) {
|
|
164
169
|
const {
|
|
165
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
170
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
166
171
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
167
172
|
headless = false, ignoreDefaultArgs, ...rest
|
|
168
173
|
} = opts;
|
|
169
174
|
const prep = await prepare({
|
|
170
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
175
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
171
176
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
172
177
|
});
|
|
173
178
|
const puppeteer = requireDriver();
|
|
@@ -181,23 +186,57 @@ async function launch(opts = {}) {
|
|
|
181
186
|
}
|
|
182
187
|
// Strip automation + forced-software WebGL defaults unless the caller took over ignoreDefaultArgs.
|
|
183
188
|
const ida = ignoreDefaultArgs === undefined ? config.IGNORE_DEFAULT_ARGS : ignoreDefaultArgs;
|
|
189
|
+
const nativeGate = readiness.required(prep.binPath);
|
|
190
|
+
const waitForInitialPage = rest.waitForInitialPage !== false;
|
|
184
191
|
let browser;
|
|
185
192
|
try {
|
|
186
|
-
|
|
193
|
+
const pending = puppeteer.launch({
|
|
187
194
|
executablePath: prep.binPath,
|
|
188
195
|
headless,
|
|
189
196
|
args: launchArgs,
|
|
190
197
|
env: prep.env,
|
|
191
|
-
defaultViewport: 'defaultViewport' in rest ? rest.defaultViewport :
|
|
198
|
+
defaultViewport: 'defaultViewport' in rest ? rest.defaultViewport :
|
|
199
|
+
(prep.mobileSeed !== null ? require('./lib/mobile').puppeteerViewport(prep.mobileSeed, prep.mobileDevice, prep.mobilePlatform) : null),
|
|
192
200
|
ignoreDefaultArgs: ida,
|
|
193
201
|
...rest,
|
|
194
|
-
|
|
202
|
+
// A rejected native gate never creates a page. Obtain the transport first,
|
|
203
|
+
// so Puppeteer does not leave its initial-page timer running after refusal.
|
|
204
|
+
waitForInitialPage: nativeGate ? false : waitForInitialPage,
|
|
205
|
+
}).then(handle => { browser = handle; return handle; });
|
|
206
|
+
await readiness.withLaunch(pending, prep.binPath, prep.statusPath, rest.timeout);
|
|
207
|
+
if (nativeGate && waitForInitialPage) {
|
|
208
|
+
await browser.waitForTarget(target => target.type() === 'page', {
|
|
209
|
+
timeout: rest.timeout === undefined ? 30000 : rest.timeout,
|
|
210
|
+
signal: rest.signal,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
195
213
|
} catch (e) {
|
|
214
|
+
// A native refusal terminates the process; closing its dead CDP connection can hang.
|
|
215
|
+
if (!readLaunchStatus(prep.statusPath) && browser) {
|
|
216
|
+
try { await browser.close(); } catch {}
|
|
217
|
+
}
|
|
196
218
|
await throwTranslated(e, prep.statusPath);
|
|
197
219
|
}
|
|
198
220
|
clearStatusFile(prep.statusPath);
|
|
199
221
|
wireProxyAuth(browser, proxyOption);
|
|
200
222
|
browser._capiumSeed = prep.finalSeed;
|
|
223
|
+
browser._capiumMobile = prep.mobileSeed !== null;
|
|
224
|
+
if (browser._capiumMobile) {
|
|
225
|
+
try {
|
|
226
|
+
const page = await browser.newPage();
|
|
227
|
+
try {
|
|
228
|
+
const mobile = require('./lib/mobile');
|
|
229
|
+
const expected = mobile.mobileProfile(prep.finalSeed, prep.mobileDevice, prep.mobilePlatform);
|
|
230
|
+
await page.setRequestInterception(true);
|
|
231
|
+
page.on('request', request => request.respond({ status: 200, contentType: 'text/html', body: '<!doctype html>' }));
|
|
232
|
+
await page.goto('https://capium-mobile-preview.invalid/', { waitUntil: 'domcontentloaded' });
|
|
233
|
+
const supported = await page.evaluate(mobile.engineProbe, expected);
|
|
234
|
+
if (!supported) throw new Error('This binary does not support the requested mobile profile/device; use Capium 1.2.1 revision 2 or later');
|
|
235
|
+
} finally { await page.close(); }
|
|
236
|
+
} catch (e) { await browser.close(); throw e; }
|
|
237
|
+
}
|
|
238
|
+
try { await require('./lib/desktop').requirePuppeteerEngine(browser, prep.browserBrand); }
|
|
239
|
+
catch (e) { await browser.close(); throw e; }
|
|
201
240
|
return browser;
|
|
202
241
|
}
|
|
203
242
|
|
|
@@ -248,7 +287,7 @@ async function launchContext(opts = {}) {
|
|
|
248
287
|
const pages = await browser.pages();
|
|
249
288
|
const page = pages[0] || (await browser.newPage());
|
|
250
289
|
if (humanize) human.humanize(page, humanPreset);
|
|
251
|
-
if (!headless) await fitWindow(browser, page);
|
|
290
|
+
if (!headless && !browser._capiumMobile) await fitWindow(browser, page);
|
|
252
291
|
if (url) await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
253
292
|
return { browser, page };
|
|
254
293
|
}
|