capiumbrowser 0.1.1 → 1.0.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 CHANGED
@@ -91,6 +91,28 @@ launch the SDK fetches the per-platform build from the license service (authenti
91
91
  checksum-verified) and caches it under `~/.capium` (`CAPIUM_HOME` overrides — it deliberately
92
92
  lives *outside* `node_modules`, so reinstalls never delete it).
93
93
 
94
+ ### Download the browser — just your license key
95
+
96
+ The browser binary is fetched with **only your Capium license key** — no separate token, no
97
+ manual download, no per-platform URL to figure out. Set the key once:
98
+
99
+ ```bash
100
+ export CAPIUM_LICENSE_KEY=cap_your_key_here
101
+ # or put a `KEY=cap_...` line in ~/.capium/license
102
+ ```
103
+
104
+ Then the right build for your OS/arch downloads automatically — either **on first launch**, or
105
+ ahead of time with the CLI:
106
+
107
+ ```bash
108
+ npx capiumbrowser install # ...or skip it: the first launch() downloads the binary for you.
109
+ ```
110
+
111
+ That same key both **authorizes the download** and **licenses the browser**, and it's passed to
112
+ the binary through the environment — never on argv. The download is checksum-verified before
113
+ anything runs and cached, so it's fetched once and reused. Run `npx capiumbrowser` (the `info`
114
+ check) to see your license, the installed build, and anything still missing.
115
+
94
116
  **Migrating?** It's a one-line change — the returned object is a standard Playwright /
95
117
  Puppeteer `Browser`:
96
118
 
