onebrowser 0.1.1 → 0.3.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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ await browsers.close();
23
23
 
24
24
  **What it does not do**: choose a provider, cap the sessions held at once, spread sessions over providers, or stop using a provider whose quota runs low. Those are policy, and belong to the program using it. onebrowser opens the session it is asked for, and tells the program what it needs to decide: the quota, and `QuotaError` when a provider refuses a session for the account's quota.
25
25
 
26
- Built in: [Steel](https://steel.dev), [TinyFish](https://tinyfish.ai), [Hyperbrowser](https://hyperbrowser.ai), [Lightpanda](https://lightpanda.io) and [Kernel](https://www.kernel.sh); a program can add its own (see [Adding a provider](#adding-a-provider)). Node 22.13 or later.
26
+ Built in: [Steel](https://steel.dev), [TinyFish](https://tinyfish.ai), [Hyperbrowser](https://hyperbrowser.ai), [Lightpanda](https://lightpanda.io), [Kernel](https://www.kernel.sh), [Anchor Browser](https://anchorbrowser.io) and [Firecrawl](https://www.firecrawl.dev); a program can add its own (see [Adding a provider](#adding-a-provider)). Node 22.13 or later.
27
27
 
28
28
  ## Interface
29
29
 
@@ -73,6 +73,8 @@ How each provider meets them:
73
73
  | `hyperbrowser` | residential proxy, paid plans | at once: a session ends with its CDP connection | `timeoutMinutes`, in whole minutes |
74
74
  | `lightpanda` | its datacenter pool in that country | at once: a session is its CDP connection | Lightpanda's own limit is 15 min |
75
75
  | `kernel` | not supported (throws) | `timeout_seconds` after the CDP client left; until then a disconnected session stays in standby, counting toward the plan's concurrent browsers | this process only |
76
+ | `anchor` | residential proxy | `idle_timeout` after the last CDP connection left, in whole minutes rounded down (at least 1) | `max_duration`, in whole minutes |
77
+ | `firecrawl` | its `location` in that country (default: the US) | `activityTtl` (no CDP commands), in whole seconds rounded down, at most 1 h | `ttl`, in whole seconds, at least 30 s and at most 1 h |
76
78
 
77
79
  ### Quota
78
80
 
@@ -83,6 +85,8 @@ How each provider meets them:
83
85
  | `hyperbrowser` | credits | `GET /api/team/credit-info`, the endpoint of Hyperbrowser's SDK (not in its API docs) |
84
86
  | `lightpanda` | browser hours of the calendar month (`monthlyHours`, default: the free plan's) | the ledger's record of its sessions: its console shows the month's usage, but its token cannot read it. So the reading is whole while the account is used only through ledgers of this machine |
85
87
  | `kernel` | browser hours of the calendar month (`monthlyHours`, default: what the free plan's credits pay for) | the uptime of the account's sessions this month, from `GET /browsers`; a session refused with 403 (spending cap) throws `QuotaError` |
88
+ | `anchor` | credits | `GET /v1/billing`; a session refused with 402 throws `QuotaError` |
89
+ | `firecrawl` | credits; the plan's credits renew at the end of each billing period | [`GET /v2/team/credit-usage`](https://docs.firecrawl.dev/api-reference/endpoint/credit-usage); a session refused with 402 throws `QuotaError` |
86
90
 
87
91
  ### API keys
88
92
 
@@ -123,7 +127,7 @@ A program can also pass an adapter of its own in `adapters`, without changing on
123
127
 
124
128
  `npm test` runs offline: fake adapters and fetch, no network, no browser.
125
129
 
126
- A release is a tag: bump `version` in `package.json` and add its entry to [CHANGELOG.md](CHANGELOG.md), then push the tag `vX.Y.Z`. The release workflow checks the tag against the version, runs the tests and publishes to npm with trusted publishing (no token stored).
130
+ A release is a tag: bump `version` in `package.json`, carry it into `package-lock.json` with `npm install --package-lock-only`, add its entry to [CHANGELOG.md](CHANGELOG.md), then push the tag `vX.Y.Z`. The release workflow checks the tag against the version, runs the tests and publishes to npm with trusted publishing (no token stored).
127
131
 
128
132
  ## License
129
133
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "onebrowser",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "One interface to remote browser providers: sessions by provider name, a usage ledger, account quotas",
5
- "keywords": ["browser", "remote-browser", "cdp", "playwright", "steel", "hyperbrowser", "lightpanda", "kernel", "tinyfish"],
5
+ "keywords": ["browser", "remote-browser", "cdp", "playwright", "steel", "hyperbrowser", "lightpanda", "kernel", "tinyfish", "anchorbrowser", "firecrawl"],
6
6
  "license": "MIT",
7
7
  "author": "Richard Wang",
8
8
  "repository": { "type": "git", "url": "git+https://github.com/i-richardwang/onebrowser.git" },
@@ -0,0 +1,54 @@
1
+ import { QuotaError } from '../errors.js';
2
+ import { json } from './http.js';
3
+
4
+ /**
5
+ * Anchor Browser (https://docs.anchorbrowser.io). Without a proxy a session uses the IP of the machine it runs on,
6
+ * a different US datacenter IP per session; proxyCountry selects Anchor's residential proxy in that country.
7
+ * Closing the CDP connection does not end a session: Anchor ends it idleTimeoutSec after its last connection
8
+ * left (idle_timeout), and after maxDurationMs in any case (max_duration), both in whole minutes. idle_timeout is
9
+ * rounded down, to at least 1: its timer runs only once the client is gone, so ending sooner cuts off no session
10
+ * in use; max_duration is rounded up, as this process closes the session at maxDurationMs. Recording, on by
11
+ * default, is turned off. Sessions are paid from the account's credits; with too few, creating one answers 402.
12
+ */
13
+ export const defaults = {
14
+ apiBase: 'https://api.anchorbrowser.io/v1',
15
+ };
16
+
17
+ const headers = (apiKey) => ({ 'anchor-api-key': apiKey });
18
+
19
+ export async function createSession(settings, apiKey, { proxyCountry, idleTimeoutSec, maxDurationMs }) {
20
+ const session = {
21
+ recording: { active: false },
22
+ timeout: { idle_timeout: Math.max(1, Math.floor(idleTimeoutSec / 60)), max_duration: Math.ceil(maxDurationMs / 60_000) },
23
+ };
24
+ if (proxyCountry) session.proxy = { active: true, type: 'anchor_proxy', country_code: proxyCountry.toLowerCase() };
25
+ let s;
26
+ try {
27
+ s = (await json(`${settings.apiBase}/sessions`, { method: 'POST', headers: headers(apiKey), body: { session }, timeoutMs: 90_000 })).data;
28
+ } catch (e) {
29
+ if (e.status === 402) throw new QuotaError(`credits used up: ${e.text}`);
30
+ throw e;
31
+ }
32
+ return { id: s.id, cdpUrl: s.cdp_url, egress: proxyCountry ? `proxy ${proxyCountry}` : 'datacenter IP' };
33
+ }
34
+
35
+ /** Succeeds also for a session that has already ended */
36
+ export async function releaseSession(settings, apiKey, session) {
37
+ await json(`${settings.apiBase}/sessions/${session.id}`, { method: 'DELETE', headers: headers(apiKey) });
38
+ }
39
+
40
+ /** The account's credits left, gifted ones included. Unlike the other endpoints, /billing answers without a `data` wrapper */
41
+ export async function quota(settings, apiKey) {
42
+ const { credits } = await json(`${settings.apiBase}/billing`, { headers: headers(apiKey) });
43
+ return { remaining: credits, unit: 'credits' };
44
+ }
45
+
46
+ /** Usage of the sessions among `sessions` that have ended, fetched by id: duration in whole seconds */
47
+ export async function sessionUsage(settings, apiKey, sessions) {
48
+ const usage = [];
49
+ for (const { id } of sessions) {
50
+ const { data: s } = await json(`${settings.apiBase}/sessions/${id}`, { headers: headers(apiKey) });
51
+ if (s.status !== 'running') usage.push({ id, durationMs: s.duration * 1000, proxyBytes: s.proxy_bytes });
52
+ }
53
+ return usage;
54
+ }
@@ -0,0 +1,59 @@
1
+ import { QuotaError } from '../errors.js';
2
+ import { json } from './http.js';
3
+
4
+ /**
5
+ * Firecrawl's Browser Sandbox (https://docs.firecrawl.dev/features/browser). A session browses from a US ISP IP
6
+ * by default, a different one per session; proxyCountry selects Firecrawl's location in that country (location).
7
+ * Closing the CDP connection does not end a session: Firecrawl ends it idleTimeoutSec after its last CDP command
8
+ * (activityTtl), and maxDurationMs after it was created in any case (ttl), both in whole seconds and at most 1 h.
9
+ * activityTtl is rounded down and ttl cut to 1 h, so that neither ends a session later than asked; ttl is rounded
10
+ * up, to at least 30 s, as this process closes the session at maxDurationMs. Its live view stream and its ad
11
+ * blocking, both on by default, are turned off. Sessions are paid from the account's credits; with too few,
12
+ * creating one answers 402.
13
+ */
14
+ export const defaults = {
15
+ apiBase: 'https://api.firecrawl.dev/v2',
16
+ };
17
+
18
+ const TTL_SEC = { min: 30, max: 3600 }; // what Firecrawl takes for ttl; activityTtl has the same maximum
19
+
20
+ const headers = (apiKey) => ({ Authorization: `Bearer ${apiKey}` });
21
+
22
+ export async function createSession(settings, apiKey, { proxyCountry, idleTimeoutSec, maxDurationMs }) {
23
+ const body = {
24
+ ttl: Math.min(TTL_SEC.max, Math.max(TTL_SEC.min, Math.ceil(maxDurationMs / 1000))),
25
+ activityTtl: Math.min(TTL_SEC.max, Math.floor(idleTimeoutSec)),
26
+ streamWebView: false,
27
+ blockAds: false,
28
+ };
29
+ if (proxyCountry) body.location = { country: proxyCountry };
30
+ let s;
31
+ try {
32
+ s = await json(`${settings.apiBase}/interact`, { method: 'POST', headers: headers(apiKey), body, timeoutMs: 90_000 });
33
+ } catch (e) {
34
+ if (e.status === 402) throw new QuotaError(`credits used up: ${e.text}`);
35
+ throw e;
36
+ }
37
+ return { id: s.id, cdpUrl: s.cdpUrl, egress: `proxy ${proxyCountry ?? 'US'}` };
38
+ }
39
+
40
+ /** Succeeds also for a session that has already ended */
41
+ export async function releaseSession(settings, apiKey, session) {
42
+ await json(`${settings.apiBase}/interact/${session.id}`, { method: 'DELETE', headers: headers(apiKey) });
43
+ }
44
+
45
+ /** The team's credits left, coupon credits included; the plan's credits renew at the end of its billing period */
46
+ export async function quota(settings, apiKey) {
47
+ const { data } = await json(`${settings.apiBase}/team/credit-usage`, { headers: headers(apiKey) });
48
+ return { remaining: data.remainingCredits, unit: 'credits', renewsAt: data.billingPeriodEnd ? Date.parse(data.billingPeriodEnd) : null };
49
+ }
50
+
51
+ /** Usage of the sessions among `sessions` that have ended, fetched by id: the session list has no durations (GET /interact/{id} is not in Firecrawl's API docs) */
52
+ export async function sessionUsage(settings, apiKey, sessions) {
53
+ const usage = [];
54
+ for (const { id } of sessions) {
55
+ const s = await json(`${settings.apiBase}/interact/${id}`, { headers: headers(apiKey) });
56
+ if (s.status !== 'running') usage.push({ id, durationMs: s.sessionDurationMs, proxyBytes: null });
57
+ }
58
+ return usage;
59
+ }
@@ -1,3 +1,5 @@
1
+ import * as anchor from './anchor.js';
2
+ import * as firecrawl from './firecrawl.js';
1
3
  import * as hyperbrowser from './hyperbrowser.js';
2
4
  import * as kernel from './kernel.js';
3
5
  import * as lightpanda from './lightpanda.js';
@@ -26,7 +28,7 @@ import * as tinyfish from './tinyfish.js';
26
28
  * Their requests go through http.js, which turns every status other than 2xx into an HttpError: an adapter
27
29
  * handles only the statuses that mean something of their own.
28
30
  */
29
- export const ADAPTERS = { tinyfish, steel, hyperbrowser, lightpanda, kernel };
31
+ export const ADAPTERS = { tinyfish, steel, hyperbrowser, lightpanda, kernel, anchor, firecrawl };
30
32
 
31
33
  const CONTRACT = ['createSession', 'releaseSession', 'sessionUsage', 'quota'];
32
34