@solarisdk/browserbase-compat 0.2.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/CHANGELOG.md +11 -0
- package/COMPAT.md +29 -4
- package/README.md +3 -2
- package/dist/client.d.ts +68 -35
- package/dist/client.js +286 -140
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +6 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -1
- package/dist/mapping.d.ts +5 -0
- package/dist/mapping.js +56 -4
- package/dist/options.d.ts +59 -0
- package/dist/options.js +112 -0
- package/dist/types.d.ts +12 -1
- package/dist/unsupported.d.ts +9 -0
- package/dist/unsupported.js +10 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
- **Every Browserbase resource and method now exists.** `secrets`, `webhooks`, `navigationPolicies`, `certificates`, `functions.secrets`, `sessions.recording.downloads` and the rest were `undefined` on the client, so calling them failed with "not a function". They now throw `SolariCompatUnsupported` naming the method (for example `secrets.create`), as do `.asResponse()` / `.withResponse()` and the raw `client.get` / `post` / `request` calls. The ones that map to Solari (sessions, contexts, live view, recording) work as before.
|
|
5
|
+
- **`keepAlive` is reported correctly.** `sessions.retrieve` and `sessions.list` said `keepAlive: false` for every session. A session created with `keepAlive: true` now reports `true`. The gateway does not return it, so the package stores it in one reserved user metadata key, `solari_compat_keep_alive`, and hides that key from the `userMetadata` you get back. That key is reserved, and `keepAlive: true` with 50 metadata keys of your own now throws. A session created some other way, or by an older version, still reports `false`.
|
|
6
|
+
- **Behaviour change: the session timeout is `api_timeout`, as in Browserbase's SDK.** `api_timeout` is sent to Solari as the session length. A lone `sessions.create({ timeout: 300 })` used to set the session length; Browserbase's SDK treats it as the request timeout (milliseconds), and so does this package now. `timeout` next to other params (`create({ timeout: 300, keepAlive: true })`) is still read as the session length, and giving it together with a different `api_timeout` throws. Change `timeout` to `api_timeout`.
|
|
7
|
+
- **Options are no longer dropped.** The client takes `defaultHeaders` and `defaultQuery`. Every method takes Browserbase's second argument: `timeout`, `maxRetries`, `headers`, `query`, `signal` (an aborted request throws `APIUserAbortError`), and on create calls `body` and `idempotencyKey`. `httpAgent`, per-call `method` / `path` / `stream` and a header that sets `Authorization` throw.
|
|
8
|
+
- **Unknown keys throw instead of vanishing.** An unknown option on the client, an unknown key in the `sessions.create` params, in `context`, `viewport`, a proxy entry, its `geolocation` or `proxySettings`, in the query of `list` / `update` / `debug`, and `browserSettings.riskPolicyId` all throw `SolariCompatUnsupported`. **If a call now throws for such a key, it never had any effect: remove it or fix the spelling.**
|
|
9
|
+
- **`projectId` now warns once** (it was ignored silently). Solari keys belong to an organisation, so it selects nothing; `strict: true` throws instead.
|
|
10
|
+
- **`sessions.list` returns every session.** It stopped after the gateway's first page of 100; it now follows the pages to the end.
|
|
11
|
+
- **Wrong values removed.** `endedAt` was the time of your call for every finished session; it is now the expiry time for a timed-out session and absent for a released one (the gateway does not say when). `contexts.retrieve` of a missing context throws `NotFoundError`. `Context` gains `projectId` and `updatedAt`, and `ContextCreateResponse` gains `uploadUrl` (empty; see COMPAT.md).
|
|
12
|
+
- The replay and saved-context downloads go through the `fetch` you gave the client, and never carry your key or default headers.
|
|
13
|
+
|
|
3
14
|
## 0.2.1
|
|
4
15
|
- **Security: the base URL check accepts only the Solari API hosts.** It used to accept any `*.getsolari.com` host, including sandbox preview hosts (`<label>-<port>.preview.getsolari.com`) that serve customer code, so a `baseURL` / `SOLARI_BASE_URL` pointed there would send your `slr_` key to code running in a sandbox. Now only the Solari API hosts and loopback are accepted.
|
|
5
16
|
|
package/COMPAT.md
CHANGED
|
@@ -23,13 +23,15 @@ Keep it private: the session id inside it is the credential.
|
|
|
23
23
|
|---|---|
|
|
24
24
|
| Key | `apiKey` option, else `SOLARI_API_KEY`, else `BROWSERBASE_API_KEY`. **A key that does not start with `slr_` is never used or sent** (it is probably a Browserbase secret): it is skipped with a warning that does not print it, and if no `slr_` key remains the constructor throws. |
|
|
25
25
|
| Endpoint | `baseURL` option or `SOLARI_BASE_URL`; default `https://api.getsolari.com`. Must be `https` on a Solari API host or loopback (preview and other `*.getsolari.com` hosts are refused: they serve customer code), otherwise the constructor throws `BrowserbaseError` ("Refusing to send a Solari API key to …"): the key is never sent to another host. |
|
|
26
|
-
| `projectId` | Accepted and
|
|
26
|
+
| `projectId` | Accepted, ignored, and **warned about once per client** (`strict: true` throws instead): Solari keys belong to an organisation, there are no projects. |
|
|
27
|
+
| Client options | `apiKey`, `baseURL`, `maxRetries` (default 2) and `timeout` (ms, default 60000) apply to every request. `defaultHeaders` and `defaultQuery` are added to every Solari request (never to presigned storage downloads); a `null` header value removes a header, and `Authorization` cannot be set. `fetch` replaces the global fetch (use it for a proxy or your own dispatcher). `maxRetries` and `timeout` read back. **`httpAgent` and any option this client does not have throw `SolariCompatUnsupported`** (they are never dropped). |
|
|
28
|
+
| Per-call options | Every method takes Browserbase's second argument: `timeout` (ms), `maxRetries`, `headers`, `query` and `signal` apply to every request the call makes (an aborted `signal` throws `APIUserAbortError`). `body` (extra JSON fields) and `idempotencyKey` apply to the create calls only (`sessions.create`, `contexts.create`); on any other call they throw. `method`, `path`, `stream`, `httpAgent`, `defaultBaseURL` and `__binary*` throw. `Authorization` cannot be set in `headers`. |
|
|
27
29
|
|
|
28
30
|
## Works
|
|
29
31
|
| Browserbase | Here |
|
|
30
32
|
|---|---|
|
|
31
33
|
| `sessions.create` | `POST /sessions`; returns `connectUrl` (CDP), `id`, `status: "RUNNING"`, `expiresAt`, `region`, `keepAlive`, `contextId`. A create is retried safely (idempotency key). |
|
|
32
|
-
| `sessions.retrieve`, `sessions.list({ status })` | Solari `active/released/expired/unknown` map to `RUNNING/COMPLETED/TIMED_OUT/ERROR`. `
|
|
34
|
+
| `sessions.retrieve`, `sessions.list({ status, q })` | Solari `active/released/expired/unknown` map to `RUNNING/COMPLETED/TIMED_OUT/ERROR`. `list` returns **every** matching session: it follows the gateway's pages to the end (it stops with a `BrowserbaseError` after 1000 pages, or if the gateway repeats a page cursor). See [Reported as unknown](#reported-as-unknown-or-empty) for the fields Solari does not have. |
|
|
33
35
|
| `sessions.update(id, { status: "REQUEST_RELEASE" })` | Releases the session. |
|
|
34
36
|
| `proxies: true`, `proxies: [{ type: "browserbase", geolocation }]` | A managed proxy; `geolocation.country/state/city` map across (a proxy needs the stealth browser, so `stealth` is switched on for you). `{ type: "none" }` means no proxy. |
|
|
35
37
|
| `browserSettings.verified`, `advancedStealth` | Solari's stealth browser. |
|
|
@@ -43,9 +45,25 @@ Keep it private: the session id inside it is the credential.
|
|
|
43
45
|
- **Recording is a DOM-event log, not video.** `sessions.recording.retrieve(id)` returns rrweb events once the replay has been uploaded (a few seconds after release; until then it throws `APIError` with `code: "ReplayPending"`). There is no HLS playlist and no MP4.
|
|
44
46
|
- **Contexts store cookies and localStorage only** (Playwright `storageState`), not IndexedDB, service workers or autofill data.
|
|
45
47
|
- **No choice of operating system or viewport at create time (known gap).** A `viewport` setting is accepted with a one-line warning and has no effect; set the viewport from your automation client (`page.setViewportSize`). `strict: true` makes it throw instead.
|
|
46
|
-
- **`
|
|
48
|
+
- **`api_timeout`, `keepAlive` and `userMetadata` work as in Browserbase.** `api_timeout` (seconds, at least 60; Browserbase's name for the session timeout) ends the session at that time, capped at your plan's maximum session length. `keepAlive: true` keeps the session after your client disconnects, until its `api_timeout` (default 1 hour), the plan maximum, or `sessions.update(..., REQUEST_RELEASE)`. `userMetadata` (up to 50 keys, 8 KB) is stored, returned by `retrieve`/`list`, and filterable with `sessions.list({ q: "user_metadata['key']:'value'" })` (conditions joined with `AND`).
|
|
49
|
+
- **The session timeout is `api_timeout`; `timeout` is the request timeout.** As in Browserbase's SDK, a lone `sessions.create({ timeout: 5000 })` is the *options* argument (a request timeout in ms), not a session setting. The older spelling `timeout` (seconds) is still read as the session timeout when it sits next to other params (`create({ timeout: 300, keepAlive: true })`); passing it together with a different `api_timeout` throws. Use `api_timeout`.
|
|
50
|
+
- **`keepAlive` is remembered in the session's user metadata.** The Solari gateway does not return `keepAlive` when you retrieve or list a session. With `keepAlive: true`, `sessions.create` therefore also stores one reserved user metadata key, `solari_compat_keep_alive`, and `retrieve` / `list` read it back (and remove it from the `userMetadata` you see). Consequences: the key is reserved (using it yourself throws), `keepAlive: true` leaves room for 49 of your own metadata keys (50 throws), and **a session that was not created through this package (version 0.3.0 or later) reports `keepAlive: false` even if it was created with keep-alive**, because the gateway does not say.
|
|
47
51
|
- **`viewport`, `blockAds: true`, `logSession: true`, `os`** are accepted with a one-line warning and have no effect. Pass `strict: true` to make them throw instead.
|
|
48
52
|
|
|
53
|
+
## Reported as unknown or empty
|
|
54
|
+
Solari has no equivalent for these fields of Browserbase's objects, so they carry a fixed value instead of a made-up one:
|
|
55
|
+
|
|
56
|
+
| Field | Value |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `Session.projectId`, `Context.projectId` | `""` (no projects) |
|
|
59
|
+
| `Session.updatedAt`, `Session.startedAt` | the create time |
|
|
60
|
+
| `Session.proxyBytes` | `0` |
|
|
61
|
+
| `Session.region` | `us-west-2` (Solari's single region) |
|
|
62
|
+
| `Session.endedAt` | the expiry time for a `TIMED_OUT` session; absent for a released one (the gateway does not say when), except in the answer to your own `sessions.update(..., REQUEST_RELEASE)`, where it is the time of that call |
|
|
63
|
+
| `Context.updatedAt` | the profile's last update when the gateway reports one, else its creation time |
|
|
64
|
+
| `ContextCreateResponse.publicKey`, `cipherAlgorithm`, `initializationVectorSize`, `uploadUrl` | `""` / `0` (Solari profiles are stored server-side, nothing is uploaded) |
|
|
65
|
+
| `SessionCreateResponse.seleniumRemoteUrl`, `signingKey` | `""` |
|
|
66
|
+
|
|
49
67
|
## Contexts
|
|
50
68
|
```ts
|
|
51
69
|
const ctx = await bb.contexts.create({ name: "shop-login" });
|
|
@@ -62,4 +80,11 @@ const page = await (await b2.newContext({ storageState: s2.storageState })).newP
|
|
|
62
80
|
Two differences from Browserbase: a saved context's **cookies** are also present in the default browser context (`browser.contexts()[0]`), but localStorage is applied only to a browser context created *with* the state (use `newContext({ storageState })` to get both), and `persist: true` saves when you release **through the same client object** (it is remembered in memory).
|
|
63
81
|
|
|
64
82
|
## Raises `SolariCompatUnsupported`
|
|
65
|
-
|
|
83
|
+
Every resource and method of Browserbase's Node SDK exists on the client; these throw, naming the method (for example `secrets.create`), and send nothing:
|
|
84
|
+
|
|
85
|
+
- **Resources Solari has no equivalent of:** `extensions` (and `extensionId`), `projects` (`retrieve`, `list`, `usage`), `agents` and `agents.runs`, `fetchAPI`, `search`, `secrets` (`create`, `retrieve`, `update`, `list`, `delete`, `getPublicKey`), `webhooks` (`create`, `retrieve`, `update`, `list`, `delete`, `rotateSecret`), `navigationPolicies`, `certificates`, `functions.secrets` (`list`, `attach`, `detach`).
|
|
86
|
+
- **Session sub-resources:** `sessions.downloads.list`, `sessions.uploads.create`, `sessions.logs.list`, `sessions.replays` (`retrieve`, `retrievePage`), `sessions.recording.downloads` (`create`, `list`). Recordings are DOM events: use `sessions.recording.retrieve`.
|
|
87
|
+
- **`contexts.update`.**
|
|
88
|
+
- **Raw responses and raw requests:** `.asResponse()` and `.withResponse()` on a returned promise; `client.get/post/put/patch/delete/getAPIList/request` (Browserbase API paths that Solari does not serve).
|
|
89
|
+
- **Settings:** the Selenium endpoint (`seleniumRemoteUrl` is always empty), external/bring-your-own proxies, per-domain proxy rules (`domainPattern`), custom CA certificates, `ignoreCertificateErrors: true`, custom captcha selectors, `riskPolicyId`, `os: "mobile" | "tablet"`, a `region` other than `us-west-2` (or `us-west`).
|
|
90
|
+
- **Any key Solari cannot map is refused, never dropped:** an unknown key in the `sessions.create` params, in `browserSettings`, in `context` (only `id`, `persist`), `viewport` (`width`, `height`), a `proxies` entry, its `geolocation` (`country`, `state`, `city`) or `proxySettings` (`caCertificates`); an unknown key in the query of `sessions.list` (`status`, `q`), `sessions.update` (`status`, `projectId`), `sessions.debug` (`expiresIn`) or `contexts.create` (`name`, `projectId`); an unknown client or per-call option.
|
package/README.md
CHANGED
|
@@ -19,8 +19,9 @@ await browser.close();
|
|
|
19
19
|
await bb.sessions.update(session.id, { status: "REQUEST_RELEASE" });
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Sessions, contexts, live view and recording are supported
|
|
23
|
-
|
|
22
|
+
Sessions, contexts, live view and recording are supported. Every other Browserbase resource and method exists but throws
|
|
23
|
+
`SolariCompatUnsupported` naming it (extensions, secrets, webhooks, projects, downloads/uploads APIs, video replays,
|
|
24
|
+
bring-your-own proxies, Selenium, ...). See [docs.getsolari.com/browserbase](https://docs.getsolari.com/browserbase) (and `COMPAT.md` in this package) for the full list
|
|
24
25
|
and every difference. Requires Node 20+. No Playwright version pin is needed: `connectUrl` is a plain CDP endpoint.
|
|
25
26
|
|
|
26
27
|
Licence: MIT. See `NOTICE`.
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Call, CompatPromise, RequestOptions } from "./options.js";
|
|
1
2
|
import type { Context, ContextCreateResponse, Session, SessionCreateParams, SessionCreateResponse, SessionLiveURLs, SessionRetrieveResponse, SessionStatus } from "./types.js";
|
|
2
3
|
export declare const DEFAULT_BASE_URL = "https://api.getsolari.com";
|
|
3
4
|
export interface BrowserbaseOptions {
|
|
@@ -9,76 +10,107 @@ export interface BrowserbaseOptions {
|
|
|
9
10
|
strict?: boolean;
|
|
10
11
|
/** Called for each soft difference; default console.warn. */
|
|
11
12
|
onWarning?: (message: string) => void;
|
|
13
|
+
/** Retries for a request that failed with a connection error or 502/503/504 (GET, DELETE and creates only). Default 2. */
|
|
12
14
|
maxRetries?: number;
|
|
13
|
-
/** Per-request timeout in ms. */
|
|
15
|
+
/** Per-request timeout in ms. Default 60000. */
|
|
14
16
|
timeout?: number;
|
|
15
|
-
/**
|
|
17
|
+
/** A fetch to use instead of the global one (for a proxy, a custom dispatcher, or a test). */
|
|
16
18
|
fetch?: typeof fetch;
|
|
19
|
+
/** Headers sent on every request. A `null` or `undefined` value removes a header. `Authorization` cannot be set. */
|
|
20
|
+
defaultHeaders?: Record<string, string | null | undefined>;
|
|
21
|
+
/** Query parameters sent on every request. */
|
|
22
|
+
defaultQuery?: Record<string, string | undefined>;
|
|
23
|
+
/** Not supported: raises. Pass `fetch` with your own dispatcher instead. */
|
|
24
|
+
httpAgent?: unknown;
|
|
17
25
|
}
|
|
18
26
|
/** Whether a base URL may receive a Solari key: https on a Solari API host, or loopback. */
|
|
19
27
|
export declare function isSolariBaseUrl(raw: string): boolean;
|
|
20
28
|
export declare class Browserbase {
|
|
21
29
|
readonly sessions: Sessions;
|
|
22
30
|
readonly contexts: Contexts;
|
|
23
|
-
readonly extensions:
|
|
24
|
-
readonly projects:
|
|
25
|
-
readonly agents:
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
readonly extensions: import("./unsupported.js").UnsupportedMethods<"create" | "retrieve" | "delete">;
|
|
32
|
+
readonly projects: import("./unsupported.js").UnsupportedMethods<"retrieve" | "list" | "usage">;
|
|
33
|
+
readonly agents: {
|
|
34
|
+
runs: import("./unsupported.js").UnsupportedMethods<"create" | "retrieve" | "list" | "listMessages">;
|
|
35
|
+
create: (...args: unknown[]) => never;
|
|
36
|
+
retrieve: (...args: unknown[]) => never;
|
|
37
|
+
delete: (...args: unknown[]) => never;
|
|
38
|
+
list: (...args: unknown[]) => never;
|
|
39
|
+
update: (...args: unknown[]) => never;
|
|
40
|
+
};
|
|
41
|
+
readonly fetchAPI: import("./unsupported.js").UnsupportedMethods<"create">;
|
|
42
|
+
readonly search: import("./unsupported.js").UnsupportedMethods<"web">;
|
|
43
|
+
readonly certificates: import("./unsupported.js").UnsupportedMethods<"create" | "retrieve" | "delete" | "list">;
|
|
44
|
+
readonly secrets: import("./unsupported.js").UnsupportedMethods<"create" | "retrieve" | "delete" | "list" | "update" | "getPublicKey">;
|
|
45
|
+
readonly functions: {
|
|
46
|
+
secrets: import("./unsupported.js").UnsupportedMethods<"list" | "attach" | "detach">;
|
|
47
|
+
};
|
|
48
|
+
readonly webhooks: import("./unsupported.js").UnsupportedMethods<"create" | "retrieve" | "delete" | "list" | "update" | "rotateSecret">;
|
|
49
|
+
readonly navigationPolicies: import("./unsupported.js").UnsupportedMethods<"create" | "retrieve" | "delete" | "list" | "update">;
|
|
28
50
|
readonly apiKey: string;
|
|
29
51
|
readonly baseURL: string;
|
|
52
|
+
/** Retries for a failed request (client default; a call can override it). */
|
|
53
|
+
readonly maxRetries: number;
|
|
54
|
+
/** Request timeout in ms (client default; a call can override it). */
|
|
55
|
+
readonly timeout: number;
|
|
30
56
|
/** @internal */ readonly strict: boolean;
|
|
31
57
|
/** @internal */ readonly warn: (m: string) => void;
|
|
32
58
|
private readonly fetchImpl;
|
|
33
|
-
private readonly
|
|
34
|
-
private readonly
|
|
59
|
+
private readonly defaultHeaders;
|
|
60
|
+
private readonly defaultQuery;
|
|
61
|
+
private projectIdWarned;
|
|
35
62
|
/** session id -> profile to save on release (browserSettings.context.persist). In-process only. */
|
|
36
63
|
/** @internal */ readonly persist: Map<string, string>;
|
|
37
64
|
constructor(opts?: BrowserbaseOptions);
|
|
38
|
-
|
|
39
|
-
|
|
65
|
+
get(..._a: unknown[]): never;
|
|
66
|
+
post(..._a: unknown[]): never;
|
|
67
|
+
put(..._a: unknown[]): never;
|
|
68
|
+
patch(..._a: unknown[]): never;
|
|
69
|
+
delete(..._a: unknown[]): never;
|
|
70
|
+
getAPIList(..._a: unknown[]): never;
|
|
71
|
+
request(..._a: unknown[]): never;
|
|
72
|
+
/** @internal `projectId` has nothing to select on Solari: say so once per client (or refuse under `strict`). */
|
|
73
|
+
noteProjectId(where: string): void;
|
|
74
|
+
/** @internal GET a presigned URL (no Solari auth header, no default headers). */
|
|
75
|
+
fetchPublic(url: string, call?: Call, defaultTimeoutMs?: number): Promise<Response>;
|
|
40
76
|
/** @internal */
|
|
41
|
-
|
|
42
|
-
idempotencyKey?: string;
|
|
43
|
-
}): Promise<Response>;
|
|
77
|
+
send(method: string, path: string, body?: unknown, call?: Call): Promise<Response>;
|
|
44
78
|
/** @internal */
|
|
45
|
-
json<T>(method: string, path: string, body?: unknown,
|
|
46
|
-
idempotencyKey?: string;
|
|
47
|
-
}): Promise<T>;
|
|
79
|
+
json<T>(method: string, path: string, body?: unknown, call?: Call): Promise<T>;
|
|
48
80
|
}
|
|
49
|
-
/** A resource whose every method throws SolariCompatUnsupported. */
|
|
50
|
-
export type UnsupportedResource = {
|
|
51
|
-
[method: string]: (...args: unknown[]) => never;
|
|
52
|
-
};
|
|
53
81
|
declare class Sessions {
|
|
54
82
|
private readonly c;
|
|
55
83
|
readonly recording: Recording;
|
|
56
|
-
readonly replays:
|
|
57
|
-
readonly logs:
|
|
58
|
-
readonly downloads:
|
|
59
|
-
readonly uploads:
|
|
84
|
+
readonly replays: import("./unsupported.js").UnsupportedMethods<"retrieve" | "retrievePage">;
|
|
85
|
+
readonly logs: import("./unsupported.js").UnsupportedMethods<"list">;
|
|
86
|
+
readonly downloads: import("./unsupported.js").UnsupportedMethods<"list">;
|
|
87
|
+
readonly uploads: import("./unsupported.js").UnsupportedMethods<"create">;
|
|
60
88
|
constructor(c: Browserbase);
|
|
61
|
-
create(params?: SessionCreateParams):
|
|
62
|
-
|
|
89
|
+
create(params?: SessionCreateParams, options?: RequestOptions): CompatPromise<SessionCreateResponse>;
|
|
90
|
+
create(options?: RequestOptions): CompatPromise<SessionCreateResponse>;
|
|
91
|
+
retrieve(id: string, options?: RequestOptions): CompatPromise<SessionRetrieveResponse>;
|
|
63
92
|
list(query?: {
|
|
64
93
|
status?: SessionStatus;
|
|
65
94
|
q?: string;
|
|
66
|
-
}):
|
|
95
|
+
}, options?: RequestOptions): CompatPromise<Session[]>;
|
|
96
|
+
list(options?: RequestOptions): CompatPromise<Session[]>;
|
|
67
97
|
/** `{ status: "REQUEST_RELEASE" }` releases the session (saving the context first when it was created with persist: true). */
|
|
68
98
|
update(id: string, params: {
|
|
69
99
|
status: "REQUEST_RELEASE";
|
|
70
100
|
projectId?: string;
|
|
71
|
-
}):
|
|
101
|
+
}, options?: RequestOptions): CompatPromise<Session>;
|
|
72
102
|
/** Live-view links. Both URLs are the same view-only share link; `pages` is empty. */
|
|
73
103
|
debug(id: string, query?: {
|
|
74
104
|
expiresIn?: number;
|
|
75
|
-
}):
|
|
105
|
+
}, options?: RequestOptions): CompatPromise<SessionLiveURLs>;
|
|
106
|
+
debug(id: string, options?: RequestOptions): CompatPromise<SessionLiveURLs>;
|
|
76
107
|
}
|
|
77
108
|
declare class Recording {
|
|
78
109
|
private readonly c;
|
|
110
|
+
readonly downloads: import("./unsupported.js").UnsupportedMethods<"create" | "list">;
|
|
79
111
|
constructor(c: Browserbase);
|
|
80
112
|
/** The session's DOM recording as an array of rrweb events. Needs `browserSettings: { recordSession: true }`; available a few seconds after release. */
|
|
81
|
-
retrieve(id: string):
|
|
113
|
+
retrieve(id: string, options?: RequestOptions): CompatPromise<unknown[]>;
|
|
82
114
|
}
|
|
83
115
|
declare class Contexts {
|
|
84
116
|
private readonly c;
|
|
@@ -87,9 +119,10 @@ declare class Contexts {
|
|
|
87
119
|
create(params?: {
|
|
88
120
|
projectId?: string;
|
|
89
121
|
name?: string;
|
|
90
|
-
}):
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
122
|
+
}, options?: RequestOptions): CompatPromise<ContextCreateResponse>;
|
|
123
|
+
create(options?: RequestOptions): CompatPromise<ContextCreateResponse>;
|
|
124
|
+
retrieve(id: string, options?: RequestOptions): CompatPromise<Context>;
|
|
125
|
+
update(..._a: unknown[]): never;
|
|
126
|
+
delete(id: string, options?: RequestOptions): CompatPromise<void>;
|
|
94
127
|
}
|
|
95
128
|
export {};
|
package/dist/client.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { gunzipSync } from "node:zlib";
|
|
2
|
-
import { APIError, BrowserbaseError, SolariCompatUnsupported, apiErrorFor, connectionErrorFor } from "./errors.js";
|
|
3
|
-
import { mapCreateParams, mapStatus } from "./mapping.js";
|
|
2
|
+
import { APIError, APIUserAbortError, BrowserbaseError, NotFoundError, SolariCompatUnsupported, apiErrorFor, connectionErrorFor } from "./errors.js";
|
|
3
|
+
import { KEEP_ALIVE_KEY, mapCreateParams, mapStatus } from "./mapping.js";
|
|
4
|
+
import { assertHeaders, compat, followUp, resolveOptions, setHeader, splitArgs } from "./options.js";
|
|
5
|
+
import { unsupportedMethods } from "./unsupported.js";
|
|
4
6
|
export const DEFAULT_BASE_URL = "https://api.getsolari.com";
|
|
5
7
|
const REGION = "us-west-2"; // reported for Browserbase-shaped objects; Solari's single region (us-west)
|
|
8
|
+
const CLIENT_OPTION_KEYS = new Set(["apiKey", "baseURL", "strict", "onWarning", "maxRetries", "timeout", "fetch", "defaultHeaders", "defaultQuery", "httpAgent"]);
|
|
6
9
|
function resolveApiKey(explicit, warn) {
|
|
7
10
|
const sources = [
|
|
8
11
|
["apiKey", explicit],
|
|
@@ -61,21 +64,51 @@ function sleep(ms) {
|
|
|
61
64
|
export class Browserbase {
|
|
62
65
|
sessions;
|
|
63
66
|
contexts;
|
|
64
|
-
extensions;
|
|
65
|
-
projects;
|
|
66
|
-
agents
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
extensions = unsupportedMethods("extensions", "browser extensions cannot be loaded", ["create", "retrieve", "delete"]);
|
|
68
|
+
projects = unsupportedMethods("projects", "Solari keys are organisation-scoped; there are no projects", ["retrieve", "list", "usage"]);
|
|
69
|
+
agents = {
|
|
70
|
+
...unsupportedMethods("agents", "Browserbase Agents is a separate product", ["create", "retrieve", "update", "list", "delete"]),
|
|
71
|
+
runs: unsupportedMethods("agents.runs", "Browserbase Agents is a separate product", ["create", "retrieve", "list", "listMessages"]),
|
|
72
|
+
};
|
|
73
|
+
fetchAPI = unsupportedMethods("fetchAPI", "Browserbase Fetch is a separate product", ["create"]);
|
|
74
|
+
search = unsupportedMethods("search", "Browserbase Search is a separate product", ["web"]);
|
|
75
|
+
certificates = unsupportedMethods("certificates", "custom CA certificates are not supported", ["create", "retrieve", "list", "delete"]);
|
|
76
|
+
secrets = unsupportedMethods("secrets", "Solari has no secret store for Browserbase Functions", ["create", "retrieve", "update", "list", "delete", "getPublicKey"]);
|
|
77
|
+
functions = {
|
|
78
|
+
secrets: unsupportedMethods("functions.secrets", "Browserbase Functions is a separate product", ["list", "attach", "detach"]),
|
|
79
|
+
};
|
|
80
|
+
webhooks = unsupportedMethods("webhooks", "Solari does not offer webhooks", ["create", "retrieve", "update", "list", "delete", "rotateSecret"]);
|
|
81
|
+
navigationPolicies = unsupportedMethods("navigationPolicies", "Solari has no stored navigation policies; pass browserSettings.allowedDomains for an egress allowlist", ["create", "retrieve", "update", "list", "delete"]);
|
|
69
82
|
apiKey;
|
|
70
83
|
baseURL;
|
|
84
|
+
/** Retries for a failed request (client default; a call can override it). */
|
|
85
|
+
maxRetries;
|
|
86
|
+
/** Request timeout in ms (client default; a call can override it). */
|
|
87
|
+
timeout;
|
|
71
88
|
/** @internal */ strict;
|
|
72
89
|
/** @internal */ warn;
|
|
73
90
|
fetchImpl;
|
|
74
|
-
|
|
75
|
-
|
|
91
|
+
defaultHeaders;
|
|
92
|
+
defaultQuery;
|
|
93
|
+
projectIdWarned = false;
|
|
76
94
|
/** session id -> profile to save on release (browserSettings.context.persist). In-process only. */
|
|
77
95
|
/** @internal */ persist = new Map();
|
|
78
96
|
constructor(opts = {}) {
|
|
97
|
+
for (const k of Object.keys(opts)) {
|
|
98
|
+
if (opts[k] === undefined)
|
|
99
|
+
continue;
|
|
100
|
+
if (k === "httpAgent")
|
|
101
|
+
throw new SolariCompatUnsupported("client option httpAgent", "pass fetch with your own dispatcher instead");
|
|
102
|
+
if (!CLIENT_OPTION_KEYS.has(k))
|
|
103
|
+
throw new SolariCompatUnsupported(`client option ${k}`, "not an option of this client");
|
|
104
|
+
}
|
|
105
|
+
if (opts.maxRetries !== undefined && !(Number.isInteger(opts.maxRetries) && opts.maxRetries >= 0)) {
|
|
106
|
+
throw new BrowserbaseError("maxRetries must be a non-negative integer");
|
|
107
|
+
}
|
|
108
|
+
if (opts.timeout !== undefined && !(typeof opts.timeout === "number" && opts.timeout > 0)) {
|
|
109
|
+
throw new BrowserbaseError("timeout must be a positive number of milliseconds");
|
|
110
|
+
}
|
|
111
|
+
assertHeaders(opts.defaultHeaders, "client option defaultHeaders");
|
|
79
112
|
this.warn = opts.onWarning ?? ((m) => console.warn(`[solari-browserbase-compat] ${m}`));
|
|
80
113
|
// Non-enumerable so console.log(client) / JSON.stringify(client) never print the key.
|
|
81
114
|
Object.defineProperty(this, "apiKey", { value: resolveApiKey(opts.apiKey, this.warn), enumerable: false });
|
|
@@ -83,61 +116,106 @@ export class Browserbase {
|
|
|
83
116
|
this.strict = opts.strict === true;
|
|
84
117
|
this.fetchImpl = opts.fetch ?? fetch;
|
|
85
118
|
this.maxRetries = opts.maxRetries ?? 2;
|
|
86
|
-
this.
|
|
119
|
+
this.timeout = opts.timeout ?? 60_000;
|
|
120
|
+
this.defaultHeaders = { ...(opts.defaultHeaders ?? {}) };
|
|
121
|
+
this.defaultQuery = { ...(opts.defaultQuery ?? {}) };
|
|
87
122
|
this.sessions = new Sessions(this);
|
|
88
123
|
this.contexts = new Contexts(this);
|
|
89
|
-
this.extensions = unsupported("extensions", "browser extensions cannot be loaded");
|
|
90
|
-
this.projects = unsupported("projects", "Solari keys are organisation-scoped; there are no projects");
|
|
91
|
-
this.agents = unsupported("agents", "Browserbase Agents is a separate product");
|
|
92
|
-
this.fetchAPI = unsupported("fetchAPI", "Browserbase Fetch is a separate product");
|
|
93
|
-
this.search = unsupported("search", "Browserbase Search is a separate product");
|
|
94
124
|
}
|
|
95
|
-
|
|
96
|
-
|
|
125
|
+
// Browserbase's raw HTTP escape hatches address Browserbase's own API paths, which Solari does not serve.
|
|
126
|
+
get(..._a) { throw rawUnsupported("get"); }
|
|
127
|
+
post(..._a) { throw rawUnsupported("post"); }
|
|
128
|
+
put(..._a) { throw rawUnsupported("put"); }
|
|
129
|
+
patch(..._a) { throw rawUnsupported("patch"); }
|
|
130
|
+
delete(..._a) { throw rawUnsupported("delete"); }
|
|
131
|
+
getAPIList(..._a) { throw rawUnsupported("getAPIList"); }
|
|
132
|
+
request(..._a) { throw rawUnsupported("request"); }
|
|
133
|
+
/** @internal `projectId` has nothing to select on Solari: say so once per client (or refuse under `strict`). */
|
|
134
|
+
noteProjectId(where) {
|
|
135
|
+
if (this.strict)
|
|
136
|
+
throw new SolariCompatUnsupported(`${where} projectId`, "Solari keys belong to an organisation; there are no projects");
|
|
137
|
+
if (this.projectIdWarned)
|
|
138
|
+
return;
|
|
139
|
+
this.projectIdWarned = true;
|
|
140
|
+
this.warn("projectId is ignored: Solari keys belong to an organisation; there are no projects.");
|
|
141
|
+
}
|
|
142
|
+
/** @internal GET a presigned URL (no Solari auth header, no default headers). */
|
|
143
|
+
async fetchPublic(url, call = {}, defaultTimeoutMs = 15_000) {
|
|
97
144
|
const ac = new AbortController();
|
|
98
|
-
const
|
|
145
|
+
const detach = linkSignal(ac, call.signal);
|
|
146
|
+
const timer = setTimeout(() => ac.abort(), call.timeout ?? defaultTimeoutMs);
|
|
99
147
|
try {
|
|
100
148
|
return await this.fetchImpl(url, { signal: ac.signal });
|
|
101
149
|
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if (call.signal?.aborted)
|
|
152
|
+
throw new APIUserAbortError();
|
|
153
|
+
throw connectionErrorFor(e);
|
|
154
|
+
}
|
|
102
155
|
finally {
|
|
103
156
|
clearTimeout(timer);
|
|
157
|
+
detach();
|
|
104
158
|
}
|
|
105
159
|
}
|
|
106
160
|
/** @internal */
|
|
107
|
-
async
|
|
161
|
+
async send(method, path, body, call = {}) {
|
|
108
162
|
const headers = { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" };
|
|
109
|
-
|
|
110
|
-
headers
|
|
111
|
-
const
|
|
163
|
+
for (const [k, v] of Object.entries(this.defaultHeaders))
|
|
164
|
+
setHeader(headers, k, v);
|
|
165
|
+
for (const [k, v] of Object.entries(call.headers ?? {}))
|
|
166
|
+
setHeader(headers, k, v);
|
|
167
|
+
if (call.idempotencyKey)
|
|
168
|
+
headers["Idempotency-Key"] = call.idempotencyKey;
|
|
169
|
+
// Extra query parameters are appended (percent-encoded) rather than re-serialising the path's own query.
|
|
170
|
+
const extra = [...Object.entries(this.defaultQuery), ...Object.entries(call.query ?? {})]
|
|
171
|
+
.filter(([, v]) => v !== undefined && v !== null)
|
|
172
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
|
173
|
+
const url = `${this.baseURL}${path}${extra.length > 0 ? (path.includes("?") ? "&" : "?") + extra.join("&") : ""}`;
|
|
174
|
+
if (call.body !== undefined) {
|
|
175
|
+
if (method === "GET")
|
|
176
|
+
throw new SolariCompatUnsupported("request option body", "a GET has no body");
|
|
177
|
+
body = { ...(body ?? {}), ...call.body };
|
|
178
|
+
}
|
|
179
|
+
const maxRetries = call.maxRetries ?? this.maxRetries;
|
|
180
|
+
const timeoutMs = call.timeout ?? this.timeout;
|
|
181
|
+
const replaySafe = method === "GET" || method === "DELETE" || call.idempotencyKey !== undefined;
|
|
112
182
|
let lastErr;
|
|
113
|
-
for (let attempt = 0; attempt <=
|
|
183
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
184
|
+
if (call.signal?.aborted)
|
|
185
|
+
throw new APIUserAbortError();
|
|
114
186
|
const ac = new AbortController();
|
|
115
|
-
const
|
|
187
|
+
const detach = linkSignal(ac, call.signal);
|
|
188
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
116
189
|
try {
|
|
117
|
-
const res = await this.fetchImpl(
|
|
190
|
+
const res = await this.fetchImpl(url, {
|
|
118
191
|
method,
|
|
119
192
|
headers,
|
|
120
193
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
121
194
|
signal: ac.signal,
|
|
122
195
|
});
|
|
123
196
|
clearTimeout(timer);
|
|
124
|
-
if (res.ok || !replaySafe || ![502, 503, 504].includes(res.status) || attempt ===
|
|
197
|
+
if (res.ok || !replaySafe || ![502, 503, 504].includes(res.status) || attempt === maxRetries)
|
|
125
198
|
return res;
|
|
126
199
|
await res.text().catch(() => { });
|
|
127
200
|
}
|
|
128
201
|
catch (e) {
|
|
129
202
|
clearTimeout(timer);
|
|
130
|
-
if (
|
|
203
|
+
if (call.signal?.aborted)
|
|
204
|
+
throw new APIUserAbortError();
|
|
205
|
+
if (!replaySafe || attempt === maxRetries)
|
|
131
206
|
throw connectionErrorFor(e);
|
|
132
207
|
lastErr = e;
|
|
133
208
|
}
|
|
209
|
+
finally {
|
|
210
|
+
detach();
|
|
211
|
+
}
|
|
134
212
|
await sleep(300 * (attempt + 1));
|
|
135
213
|
}
|
|
136
214
|
throw connectionErrorFor(lastErr ?? new Error("request failed"));
|
|
137
215
|
}
|
|
138
216
|
/** @internal */
|
|
139
|
-
async json(method, path, body,
|
|
140
|
-
const res = await this.
|
|
217
|
+
async json(method, path, body, call = {}) {
|
|
218
|
+
const res = await this.send(method, path, body, call);
|
|
141
219
|
if (res.status === 204)
|
|
142
220
|
return undefined;
|
|
143
221
|
const text = await res.text();
|
|
@@ -155,45 +233,51 @@ export class Browserbase {
|
|
|
155
233
|
return parsed;
|
|
156
234
|
}
|
|
157
235
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
hint;
|
|
161
|
-
constructor(feature, hint) {
|
|
162
|
-
this.feature = feature;
|
|
163
|
-
this.hint = hint;
|
|
164
|
-
// Every method on this object throws; a Proxy keeps the surface open-ended (create/list/retrieve/...).
|
|
165
|
-
return new Proxy(this, {
|
|
166
|
-
get: (target, prop) => {
|
|
167
|
-
if (typeof prop === "symbol" || prop === "then" || prop in target)
|
|
168
|
-
return Reflect.get(target, prop);
|
|
169
|
-
return () => {
|
|
170
|
-
throw new SolariCompatUnsupported(`${target.feature}.${prop}`, target.hint);
|
|
171
|
-
};
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
}
|
|
236
|
+
function rawUnsupported(method) {
|
|
237
|
+
return new SolariCompatUnsupported(`client.${method}()`, "Browserbase's raw HTTP methods address its own API paths, which Solari does not serve");
|
|
175
238
|
}
|
|
176
|
-
function
|
|
177
|
-
|
|
239
|
+
/** Abort `ac` when the caller's signal aborts. Returns the function that detaches the listener. */
|
|
240
|
+
function linkSignal(ac, signal) {
|
|
241
|
+
if (!signal)
|
|
242
|
+
return () => { };
|
|
243
|
+
if (signal.aborted) {
|
|
244
|
+
ac.abort();
|
|
245
|
+
return () => { };
|
|
246
|
+
}
|
|
247
|
+
const on = () => ac.abort();
|
|
248
|
+
signal.addEventListener("abort", on, { once: true });
|
|
249
|
+
return () => signal.removeEventListener("abort", on);
|
|
178
250
|
}
|
|
179
251
|
function newKey() {
|
|
180
252
|
return globalThis.crypto?.randomUUID?.() ?? `bbc-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
181
253
|
}
|
|
254
|
+
/** The keepAlive flag and the user's own metadata, from what the gateway stored (see KEEP_ALIVE_KEY). */
|
|
255
|
+
function splitMetadata(md) {
|
|
256
|
+
if (!md || typeof md !== "object")
|
|
257
|
+
return { keepAlive: false };
|
|
258
|
+
const { [KEEP_ALIVE_KEY]: marker, ...rest } = md;
|
|
259
|
+
return { keepAlive: marker === true, ...(Object.keys(rest).length > 0 ? { userMetadata: rest } : {}) };
|
|
260
|
+
}
|
|
182
261
|
function toSession(row, extra = {}) {
|
|
183
262
|
const created = row.createdAt ?? new Date().toISOString();
|
|
184
263
|
const status = extra.status ?? mapStatus(row.status);
|
|
264
|
+
const expiresAt = row.expiresAt ?? created;
|
|
265
|
+
const meta = splitMetadata(row.userMetadata);
|
|
266
|
+
// Only report an end time we know: the release call knows it (now); a timed-out session ended at its deadline; the
|
|
267
|
+
// gateway does not record when a released session ended, so that is left out rather than invented.
|
|
268
|
+
const endedAt = extra.endedAt ?? (status === "TIMED_OUT" && row.expiresAt ? row.expiresAt : undefined);
|
|
185
269
|
return {
|
|
186
270
|
id: row.id,
|
|
187
271
|
createdAt: created,
|
|
188
272
|
updatedAt: created,
|
|
189
273
|
projectId: "",
|
|
190
274
|
startedAt: created,
|
|
191
|
-
...(
|
|
192
|
-
expiresAt
|
|
275
|
+
...(endedAt ? { endedAt } : {}),
|
|
276
|
+
expiresAt,
|
|
193
277
|
status,
|
|
194
278
|
proxyBytes: 0,
|
|
195
|
-
keepAlive: extra.keepAlive ??
|
|
196
|
-
...(
|
|
279
|
+
keepAlive: extra.keepAlive ?? meta.keepAlive,
|
|
280
|
+
...(meta.userMetadata ? { userMetadata: meta.userMetadata } : {}),
|
|
197
281
|
...(extra.contextId ? { contextId: extra.contextId } : {}),
|
|
198
282
|
region: REGION,
|
|
199
283
|
};
|
|
@@ -201,102 +285,151 @@ function toSession(row, extra = {}) {
|
|
|
201
285
|
function withConnect(s, cdp) {
|
|
202
286
|
return { ...s, connectUrl: cdp, seleniumRemoteUrl: "", signingKey: "" };
|
|
203
287
|
}
|
|
288
|
+
function onlyParams(obj, known, feature) {
|
|
289
|
+
for (const k of Object.keys(obj ?? {})) {
|
|
290
|
+
if (!known.includes(k))
|
|
291
|
+
throw new SolariCompatUnsupported(`${feature}.${k}`, "this parameter is not supported");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const MAX_LIST_PAGES = 1000;
|
|
204
295
|
class Sessions {
|
|
205
296
|
c;
|
|
206
297
|
recording;
|
|
207
|
-
replays;
|
|
208
|
-
logs;
|
|
209
|
-
downloads;
|
|
210
|
-
uploads;
|
|
298
|
+
replays = unsupportedMethods("sessions.replays", "Solari records DOM events, not video; use sessions.recording.retrieve", ["retrieve", "retrievePage"]);
|
|
299
|
+
logs = unsupportedMethods("sessions.logs", "session command logs are not available", ["list"]);
|
|
300
|
+
downloads = unsupportedMethods("sessions.downloads", "file downloads cannot be retrieved through the API", ["list"]);
|
|
301
|
+
uploads = unsupportedMethods("sessions.uploads", "use your automation client's setInputFiles", ["create"]);
|
|
211
302
|
constructor(c) {
|
|
212
303
|
this.c = c;
|
|
213
304
|
this.recording = new Recording(c);
|
|
214
|
-
this.replays = unsupported("sessions.replays", "Solari records DOM events, not video; use sessions.recording.retrieve");
|
|
215
|
-
this.logs = unsupported("sessions.logs", "session command logs are not available");
|
|
216
|
-
this.downloads = unsupported("sessions.downloads", "file downloads cannot be retrieved through the API");
|
|
217
|
-
this.uploads = unsupported("sessions.uploads", "use your automation client's setInputFiles");
|
|
218
305
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.c.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
id: data.sessionId,
|
|
232
|
-
status: "active",
|
|
233
|
-
createdAt: new Date().toISOString(),
|
|
234
|
-
...(data.expiresAt ? { expiresAt: data.expiresAt } : {}),
|
|
235
|
-
...(data.userMetadata ? { userMetadata: data.userMetadata } : {}),
|
|
236
|
-
}, { keepAlive: params?.keepAlive === true, ...(ctx ? { contextId: ctx } : {}), status: "RUNNING" });
|
|
237
|
-
const out = withConnect(s, data.cdpEndpoint);
|
|
238
|
-
if (ctx) {
|
|
239
|
-
let state = data.storageState ?? null;
|
|
240
|
-
if (!state && data.storageStateUrl?.url) {
|
|
241
|
-
const r = await this.c.fetchPublic(data.storageStateUrl.url).catch(() => undefined);
|
|
242
|
-
if (r?.ok)
|
|
243
|
-
state = (await r.json().catch(() => null));
|
|
244
|
-
else
|
|
245
|
-
this.c.warn("could not download the saved context; the session starts without it");
|
|
306
|
+
create(first, second) {
|
|
307
|
+
return compat(async () => {
|
|
308
|
+
const [params, options] = splitArgs(first, second);
|
|
309
|
+
const call = resolveOptions(options, { body: true, idempotencyKey: true }, "sessions.create");
|
|
310
|
+
const mapped = mapCreateParams(params, this.c.strict);
|
|
311
|
+
if (params?.projectId !== undefined)
|
|
312
|
+
this.c.noteProjectId("sessions.create");
|
|
313
|
+
for (const w of mapped.warnings)
|
|
314
|
+
this.c.warn(w);
|
|
315
|
+
const data = await this.c.json("POST", "/sessions", Object.keys(mapped.body).length ? mapped.body : undefined, { ...call, idempotencyKey: call.idempotencyKey ?? newKey() });
|
|
316
|
+
if (!data?.sessionId || !data.cdpEndpoint) {
|
|
317
|
+
throw new BrowserbaseError(`Solari returned an unexpected session response: ${JSON.stringify(data)}`);
|
|
246
318
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
319
|
+
if (mapped.persistProfileId)
|
|
320
|
+
this.c.persist.set(data.sessionId, mapped.persistProfileId);
|
|
321
|
+
const ctx = params?.browserSettings?.context?.id;
|
|
322
|
+
const s = toSession({
|
|
323
|
+
id: data.sessionId,
|
|
324
|
+
status: "active",
|
|
325
|
+
createdAt: new Date().toISOString(),
|
|
326
|
+
...(data.expiresAt ? { expiresAt: data.expiresAt } : {}),
|
|
327
|
+
...(data.userMetadata ? { userMetadata: data.userMetadata } : {}),
|
|
328
|
+
}, { keepAlive: params?.keepAlive === true, ...(ctx ? { contextId: ctx } : {}), status: "RUNNING" });
|
|
329
|
+
const out = withConnect(s, data.cdpEndpoint);
|
|
330
|
+
if (ctx) {
|
|
331
|
+
let state = data.storageState ?? null;
|
|
332
|
+
if (!state && data.storageStateUrl?.url) {
|
|
333
|
+
const r = await this.c.fetchPublic(data.storageStateUrl.url, followUp(call)).catch(() => undefined);
|
|
334
|
+
if (r?.ok)
|
|
335
|
+
state = (await r.json().catch(() => null));
|
|
336
|
+
else
|
|
337
|
+
this.c.warn("could not download the saved context; the session starts without it");
|
|
338
|
+
}
|
|
339
|
+
out.storageState = state;
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
});
|
|
250
343
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
344
|
+
retrieve(id, options) {
|
|
345
|
+
return compat(async () => {
|
|
346
|
+
const call = resolveOptions(options, {}, "sessions.retrieve");
|
|
347
|
+
const row = await this.c.json("GET", `/sessions/${encodeURIComponent(id)}`, undefined, call);
|
|
348
|
+
return withConnect(toSession({ ...row, id: row.id ?? id }), row.cdpEndpoint ?? "");
|
|
349
|
+
});
|
|
254
350
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
351
|
+
list(first, second) {
|
|
352
|
+
return compat(async () => {
|
|
353
|
+
const [query, options] = splitArgs(first, second);
|
|
354
|
+
onlyParams(query, ["status", "q"], "sessions.list");
|
|
355
|
+
const call = resolveOptions(options, {}, "sessions.list");
|
|
356
|
+
// The gateway answers in pages (100 by default); Browserbase's list is the whole list, so follow the cursor.
|
|
357
|
+
const rows = [];
|
|
358
|
+
let cursor;
|
|
359
|
+
for (let page = 0;; page++) {
|
|
360
|
+
if (page >= MAX_LIST_PAGES)
|
|
361
|
+
throw new BrowserbaseError(`sessions.list: more than ${MAX_LIST_PAGES} pages; narrow it with q`);
|
|
362
|
+
const qs = [];
|
|
363
|
+
if (query?.q)
|
|
364
|
+
qs.push(`q=${encodeURIComponent(query.q)}`);
|
|
365
|
+
if (cursor)
|
|
366
|
+
qs.push(`cursor=${encodeURIComponent(cursor)}`);
|
|
367
|
+
const data = await this.c.json("GET", qs.length > 0 ? `/sessions?${qs.join("&")}` : "/sessions", undefined, call);
|
|
368
|
+
rows.push(...(data?.sessions ?? []));
|
|
369
|
+
if (!data?.nextCursor)
|
|
370
|
+
break;
|
|
371
|
+
if (data.nextCursor === cursor)
|
|
372
|
+
throw new BrowserbaseError("sessions.list: the gateway's cursor did not advance");
|
|
373
|
+
cursor = data.nextCursor;
|
|
374
|
+
}
|
|
375
|
+
const all = rows.map((r) => toSession(r));
|
|
376
|
+
return query?.status ? all.filter((s) => s.status === query.status) : all;
|
|
377
|
+
});
|
|
259
378
|
}
|
|
260
379
|
/** `{ status: "REQUEST_RELEASE" }` releases the session (saving the context first when it was created with persist: true). */
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
380
|
+
update(id, params, options) {
|
|
381
|
+
return compat(async () => {
|
|
382
|
+
onlyParams(params, ["status", "projectId"], "sessions.update");
|
|
383
|
+
const call = resolveOptions(options, {}, "sessions.update");
|
|
384
|
+
if (params?.status !== "REQUEST_RELEASE")
|
|
385
|
+
throw new SolariCompatUnsupported(`sessions.update status ${String(params?.status)}`, "only REQUEST_RELEASE exists");
|
|
386
|
+
if (params.projectId !== undefined)
|
|
387
|
+
this.c.noteProjectId("sessions.update");
|
|
388
|
+
const profileId = this.c.persist.get(id);
|
|
389
|
+
if (profileId) {
|
|
390
|
+
await this.c.json("POST", `/sessions/${encodeURIComponent(id)}/save-profile`, { profileId }, call);
|
|
391
|
+
this.c.persist.delete(id);
|
|
392
|
+
}
|
|
393
|
+
await this.c.json("DELETE", `/sessions/${encodeURIComponent(id)}`, undefined, call);
|
|
394
|
+
return toSession({ id, status: "released" }, { status: "COMPLETED", endedAt: new Date().toISOString() });
|
|
395
|
+
});
|
|
271
396
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
397
|
+
debug(id, first, second) {
|
|
398
|
+
return compat(async () => {
|
|
399
|
+
const [query, options] = splitArgs(first, second);
|
|
400
|
+
onlyParams(query, ["expiresIn"], "sessions.debug");
|
|
401
|
+
const call = resolveOptions(options, {}, "sessions.debug");
|
|
402
|
+
const share = await this.c.json("POST", `/sessions/${encodeURIComponent(id)}/shares`, query?.expiresIn !== undefined ? { ttlSeconds: query.expiresIn } : {}, call);
|
|
403
|
+
const row = await this.c.json("GET", `/sessions/${encodeURIComponent(id)}`, undefined, call);
|
|
404
|
+
return { debuggerFullscreenUrl: share.url, debuggerUrl: share.url, wsUrl: row.cdpEndpoint ?? "", pages: [] };
|
|
405
|
+
});
|
|
277
406
|
}
|
|
278
407
|
}
|
|
279
408
|
class Recording {
|
|
280
409
|
c;
|
|
410
|
+
downloads = unsupportedMethods("sessions.recording.downloads", "Solari records DOM events, not video; use sessions.recording.retrieve", ["create", "list"]);
|
|
281
411
|
constructor(c) {
|
|
282
412
|
this.c = c;
|
|
283
413
|
}
|
|
284
414
|
/** The session's DOM recording as an array of rrweb events. Needs `browserSettings: { recordSession: true }`; available a few seconds after release. */
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
buf =
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
415
|
+
retrieve(id, options) {
|
|
416
|
+
return compat(async () => {
|
|
417
|
+
const call = resolveOptions(options, {}, "sessions.recording.retrieve");
|
|
418
|
+
const meta = await this.c.json("GET", `/sessions/${encodeURIComponent(id)}/replay-url`, undefined, call);
|
|
419
|
+
if (!meta?.url)
|
|
420
|
+
throw new BrowserbaseError("Solari returned no replay URL");
|
|
421
|
+
const r = await this.c.fetchPublic(meta.url, followUp(call), 60_000);
|
|
422
|
+
if (!r.ok)
|
|
423
|
+
throw new APIError(r.status, `replay download failed: ${r.status}`);
|
|
424
|
+
let buf = Buffer.from(await r.arrayBuffer());
|
|
425
|
+
if (buf.length > 2 && buf[0] === 0x1f && buf[1] === 0x8b)
|
|
426
|
+
buf = gunzipSync(buf);
|
|
427
|
+
return buf
|
|
428
|
+
.toString("utf8")
|
|
429
|
+
.split("\n")
|
|
430
|
+
.filter((l) => l.trim() !== "")
|
|
431
|
+
.map((l) => JSON.parse(l));
|
|
432
|
+
});
|
|
300
433
|
}
|
|
301
434
|
}
|
|
302
435
|
class Contexts {
|
|
@@ -304,22 +437,35 @@ class Contexts {
|
|
|
304
437
|
constructor(c) {
|
|
305
438
|
this.c = c;
|
|
306
439
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
440
|
+
create(first, second) {
|
|
441
|
+
return compat(async () => {
|
|
442
|
+
const [params, options] = splitArgs(first, second);
|
|
443
|
+
onlyParams(params, ["projectId", "name"], "contexts.create");
|
|
444
|
+
const call = resolveOptions(options, { body: true, idempotencyKey: true }, "contexts.create");
|
|
445
|
+
if (params?.projectId !== undefined)
|
|
446
|
+
this.c.noteProjectId("contexts.create");
|
|
447
|
+
const p = await this.c.json("POST", "/profiles", { name: params?.name ?? `context-${newKey().slice(0, 8)}` }, call);
|
|
448
|
+
return { id: p.id, publicKey: "", cipherAlgorithm: "", initializationVectorSize: 0, uploadUrl: "" };
|
|
449
|
+
});
|
|
311
450
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
451
|
+
retrieve(id, options) {
|
|
452
|
+
return compat(async () => {
|
|
453
|
+
const call = resolveOptions(options, {}, "contexts.retrieve");
|
|
454
|
+
const list = await this.c.json("GET", "/profiles", undefined, call);
|
|
455
|
+
const p = (list ?? []).find((x) => x.id === id);
|
|
456
|
+
if (!p)
|
|
457
|
+
throw new NotFoundError(404, `context ${id} not found`, "NotFound");
|
|
458
|
+
const createdAt = p.createdAt ?? "";
|
|
459
|
+
return { id: p.id, ...(p.name ? { name: p.name } : {}), createdAt, updatedAt: p.updatedAt ?? createdAt, projectId: "" };
|
|
460
|
+
});
|
|
318
461
|
}
|
|
319
|
-
update() {
|
|
462
|
+
update(..._a) {
|
|
320
463
|
throw new SolariCompatUnsupported("contexts.update", "contexts are saved when a persist:true session is released");
|
|
321
464
|
}
|
|
322
|
-
|
|
323
|
-
|
|
465
|
+
delete(id, options) {
|
|
466
|
+
return compat(async () => {
|
|
467
|
+
const call = resolveOptions(options, {}, "contexts.delete");
|
|
468
|
+
await this.c.json("DELETE", `/profiles/${encodeURIComponent(id)}`, undefined, call);
|
|
469
|
+
});
|
|
324
470
|
}
|
|
325
471
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -42,6 +42,10 @@ export declare class APIConnectionError extends BrowserbaseError {
|
|
|
42
42
|
export declare class APIConnectionTimeoutError extends APIConnectionError {
|
|
43
43
|
constructor(message?: string, cause?: unknown);
|
|
44
44
|
}
|
|
45
|
+
/** The caller aborted the request (the `signal` passed in the request options). Same name as Browserbase's SDK. */
|
|
46
|
+
export declare class APIUserAbortError extends BrowserbaseError {
|
|
47
|
+
constructor(message?: string);
|
|
48
|
+
}
|
|
45
49
|
/** The APIError subclass for an HTTP status, as Browserbase's SDK raises them. */
|
|
46
50
|
export declare function apiErrorFor(status: number, message: string, code?: string, body?: unknown): APIError;
|
|
47
51
|
/** A fetch failure as a Browserbase-style connection error (abort/timeout vs. everything else). */
|
package/dist/errors.js
CHANGED
|
@@ -55,6 +55,12 @@ export class APIConnectionTimeoutError extends APIConnectionError {
|
|
|
55
55
|
super(message, cause);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/** The caller aborted the request (the `signal` passed in the request options). Same name as Browserbase's SDK. */
|
|
59
|
+
export class APIUserAbortError extends BrowserbaseError {
|
|
60
|
+
constructor(message = "Request was aborted.") {
|
|
61
|
+
super(message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
58
64
|
/** The APIError subclass for an HTTP status, as Browserbase's SDK raises them. */
|
|
59
65
|
export function apiErrorFor(status, message, code, body) {
|
|
60
66
|
const C = status === 400 ? BadRequestError
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Browserbase } from "./client.js";
|
|
2
2
|
export { Browserbase, DEFAULT_BASE_URL } from "./client.js";
|
|
3
|
-
export type { BrowserbaseOptions
|
|
4
|
-
export {
|
|
3
|
+
export type { BrowserbaseOptions } from "./client.js";
|
|
4
|
+
export type { UnsupportedResource } from "./unsupported.js";
|
|
5
|
+
export type { RequestOptions } from "./options.js";
|
|
6
|
+
export { CompatPromise } from "./options.js";
|
|
7
|
+
export { APIConnectionError, APIConnectionTimeoutError, APIError, APIUserAbortError, AuthenticationError, BadRequestError, BrowserbaseError, ConflictError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, SolariCompatUnsupported, UnprocessableEntityError, } from "./errors.js";
|
|
5
8
|
export { mapCreateParams, mapStatus } from "./mapping.js";
|
|
6
9
|
export type * from "./types.js";
|
|
7
10
|
export default Browserbase;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Browserbase } from "./client.js";
|
|
2
2
|
export { Browserbase, DEFAULT_BASE_URL } from "./client.js";
|
|
3
|
-
export {
|
|
3
|
+
export { CompatPromise } from "./options.js";
|
|
4
|
+
export { APIConnectionError, APIConnectionTimeoutError, APIError, APIUserAbortError, AuthenticationError, BadRequestError, BrowserbaseError, ConflictError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, SolariCompatUnsupported, UnprocessableEntityError, } from "./errors.js";
|
|
4
5
|
export { mapCreateParams, mapStatus } from "./mapping.js";
|
|
5
6
|
export default Browserbase;
|
package/dist/mapping.d.ts
CHANGED
|
@@ -7,6 +7,11 @@ export interface MappedCreate {
|
|
|
7
7
|
/** Soft differences the caller should know about (also emitted via the warn callback). */
|
|
8
8
|
warnings: string[];
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* The gateway does not return a session's keepAlive flag, so a keepAlive create also stores this reserved key in the session's
|
|
12
|
+
* user metadata, and `retrieve` / `list` read the flag back from it (and hide the key). The gateway allows 50 metadata keys.
|
|
13
|
+
*/
|
|
14
|
+
export declare const KEEP_ALIVE_KEY = "solari_compat_keep_alive";
|
|
10
15
|
/**
|
|
11
16
|
* Translate Browserbase `sessions.create` params into a Solari `POST /sessions` body.
|
|
12
17
|
* Hard gaps throw SolariCompatUnsupported. Soft gaps (a setting we cannot honour but whose absence does not change
|
package/dist/mapping.js
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import { SolariCompatUnsupported } from "./errors.js";
|
|
2
2
|
const SOLARI_REGION_ALIASES = new Set(["us-west-2"]);
|
|
3
|
+
/**
|
|
4
|
+
* The gateway does not return a session's keepAlive flag, so a keepAlive create also stores this reserved key in the session's
|
|
5
|
+
* user metadata, and `retrieve` / `list` read the flag back from it (and hide the key). The gateway allows 50 metadata keys.
|
|
6
|
+
*/
|
|
7
|
+
export const KEEP_ALIVE_KEY = "solari_compat_keep_alive";
|
|
8
|
+
const METADATA_MAX_KEYS = 50;
|
|
9
|
+
const PARAM_KEYS = new Set(["browserSettings", "extensionId", "keepAlive", "projectId", "proxies", "proxySettings", "region", "api_timeout", "timeout", "userMetadata"]);
|
|
10
|
+
const SETTING_KEYS = new Set([
|
|
11
|
+
"advancedStealth", "allowedDomains", "blockAds", "captchaImageSelector", "captchaInputSelector", "context", "extensionId",
|
|
12
|
+
"ignoreCertificateErrors", "logSession", "os", "recordSession", "riskPolicyId", "solveCaptchas", "verified", "viewport",
|
|
13
|
+
]);
|
|
14
|
+
/** Raise for any key outside `known`: a dropped key is a silent failure (and for `allowedDomains`-like keys it would fail open). */
|
|
15
|
+
function onlyKeys(obj, known, where) {
|
|
16
|
+
if (obj === undefined || obj === null || typeof obj !== "object" || Array.isArray(obj))
|
|
17
|
+
return;
|
|
18
|
+
for (const k of Object.keys(obj)) {
|
|
19
|
+
if (!known.has(k))
|
|
20
|
+
throw new SolariCompatUnsupported(`${where}.${k}`, "this setting is not supported");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const CONTEXT_KEYS = new Set(["id", "persist"]);
|
|
24
|
+
const VIEWPORT_KEYS = new Set(["width", "height"]);
|
|
25
|
+
const PROXY_KEYS = new Set(["type", "domainPattern", "geolocation", "server", "username", "password"]);
|
|
26
|
+
const GEO_KEYS = new Set(["country", "state", "city"]);
|
|
3
27
|
function proxyBody(proxies, caCerts) {
|
|
4
28
|
if (caCerts && caCerts.length > 0) {
|
|
5
29
|
throw new SolariCompatUnsupported("proxySettings.caCertificates", "custom CA certificates are not supported");
|
|
@@ -14,6 +38,8 @@ function proxyBody(proxies, caCerts) {
|
|
|
14
38
|
throw new SolariCompatUnsupported("multiple proxies / domain routing rules", "pass at most one managed proxy; per-domain routing is not supported");
|
|
15
39
|
}
|
|
16
40
|
const p = proxies[0];
|
|
41
|
+
onlyKeys(p, PROXY_KEYS, "proxies[]");
|
|
42
|
+
onlyKeys(p.geolocation, GEO_KEYS, "proxies[].geolocation");
|
|
17
43
|
if (p.domainPattern !== undefined && p.domainPattern !== "") {
|
|
18
44
|
throw new SolariCompatUnsupported("proxies[].domainPattern", "per-domain proxy routing is not supported");
|
|
19
45
|
}
|
|
@@ -42,7 +68,12 @@ function proxyBody(proxies, caCerts) {
|
|
|
42
68
|
*/
|
|
43
69
|
export function mapCreateParams(params, strict) {
|
|
44
70
|
const p = params ?? {};
|
|
71
|
+
onlyKeys(p, PARAM_KEYS, "sessions.create");
|
|
45
72
|
const bs = p.browserSettings ?? {};
|
|
73
|
+
onlyKeys(bs, SETTING_KEYS, "browserSettings");
|
|
74
|
+
onlyKeys(bs.context, CONTEXT_KEYS, "browserSettings.context");
|
|
75
|
+
onlyKeys(bs.viewport, VIEWPORT_KEYS, "browserSettings.viewport");
|
|
76
|
+
onlyKeys(p.proxySettings, new Set(["caCertificates"]), "proxySettings");
|
|
46
77
|
const warnings = [];
|
|
47
78
|
const soft = (feature, why) => {
|
|
48
79
|
if (strict)
|
|
@@ -50,6 +81,8 @@ export function mapCreateParams(params, strict) {
|
|
|
50
81
|
warnings.push(`${feature}: ${why}`);
|
|
51
82
|
};
|
|
52
83
|
// ── hard gaps ────────────────────────────────────────────────────────────────
|
|
84
|
+
if (bs.riskPolicyId !== undefined)
|
|
85
|
+
throw new SolariCompatUnsupported("browserSettings.riskPolicyId", "risk policies are a Browserbase feature with no Solari equivalent");
|
|
53
86
|
if (p.extensionId || bs.extensionId)
|
|
54
87
|
throw new SolariCompatUnsupported("extensions", "browser extensions cannot be loaded");
|
|
55
88
|
if (bs.ignoreCertificateErrors === true)
|
|
@@ -88,12 +121,31 @@ export function mapCreateParams(params, strict) {
|
|
|
88
121
|
body.allowedDomains = allowedDomains;
|
|
89
122
|
if (bs.context?.id)
|
|
90
123
|
body.profileId = bs.context.id;
|
|
91
|
-
|
|
92
|
-
|
|
124
|
+
// Browserbase's name for the session timeout is `api_timeout`; `timeout` next to other params is the older spelling.
|
|
125
|
+
if (p.api_timeout !== undefined && p.timeout !== undefined && p.api_timeout !== p.timeout) {
|
|
126
|
+
throw new SolariCompatUnsupported("sessions.create timeout", "api_timeout and timeout are both the session timeout and differ; pass api_timeout only");
|
|
127
|
+
}
|
|
128
|
+
const sessionTimeout = p.api_timeout ?? p.timeout;
|
|
129
|
+
if (sessionTimeout !== undefined)
|
|
130
|
+
body.timeoutSeconds = sessionTimeout; // the gateway validates (>= 60) and clamps to the plan maximum
|
|
93
131
|
if (p.keepAlive === true)
|
|
94
132
|
body.keepAlive = true;
|
|
95
|
-
|
|
96
|
-
|
|
133
|
+
const um = p.userMetadata;
|
|
134
|
+
if (um !== undefined && (um === null || typeof um !== "object" || Array.isArray(um))) {
|
|
135
|
+
throw new SolariCompatUnsupported("userMetadata", "expected an object of key/value pairs");
|
|
136
|
+
}
|
|
137
|
+
if (um !== undefined && KEEP_ALIVE_KEY in um) {
|
|
138
|
+
throw new SolariCompatUnsupported(`userMetadata.${KEEP_ALIVE_KEY}`, "this key is reserved: it records keepAlive so retrieve and list can report it");
|
|
139
|
+
}
|
|
140
|
+
if (p.keepAlive === true) {
|
|
141
|
+
if (um !== undefined && Object.keys(um).length >= METADATA_MAX_KEYS) {
|
|
142
|
+
throw new SolariCompatUnsupported(`keepAlive with ${Object.keys(um).length} userMetadata keys`, `one of the ${METADATA_MAX_KEYS} metadata keys is reserved for keepAlive; pass at most ${METADATA_MAX_KEYS - 1}`);
|
|
143
|
+
}
|
|
144
|
+
body.userMetadata = { ...(um ?? {}), [KEEP_ALIVE_KEY]: true };
|
|
145
|
+
}
|
|
146
|
+
else if (um !== undefined) {
|
|
147
|
+
body.userMetadata = um;
|
|
148
|
+
}
|
|
97
149
|
return {
|
|
98
150
|
body,
|
|
99
151
|
...(bs.context?.id && bs.context.persist === true ? { persistProfileId: bs.context.id } : {}),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-call options, as in Browserbase's SDK. `query`, `headers`, `timeout`, `maxRetries` and `signal` apply to every request a
|
|
3
|
+
* call makes; `body` and `idempotencyKey` only to calls that create something (`sessions.create`, `contexts.create`).
|
|
4
|
+
* The rest (`method`, `path`, `stream`, `httpAgent`, `defaultBaseURL`, `__binary*`) raise `SolariCompatUnsupported`.
|
|
5
|
+
*/
|
|
6
|
+
export interface RequestOptions {
|
|
7
|
+
query?: Record<string, unknown> | undefined;
|
|
8
|
+
body?: Record<string, unknown> | null | undefined;
|
|
9
|
+
headers?: Record<string, string | null | undefined> | undefined;
|
|
10
|
+
/** Retries for this call (overrides the client's). */
|
|
11
|
+
maxRetries?: number;
|
|
12
|
+
/** Request timeout in milliseconds (overrides the client's). Not the session timeout: that is `api_timeout` on `sessions.create`. */
|
|
13
|
+
timeout?: number;
|
|
14
|
+
signal?: AbortSignal | undefined | null;
|
|
15
|
+
/** Only on create calls. */
|
|
16
|
+
idempotencyKey?: string;
|
|
17
|
+
/** Not supported: raises. */
|
|
18
|
+
method?: string;
|
|
19
|
+
path?: string;
|
|
20
|
+
defaultBaseURL?: string | undefined;
|
|
21
|
+
stream?: boolean | undefined;
|
|
22
|
+
httpAgent?: unknown;
|
|
23
|
+
__binaryRequest?: boolean | undefined;
|
|
24
|
+
__binaryResponse?: boolean | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** Browserbase's heuristic: a non-empty object whose keys are all request options is the options argument, not the params. */
|
|
27
|
+
export declare function isRequestOptions(o: unknown): o is RequestOptions;
|
|
28
|
+
/** `fn(params?, options?)` and `fn(options?)`: which is which. */
|
|
29
|
+
export declare function splitArgs<P>(first: P | RequestOptions | undefined, second: RequestOptions | undefined): [P | undefined, RequestOptions | undefined];
|
|
30
|
+
/** What a call hands to the HTTP layer. */
|
|
31
|
+
export interface Call {
|
|
32
|
+
headers?: Record<string, string | null | undefined>;
|
|
33
|
+
query?: Record<string, unknown>;
|
|
34
|
+
body?: Record<string, unknown>;
|
|
35
|
+
timeout?: number;
|
|
36
|
+
maxRetries?: number;
|
|
37
|
+
signal?: AbortSignal | null;
|
|
38
|
+
idempotencyKey?: string;
|
|
39
|
+
}
|
|
40
|
+
/** Validate a call's options. Nothing is dropped: an option that cannot be honoured raises. */
|
|
41
|
+
export declare function resolveOptions(options: RequestOptions | undefined, allow: {
|
|
42
|
+
body?: boolean;
|
|
43
|
+
idempotencyKey?: boolean;
|
|
44
|
+
}, feature: string): Call;
|
|
45
|
+
/** A call's options for the follow-up requests of a multi-request call: no body, no idempotency key. */
|
|
46
|
+
export declare function followUp(call: Call): Call;
|
|
47
|
+
/** Headers the caller may not set: the key is the client's, not a header to override. */
|
|
48
|
+
export declare function assertHeaders(h: Record<string, string | null | undefined> | undefined, where: string): void;
|
|
49
|
+
export declare function setHeader(target: Record<string, string>, name: string, value: string | null | undefined): void;
|
|
50
|
+
/**
|
|
51
|
+
* What every method returns. Awaits like a Promise; `asResponse()` and `withResponse()` (Browserbase's APIPromise extras)
|
|
52
|
+
* raise `SolariCompatUnsupported`, because a call here can be several HTTP requests with no single response.
|
|
53
|
+
*/
|
|
54
|
+
export declare class CompatPromise<T> extends Promise<T> {
|
|
55
|
+
static get [Symbol.species](): PromiseConstructor;
|
|
56
|
+
asResponse(): never;
|
|
57
|
+
withResponse(): never;
|
|
58
|
+
}
|
|
59
|
+
export declare function compat<T>(fn: () => Promise<T>): CompatPromise<T>;
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { SolariCompatUnsupported } from "./errors.js";
|
|
2
|
+
const REQUEST_OPTION_KEYS = new Set([
|
|
3
|
+
"method", "path", "query", "body", "headers", "defaultBaseURL", "maxRetries", "stream", "timeout", "httpAgent", "signal",
|
|
4
|
+
"idempotencyKey", "__binaryRequest", "__binaryResponse",
|
|
5
|
+
]);
|
|
6
|
+
const REFUSED = {
|
|
7
|
+
method: "the HTTP method is fixed by the call",
|
|
8
|
+
path: "the path is fixed by the call",
|
|
9
|
+
defaultBaseURL: "set baseURL on the client",
|
|
10
|
+
stream: "responses are not streamed",
|
|
11
|
+
httpAgent: "pass a fetch with your own dispatcher to the client instead",
|
|
12
|
+
__binaryRequest: "binary requests are not used",
|
|
13
|
+
__binaryResponse: "binary responses are not used",
|
|
14
|
+
};
|
|
15
|
+
/** Browserbase's heuristic: a non-empty object whose keys are all request options is the options argument, not the params. */
|
|
16
|
+
export function isRequestOptions(o) {
|
|
17
|
+
if (!o || typeof o !== "object" || Array.isArray(o))
|
|
18
|
+
return false;
|
|
19
|
+
const keys = Object.keys(o);
|
|
20
|
+
return keys.length > 0 && keys.every((k) => REQUEST_OPTION_KEYS.has(k));
|
|
21
|
+
}
|
|
22
|
+
/** `fn(params?, options?)` and `fn(options?)`: which is which. */
|
|
23
|
+
export function splitArgs(first, second) {
|
|
24
|
+
if (second === undefined && isRequestOptions(first))
|
|
25
|
+
return [undefined, first];
|
|
26
|
+
return [first, second];
|
|
27
|
+
}
|
|
28
|
+
/** Validate a call's options. Nothing is dropped: an option that cannot be honoured raises. */
|
|
29
|
+
export function resolveOptions(options, allow, feature) {
|
|
30
|
+
if (options === undefined || options === null)
|
|
31
|
+
return {};
|
|
32
|
+
if (typeof options !== "object" || Array.isArray(options))
|
|
33
|
+
throw new SolariCompatUnsupported(`${feature}: request options`, "expected an object");
|
|
34
|
+
for (const k of Object.keys(options)) {
|
|
35
|
+
if (options[k] === undefined)
|
|
36
|
+
continue;
|
|
37
|
+
if (k in REFUSED)
|
|
38
|
+
throw new SolariCompatUnsupported(`${feature}: request option ${k}`, REFUSED[k]);
|
|
39
|
+
if (!REQUEST_OPTION_KEYS.has(k))
|
|
40
|
+
throw new SolariCompatUnsupported(`${feature}: request option ${k}`, "not a request option");
|
|
41
|
+
}
|
|
42
|
+
if (options.body !== undefined && options.body !== null && !allow.body) {
|
|
43
|
+
throw new SolariCompatUnsupported(`${feature}: request option body`, "this call takes no extra body fields");
|
|
44
|
+
}
|
|
45
|
+
if (options.idempotencyKey !== undefined && !allow.idempotencyKey) {
|
|
46
|
+
throw new SolariCompatUnsupported(`${feature}: request option idempotencyKey`, "only create calls take an idempotency key");
|
|
47
|
+
}
|
|
48
|
+
if (options.body !== undefined && options.body !== null && (typeof options.body !== "object" || Array.isArray(options.body))) {
|
|
49
|
+
throw new SolariCompatUnsupported(`${feature}: request option body`, "expected an object");
|
|
50
|
+
}
|
|
51
|
+
if (options.timeout !== undefined && !(typeof options.timeout === "number" && options.timeout > 0)) {
|
|
52
|
+
throw new SolariCompatUnsupported(`${feature}: request option timeout`, "expected a positive number of milliseconds");
|
|
53
|
+
}
|
|
54
|
+
if (options.maxRetries !== undefined && !(Number.isInteger(options.maxRetries) && options.maxRetries >= 0)) {
|
|
55
|
+
throw new SolariCompatUnsupported(`${feature}: request option maxRetries`, "expected a non-negative integer");
|
|
56
|
+
}
|
|
57
|
+
assertHeaders(options.headers, `${feature}: request option headers`);
|
|
58
|
+
return {
|
|
59
|
+
...(options.headers ? { headers: options.headers } : {}),
|
|
60
|
+
...(options.query ? { query: options.query } : {}),
|
|
61
|
+
...(options.body ? { body: options.body } : {}),
|
|
62
|
+
...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
|
|
63
|
+
...(options.maxRetries !== undefined ? { maxRetries: options.maxRetries } : {}),
|
|
64
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
65
|
+
...(options.idempotencyKey !== undefined ? { idempotencyKey: options.idempotencyKey } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** A call's options for the follow-up requests of a multi-request call: no body, no idempotency key. */
|
|
69
|
+
export function followUp(call) {
|
|
70
|
+
const { body: _b, idempotencyKey: _i, ...rest } = call;
|
|
71
|
+
return rest;
|
|
72
|
+
}
|
|
73
|
+
const RESERVED_HEADERS = new Set(["authorization", "idempotency-key"]);
|
|
74
|
+
/** Headers the caller may not set: the key is the client's, not a header to override. */
|
|
75
|
+
export function assertHeaders(h, where) {
|
|
76
|
+
if (h === undefined)
|
|
77
|
+
return;
|
|
78
|
+
if (h === null || typeof h !== "object" || Array.isArray(h))
|
|
79
|
+
throw new SolariCompatUnsupported(where, "expected an object");
|
|
80
|
+
for (const k of Object.keys(h)) {
|
|
81
|
+
if (RESERVED_HEADERS.has(k.toLowerCase()))
|
|
82
|
+
throw new SolariCompatUnsupported(where, `${k} cannot be set: the client sets it`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function setHeader(target, name, value) {
|
|
86
|
+
const lower = name.toLowerCase();
|
|
87
|
+
for (const k of Object.keys(target))
|
|
88
|
+
if (k.toLowerCase() === lower)
|
|
89
|
+
delete target[k];
|
|
90
|
+
if (value !== null && value !== undefined)
|
|
91
|
+
target[name] = value;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* What every method returns. Awaits like a Promise; `asResponse()` and `withResponse()` (Browserbase's APIPromise extras)
|
|
95
|
+
* raise `SolariCompatUnsupported`, because a call here can be several HTTP requests with no single response.
|
|
96
|
+
*/
|
|
97
|
+
export class CompatPromise extends Promise {
|
|
98
|
+
static get [Symbol.species]() {
|
|
99
|
+
return Promise;
|
|
100
|
+
}
|
|
101
|
+
asResponse() {
|
|
102
|
+
throw new SolariCompatUnsupported("asResponse()", "a call can make several requests; there is no single Response");
|
|
103
|
+
}
|
|
104
|
+
withResponse() {
|
|
105
|
+
throw new SolariCompatUnsupported("withResponse()", "a call can make several requests; there is no single Response");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function compat(fn) {
|
|
109
|
+
return new CompatPromise((resolve, reject) => {
|
|
110
|
+
fn().then(resolve, reject);
|
|
111
|
+
});
|
|
112
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface BrowserSettings {
|
|
|
40
40
|
verified?: boolean;
|
|
41
41
|
captchaImageSelector?: string;
|
|
42
42
|
captchaInputSelector?: string;
|
|
43
|
+
/** Not supported: raises. */
|
|
44
|
+
riskPolicyId?: string;
|
|
43
45
|
os?: "windows" | "mac" | "linux" | "mobile" | "tablet";
|
|
44
46
|
allowedDomains?: string[];
|
|
45
47
|
ignoreCertificateErrors?: boolean;
|
|
@@ -48,6 +50,9 @@ export interface SessionCreateParams {
|
|
|
48
50
|
projectId?: string;
|
|
49
51
|
extensionId?: string;
|
|
50
52
|
browserSettings?: BrowserSettings;
|
|
53
|
+
/** The session timeout in seconds (Browserbase's name for it). */
|
|
54
|
+
api_timeout?: number;
|
|
55
|
+
/** @deprecated The older spelling of `api_timeout`, read only when passed next to other params. A lone `{ timeout }` is the request timeout, as in Browserbase's SDK. */
|
|
51
56
|
timeout?: number;
|
|
52
57
|
keepAlive?: boolean;
|
|
53
58
|
proxies?: boolean | ProxyConfig[];
|
|
@@ -107,7 +112,11 @@ export interface SessionLiveURLs {
|
|
|
107
112
|
export interface Context {
|
|
108
113
|
id: string;
|
|
109
114
|
name?: string;
|
|
110
|
-
createdAt
|
|
115
|
+
createdAt: string;
|
|
116
|
+
/** Solari does not track when a profile was last saved: the same as `createdAt`. */
|
|
117
|
+
updatedAt: string;
|
|
118
|
+
/** Always empty: Solari keys belong to an organisation, there are no projects. */
|
|
119
|
+
projectId: string;
|
|
111
120
|
}
|
|
112
121
|
export interface ContextCreateResponse {
|
|
113
122
|
id: string;
|
|
@@ -115,4 +124,6 @@ export interface ContextCreateResponse {
|
|
|
115
124
|
publicKey: string;
|
|
116
125
|
cipherAlgorithm: string;
|
|
117
126
|
initializationVectorSize: number;
|
|
127
|
+
/** Empty: contexts are saved by releasing a `persist: true` session, not by uploading. */
|
|
128
|
+
uploadUrl: string;
|
|
118
129
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A resource whose methods all throw SolariCompatUnsupported (kept for the public type; see `unsupportedMethods`). */
|
|
2
|
+
export type UnsupportedResource = {
|
|
3
|
+
[method: string]: (...args: unknown[]) => never;
|
|
4
|
+
};
|
|
5
|
+
/** A resource with exactly these methods, each of which throws SolariCompatUnsupported naming `<feature>.<method>`. */
|
|
6
|
+
export type UnsupportedMethods<K extends string> = {
|
|
7
|
+
[P in K]: (...args: unknown[]) => never;
|
|
8
|
+
};
|
|
9
|
+
export declare function unsupportedMethods<const K extends string>(feature: string, hint: string, methods: readonly K[]): UnsupportedMethods<K>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SolariCompatUnsupported } from "./errors.js";
|
|
2
|
+
export function unsupportedMethods(feature, hint, methods) {
|
|
3
|
+
const out = {};
|
|
4
|
+
for (const m of methods) {
|
|
5
|
+
out[m] = () => {
|
|
6
|
+
throw new SolariCompatUnsupported(`${feature}.${m}`, hint);
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
return out;
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solarisdk/browserbase-compat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Drop-in Browserbase-style SDK surface for Solari Browser: sessions, connectUrl (CDP), contexts, live view, replay events.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|