capiumbrowser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * launch-common -- driver-agnostic pieces of a Capium launch, shared by the Playwright and
3
+ * Puppeteer front-ends: flag assembly, geoip tri-state, and the license status file.
4
+ */
5
+ 'use strict';
6
+
7
+ const crypto = require('crypto');
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+
12
+ const config = require('./config');
13
+ const proxy = require('./proxy');
14
+
15
+ /**
16
+ * Build [--proxy-server, --geoip] flags for geo coherence.
17
+ *
18
+ * The binary's CapiumGeoipResolver runs at PreBrowserStart ONLY when --geoip is present. A
19
+ * proxy on its own does NOT trigger it: the lookup is a synchronous, blocking round-trip that
20
+ * would otherwise add up to 2.5s to every proxied launch. When it does run it resolves the
21
+ * egress IP (proxy exit, else the host's real public IP) and pins WebRTC / timezone /
22
+ * geolocation / language -- so the SDK adds no probe of its own. geoipOn is tri-state:
23
+ * * null -> default: add no flag, so NO geo lookup runs (no launch latency) even with a
24
+ * proxy. Pass geoip: true to opt into proxy-exit coherence.
25
+ * * true -> ON with --geoip (resolves the egress -- proxy exit, or the host IP if proxyless).
26
+ * * false -> OFF with --geoip=false, which the binary honors FIRST (explicit opt-out).
27
+ * Returns {launchOptions, args}.
28
+ */
29
+ function proxyAndGeoArgs(spec, geoipOn) {
30
+ const { launchOptions, args } = proxy.resolveProxyConfig(spec);
31
+ const out = [...args];
32
+ if (geoipOn === null || geoipOn === undefined) {
33
+ // default: no flag, no lookup
34
+ } else if (geoipOn) {
35
+ out.push('--geoip');
36
+ } else {
37
+ out.push('--geoip=false');
38
+ }
39
+ return { launchOptions, args: out };
40
+ }
41
+
42
+ function buildArgs({ seed, platform, stealthArgs, timezone, locale, extensionPaths, extra }) {
43
+ let args = [];
44
+ if (stealthArgs) args = args.concat(config.getDefaultStealthArgs(seed, platform));
45
+ if (timezone) args.push(`--timezone=${timezone}`); // capium in-binary tz spoof
46
+ if (locale) {
47
+ args.push(`--lang=${locale}`);
48
+ args.push(`--accept-lang=${locale}`);
49
+ }
50
+ if (extensionPaths) {
51
+ const paths = Array.isArray(extensionPaths) ? extensionPaths : [extensionPaths];
52
+ const joined = paths.join(',');
53
+ args.push(`--disable-extensions-except=${joined}`);
54
+ args.push(`--load-extension=${joined}`);
55
+ }
56
+ if (extra) args = args.concat(extra);
57
+ return args;
58
+ }
59
+
60
+ /**
61
+ * Create a unique empty file and point the browser at it via CAPIUM_LICENSE_STATUS_FILE.
62
+ * On a license fail-closed the binary writes "<code>\n<message>" here, so the SDK throws a
63
+ * precise error from the binary's OWN single verification -- no second server call. A unique
64
+ * file per launch keeps concurrent launches (a fleet) from clobbering each other; an old
65
+ * binary that doesn't write it just leaves it empty -> readLaunchStatus null -> stderr
66
+ * fallback.
67
+ */
68
+ function newStatusFile(env) {
69
+ const p = path.join(os.tmpdir(),
70
+ `capium-lic-${crypto.randomBytes(6).toString('hex')}.err`);
71
+ fs.writeFileSync(p, '');
72
+ env.CAPIUM_LICENSE_STATUS_FILE = p;
73
+ return p;
74
+ }
75
+
76
+ function clearStatusFile(p) {
77
+ try {
78
+ fs.rmSync(p, { force: true });
79
+ } catch {}
80
+ }
81
+
82
+ module.exports = { proxyAndGeoArgs, buildArgs, newStatusFile, clearStatusFile };
package/lib/license.js ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * License helpers for the SDK. Enforcement is in the browser binary -- this module
3
+ * only (a) resolves the key/server so we can pass them to the binary and the
4
+ * downloader, and (b) offers a dependency-free `status()` for the CLI.
5
+ * The SDK never holds its own seat (that would double-count one launch as two
6
+ * concurrent sessions). See https://docs.capiumbrowser.com for the full model.
7
+ */
8
+ 'use strict';
9
+
10
+ const crypto = require('crypto');
11
+ const { execFileSync } = require('child_process');
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+ const path = require('path');
15
+
16
+ const { CapiumConfigError, CapiumExpiredError, CapiumServerDownError } = require('./errors');
17
+
18
+ const DEFAULT_SERVER = 'https://license.capzy.ai';
19
+
20
+ function licenseFile() {
21
+ // CAPIUM_LICENSE_FILE is a test/deploy override; the contract path is ~/.capium/license.
22
+ return process.env.CAPIUM_LICENSE_FILE || path.join(os.homedir(), '.capium', 'license');
23
+ }
24
+
25
+ function fromFile() {
26
+ let key = null;
27
+ let server = null;
28
+ let text;
29
+ try {
30
+ text = fs.readFileSync(licenseFile(), 'utf8');
31
+ } catch {
32
+ return { key, server };
33
+ }
34
+ for (let line of text.split(/\r?\n/)) {
35
+ line = line.trim();
36
+ if (!line || line.startsWith('#') || !line.includes('=')) continue;
37
+ const i = line.indexOf('=');
38
+ const k = line.slice(0, i).trim().toUpperCase();
39
+ const v = line.slice(i + 1).trim();
40
+ if (k === 'KEY' && !key) key = v;
41
+ else if (k === 'SERVER' && !server) server = v;
42
+ }
43
+ return { key, server };
44
+ }
45
+
46
+ /**
47
+ * Resolve from explicit arg then env ONLY (no file). Used to inject into the browser's
48
+ * environment -- if the key lives only in ~/.capium/license the binary reads that file
49
+ * itself, so there's nothing to inject.
50
+ */
51
+ function effective(key = null, server = null) {
52
+ return {
53
+ key: key || process.env.CAPIUM_LICENSE_KEY || null,
54
+ server: server || process.env.CAPIUM_LICENSE_SERVER || null,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Full resolution: arg -> env -> ~/.capium/license, defaulting the server.
60
+ * Throws CapiumConfigError if no key is found. Used by `capiumbrowser status`.
61
+ */
62
+ function resolve(key = null, server = null) {
63
+ ({ key, server } = effective(key, server));
64
+ if (!key || !server) {
65
+ const f = fromFile();
66
+ key = key || f.key;
67
+ server = server || f.server;
68
+ }
69
+ if (!key) {
70
+ throw new CapiumConfigError(
71
+ 'no license configured — set CAPIUM_LICENSE_KEY, pass licenseKey: ..., ' +
72
+ 'or create ~/.capium/license with a KEY= line');
73
+ }
74
+ return { key, server: (server || DEFAULT_SERVER).replace(/\/+$/, '') };
75
+ }
76
+
77
+ /**
78
+ * Signed headers for an authenticated GET (download / update). The key travels in a header
79
+ * (over TLS), never in the URL; the request target is HMAC-signed -- same scheme as the
80
+ * licensing calls. `pathAndQuery` is exactly "<path>?<query>".
81
+ */
82
+ function getHeaders(key, pathAndQuery) {
83
+ const ts = String(Math.floor(Date.now() / 1000));
84
+ const sig = crypto.createHmac('sha256', key).update(`${ts}.${pathAndQuery}`).digest('hex');
85
+ return { 'X-Capzy-License': key, 'X-Capzy-Timestamp': ts, 'X-Capzy-Signature': sig };
86
+ }
87
+
88
+ /**
89
+ * Full environment for the browser subprocess: the current env plus any explicitly-resolved
90
+ * license vars (so a key passed to launch() reaches the self-licensing binary without ever
91
+ * touching argv / `ps`).
92
+ */
93
+ function childEnv(key = null, server = null) {
94
+ const { key: k, server: s } = effective(key, server);
95
+ const env = { ...process.env };
96
+ if (k) env.CAPIUM_LICENSE_KEY = k;
97
+ if (s) env.CAPIUM_LICENSE_SERVER = s;
98
+ return env;
99
+ }
100
+
101
+ function machineId() {
102
+ let raw = null;
103
+ if (process.platform === 'linux') {
104
+ for (const p of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
105
+ try {
106
+ raw = fs.readFileSync(p, 'utf8').trim();
107
+ if (raw) break;
108
+ } catch {}
109
+ }
110
+ } else if (process.platform === 'darwin') {
111
+ try {
112
+ const out = execFileSync('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], {
113
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
114
+ });
115
+ for (const line of out.split('\n')) {
116
+ if (line.includes('IOPlatformUUID')) {
117
+ const parts = line.split('"');
118
+ raw = parts[parts.length - 2];
119
+ break;
120
+ }
121
+ }
122
+ } catch {}
123
+ } else if (process.platform === 'win32') {
124
+ try {
125
+ const out = execFileSync('reg', [
126
+ 'query', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid',
127
+ ], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
128
+ const m = out.match(/MachineGuid\s+REG_SZ\s+(\S+)/);
129
+ if (m) raw = m[1];
130
+ } catch {}
131
+ }
132
+ return raw || os.hostname();
133
+ }
134
+
135
+ function osName() {
136
+ return process.platform === 'win32' ? 'windows'
137
+ : process.platform === 'darwin' ? 'macos' : 'linux';
138
+ }
139
+
140
+ /**
141
+ * Read license entitlements (plan, sessions_cap, live_sessions, period_end).
142
+ *
143
+ * Signs a read-only `activate` with the license-key HMAC using a status-scoped device
144
+ * fingerprint + a throwaway public key, so it never disturbs a running browser's real
145
+ * device enrollment and never consumes a session slot.
146
+ */
147
+ async function status(key = null, server = null) {
148
+ ({ key, server } = resolve(key, server));
149
+ const fp = crypto.createHash('sha256').update('capium-status:' + machineId()).digest('hex');
150
+ const body = JSON.stringify({
151
+ license_key: key,
152
+ device_fp: fp,
153
+ hostname: os.hostname().slice(0, 100),
154
+ os: osName(),
155
+ arch: 'x64',
156
+ device_pubkey: crypto.randomBytes(32).toString('base64'),
157
+ });
158
+ const ts = String(Math.floor(Date.now() / 1000));
159
+ const sig = crypto.createHmac('sha256', key).update(`${ts}.${body}`).digest('hex');
160
+ let res;
161
+ try {
162
+ res = await fetch(server + '/v1/activate', {
163
+ method: 'POST',
164
+ body,
165
+ headers: {
166
+ 'X-Capzy-Timestamp': ts,
167
+ 'X-Capzy-Device-Fp': fp,
168
+ 'X-Capzy-Signature': sig,
169
+ 'Content-Type': 'application/json',
170
+ },
171
+ signal: AbortSignal.timeout(10000),
172
+ });
173
+ } catch (e) {
174
+ throw new CapiumServerDownError(`license server unreachable at ${server} (${e.message || e})`);
175
+ }
176
+ if (!res.ok) {
177
+ if (res.status === 401 || res.status === 403) {
178
+ throw new CapiumExpiredError(
179
+ `license rejected (${res.status}) — expired, revoked, or invalid key`);
180
+ }
181
+ throw new CapiumServerDownError(`status failed: HTTP ${res.status}`);
182
+ }
183
+ const data = await res.json();
184
+ const out = {};
185
+ for (const f of ['plan', 'sessions_cap', 'live_sessions', 'period_end', 'heartbeat_every']) {
186
+ out[f] = data[f] !== undefined ? data[f] : null;
187
+ }
188
+ return out;
189
+ }
190
+
191
+ /**
192
+ * Instant, OFFLINE no-key gate: resolve() throws CapiumConfigError if no key is set in
193
+ * arg/env/~/.capium/license, so "you forgot the license" surfaces as a typed error BEFORE
194
+ * a browser is spun up instead of an opaque driver "browser closed" crash.
195
+ *
196
+ * Deliberately does NOT hit the server. The browser binary is the SINGLE network verifier
197
+ * (activate -> session -> heartbeat); duplicating that with an SDK-side check would double
198
+ * the load on the licensing service. Expired / revoked / seat-limit / server-down all come
199
+ * back from the binary's one check and are surfaced from its status file via
200
+ * errors.readLaunchStatus -- no second round-trip.
201
+ */
202
+ function preflight(key = null, server = null) {
203
+ resolve(key, server); // -> CapiumConfigError when no key is configured
204
+ }
205
+
206
+ module.exports = {
207
+ DEFAULT_SERVER,
208
+ effective,
209
+ resolve,
210
+ getHeaders,
211
+ childEnv,
212
+ status,
213
+ preflight,
214
+ _fromFile: fromFile,
215
+ _licenseFile: licenseFile,
216
+ _machineId: machineId,
217
+ };
package/lib/proxy.js ADDED
@@ -0,0 +1,218 @@
1
+ /**
2
+ * proxy -- proxy URL construction for the SDK.
3
+ *
4
+ * buildProxyArg(spec) -> "--proxy-server=..." flag with inline credentials,
5
+ * or null for no proxy. Handles HTTP/HTTPS/SOCKS5,
6
+ * object specs and string URLs.
7
+ */
8
+ 'use strict';
9
+
10
+ const UNRESERVED = /^[A-Za-z0-9\-._~]$/;
11
+
12
+ /**
13
+ * Lenient percent-decode (Python urllib.parse.unquote semantics): valid %XX sequences are
14
+ * decoded, anything malformed (a lone '%') is left as-is. decodeURIComponent would throw.
15
+ */
16
+ function lenientUnquote(s) {
17
+ return String(s).replace(/%([0-9A-Fa-f]{2})/g, (_, h) =>
18
+ String.fromCharCode(parseInt(h, 16)));
19
+ }
20
+
21
+ /**
22
+ * Full RFC-3986 percent-encoding of one --proxy-server userinfo field.
23
+ *
24
+ * Encode EVERYTHING outside the unreserved set -- ',', ';', '=', '+', ':', '@' all become
25
+ * %XX. The reserved chars matter because Chromium's --proxy-server value has its own
26
+ * grammar: ',' and ';' separate proxy rules and '=' is the scheme=proxy delimiter, so a raw
27
+ * one splits the config into garbage and the tunnel dies with ERR_TUNNEL_CONNECTION_FAILED
28
+ * (the Apify "groups-X,country-Y,session-Z" username failure).
29
+ *
30
+ * The capium binary percent-DECODES the inline user:pass@ before it authenticates, so the
31
+ * proxy receives the original literal characters. Encode-in-the-SDK / decode-in-the-binary
32
+ * is the canonical pairing -- nothing is left raw.
33
+ *
34
+ * IDEMPOTENT: unquote first, so a credential that ALREADY arrived percent-encoded isn't
35
+ * double-encoded (',' -> '%2C' -> '%252C'; the binary decodes only once, so the proxy would
36
+ * see a literal "%2C" -> ERR_TUNNEL_CONNECTION_FAILED). Mirrors the CDP path
37
+ * (toDriverProxy), which unquotes too, so inline and CDP normalize identically.
38
+ */
39
+ function encodeCred(s) {
40
+ const raw = lenientUnquote(s);
41
+ let out = '';
42
+ for (const ch of raw) {
43
+ if (UNRESERVED.test(ch)) {
44
+ out += ch;
45
+ } else {
46
+ // encode as UTF-8 bytes
47
+ for (const b of Buffer.from(ch, 'utf8')) {
48
+ out += '%' + b.toString(16).toUpperCase().padStart(2, '0');
49
+ }
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+
55
+ function proxyUrl(scheme, user, pw, host, port) {
56
+ return `${scheme}://${encodeCred(user)}:${encodeCred(pw)}@${host}:${Math.trunc(Number(port))}`;
57
+ }
58
+
59
+ /**
60
+ * (scheme, host, port, user, pass) from an object spec or a string URL. Creds are the raw
61
+ * (already percent-decoded) values.
62
+ */
63
+ function specParts(spec) {
64
+ if (spec && typeof spec === 'object') {
65
+ const server = spec.server || '';
66
+ const u = new URL(server.includes('://') ? server : 'http://' + server);
67
+ return {
68
+ scheme: (u.protocol || 'http:').replace(/:$/, '') || 'http',
69
+ host: u.hostname,
70
+ port: u.port ? Number(u.port) : null,
71
+ user: spec.username || '',
72
+ pass: spec.password || '',
73
+ };
74
+ }
75
+ const raw = String(spec);
76
+ const u = new URL(raw.includes('://') ? raw : 'http://' + raw);
77
+ return {
78
+ scheme: (u.protocol || 'http:').replace(/:$/, '') || 'http',
79
+ host: u.hostname,
80
+ port: u.port ? Number(u.port) : null,
81
+ // WHATWG URL does NOT percent-decode username/password; unquote to raw like urlsplit use.
82
+ user: lenientUnquote(u.username || ''),
83
+ pass: lenientUnquote(u.password || ''),
84
+ };
85
+ }
86
+
87
+ function isNoProxy(spec) {
88
+ return !spec || spec === 'none' || spec === '0' || spec === false;
89
+ }
90
+
91
+ /**
92
+ * Return a --proxy-server=... flag for the capium binary, or null for no proxy.
93
+ *
94
+ * Accepted forms:
95
+ * null / "" / "none" / "0" / false -> no proxy
96
+ * "http://user:pass@host:port" -> HTTP/HTTPS proxy with credentials
97
+ * "socks5://user:pass@host:port" -> SOCKS5 with credentials
98
+ * "socks5://host:port" -> SOCKS5, no auth
99
+ * {server: "scheme://host:port", -> object spec; username/password are
100
+ * username: "...", password: "..."} optional -- no-auth if omitted
101
+ */
102
+ function buildProxyArg(spec) {
103
+ if (isNoProxy(spec)) return null;
104
+
105
+ if (spec && typeof spec === 'object') {
106
+ const { scheme, host, port, user, pass } = specParts(spec);
107
+ if (user) return '--proxy-server=' + proxyUrl(scheme, user, pass, host, port);
108
+ return `--proxy-server=${spec.server}`;
109
+ }
110
+
111
+ const { scheme, host, port, user, pass } = specParts(spec);
112
+ if (user) return '--proxy-server=' + proxyUrl(scheme, user, pass, host, port);
113
+ const portStr = port ? `:${port}` : '';
114
+ return `--proxy-server=${scheme}://${host}${portStr}`;
115
+ }
116
+
117
+ // ---- proxy channel selection --------------------------------------------------------------
118
+ // Everything now goes through the native inline --proxy-server flag. The capium binary
119
+ // authenticates from the inline user:pass@ itself:
120
+ // * SOCKS5 -> RFC 1929 username/password in the handshake (socks_connect_job.cc)
121
+ // * HTTP/S -> PREEMPTIVE Proxy-Authorization on the FIRST CONNECT
122
+ // (http_proxy_client_socket.cc; capium-preemptive-proxy-auth.patch)
123
+ // Preemptive auth means the proxy never has to answer 407, which closes the reactive-auth
124
+ // retry loop (net::ERR_TOO_MANY_RETRIES) that inline credentials -- especially country-
125
+ // targeted / special-char usernames like "groups-X,country-Y,session-Z" -- used to trigger.
126
+ //
127
+ // The legacy driver proxy option (CDP auth, answers the 407 over CDP) remains only as an
128
+ // escape hatch for a binary WITHOUT the preemptive patch -- opt in with CAPIUM_INLINE_PROXY_AUTH=0.
129
+
130
+ /**
131
+ * Whether the capium binary authenticates from inline --proxy-server credentials.
132
+ *
133
+ * Default true: the capium binary decodes the inline user:pass@ itself (SOCKS5 RFC 1929 +
134
+ * preemptive HTTP Proxy-Authorization), so ALL proxies use the native --proxy-server flag.
135
+ * Set CAPIUM_INLINE_PROXY_AUTH=0 to force the legacy CDP-auth path for credentialed
136
+ * HTTP/HTTPS (only needed on a binary that lacks the preemptive-proxy-auth patch).
137
+ */
138
+ function binarySupportsInlineProxyAuth() {
139
+ const v = process.env.CAPIUM_INLINE_PROXY_AUTH;
140
+ if (v !== undefined && v !== null) {
141
+ return ['1', 'true', 'yes', 'on'].includes(v.trim().toLowerCase());
142
+ }
143
+ return true;
144
+ }
145
+
146
+ function specIsSocks(spec) {
147
+ return specParts(spec).scheme.toLowerCase().startsWith('socks');
148
+ }
149
+
150
+ /**
151
+ * Optional proxy-bypass list from an object spec (the Playwright 'bypass' key).
152
+ *
153
+ * Comma-separated hosts/patterns that should connect DIRECT, skipping the proxy
154
+ * (e.g. "localhost,*.internal"). Only object specs carry it; a string proxy URL has
155
+ * nowhere to put it. Inline -> --proxy-bypass-list=<v>, CDP -> the option's own 'bypass'.
156
+ */
157
+ function specBypass(spec) {
158
+ if (spec && typeof spec === 'object') return spec.bypass || null;
159
+ return null;
160
+ }
161
+
162
+ /**
163
+ * Normalize a spec into a driver proxy option {server, username?, password?}. Credentials
164
+ * are passed RAW -- the driver handles CDP-auth escaping itself, so nothing is re-encoded.
165
+ */
166
+ function toDriverProxy(spec) {
167
+ const { scheme, host, port, user, pass } = specParts(spec);
168
+ let server = `${scheme}://${host}`;
169
+ if (port) server += `:${port}`;
170
+ const d = { server };
171
+ if (user) {
172
+ d.username = lenientUnquote(user);
173
+ d.password = lenientUnquote(pass || '');
174
+ }
175
+ const bypass = specBypass(spec);
176
+ if (bypass) d.bypass = bypass;
177
+ return d;
178
+ }
179
+
180
+ /**
181
+ * Return {launchOptions, args} for launching the capium binary.
182
+ *
183
+ * launchOptions -> merged into the driver's launch options (e.g. {proxy: {...}}); args ->
184
+ * appended to the Chromium arg list. Exactly one carries the proxy for any spec:
185
+ * * DEFAULT: everything -> args (native inline --proxy-server; binary self-auths)
186
+ * * escape hatch (CAPIUM_INLINE_PROXY_AUTH=0): credentialed HTTP/HTTPS -> launchOptions
187
+ * (driver CDP auth), for a binary lacking the preemptive-proxy-auth patch. SOCKS5
188
+ * can NEVER go this way (Chrome must receive it inline), so SOCKS always stays inline.
189
+ *
190
+ * An object spec's optional "bypass" (comma-separated DIRECT hosts) is carried too:
191
+ * --proxy-bypass-list=<v> on the inline paths, or the option's own "bypass" on CDP.
192
+ */
193
+ function resolveProxyConfig(spec, inlineAuth = null) {
194
+ if (isNoProxy(spec)) return { launchOptions: {}, args: [] };
195
+ if (inlineAuth === null || inlineAuth === undefined) {
196
+ inlineAuth = binarySupportsInlineProxyAuth();
197
+ }
198
+ const hasCreds = Boolean(specParts(spec).user);
199
+ if (specIsSocks(spec) || !hasCreds || inlineAuth) {
200
+ const arg = buildProxyArg(spec);
201
+ if (!arg) return { launchOptions: {}, args: [] };
202
+ const args = [arg];
203
+ const bypass = specBypass(spec);
204
+ if (bypass) args.push(`--proxy-bypass-list=${bypass}`);
205
+ return { launchOptions: {}, args };
206
+ }
207
+ return { launchOptions: { proxy: toDriverProxy(spec) }, args: [] };
208
+ }
209
+
210
+ module.exports = {
211
+ buildProxyArg,
212
+ resolveProxyConfig,
213
+ binarySupportsInlineProxyAuth,
214
+ toDriverProxy,
215
+ _encodeCred: encodeCred,
216
+ _lenientUnquote: lenientUnquote,
217
+ _specParts: specParts,
218
+ };
package/lib/version.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Version of the capium Node SDK (independent of the browser binary version).
3
+ *
4
+ * The SDK version and the Capium/Chromium binary version move on separate cadences: you update
5
+ * the SDK with `npm install -U capiumbrowser`, and the SDK fetches the *default* binary build to
6
+ * match. Crucially that default is **per platform** -- each OS/arch reaches a stable Capium build
7
+ * independently (Windows can sit on 150 while macOS is on 151).
8
+ *
9
+ * The stable build for each OS is not hardcoded here; it comes from `channels.json`, which ships
10
+ * inside the package and is the single source of truth (the release pipeline edits that file to
11
+ * promote an OS to a new stable, and the same file is published to the download server). This
12
+ * module just loads it; FALLBACK_VERSIONS is used only if the bundled manifest is missing or
13
+ * corrupt in an install.
14
+ *
15
+ * Override order at download time: CAPIUM_VERSION env > per-OS stable from channels.json.
16
+ */
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const os = require('os');
21
+ const path = require('path');
22
+
23
+ const SDK_VERSION = require('../package.json').version;
24
+
25
+ // Used only if channels.json can't be read (corrupt/removed install). ensureBinary() raises a
26
+ // clear error long before the "unsupported platform" case matters; this keeps lookups total.
27
+ const FALLBACK_VERSIONS = {
28
+ 'windows-x64': '152.0.7977.65', // real Chrome 152 stable per OS (win/mac .65, linux .64)
29
+ 'macos-arm64': '152.0.7977.65',
30
+ 'linux-x64': '152.0.7977.64',
31
+ 'linux-arm64': '152.0.7977.64', // declared; not yet published (see channels.json)
32
+ };
33
+ const DEFAULT_BINARY_VERSION = '152.0.7977.65';
34
+
35
+ function loadStable() {
36
+ // The `channels.stable` map from the bundled manifest, or null if unreadable.
37
+ try {
38
+ const raw = fs.readFileSync(path.join(__dirname, 'channels.json'), 'utf8');
39
+ const stable = JSON.parse(raw).channels.stable;
40
+ return stable && typeof stable === 'object' ? stable : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ const STABLE = loadStable();
47
+
48
+ function versionsFromManifest() {
49
+ if (!STABLE) return { ...FALLBACK_VERSIONS };
50
+ const out = {};
51
+ for (const [tag, entry] of Object.entries(STABLE)) {
52
+ if (entry && typeof entry === 'object' && entry.version) out[tag] = entry.version;
53
+ }
54
+ return Object.keys(out).length ? out : { ...FALLBACK_VERSIONS };
55
+ }
56
+
57
+ // Default stable Capium binary build per distro tag, resolved from channels.json.
58
+ const CAPIUM_BINARY_VERSIONS = versionsFromManifest();
59
+
60
+ /** The current stable Capium binary version for a distro tag (e.g. 'windows-x64'). */
61
+ function binaryVersionFor(tag) {
62
+ return CAPIUM_BINARY_VERSIONS[tag] || DEFAULT_BINARY_VERSION;
63
+ }
64
+
65
+ /**
66
+ * False only when the manifest declares the tag but marks it not-yet-published. Unknown
67
+ * tags / missing manifest return true so the download layer's own error/404 speaks instead.
68
+ */
69
+ function isPublished(tag) {
70
+ if (!STABLE) return true;
71
+ const entry = STABLE[tag];
72
+ if (!entry || typeof entry !== 'object') return true;
73
+ return entry.published !== false;
74
+ }
75
+
76
+ /**
77
+ * Best-effort distro tag for the current host, mirroring download.js's mapping. Kept
78
+ * dependency-free (no require of download.js) so version.js stays loadable in isolation.
79
+ */
80
+ function hostTag() {
81
+ const p = process.platform;
82
+ const osName = p === 'darwin' ? 'macos' : p === 'win32' ? 'windows' : 'linux';
83
+ const m = os.arch();
84
+ const arch = m === 'x64' || m === 'ia32' ? 'x64' : m === 'arm64' ? 'arm64' : m;
85
+ return `${osName}-${arch}`;
86
+ }
87
+
88
+ // The stable build this SDK targets for THIS host (CLI display, examples). Code that fetches a
89
+ // build should prefer binaryVersionFor(tag) with the tag it already resolved.
90
+ const CAPIUM_BINARY_VERSION = binaryVersionFor(hostTag());
91
+
92
+ module.exports = {
93
+ SDK_VERSION,
94
+ CAPIUM_BINARY_VERSION,
95
+ CAPIUM_BINARY_VERSIONS,
96
+ binaryVersionFor,
97
+ isPublished,
98
+ hostTag,
99
+ _FALLBACK_VERSIONS: FALLBACK_VERSIONS,
100
+ };
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "capiumbrowser",
3
+ "version": "0.1.0",
4
+ "description": "Drop-in stealth browser SDK on top of Playwright or Puppeteer (Capium Chromium).",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "author": "Capzy (https://capzy.ai)",
7
+ "homepage": "https://capiumbrowser.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/capzy-ai/capiumbrowser.git",
11
+ "directory": "capiumbrowser-node"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/capzy-ai/capiumbrowser/issues"
15
+ },
16
+ "keywords": [
17
+ "playwright",
18
+ "puppeteer",
19
+ "stealth",
20
+ "chromium",
21
+ "browser",
22
+ "fingerprint",
23
+ "automation"
24
+ ],
25
+ "main": "index.js",
26
+ "types": "index.d.ts",
27
+ "bin": {
28
+ "capiumbrowser": "bin/capiumbrowser.js"
29
+ },
30
+ "exports": {
31
+ ".": "./index.js",
32
+ "./playwright": "./playwright.js",
33
+ "./puppeteer": "./puppeteer.js",
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "index.js",
38
+ "index.d.ts",
39
+ "playwright.js",
40
+ "puppeteer.js",
41
+ "bin/",
42
+ "lib/",
43
+ "README.md",
44
+ "LICENSE"
45
+ ],
46
+ "engines": {
47
+ "node": ">=18.17"
48
+ },
49
+ "scripts": {
50
+ "test": "node --test"
51
+ },
52
+ "dependencies": {
53
+ "tar": "^7.4.0"
54
+ },
55
+ "peerDependencies": {
56
+ "playwright-core": ">=1.40.0",
57
+ "puppeteer-core": ">=21.0.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "playwright-core": {
61
+ "optional": true
62
+ },
63
+ "puppeteer-core": {
64
+ "optional": true
65
+ }
66
+ }
67
+ }