chromiumfish 0.1.0 → 0.1.3
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 +26 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/ip2tz.d.ts +31 -0
- package/dist/ip2tz.js +311 -0
- package/dist/launcher.d.ts +6 -0
- package/dist/launcher.js +29 -1
- package/dist/version.d.ts +20 -1
- package/dist/version.js +26 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,9 +32,27 @@ await browser.close();
|
|
|
32
32
|
| `windowSize` | `[1920, 1080]` | Window dimensions (`null` to omit). |
|
|
33
33
|
| `version` | pinned | Override the browser build version. |
|
|
34
34
|
| `download` | `true` | Auto-download the build if missing. |
|
|
35
|
+
| `timezone` | — | `"auto"` resolves the egress IP's IANA timezone via the downloadable `ip2tz` DB and sets the browser's `TZ`; an IANA string (e.g. `"Europe/Berlin"`) is used verbatim. |
|
|
35
36
|
| `args` | — | Extra Chromium flags. |
|
|
36
37
|
| _...rest_ | — | Forwarded to `chromium.launch()`. |
|
|
37
38
|
|
|
39
|
+
### IP-to-Timezone
|
|
40
|
+
|
|
41
|
+
`timezone: "auto"` aligns the browser clock with the egress IP (handy behind a
|
|
42
|
+
proxy). It uses a compact `ip2tz` database downloaded once and cached; you can
|
|
43
|
+
also query it directly:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { lookupTimezone, resolveTimezone } from "chromiumfish";
|
|
47
|
+
|
|
48
|
+
await lookupTimezone("8.8.8.8"); // -> "America/Los_Angeles"
|
|
49
|
+
await resolveTimezone(); // own egress IP -> timezone
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The DB auto-updates: it tracks the `latest` monthly build (cached, re-checked
|
|
53
|
+
weekly), so you get fresh data without upgrading the SDK. Pin a fixed version
|
|
54
|
+
with `CHROMIUMFISH_GEOIP_VERSION=2026.06` for reproducibility.
|
|
55
|
+
|
|
38
56
|
## CLI
|
|
39
57
|
|
|
40
58
|
```bash
|
|
@@ -47,6 +65,14 @@ npx chromiumfish --version
|
|
|
47
65
|
Builds are cached under `~/.cache/chromiumfish/<version>/` (override with
|
|
48
66
|
`CHROMIUMFISH_CACHE_DIR`). Pin a build with `CHROMIUMFISH_VERSION`.
|
|
49
67
|
|
|
68
|
+
## Attribution
|
|
69
|
+
|
|
70
|
+
IP Geolocation by <a href='https://db-ip.com'>DB-IP</a> — the `ip2tz` timezone
|
|
71
|
+
database is derived from [DB-IP City Lite][dbip], used under [CC BY 4.0][ccby].
|
|
72
|
+
|
|
73
|
+
[dbip]: https://db-ip.com/db/download/ip-to-city-lite
|
|
74
|
+
[ccby]: https://creativecommons.org/licenses/by/4.0/
|
|
75
|
+
|
|
50
76
|
## License
|
|
51
77
|
|
|
52
78
|
MIT © Arman Hossain. See the [repository](https://github.com/arman-bd/chromiumfish).
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ChromiumFish, buildArgs, BASE_ARGS } from "./launcher.js";
|
|
2
2
|
export type { ChromiumFishOptions } from "./launcher.js";
|
|
3
3
|
export { fetchBrowser, binaryPath, installDir, cacheRoot, platformSlug, findBinary } from "./fetch.js";
|
|
4
|
-
export {
|
|
4
|
+
export { Ip2TzDB, fetchDb, lookupTimezone, resolveTimezone, resolveVersion as resolveGeoipVersion, egressIp, assetName as ip2tzAssetName, dbPath as ip2tzDbPath, } from "./ip2tz.js";
|
|
5
|
+
export { SDK_VERSION, DEFAULT_BROWSER_VERSION, DEFAULT_GEOIP_VERSION, GEOIP_FALLBACK_VERSION, RELEASE_REPO, browserVersion, releaseBaseUrl, geoipVersion, geoipBaseUrl, } from "./version.js";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { ChromiumFish, buildArgs, BASE_ARGS } from "./launcher.js";
|
|
2
2
|
export { fetchBrowser, binaryPath, installDir, cacheRoot, platformSlug, findBinary } from "./fetch.js";
|
|
3
|
-
export {
|
|
3
|
+
export { Ip2TzDB, fetchDb, lookupTimezone, resolveTimezone, resolveVersion as resolveGeoipVersion, egressIp, assetName as ip2tzAssetName, dbPath as ip2tzDbPath, } from "./ip2tz.js";
|
|
4
|
+
export { SDK_VERSION, DEFAULT_BROWSER_VERSION, DEFAULT_GEOIP_VERSION, GEOIP_FALLBACK_VERSION, RELEASE_REPO, browserVersion, releaseBaseUrl, geoipVersion, geoipBaseUrl, } from "./version.js";
|
package/dist/ip2tz.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a configured version (possibly the `"latest"` sentinel) to a concrete
|
|
3
|
+
* version like `"2026.06"`. Uses a cached pointer while fresh (< TTL); otherwise
|
|
4
|
+
* fetches the geoip-latest manifest. Falls back to a stale pointer, then to the
|
|
5
|
+
* compiled-in floor, so resolution never hard-fails offline.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveVersion(version?: string, download?: boolean): Promise<string>;
|
|
8
|
+
export declare function assetName(version?: string): string;
|
|
9
|
+
export declare function dbPath(version?: string): string;
|
|
10
|
+
export declare function fetchDb(version?: string, force?: boolean): Promise<string>;
|
|
11
|
+
export declare class Ip2TzDB {
|
|
12
|
+
private tz;
|
|
13
|
+
private v4;
|
|
14
|
+
private v6;
|
|
15
|
+
private v4Count;
|
|
16
|
+
private v6Count;
|
|
17
|
+
readonly buildEpoch: number;
|
|
18
|
+
constructor(blob: Buffer);
|
|
19
|
+
static load(file: string): Ip2TzDB;
|
|
20
|
+
/** Index of the rightmost record whose start <= key (fixed-width BE compare). */
|
|
21
|
+
private static rightmost;
|
|
22
|
+
lookup(ip: string): string | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function lookupTimezone(ip: string, version?: string, download?: boolean): Promise<string | null>;
|
|
25
|
+
export declare function egressIp(proxy?: string, timeoutMs?: number): Promise<string | null>;
|
|
26
|
+
export declare function resolveTimezone(opts?: {
|
|
27
|
+
ip?: string;
|
|
28
|
+
proxy?: string;
|
|
29
|
+
version?: string;
|
|
30
|
+
download?: boolean;
|
|
31
|
+
}): Promise<string | null>;
|
package/dist/ip2tz.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IP-to-Timezone lookup backed by the downloadable `ip2tz` release asset.
|
|
3
|
+
*
|
|
4
|
+
* IP Geolocation by DB-IP (https://db-ip.com), CC BY 4.0.
|
|
5
|
+
*
|
|
6
|
+
* The asset is built offline by `packages/geoip/build_ip2tz.py` from DB-IP City
|
|
7
|
+
* Lite and published to GitHub Releases. At runtime this module downloads it
|
|
8
|
+
* once (SHA-256 verified, cached next to the browser build), then answers
|
|
9
|
+
* IP -> IANA timezone with a binary search over the raw record bytes — no
|
|
10
|
+
* per-call network round trip.
|
|
11
|
+
*
|
|
12
|
+
* import { lookupTimezone, resolveTimezone } from "chromiumfish";
|
|
13
|
+
* await lookupTimezone("8.8.8.8"); // -> "America/Los_Angeles"
|
|
14
|
+
* await resolveTimezone(); // probe own egress IP -> its timezone
|
|
15
|
+
*
|
|
16
|
+
* Binary format is documented in build_ip2tz.py; reader and builder must stay
|
|
17
|
+
* in lock-step.
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as https from "node:https";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import { cacheRoot } from "./fetch.js";
|
|
24
|
+
import { GEOIP_FALLBACK_VERSION, geoipBaseUrl, geoipLatestManifestUrl, geoipVersion } from "./version.js";
|
|
25
|
+
const MAGIC = Buffer.from("IP2TZ\x01", "latin1");
|
|
26
|
+
const V4_REC = 6; // uint32 start + uint16 tz_idx
|
|
27
|
+
const V6_REC = 18; // 16-byte start + uint16 tz_idx
|
|
28
|
+
const EGRESS_PROBE = "https://ipinfo.io/json";
|
|
29
|
+
const LATEST = "latest";
|
|
30
|
+
// Seconds a resolved "latest" pointer is trusted before re-checking the
|
|
31
|
+
// manifest. Override with CHROMIUMFISH_GEOIP_TTL.
|
|
32
|
+
const LATEST_TTL = Number(process.env.CHROMIUMFISH_GEOIP_TTL) || 7 * 24 * 3600;
|
|
33
|
+
function geoipDir() {
|
|
34
|
+
return path.join(cacheRoot(), "geoip");
|
|
35
|
+
}
|
|
36
|
+
function pointerPath() {
|
|
37
|
+
return path.join(geoipDir(), "latest.json");
|
|
38
|
+
}
|
|
39
|
+
function readPointer() {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(fs.readFileSync(pointerPath(), "utf8"));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function pointerFresh() {
|
|
48
|
+
try {
|
|
49
|
+
return Date.now() - fs.statSync(pointerPath()).mtimeMs < LATEST_TTL * 1000;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Best-effort resolution with NO network: fresh cached pointer, else floor. */
|
|
56
|
+
function resolveVersionSync(version = geoipVersion()) {
|
|
57
|
+
if (version !== LATEST)
|
|
58
|
+
return version;
|
|
59
|
+
const p = readPointer();
|
|
60
|
+
if (p?.version && pointerFresh())
|
|
61
|
+
return p.version;
|
|
62
|
+
return p?.version || GEOIP_FALLBACK_VERSION;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a configured version (possibly the `"latest"` sentinel) to a concrete
|
|
66
|
+
* version like `"2026.06"`. Uses a cached pointer while fresh (< TTL); otherwise
|
|
67
|
+
* fetches the geoip-latest manifest. Falls back to a stale pointer, then to the
|
|
68
|
+
* compiled-in floor, so resolution never hard-fails offline.
|
|
69
|
+
*/
|
|
70
|
+
export async function resolveVersion(version = geoipVersion(), download = true) {
|
|
71
|
+
if (version !== LATEST)
|
|
72
|
+
return version;
|
|
73
|
+
const cached = readPointer();
|
|
74
|
+
if (cached?.version && pointerFresh())
|
|
75
|
+
return cached.version;
|
|
76
|
+
if (download) {
|
|
77
|
+
try {
|
|
78
|
+
const manifest = JSON.parse((await get(geoipLatestManifestUrl())).toString("utf8"));
|
|
79
|
+
if (manifest?.version) {
|
|
80
|
+
fs.mkdirSync(geoipDir(), { recursive: true });
|
|
81
|
+
fs.writeFileSync(pointerPath(), JSON.stringify(manifest));
|
|
82
|
+
return manifest.version;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
process.stderr.write(`[chromiumfish] could not resolve latest ip2tz version: ${e?.message || e}\n`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return cached?.version || GEOIP_FALLBACK_VERSION;
|
|
90
|
+
}
|
|
91
|
+
export function assetName(version = geoipVersion()) {
|
|
92
|
+
return `ip2tz-${resolveVersionSync(version)}.bin`;
|
|
93
|
+
}
|
|
94
|
+
export function dbPath(version = geoipVersion()) {
|
|
95
|
+
return path.join(geoipDir(), `ip2tz-${resolveVersionSync(version)}.bin`);
|
|
96
|
+
}
|
|
97
|
+
function get(url) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const go = (u) => https
|
|
100
|
+
.get(u, (res) => {
|
|
101
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
102
|
+
res.resume();
|
|
103
|
+
return go(res.headers.location);
|
|
104
|
+
}
|
|
105
|
+
if (res.statusCode !== 200) {
|
|
106
|
+
res.resume();
|
|
107
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
108
|
+
}
|
|
109
|
+
const chunks = [];
|
|
110
|
+
res.on("data", (c) => chunks.push(c));
|
|
111
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
112
|
+
})
|
|
113
|
+
.on("error", reject);
|
|
114
|
+
go(url);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export async function fetchDb(version = geoipVersion(), force = false) {
|
|
118
|
+
const v = await resolveVersion(version); // concrete, e.g. "2026.06"
|
|
119
|
+
const dest = path.join(geoipDir(), `ip2tz-${v}.bin`);
|
|
120
|
+
if (fs.existsSync(dest) && !force)
|
|
121
|
+
return dest;
|
|
122
|
+
const url = `${geoipBaseUrl(v)}/ip2tz-${v}.bin`;
|
|
123
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
124
|
+
process.stderr.write(`[chromiumfish] downloading ${url}\n`);
|
|
125
|
+
const blob = await get(url);
|
|
126
|
+
try {
|
|
127
|
+
const expected = (await get(`${url}.sha256`)).toString("utf8").trim().split(/\s+/)[0];
|
|
128
|
+
const actual = createHash("sha256").update(blob).digest("hex");
|
|
129
|
+
if (actual !== expected)
|
|
130
|
+
throw new Error(`ip2tz checksum mismatch: ${actual} !== ${expected}`);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
if (String(e?.message || e).includes("HTTP"))
|
|
134
|
+
process.stderr.write("[chromiumfish] warning: no ip2tz .sha256 published, skipping verify\n");
|
|
135
|
+
else
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
const tmp = `${dest}.part`;
|
|
139
|
+
fs.writeFileSync(tmp, blob);
|
|
140
|
+
fs.renameSync(tmp, dest);
|
|
141
|
+
return dest;
|
|
142
|
+
}
|
|
143
|
+
export class Ip2TzDB {
|
|
144
|
+
tz = [];
|
|
145
|
+
v4;
|
|
146
|
+
v6;
|
|
147
|
+
v4Count;
|
|
148
|
+
v6Count;
|
|
149
|
+
buildEpoch;
|
|
150
|
+
constructor(blob) {
|
|
151
|
+
if (!blob.subarray(0, MAGIC.length).equals(MAGIC))
|
|
152
|
+
throw new Error("not an ip2tz database (bad magic)");
|
|
153
|
+
this.buildEpoch = blob.readUInt32BE(6);
|
|
154
|
+
const tzCount = blob.readUInt16BE(10);
|
|
155
|
+
this.v4Count = blob.readUInt32BE(12);
|
|
156
|
+
this.v6Count = blob.readUInt32BE(16);
|
|
157
|
+
let off = 20;
|
|
158
|
+
for (let i = 0; i < tzCount; i++) {
|
|
159
|
+
const len = blob.readUInt8(off);
|
|
160
|
+
off += 1;
|
|
161
|
+
this.tz.push(blob.toString("utf8", off, off + len));
|
|
162
|
+
off += len;
|
|
163
|
+
}
|
|
164
|
+
this.v4 = blob.subarray(off, off + this.v4Count * V4_REC);
|
|
165
|
+
off += this.v4Count * V4_REC;
|
|
166
|
+
this.v6 = blob.subarray(off, off + this.v6Count * V6_REC);
|
|
167
|
+
}
|
|
168
|
+
static load(file) {
|
|
169
|
+
return new Ip2TzDB(fs.readFileSync(file));
|
|
170
|
+
}
|
|
171
|
+
/** Index of the rightmost record whose start <= key (fixed-width BE compare). */
|
|
172
|
+
static rightmost(block, count, rec, keylen, key) {
|
|
173
|
+
let lo = 0;
|
|
174
|
+
let hi = count;
|
|
175
|
+
while (lo < hi) {
|
|
176
|
+
const mid = (lo + hi) >>> 1;
|
|
177
|
+
const base = mid * rec;
|
|
178
|
+
// block.compare(key, ...) returns sign(record_start - key); advance when
|
|
179
|
+
// record_start <= key so we land on the rightmost such record.
|
|
180
|
+
if (block.compare(key, 0, keylen, base, base + keylen) <= 0)
|
|
181
|
+
lo = mid + 1;
|
|
182
|
+
else
|
|
183
|
+
hi = mid;
|
|
184
|
+
}
|
|
185
|
+
return lo - 1;
|
|
186
|
+
}
|
|
187
|
+
lookup(ip) {
|
|
188
|
+
const v4 = parseV4(ip);
|
|
189
|
+
if (v4) {
|
|
190
|
+
const i = Ip2TzDB.rightmost(this.v4, this.v4Count, V4_REC, 4, v4);
|
|
191
|
+
if (i < 0)
|
|
192
|
+
return null;
|
|
193
|
+
return this.tz[this.v4.readUInt16BE(i * V4_REC + 4)] || null;
|
|
194
|
+
}
|
|
195
|
+
const v6 = parseV6(ip);
|
|
196
|
+
if (v6) {
|
|
197
|
+
// IPv4-mapped (::ffff:a.b.c.d) lives in the v4 subtree — route it there.
|
|
198
|
+
const mapped = v6.readBigUInt64BE(0) === 0n && v6.readUInt16BE(8) === 0 && v6.readUInt16BE(10) === 0xffff;
|
|
199
|
+
if (mapped) {
|
|
200
|
+
const key = v6.subarray(12, 16);
|
|
201
|
+
const i = Ip2TzDB.rightmost(this.v4, this.v4Count, V4_REC, 4, key);
|
|
202
|
+
return i < 0 ? null : this.tz[this.v4.readUInt16BE(i * V4_REC + 4)] || null;
|
|
203
|
+
}
|
|
204
|
+
const i = Ip2TzDB.rightmost(this.v6, this.v6Count, V6_REC, 16, v6);
|
|
205
|
+
if (i < 0)
|
|
206
|
+
return null;
|
|
207
|
+
return this.tz[this.v6.readUInt16BE(i * V6_REC + 16)] || null;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** 4-byte BE buffer for a dotted IPv4, or null. */
|
|
213
|
+
function parseV4(ip) {
|
|
214
|
+
const m = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
215
|
+
if (!m)
|
|
216
|
+
return null;
|
|
217
|
+
const b = Buffer.alloc(4);
|
|
218
|
+
for (let i = 0; i < 4; i++) {
|
|
219
|
+
const n = Number(m[i + 1]);
|
|
220
|
+
if (n > 255)
|
|
221
|
+
return null;
|
|
222
|
+
b[i] = n;
|
|
223
|
+
}
|
|
224
|
+
return b;
|
|
225
|
+
}
|
|
226
|
+
/** 16-byte BE buffer for an IPv6 string (handles ::, IPv4-mapped), or null. */
|
|
227
|
+
function parseV6(ip) {
|
|
228
|
+
if (!ip.includes(":"))
|
|
229
|
+
return null;
|
|
230
|
+
// Map ::ffff:1.2.3.4 form by expanding its trailing dotted quad to hex.
|
|
231
|
+
let s = ip;
|
|
232
|
+
const dotted = s.match(/(.*:)(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
233
|
+
if (dotted) {
|
|
234
|
+
const v4 = parseV4(`${dotted[2]}.${dotted[3]}.${dotted[4]}.${dotted[5]}`);
|
|
235
|
+
if (!v4)
|
|
236
|
+
return null;
|
|
237
|
+
s = dotted[1] + v4.subarray(0, 2).toString("hex") + ":" + v4.subarray(2, 4).toString("hex");
|
|
238
|
+
}
|
|
239
|
+
const halves = s.split("::");
|
|
240
|
+
if (halves.length > 2)
|
|
241
|
+
return null;
|
|
242
|
+
const head = halves[0] ? halves[0].split(":") : [];
|
|
243
|
+
const tail = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
|
|
244
|
+
const fill = 8 - head.length - tail.length;
|
|
245
|
+
if (fill < 0 || (halves.length === 1 && head.length !== 8))
|
|
246
|
+
return null;
|
|
247
|
+
const groups = [...head, ...Array(halves.length === 2 ? fill : 0).fill("0"), ...tail];
|
|
248
|
+
if (groups.length !== 8)
|
|
249
|
+
return null;
|
|
250
|
+
const b = Buffer.alloc(16);
|
|
251
|
+
for (let i = 0; i < 8; i++) {
|
|
252
|
+
const n = parseInt(groups[i] || "0", 16);
|
|
253
|
+
if (Number.isNaN(n) || n > 0xffff)
|
|
254
|
+
return null;
|
|
255
|
+
b.writeUInt16BE(n, i * 2);
|
|
256
|
+
}
|
|
257
|
+
return b;
|
|
258
|
+
}
|
|
259
|
+
let cached = null;
|
|
260
|
+
async function getDb(version = geoipVersion(), download = true) {
|
|
261
|
+
if (cached)
|
|
262
|
+
return cached;
|
|
263
|
+
const v = await resolveVersion(version, download);
|
|
264
|
+
let p = path.join(geoipDir(), `ip2tz-${v}.bin`);
|
|
265
|
+
if (!fs.existsSync(p)) {
|
|
266
|
+
if (!download)
|
|
267
|
+
throw new Error("ip2tz DB not installed. Call fetchDb().");
|
|
268
|
+
p = await fetchDb(v);
|
|
269
|
+
}
|
|
270
|
+
cached = Ip2TzDB.load(p);
|
|
271
|
+
return cached;
|
|
272
|
+
}
|
|
273
|
+
export async function lookupTimezone(ip, version = geoipVersion(), download = true) {
|
|
274
|
+
return (await getDb(version, download)).lookup(ip);
|
|
275
|
+
}
|
|
276
|
+
export function egressIp(proxy, timeoutMs = 8000) {
|
|
277
|
+
// Note: proxy support for the probe would require an https-proxy agent; when
|
|
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;
|
|
281
|
+
return new Promise((resolve) => {
|
|
282
|
+
const req = https.get(EGRESS_PROBE, { headers: { "User-Agent": "chromiumfish" } }, (res) => {
|
|
283
|
+
let data = "";
|
|
284
|
+
res.setEncoding("utf8");
|
|
285
|
+
res.on("data", (c) => (data += c));
|
|
286
|
+
res.on("end", () => {
|
|
287
|
+
try {
|
|
288
|
+
resolve(JSON.parse(data).ip || null);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
resolve(null);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
req.on("error", () => resolve(null));
|
|
296
|
+
req.setTimeout(timeoutMs, () => {
|
|
297
|
+
req.destroy();
|
|
298
|
+
resolve(null);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
export async function resolveTimezone(opts = {}) {
|
|
303
|
+
const { proxy, version = geoipVersion(), download = true } = opts;
|
|
304
|
+
let ip = opts.ip;
|
|
305
|
+
if (!ip) {
|
|
306
|
+
ip = (await egressIp(proxy)) || undefined;
|
|
307
|
+
if (!ip)
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return lookupTimezone(ip, version, download);
|
|
311
|
+
}
|
package/dist/launcher.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ export interface ChromiumFishOptions extends Omit<LaunchOptions, "executablePath
|
|
|
16
16
|
version?: string;
|
|
17
17
|
/** Auto-download the build if missing. Defaults to true. */
|
|
18
18
|
download?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Timezone handling. `"auto"` probes the egress IP and resolves it against
|
|
21
|
+
* the downloadable ip2tz DB; an IANA string (e.g. `"Europe/Berlin"`) is used
|
|
22
|
+
* verbatim; omit to leave the timezone untouched.
|
|
23
|
+
*/
|
|
24
|
+
timezone?: string;
|
|
19
25
|
}
|
|
20
26
|
export declare function buildArgs(opts: ChromiumFishOptions): string[];
|
|
21
27
|
/**
|
package/dist/launcher.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { chromium } from "playwright-core";
|
|
2
2
|
import { binaryPath } from "./fetch.js";
|
|
3
|
+
import { resolveTimezone } from "./ip2tz.js";
|
|
3
4
|
/**
|
|
4
5
|
* Flags that keep the GPU-less / SwiftShader path working and the persona
|
|
5
6
|
* engine happy. Mirrors the production launch_lean.sh defaults (minus anything
|
|
@@ -13,6 +14,17 @@ export const BASE_ARGS = [
|
|
|
13
14
|
"--use-angle=swiftshader",
|
|
14
15
|
"--enable-unsafe-swiftshader",
|
|
15
16
|
];
|
|
17
|
+
/** Flatten a Playwright proxy option into a probe URL, or undefined. */
|
|
18
|
+
function proxyToUrl(proxy) {
|
|
19
|
+
if (!proxy?.server)
|
|
20
|
+
return undefined;
|
|
21
|
+
const { server, username, password } = proxy;
|
|
22
|
+
if (username && server.includes("://")) {
|
|
23
|
+
const [scheme, host] = server.split("://", 2);
|
|
24
|
+
return `${scheme}://${username}:${password ?? ""}@${host}`;
|
|
25
|
+
}
|
|
26
|
+
return server;
|
|
27
|
+
}
|
|
16
28
|
export function buildArgs(opts) {
|
|
17
29
|
const args = [...BASE_ARGS];
|
|
18
30
|
if (opts.personaSeed !== undefined)
|
|
@@ -31,12 +43,28 @@ export function buildArgs(opts) {
|
|
|
31
43
|
* const browser = await ChromiumFish({ personaSeed: 27182, headless: true });
|
|
32
44
|
*/
|
|
33
45
|
export async function ChromiumFish(opts = {}) {
|
|
34
|
-
const { personaSeed, headless = true, windowSize, version, download = true, args, ...launch } = opts;
|
|
46
|
+
const { personaSeed, headless = true, windowSize, version, download = true, timezone, args, ...launch } = opts;
|
|
35
47
|
const executablePath = await binaryPath(version, download);
|
|
48
|
+
// Resolve the timezone before launch: "auto" -> egress IP via the ip2tz DB,
|
|
49
|
+
// an IANA string -> used as-is. Inject as the TZ env var so Chromium's ICU
|
|
50
|
+
// adopts it at process init (the production timezone source of truth).
|
|
51
|
+
let tz = null;
|
|
52
|
+
if (timezone) {
|
|
53
|
+
tz = timezone === "auto" ? await resolveTimezone({ proxy: proxyToUrl(launch.proxy), download }) : timezone;
|
|
54
|
+
}
|
|
55
|
+
let env = launch.env;
|
|
56
|
+
if (tz) {
|
|
57
|
+
env = {
|
|
58
|
+
...process.env,
|
|
59
|
+
...(launch.env ?? {}),
|
|
60
|
+
TZ: tz,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
36
63
|
return chromium.launch({
|
|
37
64
|
executablePath,
|
|
38
65
|
headless,
|
|
39
66
|
args: buildArgs({ personaSeed, windowSize, args }),
|
|
40
67
|
...launch,
|
|
68
|
+
...(env ? { env } : {}),
|
|
41
69
|
});
|
|
42
70
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -6,10 +6,29 @@
|
|
|
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.3";
|
|
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. */
|
|
13
13
|
export declare const RELEASE_REPO = "arman-bd/chromiumfish";
|
|
14
|
+
/**
|
|
15
|
+
* IP-to-Timezone database, built by `packages/geoip/build_ip2tz.py`.
|
|
16
|
+
* IP Geolocation by DB-IP (https://db-ip.com), CC BY 4.0.
|
|
17
|
+
*
|
|
18
|
+
* Default `"latest"` auto-tracks the newest monthly build: the SDK reads a small
|
|
19
|
+
* pointer (the `geoip-latest` release manifest) to discover the current concrete
|
|
20
|
+
* version, so no SDK republish is needed when a new DB ships. Pin a specific
|
|
21
|
+
* version with `CHROMIUMFISH_GEOIP_VERSION` (e.g. `"2026.06"`) for reproducibility.
|
|
22
|
+
*/
|
|
23
|
+
export declare const DEFAULT_GEOIP_VERSION = "latest";
|
|
24
|
+
/**
|
|
25
|
+
* Concrete version used when `"latest"` cannot be resolved (offline + no cached
|
|
26
|
+
* pointer). Bump occasionally so the offline floor stays recent.
|
|
27
|
+
*/
|
|
28
|
+
export declare const GEOIP_FALLBACK_VERSION = "2026.06";
|
|
14
29
|
export declare function browserVersion(): string;
|
|
15
30
|
export declare function releaseBaseUrl(version?: string): string;
|
|
31
|
+
export declare function geoipVersion(): string;
|
|
32
|
+
export declare function geoipBaseUrl(version?: string): string;
|
|
33
|
+
/** Stable URL of the pointer that names the current concrete DB version. */
|
|
34
|
+
export declare function geoipLatestManifestUrl(): string;
|
package/dist/version.js
CHANGED
|
@@ -6,14 +6,39 @@
|
|
|
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.3";
|
|
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. */
|
|
13
13
|
export const RELEASE_REPO = "arman-bd/chromiumfish";
|
|
14
|
+
/**
|
|
15
|
+
* IP-to-Timezone database, built by `packages/geoip/build_ip2tz.py`.
|
|
16
|
+
* IP Geolocation by DB-IP (https://db-ip.com), CC BY 4.0.
|
|
17
|
+
*
|
|
18
|
+
* Default `"latest"` auto-tracks the newest monthly build: the SDK reads a small
|
|
19
|
+
* pointer (the `geoip-latest` release manifest) to discover the current concrete
|
|
20
|
+
* version, so no SDK republish is needed when a new DB ships. Pin a specific
|
|
21
|
+
* version with `CHROMIUMFISH_GEOIP_VERSION` (e.g. `"2026.06"`) for reproducibility.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_GEOIP_VERSION = "latest";
|
|
24
|
+
/**
|
|
25
|
+
* Concrete version used when `"latest"` cannot be resolved (offline + no cached
|
|
26
|
+
* pointer). Bump occasionally so the offline floor stays recent.
|
|
27
|
+
*/
|
|
28
|
+
export const GEOIP_FALLBACK_VERSION = "2026.06";
|
|
14
29
|
export function browserVersion() {
|
|
15
30
|
return process.env.CHROMIUMFISH_VERSION || DEFAULT_BROWSER_VERSION;
|
|
16
31
|
}
|
|
17
32
|
export function releaseBaseUrl(version = browserVersion()) {
|
|
18
33
|
return `https://github.com/${RELEASE_REPO}/releases/download/v${version}`;
|
|
19
34
|
}
|
|
35
|
+
export function geoipVersion() {
|
|
36
|
+
return process.env.CHROMIUMFISH_GEOIP_VERSION || DEFAULT_GEOIP_VERSION;
|
|
37
|
+
}
|
|
38
|
+
export function geoipBaseUrl(version = geoipVersion()) {
|
|
39
|
+
return `https://github.com/${RELEASE_REPO}/releases/download/geoip-${version}`;
|
|
40
|
+
}
|
|
41
|
+
/** Stable URL of the pointer that names the current concrete DB version. */
|
|
42
|
+
export function geoipLatestManifestUrl() {
|
|
43
|
+
return `https://github.com/${RELEASE_REPO}/releases/download/geoip-latest/latest.json`;
|
|
44
|
+
}
|
package/package.json
CHANGED