package/lib/channels.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
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 (so the server/website can resolve 'latest stable' without a new SDK release). `version` is the Chromium base of the immutable artifact folder; `revision` is the cap<N> re-spin counter. `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.",
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 base; `revision` is the re-spin counter -- the served R2 folder is chromium-v<version>/ for revision 1 (the base build) and chromium-v<version>-r<N>/ for a re-spin (N>=2), so publishing a re-spin never overwrites the base 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-09-01",
4
+ "updated": "2026-09-08",
5
5
  "channels": {
6
6
  "stable": {
7
- "windows-x64": { "version": "152.0.7977.65", "revision": 1, "sha256": "c3cf933325907b36a157ed74db80c1acb1d3aca0faa93c3bb224fe735cafc4a5", "published": true },
8
- "macos-arm64": { "version": "152.0.7977.65", "revision": 1, "sha256": "e328158c5a272594e01d2135eb418f649f46ee5b0a80fbc79f6923dcc809be01", "published": true },
9
- "linux-x64": { "version": "152.0.7977.64", "revision": 1, "sha256": "aeb36b509d26e40969d64502f0a2a3f0ad73c7cdd8c42c5fab8a54e953bc3b3a", "published": true },
7
+ "windows-x64": { "version": "152.0.7977.65", "revision": 2, "sha256": "1556ad4fd5fe555b3c427be5e868bcace705f271ec3bcd6be4674cae8163ffc6", "published": true },
8
+ "macos-arm64": { "version": "152.0.7977.65", "revision": 2, "sha256": "a71654da4b0e1b7ab2f8374e62ba5378decc5b7762953cdbe6946781d8e08c33", "published": true },
9
+ "linux-x64": { "version": "152.0.7977.64", "revision": 2, "sha256": "cf26c55e86038eb33e1de607ade5619f0494188624b30bb508fa9429c0e98a17", "published": true },
10
10
  "linux-arm64": { "version": "152.0.7977.64", "revision": 1, "sha256": null, "published": false }
11
11
  }
12
12
  }
package/lib/download.js CHANGED
@@ -41,9 +41,10 @@ const tar = require('tar');
41
41
  const config = require('./config');
42
42
  const license = require('./license');
43
43
  const { CapiumError, CapiumExpiredError, CapiumServerDownError } = require('./errors');
44
- const { binaryVersionFor, isPublished } = require('./version');
44
+ const { SDK_VERSION, binaryVersionFor, binaryRevisionFor, buildId, isPublished } = require('./version');
45
45
 
46
46
  const NET_TIMEOUT_MS = 300000;
47
+ const USER_AGENT = `capiumbrowser/${SDK_VERSION}`;
47
48
 
48
49
  // (normalized os, normalized arch) -> distro tag used in the filename/URL.
49
50
  const SUPPORTED = {
@@ -86,11 +87,16 @@ function downloadTag(system = null, machine = null) {
86
87
  }
87
88
 
88
89
  /**
89
- * The signed request path for a build. Filename is version-independent; only the folder
90
- * carries the version (per the '...end name will all be the same' contract).
90
+ * The signed request path for a build: version in the folder, tag in the filename
91
+ * (`/download/distro/chromium-v<version>/capiumbrowser-<tag>.tar.gz`). Per-OS versions work
92
+ * because each tag lives under its own chromium-v<version>/ folder (win/mac .65 while linux
93
+ * is .64). Key travels in the X-Capzy-License header, the path is HMAC-signed, and the bytes
94
+ * are verified against the response's X-Capzy-SHA256.
91
95
  */
92
- function distroPath(version, tag) {
93
- return `/download/distro/chromium-v${version}/capiumbrowser-${tag}.tar.gz`;
96
+ function distroPath(version, tag, revision = 1) {
97
+ // A same-engine re-spin (revision>=2) lands in its own immutable folder
98
+ // chromium-v<version>-r<N>/, so publishing it never overwrites the base artifact older SDKs fetch.
99
+ return `/download/distro/chromium-v${buildId(version, revision)}/capiumbrowser-${tag}.tar.gz`;
94
100
  }
95
101
 
96
102
  // ---- extraction ------------------------------------------------------------------------------
@@ -216,8 +222,12 @@ function removeInstalls(root) {
216
222
  */
217
223
  async function download(url, headers, dest, version, tag) {
218
224
  let res;
225
+ // Identify the SDK on every request. undici otherwise sends a default UA that Cloudflare's
226
+ // Browser Integrity Check rejects as a bot (403) before the request reaches the origin. A
227
+ // stable product UA is both correct hygiene and what the edge allows.
228
+ const h = Object.assign({ 'User-Agent': USER_AGENT }, headers || {});
219
229
  try {
220
- res = await fetch(url, { headers: headers || {}, signal: AbortSignal.timeout(NET_TIMEOUT_MS) });
230
+ res = await fetch(url, { headers: h, signal: AbortSignal.timeout(NET_TIMEOUT_MS) });
221
231
  } catch (e) {
222
232
  throw new CapiumServerDownError(
223
233
  `could not reach the download server ${url}: ${e.cause?.message || e.message || e}`);
@@ -281,14 +291,23 @@ async function ensureBinary({ version = null, licenseKey = null, server = null }
281
291
  'Set CAPIUM_VERSION to pin a specific build if one exists.');
282
292
  }
283
293
 
284
- version = version || process.env.CAPIUM_VERSION || binaryVersionFor(tag);
294
+ const envVer = process.env.CAPIUM_VERSION;
295
+ version = version || envVer || binaryVersionFor(tag);
296
+ // A same Chromium version can be re-spun (r2, r3, ...). CAPIUM_REVISION overrides; an explicit
297
+ // CAPIUM_VERSION pin defaults to the base (r1) unless a revision is given; otherwise the per-OS
298
+ // revision comes from channels.json.
299
+ let revision;
300
+ if (process.env.CAPIUM_REVISION) revision = parseInt(process.env.CAPIUM_REVISION, 10);
301
+ else if (envVer) revision = 1;
302
+ else revision = binaryRevisionFor(tag);
303
+ const bid = buildId(version, revision);
285
304
 
286
- // Reuse the cached binary ONLY if it's already the target version. A different (older)
287
- // version -- e.g. after `npm install capiumbrowser@latest` bumps this OS's pinned build --
288
- // falls through to fetch the new build and drop the old one.
305
+ // Reuse the cached binary ONLY if it's already this exact build (version + revision). A
306
+ // different build -- e.g. after `npm install capiumbrowser@latest` bumps this OS's pinned build
307
+ // or re-spin -- falls through to fetch the new one and drop the old.
289
308
  try {
290
309
  const existing = config.findBinary();
291
- if (readInstalledVersion(existing) === version) return existing;
310
+ if (readInstalledVersion(existing) === bid) return existing;
292
311
  } catch {}
293
312
 
294
313
  let url;
@@ -299,7 +318,7 @@ async function ensureBinary({ version = null, licenseKey = null, server = null }
299
318
  } else {
300
319
  // Signed path-based download: key in a header, PATH HMAC-signed, sha256-verified.
301
320
  const { key, server: srv } = license.resolve(licenseKey, server); // -> CapiumConfigError if no key
302
- const p = distroPath(version, tag);
321
+ const p = distroPath(version, tag, revision);
303
322
  url = srv + p;
304
323
  headers = license.getHeaders(key, p);
305
324
  }
@@ -319,7 +338,7 @@ async function ensureBinary({ version = null, licenseKey = null, server = null }
319
338
  // marker files, and findBinary only accepts a bare chrome.exe next to one -- the
320
339
  // downloader vouches for the directory it just extracted.
321
340
  try {
322
- fs.writeFileSync(path.join(distroDir, MARKER), String(version));
341
+ fs.writeFileSync(path.join(distroDir, MARKER), String(bid));
323
342
  } catch {}
324
343
  } finally {
325
344
  try {
@@ -335,7 +354,7 @@ async function ensureBinary({ version = null, licenseKey = null, server = null }
335
354
  `downloaded and extracted the ${tag} build but no launch target was found afterwards ` +
336
355
  `(unexpected archive layout under ${root}).`);
337
356
  }
338
- stampVersion(binPath, version); // so a later version bump knows to re-download
357
+ stampVersion(binPath, bid); // so a later version/revision bump knows to re-download
339
358
  // Re-apply exec bits the wrapper + engine binaries need (zip drops unix perms).
340
359
  if (process.platform !== 'win32') {
341
360
  for (const p of [binPath, path.join(path.dirname(binPath), 'chrome')]) {
package/lib/license.js CHANGED
@@ -14,8 +14,10 @@ const os = require('os');
14
14
  const path = require('path');
15
15
 
16
16
  const { CapiumConfigError, CapiumExpiredError, CapiumServerDownError } = require('./errors');
17
+ const { SDK_VERSION } = require('./version');
17
18
 
18
19
  const DEFAULT_SERVER = 'https://license.capzy.ai';
20
+ const USER_AGENT = `capiumbrowser/${SDK_VERSION}`;
19
21
 
20
22
  function licenseFile() {
21
23
  // CAPIUM_LICENSE_FILE is a test/deploy override; the contract path is ~/.capium/license.
@@ -167,6 +169,7 @@ async function status(key = null, server = null) {
167
169
  'X-Capzy-Device-Fp': fp,
168
170
  'X-Capzy-Signature': sig,
169
171
  'Content-Type': 'application/json',
172
+ 'User-Agent': USER_AGENT,
170
173
  },
171
174
  signal: AbortSignal.timeout(10000),
172
175
  });
package/lib/version.js CHANGED
@@ -62,6 +62,38 @@ function binaryVersionFor(tag) {
62
62
  return CAPIUM_BINARY_VERSIONS[tag] || DEFAULT_BINARY_VERSION;
63
63
  }
64
64
 
65
+ function revisionsFromManifest() {
66
+ if (!STABLE) return {};
67
+ const out = {};
68
+ for (const [tag, entry] of Object.entries(STABLE)) {
69
+ if (entry && typeof entry === 'object' && Number.isInteger(entry.revision)) out[tag] = entry.revision;
70
+ }
71
+ return out;
72
+ }
73
+
74
+ // Re-spin variant per tag (>=1). Same Chromium version can be rebuilt (a stealth/fingerprint
75
+ // change with no engine bump); each re-spin increments this. 1 = the base build.
76
+ const CAPIUM_BINARY_REVISIONS = revisionsFromManifest();
77
+
78
+ /**
79
+ * The re-spin variant (>=1) for a distro tag. 1 = the base build (suffix-less folder); 2,3,...
80
+ * are re-spins of the SAME Chromium version served from chromium-v<version>-r<N>/ (so a re-spin
81
+ * never overwrites the base artifact older SDKs still fetch). A version bump resets this to 1.
82
+ */
83
+ function binaryRevisionFor(tag) {
84
+ return CAPIUM_BINARY_REVISIONS[tag] || 1;
85
+ }
86
+
87
+ /**
88
+ * Stable identity of one specific build = the R2 folder discriminator AND the install-cache key.
89
+ * revision 1 -> the version itself (chromium-v<version>/, the base build); N>=2 -> '<version>-r<N>'
90
+ * (chromium-v<version>-r<N>/). So a revision bump on an unchanged engine still re-downloads.
91
+ */
92
+ function buildId(version, revision) {
93
+ const n = parseInt(revision, 10);
94
+ return !n || n < 2 ? version : `${version}-r${n}`;
95
+ }
96
+
65
97
  /**
66
98
  * False only when the manifest declares the tag but marks it not-yet-published. Unknown
67
99
  * tags / missing manifest return true so the download layer's own error/404 speaks instead.
@@ -93,7 +125,10 @@ module.exports = {
93
125
  SDK_VERSION,
94
126
  CAPIUM_BINARY_VERSION,
95
127
  CAPIUM_BINARY_VERSIONS,
128
+ CAPIUM_BINARY_REVISIONS,
96
129
  binaryVersionFor,
130
+ binaryRevisionFor,
131
+ buildId,
97
132
  isPublished,
98
133
  hostTag,
99
134
  _FALLBACK_VERSIONS: FALLBACK_VERSIONS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capiumbrowser",
3
- "version": "0.1.1",
3
+ "version": "1.0.1",
4
4
  "description": "Drop-in stealth browser SDK on top of Playwright or Puppeteer (Capium Chromium).",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Capzy (https://capzy.ai)",