onebrowser 0.1.1 → 0.2.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) and [Anchor Browser](https://anchorbrowser.io); 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,7 @@ 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 |
76
77
 
77
78
  ### Quota
78
79
 
@@ -83,6 +84,7 @@ How each provider meets them:
83
84
  | `hyperbrowser` | credits | `GET /api/team/credit-info`, the endpoint of Hyperbrowser's SDK (not in its API docs) |
84
85
  | `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
86
  | `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` |
87
+ | `anchor` | credits | `GET /v1/billing`; a session refused with 402 throws `QuotaError` |
86
88
 
87
89
  ### API keys
88
90
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "onebrowser",
3
- "version": "0.1.1",
3
+ "version": "0.2.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"],
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
+ }
@@ -1,3 +1,4 @@
1
+ import * as anchor from './anchor.js';
1
2
  import * as hyperbrowser from './hyperbrowser.js';
2
3
  import * as kernel from './kernel.js';
3
4
  import * as lightpanda from './lightpanda.js';
@@ -26,7 +27,7 @@ import * as tinyfish from './tinyfish.js';
26
27
  * Their requests go through http.js, which turns every status other than 2xx into an HttpError: an adapter
27
28
  * handles only the statuses that mean something of their own.
28
29
  */
29
- export const ADAPTERS = { tinyfish, steel, hyperbrowser, lightpanda, kernel };
30
+ export const ADAPTERS = { tinyfish, steel, hyperbrowser, lightpanda, kernel, anchor };
30
31
 
31
32
  const CONTRACT = ['createSession', 'releaseSession', 'sessionUsage', 'quota'];
32
33