onebrowser 0.6.0 → 0.7.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
@@ -45,6 +45,8 @@ Built in: [Steel](https://steel.dev), [TinyFish](https://tinyfish.ai), [Hyperbro
45
45
  | `accounts(provider?)` | The accounts with an API key, in the providers' order and then the keys': all of them, or the provider's (throws for an unknown provider) |
46
46
  | `syncUsage()` | Stores the providers' records (duration, proxy traffic) of ended sessions: `{ reported, failed: [{ account, error }] }` |
47
47
  | `usage({ since })` | Sessions opened since `since` (ms) per UTC day, account and client: `sessions`, `minutes` (the provider's record, else the open-to-close timing), `untimed`, `unsynced`, `proxyBytes` |
48
+ | `sessions({ since })` | The ledger's sessions opened since `since` (ms), oldest first, each as recorded: `provider`, `account`, `id`, `client`, `openedAt`, `endsBy`, `closedAt`, `durationMs`, `proxyBytes`, `syncedAt` (times in ms; `closedAt` and `syncedAt` null until then, `durationMs` and `proxyBytes` null without the provider's record) |
49
+ | `ledgerId` | The ledger's own id, kept for its life: a program that copies the ledger's record elsewhere tells it from another ledger's by it |
48
50
  | `close()` | Closes the sessions still open and the ledger |
49
51
 
50
52
  An account has `id` (`steel#3f9a1c`: the provider's name and the key's fingerprint), `provider`, `fingerprint`, and:
@@ -112,7 +114,7 @@ An account is known by its `id`, the provider's name and the first 6 hex digits
112
114
 
113
115
  ### The ledger
114
116
 
115
- One SQLite file per machine by default: `$ONEBROWSER_DB`, else `onebrowser/sessions.db` in `$XDG_DATA_HOME` (default `~/.local/share`), shared by every program on the machine under its own `client`. A session is recorded when its provider has created it (it is billed from then on), with its account and when it ends at the latest; closed when its provider has released it; and completed with the provider's own record by `syncUsage()`. A ledger made by an earlier onebrowser is brought up to date when opened; one made by a later onebrowser is refused. Sessions recorded by onebrowser 0.5 or earlier have no account: they become the provider's only account's when it has one here, and stay without one while it has several.
117
+ One SQLite file per machine by default: `$ONEBROWSER_DB`, else `onebrowser/sessions.db` in `$XDG_DATA_HOME` (default `~/.local/share`), shared by every program on the machine under its own `client`. A session is recorded when its provider has created it (it is billed from then on), with its account and when it ends at the latest; closed when its provider has released it; and completed with the provider's own record by `syncUsage()`. Nothing removes a session from the ledger, so its record is whole from the ledger's first session on. Each ledger has an id of its own (`ledgerId`), given when it is made (or first opened by onebrowser 0.7 or later) and kept for its life. A ledger made by an earlier onebrowser is brought up to date when opened; one made by a later onebrowser is refused. Sessions recorded by onebrowser 0.5 or earlier have no account: they become the provider's only account's when it has one here, and stay without one while it has several.
116
118
 
117
119
  ## Command line
118
120
 
@@ -125,7 +127,7 @@ npx onebrowser usage --days 7 # the ledger's sessions per UTC day, accoun
125
127
  npx onebrowser usage --json --env-file .env
126
128
  ```
127
129
 
128
- `usage` first fetches the providers' records of the sessions that have ended, as `syncUsage()` does. With `--json`, `quota` prints an array of what `readQuota()` returns, with the account: `{ account, provider, remaining, unbilled, unit, renewsAt }`, or `{ account, provider, error }` for a failed reading; `usage` prints `{ ledger, since, sessions, failed }`, where `sessions` is what `usage()` returns and `failed` what `syncUsage()` could not fetch. API keys are read as by the library, from the environment (`--env-file PATH` loads a file into it first). Exit status: 0 done, 1 a reading failed, 2 the command line is wrong.
130
+ `usage` first fetches the providers' records of the sessions that have ended, as `syncUsage()` does. With `--json`, `quota` prints an array of what `readQuota()` returns, with the account: `{ account, provider, remaining, unbilled, unit, renewsAt }`, or `{ account, provider, error }` for a failed reading; `usage` prints `{ ledger, ledgerId, since, sessions, failed }`, where `sessions` is what `usage()` returns and `failed` what `syncUsage()` could not fetch. API keys are read as by the library, from the environment (`--env-file PATH` loads a file into it first). Exit status: 0 done, 1 a reading failed, 2 the command line is wrong.
129
131
 
130
132
  ## Adding a provider
131
133
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onebrowser",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "One interface to remote browser providers: sessions by account (an API key at a provider), a usage ledger, account quotas",
5
5
  "keywords": ["browser", "remote-browser", "cdp", "playwright", "steel", "hyperbrowser", "lightpanda", "kernel", "tinyfish", "anchorbrowser", "firecrawl"],
6
6
  "license": "MIT",
package/src/cli.js CHANGED
@@ -21,8 +21,8 @@ const GLOBAL_OPTIONS = {
21
21
  /** The accounts `names` ask for: each name a provider's (it must have an API key) or one account's id */
22
22
  function askedAccounts(browsers, names) {
23
23
  return names.flatMap((name) => {
24
- const own = browsers.accounts().filter((a) => a.id === name);
25
- if (own.length) return own;
24
+ const own = browsers.accounts().find((a) => a.id === name);
25
+ if (own) return [own];
26
26
  if (!browsers.providers.includes(name)) throw new CommandLineError(`unknown provider or account ${name} (expected ${browsers.providers.join(', ')}, or an account id)`);
27
27
  const accounts = browsers.accounts(name);
28
28
  if (!accounts.length) throw new Error(`${name} has no API key: set ${apiKeyEnv(name)} or ${apiKeyEnv(name)}_FILE`);
@@ -63,7 +63,7 @@ const COMMANDS = {
63
63
  const { failed } = await browsers.syncUsage();
64
64
  for (const f of failed) err.write(`${f.account}: the provider's records were not fetched (${f.error})\n`);
65
65
  const sessions = browsers.usage({ since });
66
- if (json) out.write(`${JSON.stringify({ ledger: browsers.ledgerPath, since: day(since), sessions, failed }, null, 2)}\n`);
66
+ if (json) out.write(`${JSON.stringify({ ledger: browsers.ledgerPath, ledgerId: browsers.ledgerId, since: day(since), sessions, failed }, null, 2)}\n`);
67
67
  else {
68
68
  out.write(`sessions since ${day(since)} in ${browsers.ledgerPath}:\n`);
69
69
  if (!sessions.length) out.write(' none\n');
package/src/index.js CHANGED
@@ -42,10 +42,7 @@ export class OneBrowser {
42
42
  }
43
43
  }
44
44
 
45
- /**
46
- * The ledger, opened on first use. Sessions recorded before accounts were (onebrowser 0.5 and earlier) are
47
- * the provider's only account's, when it has one here; with several, no one knows which they belong to.
48
- */
45
+ /** The ledger, opened on first use; a provider with one account here claims its sessions recorded without one */
49
46
  get #store() {
50
47
  if (!this.#ledger) {
51
48
  this.#ledger = new Ledger(this.ledgerPath);
@@ -56,6 +53,11 @@ export class OneBrowser {
56
53
  return this.#ledger;
57
54
  }
58
55
 
56
+ /** The ledger's own id, kept for the ledger's life: it tells its record from another ledger's */
57
+ get ledgerId() {
58
+ return this.#store.id;
59
+ }
60
+
59
61
  /** Names of the providers it has adapters for */
60
62
  get providers() {
61
63
  return [...this.#providers];
@@ -96,6 +98,16 @@ export class OneBrowser {
96
98
  return this.#store.usage(since);
97
99
  }
98
100
 
101
+ /**
102
+ * The ledger's sessions opened since `since` (ms), oldest first, each as recorded:
103
+ * [{ provider, account, id, client, openedAt, endsBy, closedAt, durationMs, proxyBytes, syncedAt }], times in
104
+ * ms (closedAt, syncedAt null until then; durationMs and proxyBytes null without the provider's record). account:
105
+ * the provider's name alone for a session whose account is not known.
106
+ */
107
+ sessions({ since }) {
108
+ return this.#store.sessions(since);
109
+ }
110
+
99
111
  /** Closes every session still open and the ledger */
100
112
  async close() {
101
113
  await Promise.all(this.#accounts.map((a) => a.close()));
package/src/ledger.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
4
5
  import { DatabaseSync } from 'node:sqlite';
5
6
 
6
7
  /**
@@ -18,8 +19,8 @@ export function defaultLedgerPath() {
18
19
  const SCHEMA = `
19
20
  CREATE TABLE sessions (
20
21
  provider TEXT NOT NULL,
21
- account TEXT, -- the account it is billed to (Account.id); empty when not known: recorded
22
- -- before onebrowser 0.6, by a provider with several accounts here since
22
+ account TEXT, -- the account it is billed to (Account.id); empty when recorded before the
23
+ -- ledger had accounts (schema 2) and not claimed since (claim())
23
24
  id TEXT NOT NULL, -- the session id its provider gave it
24
25
  client TEXT NOT NULL, -- the program that opened it (OneBrowser's client)
25
26
  opened_at TEXT NOT NULL, -- when it was asked for; its provider created it right after
@@ -65,6 +66,13 @@ const UPGRADES = {
65
66
  DROP TABLE sessions_2;`,
66
67
  };
67
68
 
69
+ /**
70
+ * Beside the schema, meta holds the ledger's own id (`id`): a program that copies the ledger's record elsewhere
71
+ * tells it from another ledger's by it. It came with onebrowser 0.7 without a schema step, made whenever it is
72
+ * missing, so that a ledger a 0.7 has opened still opens in 0.6, which knows schema 3 too and ignores the table.
73
+ */
74
+ const META = 'CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL) WITHOUT ROWID;';
75
+
68
76
  /** ms from one ISO time to another */
69
77
  const msBetween = (from, to) => `CAST(ROUND((julianday(${to}) - julianday(${from})) * 86400000) AS INTEGER)`;
70
78
 
@@ -96,16 +104,29 @@ export class Ledger {
96
104
  /** Makes the ledger, or brings an earlier one up to date, in one transaction: another process may be opening it too */
97
105
  #migrate() {
98
106
  const version = () => this.#db.prepare('PRAGMA user_version').get().user_version;
99
- if (version() === VERSION) return;
100
- this.tx(() => {
101
- if (!this.#db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'sessions'").get()) this.#db.exec(SCHEMA);
102
- else {
103
- const from = version() || 1;
104
- if (from > VERSION) throw new Error(`onebrowser: the ledger ${this.#file} was made by a later onebrowser (schema ${from}; this one knows ${VERSION})`);
105
- for (let v = from; v < VERSION; v++) this.#db.exec(UPGRADES[v]);
106
- }
107
- this.#db.exec(`PRAGMA user_version = ${VERSION}`);
108
- });
107
+ const has = (table) => this.#db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?").get(table);
108
+ if (version() !== VERSION) {
109
+ this.tx(() => {
110
+ if (!has('sessions')) this.#db.exec(SCHEMA);
111
+ else {
112
+ const from = version() || 1;
113
+ if (from > VERSION) throw new Error(`onebrowser: the ledger ${this.#file} was made by a later onebrowser (schema ${from}; this one knows ${VERSION})`);
114
+ for (let v = from; v < VERSION; v++) this.#db.exec(UPGRADES[v]);
115
+ }
116
+ this.#db.exec(`PRAGMA user_version = ${VERSION}`);
117
+ });
118
+ }
119
+ if (!has('meta')) {
120
+ this.tx(() => {
121
+ this.#db.exec(META);
122
+ this.#db.prepare("INSERT OR IGNORE INTO meta (key, value) VALUES ('id', ?)").run(randomUUID());
123
+ });
124
+ }
125
+ }
126
+
127
+ /** This ledger's id, given when it was made or first opened by a onebrowser that has ids, and kept for life */
128
+ get id() {
129
+ return this.#q("SELECT value FROM meta WHERE key = 'id'").get().value;
109
130
  }
110
131
 
111
132
  #q(sql) {
@@ -122,7 +143,7 @@ export class Ledger {
122
143
  this.#q('UPDATE sessions SET closed_at=? WHERE account=? AND id=? AND closed_at IS NULL').run(iso(closedAt), account, id);
123
144
  }
124
145
 
125
- /** The provider's sessions whose account is not known are `account`'s */
146
+ /** The provider's sessions without an account (recorded before schema 3) are `account`'s: the provider's only one */
126
147
  claim(provider, account) {
127
148
  this.#q('UPDATE sessions SET account=? WHERE provider=? AND account IS NULL').run(account, provider);
128
149
  }
@@ -173,6 +194,17 @@ export class Ledger {
173
194
  ).all(iso(since));
174
195
  }
175
196
 
197
+ /** See OneBrowser.sessions() */
198
+ sessions(since) {
199
+ return this.#q(
200
+ `SELECT provider, COALESCE(account, provider) account, id, client, opened_at openedAt, ends_by endsBy, closed_at closedAt,
201
+ duration_ms durationMs, proxy_bytes proxyBytes, synced_at syncedAt
202
+ FROM sessions WHERE opened_at >= ? ORDER BY opened_at, provider, id`,
203
+ )
204
+ .all(iso(since))
205
+ .map((r) => ({ ...r, openedAt: Date.parse(r.openedAt), endsBy: Date.parse(r.endsBy), closedAt: r.closedAt && Date.parse(r.closedAt), syncedAt: r.syncedAt && Date.parse(r.syncedAt) }));
206
+ }
207
+
176
208
  close() {
177
209
  this.#db.close();
178
210
  }