@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 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 ignored: Solari keys belong to an organisation, there are no projects. |
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`. `proxyBytes` is `0`, `startedAt` is the create time. |
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
- - **`timeout`, `keepAlive` and `userMetadata` work as in Browserbase.** `timeout` (seconds, at least 60) 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 `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`).
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
- Extensions (`extensionId`), the Selenium endpoint (`seleniumRemoteUrl` is always empty), downloads (`sessions.downloads`) and uploads (`sessions.uploads`) APIs, video replays (`sessions.replays`), session logs (`sessions.logs`), external/bring-your-own proxies, per-domain proxy rules (`domainPattern`), custom CA certificates, `ignoreCertificateErrors: true`, custom captcha selectors, `os: "mobile" | "tablet"`, a `region` other than `us-west-2` (or `us-west`), `contexts.update`, and the separate `projects`, `extensions`, `agents`, `fetchAPI` and `search` resources.
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; extensions, downloads/uploads APIs, video replays,
23
- bring-your-own proxies and Selenium raise `SolariCompatUnsupported`. See [docs.getsolari.com/browserbase](https://docs.getsolari.com/browserbase) (and `COMPAT.md` in this package) for the full list
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
- /** Test hook. */
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: UnsupportedResource;
24
- readonly projects: UnsupportedResource;
25
- readonly agents: UnsupportedResource;
26
- readonly fetchAPI: UnsupportedResource;
27
- readonly search: UnsupportedResource;
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 maxRetries;
34
- private readonly timeoutMs;
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
- /** @internal GET a presigned URL (no Solari auth header). */
39
- fetchPublic(url: string): Promise<Response>;
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
- request(method: string, path: string, body?: unknown, opts?: {
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, opts?: {
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: UnsupportedResource;
57
- readonly logs: UnsupportedResource;
58
- readonly downloads: UnsupportedResource;
59
- readonly uploads: UnsupportedResource;
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): Promise<SessionCreateResponse>;
62
- retrieve(id: string): Promise<SessionRetrieveResponse>;
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
- }): Promise<Session[]>;
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
- }): Promise<Session>;
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
- }): Promise<SessionLiveURLs>;
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): Promise<unknown[]>;
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
- }): Promise<ContextCreateResponse>;
91
- retrieve(id: string): Promise<Context>;
92
- update(): never;
93
- delete(id: string): Promise<void>;
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
- fetchAPI;
68
- search;
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
- maxRetries;
75
- timeoutMs;
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.timeoutMs = opts.timeout ?? 60_000;
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
- /** @internal GET a presigned URL (no Solari auth header). */
96
- async fetchPublic(url) {
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 timer = setTimeout(() => ac.abort(), 15_000);
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 request(method, path, body, opts) {
161
+ async send(method, path, body, call = {}) {
108
162
  const headers = { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" };
109
- if (opts?.idempotencyKey)
110
- headers["Idempotency-Key"] = opts.idempotencyKey;
111
- const replaySafe = method === "GET" || method === "DELETE" || opts?.idempotencyKey !== undefined;
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 <= this.maxRetries; 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 timer = setTimeout(() => ac.abort(), this.timeoutMs);
187
+ const detach = linkSignal(ac, call.signal);
188
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
116
189
  try {
117
- const res = await this.fetchImpl(`${this.baseURL}${path}`, {
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 === this.maxRetries)
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 (!replaySafe || attempt === this.maxRetries)
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, opts) {
140
- const res = await this.request(method, path, body, opts);
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
- class UnsupportedImpl {
159
- feature;
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 unsupported(feature, hint) {
177
- return new UnsupportedImpl(feature, hint);
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
- ...(status === "COMPLETED" || status === "TIMED_OUT" ? { endedAt: new Date().toISOString() } : {}),
192
- expiresAt: row.expiresAt ?? created,
275
+ ...(endedAt ? { endedAt } : {}),
276
+ expiresAt,
193
277
  status,
194
278
  proxyBytes: 0,
195
- keepAlive: extra.keepAlive ?? false,
196
- ...(row.userMetadata ? { userMetadata: row.userMetadata } : {}),
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
- async create(params) {
220
- const mapped = mapCreateParams(params, this.c.strict);
221
- for (const w of mapped.warnings)
222
- this.c.warn(w);
223
- const data = await this.c.json("POST", "/sessions", Object.keys(mapped.body).length ? mapped.body : undefined, { idempotencyKey: newKey() });
224
- if (!data?.sessionId || !data.cdpEndpoint) {
225
- throw new BrowserbaseError(`Solari returned an unexpected session response: ${JSON.stringify(data)}`);
226
- }
227
- if (mapped.persistProfileId)
228
- this.c.persist.set(data.sessionId, mapped.persistProfileId);
229
- const ctx = params?.browserSettings?.context?.id;
230
- const s = toSession({
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
- out.storageState = state;
248
- }
249
- return out;
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
- async retrieve(id) {
252
- const row = await this.c.json("GET", `/sessions/${encodeURIComponent(id)}`);
253
- return withConnect(toSession({ ...row, id: row.id ?? id }), row.cdpEndpoint ?? "");
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
- async list(query) {
256
- const data = await this.c.json("GET", query?.q ? `/sessions?q=${encodeURIComponent(query.q)}` : "/sessions");
257
- const all = (data?.sessions ?? []).map((r) => toSession(r));
258
- return query?.status ? all.filter((s) => s.status === query.status) : all;
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
- async update(id, params) {
262
- if (params?.status !== "REQUEST_RELEASE")
263
- throw new SolariCompatUnsupported(`sessions.update status ${String(params?.status)}`, "only REQUEST_RELEASE exists");
264
- const profileId = this.c.persist.get(id);
265
- if (profileId) {
266
- await this.c.json("POST", `/sessions/${encodeURIComponent(id)}/save-profile`, { profileId });
267
- this.c.persist.delete(id);
268
- }
269
- await this.c.json("DELETE", `/sessions/${encodeURIComponent(id)}`);
270
- return toSession({ id, status: "released" }, { status: "COMPLETED" });
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
- /** Live-view links. Both URLs are the same view-only share link; `pages` is empty. */
273
- async debug(id, query) {
274
- const share = await this.c.json("POST", `/sessions/${encodeURIComponent(id)}/shares`, query?.expiresIn !== undefined ? { ttlSeconds: query.expiresIn } : {});
275
- const row = await this.c.json("GET", `/sessions/${encodeURIComponent(id)}`);
276
- return { debuggerFullscreenUrl: share.url, debuggerUrl: share.url, wsUrl: row.cdpEndpoint ?? "", pages: [] };
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
- async retrieve(id) {
286
- const meta = await this.c.json("GET", `/sessions/${encodeURIComponent(id)}/replay-url`);
287
- if (!meta?.url)
288
- throw new BrowserbaseError("Solari returned no replay URL");
289
- const r = await fetch(meta.url);
290
- if (!r.ok)
291
- throw new APIError(r.status, `replay download failed: ${r.status}`);
292
- let buf = Buffer.from(await r.arrayBuffer());
293
- if (buf.length > 2 && buf[0] === 0x1f && buf[1] === 0x8b)
294
- buf = gunzipSync(buf);
295
- return buf
296
- .toString("utf8")
297
- .split("\n")
298
- .filter((l) => l.trim() !== "")
299
- .map((l) => JSON.parse(l));
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
- /** A Context is a Solari profile (cookies + localStorage). */
308
- async create(params) {
309
- const p = await this.c.json("POST", "/profiles", { name: params?.name ?? `context-${newKey().slice(0, 8)}` });
310
- return { id: p.id, publicKey: "", cipherAlgorithm: "", initializationVectorSize: 0 };
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
- async retrieve(id) {
313
- const list = await this.c.json("GET", "/profiles");
314
- const p = (list ?? []).find((x) => x.id === id);
315
- if (!p)
316
- throw new APIError(404, `context ${id} not found`, "NotFound");
317
- return { id: p.id, ...(p.name ? { name: p.name } : {}), ...(p.createdAt ? { createdAt: p.createdAt } : {}) };
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
- async delete(id) {
323
- await this.c.json("DELETE", `/profiles/${encodeURIComponent(id)}`);
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, UnsupportedResource } from "./client.js";
4
- export { APIConnectionError, APIConnectionTimeoutError, APIError, AuthenticationError, BadRequestError, BrowserbaseError, ConflictError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, SolariCompatUnsupported, UnprocessableEntityError, } from "./errors.js";
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 { APIConnectionError, APIConnectionTimeoutError, APIError, AuthenticationError, BadRequestError, BrowserbaseError, ConflictError, InternalServerError, NotFoundError, PermissionDeniedError, RateLimitError, SolariCompatUnsupported, UnprocessableEntityError, } from "./errors.js";
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
- if (p.timeout !== undefined)
92
- body.timeoutSeconds = p.timeout; // the gateway validates (>= 60) and clamps to the plan maximum
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
- if (p.userMetadata !== undefined)
96
- body.userMetadata = p.userMetadata;
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>;
@@ -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?: string;
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.2.1",
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",