capiumbrowser 1.2.0 → 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 +44 -5
- package/index.d.ts +11 -2
- package/lib/android-phones.json +99 -0
- package/lib/browser-brands.json +47 -0
- package/lib/channels.json +7 -7
- package/lib/config.js +25 -3
- package/lib/desktop.js +68 -0
- package/lib/ios-phones.json +19 -0
- package/lib/launch-common.js +3 -3
- package/lib/license-readiness.js +4 -1
- package/lib/mobile.js +104 -0
- package/package.json +1 -1
- package/playwright.js +51 -15
- package/puppeteer.js +32 -10
package/README.md
CHANGED
|
@@ -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)
|
|
@@ -340,7 +341,9 @@ Python SDK, in camelCase.
|
|
|
340
341
|
| Option | Type | Default | Description |
|
|
341
342
|
|---|---|---|---|
|
|
342
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)). |
|
|
343
|
-
| `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`. |
|
|
344
347
|
| `headless` | `boolean` | `false` | Headed is recommended (reads cleaner). On a server, run headed under Xvfb. |
|
|
345
348
|
| `stealthArgs` | `boolean` | `true` | Apply the default coherent fingerprint flags. Set `false` to build the whole command line yourself via `args`. |
|
|
346
349
|
| `args` | `string[]` | — | Extra raw Chrome flags appended verbatim (see [Fingerprint switches](#fingerprint-switches)). |
|
|
@@ -442,6 +445,43 @@ Full runnable version (personas, persistent profiles, a fleet, proxy+geo, full c
|
|
|
442
445
|
|
|
443
446
|
---
|
|
444
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
|
+
|
|
445
485
|
## Fingerprint switches
|
|
446
486
|
|
|
447
487
|
The SDK sets the essentials from `seed`/`platform`. Everything below is available for precise control —
|
|
@@ -670,13 +710,12 @@ launches **headed**, which reads cleaner than headless.
|
|
|
670
710
|
| Linux arm64 | `linux-arm64` | Declared in the manifest; not yet published or covered by the three-host audit |
|
|
671
711
|
|
|
672
712
|
The SDK auto-detects your host and downloads the matching tag; macOS Intel and Windows ARM are **not**
|
|
673
|
-
published and throw a clear error. The **1.2.
|
|
674
|
-
supported hosts.
|
|
675
|
-
artifacts are published. Linux arm64 remains unpublished.
|
|
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.
|
|
676
715
|
|
|
677
716
|
---
|
|
678
717
|
|
|
679
|
-
The
|
|
718
|
+
The launch APIs wait for the packaged browser's licensing decision before
|
|
680
719
|
returning a handle. Invalid keys raise a typed error during launch.
|
|
681
720
|
|
|
682
721
|
## CLI & environment
|
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
|
@@ -6,28 +6,28 @@
|
|
|
6
6
|
"stable": {
|
|
7
7
|
"windows-x64": {
|
|
8
8
|
"version": "155.0.8059.39",
|
|
9
|
-
"revision":
|
|
10
|
-
"sha256": "
|
|
9
|
+
"revision": 2,
|
|
10
|
+
"sha256": "25ef98b8713c2c9fb62615b4c9aa0d7a3bd20c76320ec3d99e848775b1d4fdd6",
|
|
11
11
|
"published": true,
|
|
12
12
|
"license_status_protocol": 1
|
|
13
13
|
},
|
|
14
14
|
"macos-arm64": {
|
|
15
15
|
"version": "155.0.8059.39",
|
|
16
|
-
"revision":
|
|
17
|
-
"sha256": "
|
|
16
|
+
"revision": 2,
|
|
17
|
+
"sha256": "2c39538576a8c5f63b39ac1f09cdb1cc5f7d2b6899ed7625752c6c4c2737a90f",
|
|
18
18
|
"published": true,
|
|
19
19
|
"license_status_protocol": 1
|
|
20
20
|
},
|
|
21
21
|
"linux-x64": {
|
|
22
22
|
"version": "155.0.8059.39",
|
|
23
|
-
"revision":
|
|
24
|
-
"sha256": "
|
|
23
|
+
"revision": 2,
|
|
24
|
+
"sha256": "87889050d6a1e46a8aabe4800f8fff5e474a07cd23e22a66f79b830f5544487c",
|
|
25
25
|
"published": true,
|
|
26
26
|
"license_status_protocol": 1
|
|
27
27
|
},
|
|
28
28
|
"linux-arm64": {
|
|
29
29
|
"version": "155.0.8059.39",
|
|
30
|
-
"revision":
|
|
30
|
+
"revision": 2,
|
|
31
31
|
"sha256": null,
|
|
32
32
|
"published": false
|
|
33
33
|
}
|
package/lib/config.js
CHANGED
|
@@ -48,13 +48,35 @@ function hasScreenOverride(extra) {
|
|
|
48
48
|
* Return the default per-launch fingerprint flags.
|
|
49
49
|
*
|
|
50
50
|
* seed: int identity seed (a stable, coherent device per seed). Random if null.
|
|
51
|
-
* platform: "windows" | "macos" | "linux" -- the
|
|
51
|
+
* platform: "windows" | "macos" | "linux" | "android" | "ios" -- the OS persona.
|
|
52
|
+
* mobileDevice: optional Android catalog ID; omitted keeps the seed modulo 5 mapping.
|
|
52
53
|
* screen: null uses the binary's seeded screen and DPR. Development 155 defaults
|
|
53
54
|
* vary within 1920x1080. Headless starts with an 800x600 window; its screen
|
|
54
55
|
* remains independent of the viewport. An explicit [w,h] overrides the screen
|
|
55
56
|
* and startup window, including sizes above the default cap.
|
|
56
57
|
*/
|
|
57
|
-
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
|
+
}
|
|
58
80
|
const args = [];
|
|
59
81
|
if (seed !== null && seed !== undefined) {
|
|
60
82
|
const s = Math.trunc(Number(seed));
|
|
@@ -106,7 +128,7 @@ function getDefaultStealthArgs(seed = null, platform = 'windows', screen = null)
|
|
|
106
128
|
args.push(`--fingerprint-screen-width=${sw}`);
|
|
107
129
|
args.push(`--fingerprint-screen-height=${sh}`);
|
|
108
130
|
}
|
|
109
|
-
return args;
|
|
131
|
+
return args.concat(desktopArgs);
|
|
110
132
|
}
|
|
111
133
|
|
|
112
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 };
|
|
@@ -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));
|
|
46
|
+
args = args.concat(config.getDefaultStealthArgs(seed, platform, null, mobileDevice, browserBrand));
|
|
47
47
|
// Keep the engine's seeded screen. Use initial window bounds only when
|
|
48
48
|
// the caller has not provided screen/window flags.
|
|
49
|
-
if (headless && !config.hasScreenOverride(extra)) {
|
|
49
|
+
if (headless && !['android', 'ios'].includes(platform) && !config.hasScreenOverride(extra)) {
|
|
50
50
|
args = args.concat(config.getHeadlessDefaultArgs());
|
|
51
51
|
}
|
|
52
52
|
}
|
package/lib/license-readiness.js
CHANGED
|
@@ -29,7 +29,10 @@ async function wait(binary, statusPath, timeoutMs = 30000, signal = null) {
|
|
|
29
29
|
try {
|
|
30
30
|
if (fs.readFileSync(statusPath, 'utf8').trim() === '0\nCAPIUM_LICENSE_READY') return;
|
|
31
31
|
} catch (error) {
|
|
32
|
-
|
|
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;
|
|
33
36
|
}
|
|
34
37
|
if (Date.now() >= deadline) {
|
|
35
38
|
throw new CapiumServerDownError('Timed out waiting for the browser license startup decision');
|
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/package.json
CHANGED
package/playwright.js
CHANGED
|
@@ -76,24 +76,27 @@ async function resolveBinary(binary, licenseKey) {
|
|
|
76
76
|
*/
|
|
77
77
|
async function prepare(opts) {
|
|
78
78
|
const {
|
|
79
|
-
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,
|
|
80
80
|
stealthArgs = true, timezone = null, locale = null, extensionPaths = null,
|
|
81
81
|
binary = null, licenseKey = null, licenseServer = null,
|
|
82
82
|
licenseThroughProxy = false, licensePreflight = true, headless = false,
|
|
83
83
|
} = opts;
|
|
84
|
+
require('./lib/desktop').validateLaunch(browserBrand, platform, binary, args, stealthArgs);
|
|
85
|
+
require('./lib/mobile').validateLaunch(platform, binary, args, stealthArgs, mobileDevice);
|
|
84
86
|
if (licensePreflight) license.preflight(licenseKey, licenseServer);
|
|
85
87
|
const { key } = license.effective(licenseKey, licenseServer);
|
|
86
88
|
const binPath = await resolveBinary(binary, key);
|
|
87
89
|
const finalSeed = seed === null || seed === undefined ? config.newSeed() : seed;
|
|
88
90
|
const { launchOptions, args: proxyArgs } = proxyAndGeoArgs(proxy, geoip);
|
|
89
91
|
const launchArgs = proxyArgs.concat(buildArgs({
|
|
90
|
-
seed: finalSeed, platform, stealthArgs, timezone, locale, extensionPaths, extra: args,
|
|
92
|
+
seed: finalSeed, platform, mobileDevice, browserBrand, stealthArgs, timezone, locale, extensionPaths, extra: args,
|
|
91
93
|
headless,
|
|
92
94
|
}));
|
|
93
95
|
if (licenseThroughProxy) launchArgs.push('--license-through-proxy');
|
|
94
96
|
const env = license.childEnv(licenseKey, licenseServer);
|
|
95
97
|
const statusPath = newStatusFile(env);
|
|
96
|
-
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 };
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
async function throwTranslated(e, statusPath) {
|
|
@@ -117,12 +120,12 @@ async function throwTranslated(e, statusPath) {
|
|
|
117
120
|
*/
|
|
118
121
|
async function buildLaunchOptions(opts = {}) {
|
|
119
122
|
const {
|
|
120
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
123
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
121
124
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
122
125
|
headless = false, ...rest
|
|
123
126
|
} = opts;
|
|
124
127
|
const prep = await prepare({
|
|
125
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
128
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
126
129
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
127
130
|
});
|
|
128
131
|
return {
|
|
@@ -140,12 +143,13 @@ async function buildLaunchOptions(opts = {}) {
|
|
|
140
143
|
* avoid; this closes the plain-launch() path too). An explicit viewport (including a
|
|
141
144
|
* user-passed null) is always honored.
|
|
142
145
|
*/
|
|
143
|
-
function applyViewportDefaults(browser) {
|
|
146
|
+
function applyViewportDefaults(browser, mobileSeed = null, mobileDevice = null, mobilePlatform = 'android') {
|
|
144
147
|
for (const method of ['newContext', 'newPage']) {
|
|
145
148
|
const orig = browser[method];
|
|
146
149
|
if (typeof orig !== 'function') continue;
|
|
147
150
|
browser[method] = function (options = {}, ...more) {
|
|
148
|
-
if (
|
|
151
|
+
if (mobileSeed !== null) options = require('./lib/mobile').contextOptions(mobileSeed, options, mobileDevice, mobilePlatform);
|
|
152
|
+
else if (!('viewport' in options)) options = { ...options, viewport: null };
|
|
149
153
|
return orig.call(this, options, ...more);
|
|
150
154
|
};
|
|
151
155
|
}
|
|
@@ -171,12 +175,12 @@ function applyViewportDefaults(browser) {
|
|
|
171
175
|
*/
|
|
172
176
|
async function launch(opts = {}) {
|
|
173
177
|
const {
|
|
174
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
178
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
175
179
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
176
180
|
headless = false, ...rest
|
|
177
181
|
} = opts;
|
|
178
182
|
const prep = await prepare({
|
|
179
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
183
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
180
184
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
181
185
|
});
|
|
182
186
|
const pw = requireDriver();
|
|
@@ -197,7 +201,33 @@ async function launch(opts = {}) {
|
|
|
197
201
|
}
|
|
198
202
|
clearStatusFile(prep.statusPath);
|
|
199
203
|
browser._capiumSeed = prep.finalSeed;
|
|
200
|
-
|
|
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(); }
|
|
201
231
|
}
|
|
202
232
|
|
|
203
233
|
/**
|
|
@@ -207,17 +237,18 @@ async function launch(opts = {}) {
|
|
|
207
237
|
*/
|
|
208
238
|
async function launchPersistentContext(userDataDir, opts = {}) {
|
|
209
239
|
const {
|
|
210
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
240
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
211
241
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
212
242
|
headless = false, ...rest
|
|
213
243
|
} = opts;
|
|
214
244
|
const prep = await prepare({
|
|
215
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
245
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
216
246
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
217
247
|
});
|
|
218
248
|
// Default to the page filling the window (see launchContext) unless the caller pinned a
|
|
219
249
|
// viewport themselves. viewport: null is Playwright-JS for "no fixed viewport".
|
|
220
|
-
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;
|
|
221
252
|
const pw = requireDriver();
|
|
222
253
|
let ctx;
|
|
223
254
|
try {
|
|
@@ -236,6 +267,11 @@ async function launchPersistentContext(userDataDir, opts = {}) {
|
|
|
236
267
|
}
|
|
237
268
|
clearStatusFile(prep.statusPath);
|
|
238
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; }
|
|
239
275
|
return ctx;
|
|
240
276
|
}
|
|
241
277
|
|
|
@@ -283,10 +319,10 @@ async function launchContext(opts = {}) {
|
|
|
283
319
|
// viewport: null -- let the page fill the real window (fitWindow sizes the window to the
|
|
284
320
|
// persona's screen). A fixed 1280x720 emulated viewport would leave the page smaller than
|
|
285
321
|
// the window AND mismatch window.screen -- an incoherent fingerprint.
|
|
286
|
-
const context = browser.contexts()[0] || (await browser.newContext({ viewport: null }));
|
|
322
|
+
const context = browser.contexts()[0] || (await browser.newContext(browser._capiumMobile ? {} : { viewport: null }));
|
|
287
323
|
if (humanize) human.humanize(context, humanPreset);
|
|
288
324
|
const page = context.pages()[0] || (await context.newPage());
|
|
289
|
-
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
|
|
290
326
|
if (url) await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
291
327
|
return { browser, context, page };
|
|
292
328
|
}
|
package/puppeteer.js
CHANGED
|
@@ -76,24 +76,27 @@ async function resolveBinary(binary, licenseKey) {
|
|
|
76
76
|
|
|
77
77
|
async function prepare(opts) {
|
|
78
78
|
const {
|
|
79
|
-
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,
|
|
80
80
|
stealthArgs = true, timezone = null, locale = null, extensionPaths = null,
|
|
81
81
|
binary = null, licenseKey = null, licenseServer = null,
|
|
82
82
|
licenseThroughProxy = false, licensePreflight = true, headless = false,
|
|
83
83
|
} = opts;
|
|
84
|
+
require('./lib/desktop').validateLaunch(browserBrand, platform, binary, args, stealthArgs);
|
|
85
|
+
require('./lib/mobile').validateLaunch(platform, binary, args, stealthArgs, mobileDevice);
|
|
84
86
|
if (licensePreflight) license.preflight(licenseKey, licenseServer);
|
|
85
87
|
const { key } = license.effective(licenseKey, licenseServer);
|
|
86
88
|
const binPath = await resolveBinary(binary, key);
|
|
87
89
|
const finalSeed = seed === null || seed === undefined ? config.newSeed() : seed;
|
|
88
90
|
const { launchOptions, args: proxyArgs } = proxyAndGeoArgs(proxy, geoip);
|
|
89
91
|
const launchArgs = proxyArgs.concat(buildArgs({
|
|
90
|
-
seed: finalSeed, platform, stealthArgs, timezone, locale, extensionPaths, extra: args,
|
|
92
|
+
seed: finalSeed, platform, mobileDevice, browserBrand, stealthArgs, timezone, locale, extensionPaths, extra: args,
|
|
91
93
|
headless,
|
|
92
94
|
}));
|
|
93
95
|
if (licenseThroughProxy) launchArgs.push('--license-through-proxy');
|
|
94
96
|
const env = license.childEnv(licenseKey, licenseServer);
|
|
95
97
|
const statusPath = newStatusFile(env);
|
|
96
|
-
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 };
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
/**
|
|
@@ -131,12 +134,12 @@ async function throwTranslated(e, statusPath) {
|
|
|
131
134
|
*/
|
|
132
135
|
async function buildLaunchOptions(opts = {}) {
|
|
133
136
|
const {
|
|
134
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
137
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
135
138
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
136
139
|
headless = false, ignoreDefaultArgs, ...rest
|
|
137
140
|
} = opts;
|
|
138
141
|
const prep = await prepare({
|
|
139
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
142
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
140
143
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
141
144
|
});
|
|
142
145
|
const proxyOption = prep.launchOptions.proxy || null;
|
|
@@ -150,7 +153,8 @@ async function buildLaunchOptions(opts = {}) {
|
|
|
150
153
|
headless,
|
|
151
154
|
args: launchArgs,
|
|
152
155
|
env: prep.env,
|
|
153
|
-
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),
|
|
154
158
|
ignoreDefaultArgs: ignoreDefaultArgs === undefined ? config.IGNORE_DEFAULT_ARGS : ignoreDefaultArgs,
|
|
155
159
|
...rest,
|
|
156
160
|
};
|
|
@@ -163,12 +167,12 @@ async function buildLaunchOptions(opts = {}) {
|
|
|
163
167
|
*/
|
|
164
168
|
async function launch(opts = {}) {
|
|
165
169
|
const {
|
|
166
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
170
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
167
171
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight,
|
|
168
172
|
headless = false, ignoreDefaultArgs, ...rest
|
|
169
173
|
} = opts;
|
|
170
174
|
const prep = await prepare({
|
|
171
|
-
seed, platform, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
175
|
+
seed, platform, mobileDevice, browserBrand, proxy, geoip, args, stealthArgs, timezone, locale, extensionPaths,
|
|
172
176
|
binary, licenseKey, licenseServer, licenseThroughProxy, licensePreflight, headless,
|
|
173
177
|
});
|
|
174
178
|
const puppeteer = requireDriver();
|
|
@@ -191,7 +195,8 @@ async function launch(opts = {}) {
|
|
|
191
195
|
headless,
|
|
192
196
|
args: launchArgs,
|
|
193
197
|
env: prep.env,
|
|
194
|
-
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),
|
|
195
200
|
ignoreDefaultArgs: ida,
|
|
196
201
|
...rest,
|
|
197
202
|
// A rejected native gate never creates a page. Obtain the transport first,
|
|
@@ -215,6 +220,23 @@ async function launch(opts = {}) {
|
|
|
215
220
|
clearStatusFile(prep.statusPath);
|
|
216
221
|
wireProxyAuth(browser, proxyOption);
|
|
217
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; }
|
|
218
240
|
return browser;
|
|
219
241
|
}
|
|
220
242
|
|
|
@@ -265,7 +287,7 @@ async function launchContext(opts = {}) {
|
|
|
265
287
|
const pages = await browser.pages();
|
|
266
288
|
const page = pages[0] || (await browser.newPage());
|
|
267
289
|
if (humanize) human.humanize(page, humanPreset);
|
|
268
|
-
if (!headless) await fitWindow(browser, page);
|
|
290
|
+
if (!headless && !browser._capiumMobile) await fitWindow(browser, page);
|
|
269
291
|
if (url) await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
270
292
|
return { browser, page };
|
|
271
293
|
}
|