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 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 { SDK_VERSION, DEFAULT_BROWSER_VERSION, RELEASE_REPO, browserVersion, releaseBaseUrl, } from "./version.js";
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 { SDK_VERSION, DEFAULT_BROWSER_VERSION, RELEASE_REPO, browserVersion, releaseBaseUrl, } from "./version.js";
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";
@@ -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
+ }
@@ -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.0";
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.0";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chromiumfish",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Stealth Chromium build with a drop-in Playwright harness — fetches and launches the ChromiumFish browser.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",