chromiumfish 0.1.3 → 0.1.4
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 +2 -2
- package/dist/fetch.js +56 -21
- package/dist/ip2tz.js +137 -30
- package/dist/launcher.d.ts +3 -3
- package/dist/launcher.js +7 -3
- package/dist/version.d.ts +9 -1
- package/dist/version.js +16 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npx chromiumfish fetch # download + cache the browser build
|
|
|
12
12
|
```javascript
|
|
13
13
|
import { ChromiumFish } from "chromiumfish";
|
|
14
14
|
|
|
15
|
-
const browser = await ChromiumFish({ personaSeed:
|
|
15
|
+
const browser = await ChromiumFish({ personaSeed: "alpha-7", headless: true });
|
|
16
16
|
const page = await browser.newPage();
|
|
17
17
|
await page.goto("https://abrahamjuliot.github.io/creepjs/");
|
|
18
18
|
await page.screenshot({ path: "fp.png" });
|
|
@@ -26,7 +26,7 @@ await browser.close();
|
|
|
26
26
|
|
|
27
27
|
| Option | Default | Description |
|
|
28
28
|
|--------|---------|-------------|
|
|
29
|
-
| `personaSeed` | — |
|
|
29
|
+
| `personaSeed` | — | String id for a stable, internally-consistent fingerprint persona (any string; a number works too). |
|
|
30
30
|
| `headless` | `true` | Run headless (SwiftShader). |
|
|
31
31
|
| `proxy` | — | Playwright proxy object, e.g. `{ server, username, password }`. |
|
|
32
32
|
| `windowSize` | `[1920, 1080]` | Window dimensions (`null` to omit). |
|
package/dist/fetch.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as fs from "node:fs";
|
|
|
11
11
|
import * as https from "node:https";
|
|
12
12
|
import * as os from "node:os";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
-
import { browserVersion, releaseBaseUrl } from "./version.js";
|
|
14
|
+
import { assertSafeVersion, browserVersion, releaseBaseUrl } from "./version.js";
|
|
15
15
|
export function cacheRoot() {
|
|
16
16
|
const env = process.env.CHROMIUMFISH_CACHE_DIR;
|
|
17
17
|
if (env)
|
|
@@ -35,12 +35,13 @@ export function platformSlug() {
|
|
|
35
35
|
throw new Error(`unsupported platform: ${process.platform}`);
|
|
36
36
|
}
|
|
37
37
|
function assetName(version) {
|
|
38
|
+
assertSafeVersion(version);
|
|
38
39
|
const slug = platformSlug();
|
|
39
40
|
const ext = slug.startsWith("win") ? "zip" : "tar.gz";
|
|
40
41
|
return `chromiumfish-${version}-${slug}.${ext}`;
|
|
41
42
|
}
|
|
42
43
|
export function installDir(version = browserVersion()) {
|
|
43
|
-
return path.join(cacheRoot(), version, platformSlug());
|
|
44
|
+
return path.join(cacheRoot(), assertSafeVersion(version), platformSlug());
|
|
44
45
|
}
|
|
45
46
|
const BINARY_NAMES = ["chromiumfish", "chrome", "chromiumfish.exe", "chrome.exe", "ChromiumFish"];
|
|
46
47
|
export function findBinary(root) {
|
|
@@ -48,8 +49,15 @@ export function findBinary(root) {
|
|
|
48
49
|
return null;
|
|
49
50
|
for (const name of BINARY_NAMES) {
|
|
50
51
|
const direct = path.join(root, name);
|
|
51
|
-
if
|
|
52
|
-
|
|
52
|
+
// statSync can throw if the file is removed between existsSync and stat
|
|
53
|
+
// (TOCTOU); treat any stat failure as "not a usable binary here".
|
|
54
|
+
try {
|
|
55
|
+
if (fs.statSync(direct).isFile())
|
|
56
|
+
return direct;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* keep looking */
|
|
60
|
+
}
|
|
53
61
|
}
|
|
54
62
|
const stack = [root];
|
|
55
63
|
while (stack.length) {
|
|
@@ -64,11 +72,15 @@ export function findBinary(root) {
|
|
|
64
72
|
}
|
|
65
73
|
return null;
|
|
66
74
|
}
|
|
75
|
+
// Idle-timeout (ms) applied to every download/fetch socket. A stalled server
|
|
76
|
+
// (no bytes for this long) aborts instead of hanging the launch forever.
|
|
77
|
+
const DOWNLOAD_IDLE_TIMEOUT_MS = 60_000;
|
|
78
|
+
const FETCH_IDLE_TIMEOUT_MS = 30_000;
|
|
67
79
|
function download(url, dest) {
|
|
68
80
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
69
81
|
return new Promise((resolve, reject) => {
|
|
70
82
|
const get = (u) => {
|
|
71
|
-
https.get(u, (res) => {
|
|
83
|
+
const req = https.get(u, (res) => {
|
|
72
84
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
73
85
|
res.resume();
|
|
74
86
|
return get(res.headers.location);
|
|
@@ -80,15 +92,31 @@ function download(url, dest) {
|
|
|
80
92
|
const total = Number(res.headers["content-length"] || 0);
|
|
81
93
|
let read = 0;
|
|
82
94
|
const out = fs.createWriteStream(dest);
|
|
95
|
+
// On any failure: tear down both streams and remove the partial file
|
|
96
|
+
// so a later run doesn't trip over a truncated/corrupt download.
|
|
97
|
+
const fail = (err) => {
|
|
98
|
+
res.destroy();
|
|
99
|
+
out.destroy();
|
|
100
|
+
try {
|
|
101
|
+
fs.rmSync(dest, { force: true });
|
|
102
|
+
}
|
|
103
|
+
catch { /* best effort */ }
|
|
104
|
+
reject(err);
|
|
105
|
+
};
|
|
83
106
|
res.on("data", (c) => {
|
|
84
107
|
read += c.length;
|
|
85
108
|
if (total)
|
|
86
109
|
process.stderr.write(`\r[chromiumfish] ${Math.floor((read / total) * 100)}%`);
|
|
87
110
|
});
|
|
111
|
+
res.on("error", fail);
|
|
88
112
|
res.pipe(out);
|
|
89
113
|
out.on("finish", () => { process.stderr.write("\n"); out.close(() => resolve()); });
|
|
90
|
-
out.on("error",
|
|
91
|
-
})
|
|
114
|
+
out.on("error", fail);
|
|
115
|
+
});
|
|
116
|
+
req.on("error", reject);
|
|
117
|
+
req.setTimeout(DOWNLOAD_IDLE_TIMEOUT_MS, () => {
|
|
118
|
+
req.destroy(new Error(`download timed out (no data for ${DOWNLOAD_IDLE_TIMEOUT_MS}ms) for ${u}`));
|
|
119
|
+
});
|
|
92
120
|
};
|
|
93
121
|
process.stderr.write(`[chromiumfish] downloading ${url}\n`);
|
|
94
122
|
get(url);
|
|
@@ -96,20 +124,27 @@ function download(url, dest) {
|
|
|
96
124
|
}
|
|
97
125
|
function fetchText(url) {
|
|
98
126
|
return new Promise((resolve, reject) => {
|
|
99
|
-
const get = (u) =>
|
|
100
|
-
|
|
101
|
-
res.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
res.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
127
|
+
const get = (u) => {
|
|
128
|
+
const req = https.get(u, (res) => {
|
|
129
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
130
|
+
res.resume();
|
|
131
|
+
return get(res.headers.location);
|
|
132
|
+
}
|
|
133
|
+
if (res.statusCode !== 200) {
|
|
134
|
+
res.resume();
|
|
135
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
136
|
+
}
|
|
137
|
+
let data = "";
|
|
138
|
+
res.setEncoding("utf8");
|
|
139
|
+
res.on("data", (c) => (data += c));
|
|
140
|
+
res.on("end", () => resolve(data));
|
|
141
|
+
res.on("error", reject);
|
|
142
|
+
});
|
|
143
|
+
req.on("error", reject);
|
|
144
|
+
req.setTimeout(FETCH_IDLE_TIMEOUT_MS, () => {
|
|
145
|
+
req.destroy(new Error(`request timed out for ${u}`));
|
|
146
|
+
});
|
|
147
|
+
};
|
|
113
148
|
get(url);
|
|
114
149
|
});
|
|
115
150
|
}
|
package/dist/ip2tz.js
CHANGED
|
@@ -18,10 +18,12 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { createHash } from "node:crypto";
|
|
20
20
|
import * as fs from "node:fs";
|
|
21
|
+
import * as http from "node:http";
|
|
21
22
|
import * as https from "node:https";
|
|
22
23
|
import * as path from "node:path";
|
|
24
|
+
import * as tls from "node:tls";
|
|
23
25
|
import { cacheRoot } from "./fetch.js";
|
|
24
|
-
import { GEOIP_FALLBACK_VERSION, geoipBaseUrl, geoipLatestManifestUrl, geoipVersion } from "./version.js";
|
|
26
|
+
import { assertSafeVersion, GEOIP_FALLBACK_VERSION, geoipBaseUrl, geoipLatestManifestUrl, geoipVersion, } from "./version.js";
|
|
25
27
|
const MAGIC = Buffer.from("IP2TZ\x01", "latin1");
|
|
26
28
|
const V4_REC = 6; // uint32 start + uint16 tz_idx
|
|
27
29
|
const V6_REC = 18; // 16-byte start + uint16 tz_idx
|
|
@@ -89,33 +91,42 @@ export async function resolveVersion(version = geoipVersion(), download = true)
|
|
|
89
91
|
return cached?.version || GEOIP_FALLBACK_VERSION;
|
|
90
92
|
}
|
|
91
93
|
export function assetName(version = geoipVersion()) {
|
|
92
|
-
return `ip2tz-${resolveVersionSync(version)}.bin`;
|
|
94
|
+
return `ip2tz-${assertSafeVersion(resolveVersionSync(version))}.bin`;
|
|
93
95
|
}
|
|
94
96
|
export function dbPath(version = geoipVersion()) {
|
|
95
|
-
return path.join(geoipDir(), `ip2tz-${resolveVersionSync(version)}.bin`);
|
|
97
|
+
return path.join(geoipDir(), `ip2tz-${assertSafeVersion(resolveVersionSync(version))}.bin`);
|
|
96
98
|
}
|
|
99
|
+
// Idle-timeout (ms) for the geoip manifest / DB / checksum fetches so a
|
|
100
|
+
// stalled server can't hang resolution or download forever.
|
|
101
|
+
const GET_IDLE_TIMEOUT_MS = 30_000;
|
|
97
102
|
function get(url) {
|
|
98
103
|
return new Promise((resolve, reject) => {
|
|
99
|
-
const go = (u) =>
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
res.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
res.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
const go = (u) => {
|
|
105
|
+
const req = https
|
|
106
|
+
.get(u, (res) => {
|
|
107
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
108
|
+
res.resume();
|
|
109
|
+
return go(res.headers.location);
|
|
110
|
+
}
|
|
111
|
+
if (res.statusCode !== 200) {
|
|
112
|
+
res.resume();
|
|
113
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
114
|
+
}
|
|
115
|
+
const chunks = [];
|
|
116
|
+
res.on("data", (c) => chunks.push(c));
|
|
117
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
118
|
+
res.on("error", reject);
|
|
119
|
+
})
|
|
120
|
+
.on("error", reject);
|
|
121
|
+
req.setTimeout(GET_IDLE_TIMEOUT_MS, () => {
|
|
122
|
+
req.destroy(new Error(`request timed out for ${u}`));
|
|
123
|
+
});
|
|
124
|
+
};
|
|
114
125
|
go(url);
|
|
115
126
|
});
|
|
116
127
|
}
|
|
117
128
|
export async function fetchDb(version = geoipVersion(), force = false) {
|
|
118
|
-
const v = await resolveVersion(version); // concrete, e.g. "2026.06"
|
|
129
|
+
const v = assertSafeVersion(await resolveVersion(version)); // concrete, e.g. "2026.06"
|
|
119
130
|
const dest = path.join(geoipDir(), `ip2tz-${v}.bin`);
|
|
120
131
|
if (fs.existsSync(dest) && !force)
|
|
121
132
|
return dest;
|
|
@@ -256,28 +267,29 @@ function parseV6(ip) {
|
|
|
256
267
|
}
|
|
257
268
|
return b;
|
|
258
269
|
}
|
|
259
|
-
|
|
270
|
+
// Keyed by *resolved* concrete version so a later lookup with a different
|
|
271
|
+
// version doesn't silently reuse the first DB loaded.
|
|
272
|
+
const cache = new Map();
|
|
260
273
|
async function getDb(version = geoipVersion(), download = true) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
274
|
+
const v = assertSafeVersion(await resolveVersion(version, download));
|
|
275
|
+
const existing = cache.get(v);
|
|
276
|
+
if (existing)
|
|
277
|
+
return existing;
|
|
264
278
|
let p = path.join(geoipDir(), `ip2tz-${v}.bin`);
|
|
265
279
|
if (!fs.existsSync(p)) {
|
|
266
280
|
if (!download)
|
|
267
281
|
throw new Error("ip2tz DB not installed. Call fetchDb().");
|
|
268
282
|
p = await fetchDb(v);
|
|
269
283
|
}
|
|
270
|
-
|
|
271
|
-
|
|
284
|
+
const db = Ip2TzDB.load(p);
|
|
285
|
+
cache.set(v, db);
|
|
286
|
+
return db;
|
|
272
287
|
}
|
|
273
288
|
export async function lookupTimezone(ip, version = geoipVersion(), download = true) {
|
|
274
289
|
return (await getDb(version, download)).lookup(ip);
|
|
275
290
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// a proxy is configured we currently probe direct and rely on the caller to
|
|
279
|
-
// pass an explicit IP if egress differs.
|
|
280
|
-
void proxy;
|
|
291
|
+
/** Probe the egress IP directly (no proxy). */
|
|
292
|
+
function egressDirect(timeoutMs) {
|
|
281
293
|
return new Promise((resolve) => {
|
|
282
294
|
const req = https.get(EGRESS_PROBE, { headers: { "User-Agent": "chromiumfish" } }, (res) => {
|
|
283
295
|
let data = "";
|
|
@@ -299,6 +311,101 @@ export function egressIp(proxy, timeoutMs = 8000) {
|
|
|
299
311
|
});
|
|
300
312
|
});
|
|
301
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Probe the egress IP *through an HTTP(S) proxy* by CONNECT-tunnelling to the
|
|
316
|
+
* probe host, so the resolved timezone matches the proxy's exit — the whole
|
|
317
|
+
* point of timezone:"auto". Mirrors what the Python SDK gets from urllib's
|
|
318
|
+
* ProxyHandler. Returns null on any failure (never falls back to a direct
|
|
319
|
+
* probe, which would report the machine's real-IP timezone).
|
|
320
|
+
*/
|
|
321
|
+
function egressViaProxy(proxy, timeoutMs) {
|
|
322
|
+
return new Promise((resolve) => {
|
|
323
|
+
let pu;
|
|
324
|
+
let tu;
|
|
325
|
+
try {
|
|
326
|
+
pu = new URL(proxy);
|
|
327
|
+
tu = new URL(EGRESS_PROBE);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return resolve(null);
|
|
331
|
+
}
|
|
332
|
+
let settled = false;
|
|
333
|
+
const done = (ip) => {
|
|
334
|
+
if (!settled) {
|
|
335
|
+
settled = true;
|
|
336
|
+
resolve(ip);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
const headers = {};
|
|
340
|
+
if (pu.username) {
|
|
341
|
+
const creds = `${decodeURIComponent(pu.username)}:${decodeURIComponent(pu.password)}`;
|
|
342
|
+
headers["Proxy-Authorization"] = "Basic " + Buffer.from(creds).toString("base64");
|
|
343
|
+
}
|
|
344
|
+
const connectReq = http.request({
|
|
345
|
+
host: pu.hostname,
|
|
346
|
+
port: Number(pu.port) || (pu.protocol === "https:" ? 443 : 80),
|
|
347
|
+
method: "CONNECT",
|
|
348
|
+
path: `${tu.hostname}:443`,
|
|
349
|
+
headers,
|
|
350
|
+
timeout: timeoutMs,
|
|
351
|
+
});
|
|
352
|
+
connectReq.on("connect", (res, socket) => {
|
|
353
|
+
if (res.statusCode !== 200) {
|
|
354
|
+
socket.destroy();
|
|
355
|
+
return done(null);
|
|
356
|
+
}
|
|
357
|
+
const tlsSock = tls.connect({ socket, servername: tu.hostname }, () => {
|
|
358
|
+
// HTTP/1.0 so the response isn't chunk-encoded (simpler to parse).
|
|
359
|
+
tlsSock.write(`GET ${tu.pathname} HTTP/1.0\r\nHost: ${tu.hostname}\r\n` +
|
|
360
|
+
`User-Agent: chromiumfish\r\nAccept: application/json\r\nConnection: close\r\n\r\n`);
|
|
361
|
+
});
|
|
362
|
+
let raw = "";
|
|
363
|
+
tlsSock.setEncoding("utf8");
|
|
364
|
+
tlsSock.setTimeout(timeoutMs, () => {
|
|
365
|
+
tlsSock.destroy();
|
|
366
|
+
done(null);
|
|
367
|
+
});
|
|
368
|
+
tlsSock.on("data", (d) => (raw += d));
|
|
369
|
+
tlsSock.on("end", () => {
|
|
370
|
+
const body = raw.split("\r\n\r\n").slice(1).join("\r\n\r\n");
|
|
371
|
+
const m = body.match(/\{[\s\S]*\}/);
|
|
372
|
+
try {
|
|
373
|
+
done(m ? JSON.parse(m[0]).ip || null : null);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
done(null);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
tlsSock.on("error", () => done(null));
|
|
380
|
+
});
|
|
381
|
+
connectReq.on("error", () => done(null));
|
|
382
|
+
connectReq.on("timeout", () => {
|
|
383
|
+
connectReq.destroy();
|
|
384
|
+
done(null);
|
|
385
|
+
});
|
|
386
|
+
connectReq.end();
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
export function egressIp(proxy, timeoutMs = 8000) {
|
|
390
|
+
if (proxy) {
|
|
391
|
+
let scheme = "";
|
|
392
|
+
try {
|
|
393
|
+
scheme = new URL(proxy).protocol;
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
/* invalid proxy URL */
|
|
397
|
+
}
|
|
398
|
+
if (scheme === "http:" || scheme === "https:")
|
|
399
|
+
return egressViaProxy(proxy, timeoutMs);
|
|
400
|
+
// SOCKS / unknown schemes aren't supported for the probe. Return null
|
|
401
|
+
// (leave the timezone unset) rather than probing the direct connection
|
|
402
|
+
// and reporting the machine's real-IP timezone — the incoherence we want
|
|
403
|
+
// to avoid.
|
|
404
|
+
process.stderr.write(`[chromiumfish] egress probe: unsupported proxy scheme '${scheme || proxy}'; skipping timezone resolution\n`);
|
|
405
|
+
return Promise.resolve(null);
|
|
406
|
+
}
|
|
407
|
+
return egressDirect(timeoutMs);
|
|
408
|
+
}
|
|
302
409
|
export async function resolveTimezone(opts = {}) {
|
|
303
410
|
const { proxy, version = geoipVersion(), download = true } = opts;
|
|
304
411
|
let ip = opts.ip;
|
package/dist/launcher.d.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { type Browser, type LaunchOptions } from "playwright-core";
|
|
|
6
6
|
*/
|
|
7
7
|
export declare const BASE_ARGS: string[];
|
|
8
8
|
export interface ChromiumFishOptions extends Omit<LaunchOptions, "executablePath"> {
|
|
9
|
-
/**
|
|
10
|
-
personaSeed?:
|
|
9
|
+
/** String id for a stable, internally-consistent fingerprint persona. */
|
|
10
|
+
personaSeed?: string;
|
|
11
11
|
/** Run headless (SwiftShader). Defaults to true. */
|
|
12
12
|
headless?: boolean;
|
|
13
13
|
/** Window dimensions; defaults to [1920, 1080]. Pass null to omit. */
|
|
@@ -28,6 +28,6 @@ export declare function buildArgs(opts: ChromiumFishOptions): string[];
|
|
|
28
28
|
* Launch ChromiumFish and return a standard Playwright `Browser`.
|
|
29
29
|
*
|
|
30
30
|
* import { ChromiumFish } from "chromiumfish";
|
|
31
|
-
* const browser = await ChromiumFish({ personaSeed:
|
|
31
|
+
* const browser = await ChromiumFish({ personaSeed: "alpha-7", headless: true });
|
|
32
32
|
*/
|
|
33
33
|
export declare function ChromiumFish(opts?: ChromiumFishOptions): Promise<Browser>;
|
package/dist/launcher.js
CHANGED
|
@@ -20,8 +20,12 @@ function proxyToUrl(proxy) {
|
|
|
20
20
|
return undefined;
|
|
21
21
|
const { server, username, password } = proxy;
|
|
22
22
|
if (username && server.includes("://")) {
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
const idx = server.indexOf("://");
|
|
24
|
+
const scheme = server.slice(0, idx);
|
|
25
|
+
const host = server.slice(idx + 3);
|
|
26
|
+
// Percent-encode credentials so a password containing ':' / '@' / '/'
|
|
27
|
+
// can't corrupt the URL; the egress probe decodes them again.
|
|
28
|
+
return `${scheme}://${encodeURIComponent(username)}:${encodeURIComponent(password ?? "")}@${host}`;
|
|
25
29
|
}
|
|
26
30
|
return server;
|
|
27
31
|
}
|
|
@@ -40,7 +44,7 @@ export function buildArgs(opts) {
|
|
|
40
44
|
* Launch ChromiumFish and return a standard Playwright `Browser`.
|
|
41
45
|
*
|
|
42
46
|
* import { ChromiumFish } from "chromiumfish";
|
|
43
|
-
* const browser = await ChromiumFish({ personaSeed:
|
|
47
|
+
* const browser = await ChromiumFish({ personaSeed: "alpha-7", headless: true });
|
|
44
48
|
*/
|
|
45
49
|
export async function ChromiumFish(opts = {}) {
|
|
46
50
|
const { personaSeed, headless = true, windowSize, version, download = true, timezone, args, ...launch } = opts;
|
package/dist/version.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* SDK downloads by default; override it with `CHROMIUMFISH_VERSION`.
|
|
7
7
|
*/
|
|
8
8
|
/** SDK package version (kept in sync with package.json). */
|
|
9
|
-
export declare const SDK_VERSION = "0.1.
|
|
9
|
+
export declare const SDK_VERSION = "0.1.4";
|
|
10
10
|
/** Default ChromiumFish browser build to fetch. Matches src/chrome/VERSION. */
|
|
11
11
|
export declare const DEFAULT_BROWSER_VERSION = "150.0.7844";
|
|
12
12
|
/** Public repo hosting the release assets. */
|
|
@@ -26,6 +26,14 @@ export declare const DEFAULT_GEOIP_VERSION = "latest";
|
|
|
26
26
|
* pointer). Bump occasionally so the offline floor stays recent.
|
|
27
27
|
*/
|
|
28
28
|
export declare const GEOIP_FALLBACK_VERSION = "2026.06";
|
|
29
|
+
/**
|
|
30
|
+
* Reject version strings that aren't a plain build tag. Versions are
|
|
31
|
+
* interpolated into filesystem cache paths and release URLs, so a crafted
|
|
32
|
+
* value like `../../../etc` would escape the cache dir (path traversal).
|
|
33
|
+
* Real tags are digits, dots, and hyphens (e.g. "150.0.7844", "2026.06",
|
|
34
|
+
* "latest").
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertSafeVersion(version: string): string;
|
|
29
37
|
export declare function browserVersion(): string;
|
|
30
38
|
export declare function releaseBaseUrl(version?: string): string;
|
|
31
39
|
export declare function geoipVersion(): string;
|
package/dist/version.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* SDK downloads by default; override it with `CHROMIUMFISH_VERSION`.
|
|
7
7
|
*/
|
|
8
8
|
/** SDK package version (kept in sync with package.json). */
|
|
9
|
-
export const SDK_VERSION = "0.1.
|
|
9
|
+
export const SDK_VERSION = "0.1.4";
|
|
10
10
|
/** Default ChromiumFish browser build to fetch. Matches src/chrome/VERSION. */
|
|
11
11
|
export const DEFAULT_BROWSER_VERSION = "150.0.7844";
|
|
12
12
|
/** Public repo hosting the release assets. */
|
|
@@ -26,14 +26,27 @@ export const DEFAULT_GEOIP_VERSION = "latest";
|
|
|
26
26
|
* pointer). Bump occasionally so the offline floor stays recent.
|
|
27
27
|
*/
|
|
28
28
|
export const GEOIP_FALLBACK_VERSION = "2026.06";
|
|
29
|
+
/**
|
|
30
|
+
* Reject version strings that aren't a plain build tag. Versions are
|
|
31
|
+
* interpolated into filesystem cache paths and release URLs, so a crafted
|
|
32
|
+
* value like `../../../etc` would escape the cache dir (path traversal).
|
|
33
|
+
* Real tags are digits, dots, and hyphens (e.g. "150.0.7844", "2026.06",
|
|
34
|
+
* "latest").
|
|
35
|
+
*/
|
|
36
|
+
export function assertSafeVersion(version) {
|
|
37
|
+
if (!/^[A-Za-z0-9._-]+$/.test(version) || version === "." || version === "..") {
|
|
38
|
+
throw new Error(`invalid version string: ${JSON.stringify(version)}`);
|
|
39
|
+
}
|
|
40
|
+
return version;
|
|
41
|
+
}
|
|
29
42
|
export function browserVersion() {
|
|
30
|
-
return process.env.CHROMIUMFISH_VERSION || DEFAULT_BROWSER_VERSION;
|
|
43
|
+
return assertSafeVersion(process.env.CHROMIUMFISH_VERSION || DEFAULT_BROWSER_VERSION);
|
|
31
44
|
}
|
|
32
45
|
export function releaseBaseUrl(version = browserVersion()) {
|
|
33
46
|
return `https://github.com/${RELEASE_REPO}/releases/download/v${version}`;
|
|
34
47
|
}
|
|
35
48
|
export function geoipVersion() {
|
|
36
|
-
return process.env.CHROMIUMFISH_GEOIP_VERSION || DEFAULT_GEOIP_VERSION;
|
|
49
|
+
return assertSafeVersion(process.env.CHROMIUMFISH_GEOIP_VERSION || DEFAULT_GEOIP_VERSION);
|
|
37
50
|
}
|
|
38
51
|
export function geoipBaseUrl(version = geoipVersion()) {
|
|
39
52
|
return `https://github.com/${RELEASE_REPO}/releases/download/geoip-${version}`;
|
package/package.json
CHANGED