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 +4 -2
- package/package.json +1 -1
- package/src/cli.js +3 -3
- package/src/index.js +16 -4
- package/src/ledger.js +45 -13
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.
|
|
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().
|
|
25
|
-
if (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
|
|
22
|
-
--
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
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
|
}
|