@solarisdk/browserbase-compat 0.2.0 → 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 +14 -0
- package/COMPAT.md +31 -6
- package/README.md +3 -2
- package/dist/client.d.ts +69 -36
- package/dist/client.js +291 -143
- 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/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],
|
|
@@ -19,7 +22,9 @@ function resolveApiKey(explicit, warn) {
|
|
|
19
22
|
}
|
|
20
23
|
throw new BrowserbaseError("No Solari API key: pass apiKey or set SOLARI_API_KEY (keys start with slr_)");
|
|
21
24
|
}
|
|
22
|
-
/**
|
|
25
|
+
/** The only Solari hosts that may receive a key (the API gateways). Not every *.getsolari.com host: preview hosts (<label>-<port>.preview.getsolari.com) serve customer code. */
|
|
26
|
+
const SOLARI_API_HOST = /^api(-sta(-gcp)?)?\.getsolari\.com$/;
|
|
27
|
+
/** Whether a base URL may receive a Solari key: https on a Solari API host, or loopback. */
|
|
23
28
|
export function isSolariBaseUrl(raw) {
|
|
24
29
|
let u;
|
|
25
30
|
try {
|
|
@@ -31,7 +36,7 @@ export function isSolariBaseUrl(raw) {
|
|
|
31
36
|
if (u.username || u.password)
|
|
32
37
|
return false; // never a URL with credentials in it
|
|
33
38
|
const host = u.hostname.toLowerCase().replace(/\.$/, "");
|
|
34
|
-
if (u.protocol === "https:" && (host
|
|
39
|
+
if (u.protocol === "https:" && SOLARI_API_HOST.test(host))
|
|
35
40
|
return true;
|
|
36
41
|
const loopback = host === "localhost" || host === "[::1]" || /^127(\.\d{1,3}){3}$/.test(host);
|
|
37
42
|
return loopback && (u.protocol === "http:" || u.protocol === "https:");
|
|
@@ -49,7 +54,7 @@ function resolveBaseUrl(explicit) {
|
|
|
49
54
|
if (!given)
|
|
50
55
|
return DEFAULT_BASE_URL;
|
|
51
56
|
if (!isSolariBaseUrl(given)) {
|
|
52
|
-
throw new BrowserbaseError(`Refusing to send a Solari API key to ${describeHost(given)}: it
|
|
57
|
+
throw new BrowserbaseError(`Refusing to send a Solari API key to ${describeHost(given)}: it is not a Solari API host or loopback.`);
|
|
53
58
|
}
|
|
54
59
|
return given.replace(/\/+$/, "");
|
|
55
60
|
}
|
|
@@ -59,21 +64,51 @@ function sleep(ms) {
|
|
|
59
64
|
export class Browserbase {
|
|
60
65
|
sessions;
|
|
61
66
|
contexts;
|
|
62
|
-
extensions;
|
|
63
|
-
projects;
|
|
64
|
-
agents
|
|
65
|
-
|
|
66
|
-
|
|
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"]);
|
|
67
82
|
apiKey;
|
|
68
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;
|
|
69
88
|
/** @internal */ strict;
|
|
70
89
|
/** @internal */ warn;
|
|
71
90
|
fetchImpl;
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
defaultHeaders;
|
|
92
|
+
defaultQuery;
|
|
93
|
+
projectIdWarned = false;
|
|
74
94
|
/** session id -> profile to save on release (browserSettings.context.persist). In-process only. */
|
|
75
95
|
/** @internal */ persist = new Map();
|
|
76
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");
|
|
77
112
|
this.warn = opts.onWarning ?? ((m) => console.warn(`[solari-browserbase-compat] ${m}`));
|
|
78
113
|
// Non-enumerable so console.log(client) / JSON.stringify(client) never print the key.
|
|
79
114
|
Object.defineProperty(this, "apiKey", { value: resolveApiKey(opts.apiKey, this.warn), enumerable: false });
|
|
@@ -81,61 +116,106 @@ export class Browserbase {
|
|
|
81
116
|
this.strict = opts.strict === true;
|
|
82
117
|
this.fetchImpl = opts.fetch ?? fetch;
|
|
83
118
|
this.maxRetries = opts.maxRetries ?? 2;
|
|
84
|
-
this.
|
|
119
|
+
this.timeout = opts.timeout ?? 60_000;
|
|
120
|
+
this.defaultHeaders = { ...(opts.defaultHeaders ?? {}) };
|
|
121
|
+
this.defaultQuery = { ...(opts.defaultQuery ?? {}) };
|
|
85
122
|
this.sessions = new Sessions(this);
|
|
86
123
|
this.contexts = new Contexts(this);
|
|
87
|
-
this.extensions = unsupported("extensions", "browser extensions cannot be loaded");
|
|
88
|
-
this.projects = unsupported("projects", "Solari keys are organisation-scoped; there are no projects");
|
|
89
|
-
this.agents = unsupported("agents", "Browserbase Agents is a separate product");
|
|
90
|
-
this.fetchAPI = unsupported("fetchAPI", "Browserbase Fetch is a separate product");
|
|
91
|
-
this.search = unsupported("search", "Browserbase Search is a separate product");
|
|
92
124
|
}
|
|
93
|
-
|
|
94
|
-
|
|
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) {
|
|
95
144
|
const ac = new AbortController();
|
|
96
|
-
const
|
|
145
|
+
const detach = linkSignal(ac, call.signal);
|
|
146
|
+
const timer = setTimeout(() => ac.abort(), call.timeout ?? defaultTimeoutMs);
|
|
97
147
|
try {
|
|
98
148
|
return await this.fetchImpl(url, { signal: ac.signal });
|
|
99
149
|
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if (call.signal?.aborted)
|
|
152
|
+
throw new APIUserAbortError();
|
|
153
|
+
throw connectionErrorFor(e);
|
|
154
|
+
}
|
|
100
155
|
finally {
|
|
101
156
|
clearTimeout(timer);
|
|
157
|
+
detach();
|
|
102
158
|
}
|
|
103
159
|
}
|
|
104
160
|
/** @internal */
|
|
105
|
-
async
|
|
161
|
+
async send(method, path, body, call = {}) {
|
|
106
162
|
const headers = { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json" };
|
|
107
|
-
|
|
108
|
-
headers
|
|
109
|
-
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;
|
|
110
182
|
let lastErr;
|
|
111
|
-
for (let attempt = 0; attempt <=
|
|
183
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
184
|
+
if (call.signal?.aborted)
|
|
185
|
+
throw new APIUserAbortError();
|
|
112
186
|
const ac = new AbortController();
|
|
113
|
-
const
|
|
187
|
+
const detach = linkSignal(ac, call.signal);
|
|
188
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
114
189
|
try {
|
|
115
|
-
const res = await this.fetchImpl(
|
|
190
|
+
const res = await this.fetchImpl(url, {
|
|
116
191
|
method,
|
|
117
192
|
headers,
|
|
118
193
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
119
194
|
signal: ac.signal,
|
|
120
195
|
});
|
|
121
196
|
clearTimeout(timer);
|
|
122
|
-
if (res.ok || !replaySafe || ![502, 503, 504].includes(res.status) || attempt ===
|
|
197
|
+
if (res.ok || !replaySafe || ![502, 503, 504].includes(res.status) || attempt === maxRetries)
|
|
123
198
|
return res;
|
|
124
199
|
await res.text().catch(() => { });
|
|
125
200
|
}
|
|
126
201
|
catch (e) {
|
|
127
202
|
clearTimeout(timer);
|
|
128
|
-
if (
|
|
203
|
+
if (call.signal?.aborted)
|
|
204
|
+
throw new APIUserAbortError();
|
|
205
|
+
if (!replaySafe || attempt === maxRetries)
|
|
129
206
|
throw connectionErrorFor(e);
|
|
130
207
|
lastErr = e;
|
|
131
208
|
}
|
|
209
|
+
finally {
|
|
210
|
+
detach();
|
|
211
|
+
}
|
|
132
212
|
await sleep(300 * (attempt + 1));
|
|
133
213
|
}
|
|
134
214
|
throw connectionErrorFor(lastErr ?? new Error("request failed"));
|
|
135
215
|
}
|
|
136
216
|
/** @internal */
|
|
137
|
-
async json(method, path, body,
|
|
138
|
-
const res = await this.
|
|
217
|
+
async json(method, path, body, call = {}) {
|
|
218
|
+
const res = await this.send(method, path, body, call);
|
|
139
219
|
if (res.status === 204)
|
|
140
220
|
return undefined;
|
|
141
221
|
const text = await res.text();
|
|
@@ -153,45 +233,51 @@ export class Browserbase {
|
|
|
153
233
|
return parsed;
|
|
154
234
|
}
|
|
155
235
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
hint;
|
|
159
|
-
constructor(feature, hint) {
|
|
160
|
-
this.feature = feature;
|
|
161
|
-
this.hint = hint;
|
|
162
|
-
// Every method on this object throws; a Proxy keeps the surface open-ended (create/list/retrieve/...).
|
|
163
|
-
return new Proxy(this, {
|
|
164
|
-
get: (target, prop) => {
|
|
165
|
-
if (typeof prop === "symbol" || prop === "then" || prop in target)
|
|
166
|
-
return Reflect.get(target, prop);
|
|
167
|
-
return () => {
|
|
168
|
-
throw new SolariCompatUnsupported(`${target.feature}.${prop}`, target.hint);
|
|
169
|
-
};
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
}
|
|
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");
|
|
173
238
|
}
|
|
174
|
-
function
|
|
175
|
-
|
|
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);
|
|
176
250
|
}
|
|
177
251
|
function newKey() {
|
|
178
252
|
return globalThis.crypto?.randomUUID?.() ?? `bbc-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
179
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
|
+
}
|
|
180
261
|
function toSession(row, extra = {}) {
|
|
181
262
|
const created = row.createdAt ?? new Date().toISOString();
|
|
182
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);
|
|
183
269
|
return {
|
|
184
270
|
id: row.id,
|
|
185
271
|
createdAt: created,
|
|
186
272
|
updatedAt: created,
|
|
187
273
|
projectId: "",
|
|
188
274
|
startedAt: created,
|
|
189
|
-
...(
|
|
190
|
-
expiresAt
|
|
275
|
+
...(endedAt ? { endedAt } : {}),
|
|
276
|
+
expiresAt,
|
|
191
277
|
status,
|
|
192
278
|
proxyBytes: 0,
|
|
193
|
-
keepAlive: extra.keepAlive ??
|
|
194
|
-
...(
|
|
279
|
+
keepAlive: extra.keepAlive ?? meta.keepAlive,
|
|
280
|
+
...(meta.userMetadata ? { userMetadata: meta.userMetadata } : {}),
|
|
195
281
|
...(extra.contextId ? { contextId: extra.contextId } : {}),
|
|
196
282
|
region: REGION,
|
|
197
283
|
};
|
|
@@ -199,102 +285,151 @@ function toSession(row, extra = {}) {
|
|
|
199
285
|
function withConnect(s, cdp) {
|
|
200
286
|
return { ...s, connectUrl: cdp, seleniumRemoteUrl: "", signingKey: "" };
|
|
201
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;
|
|
202
295
|
class Sessions {
|
|
203
296
|
c;
|
|
204
297
|
recording;
|
|
205
|
-
replays;
|
|
206
|
-
logs;
|
|
207
|
-
downloads;
|
|
208
|
-
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"]);
|
|
209
302
|
constructor(c) {
|
|
210
303
|
this.c = c;
|
|
211
304
|
this.recording = new Recording(c);
|
|
212
|
-
this.replays = unsupported("sessions.replays", "Solari records DOM events, not video; use sessions.recording.retrieve");
|
|
213
|
-
this.logs = unsupported("sessions.logs", "session command logs are not available");
|
|
214
|
-
this.downloads = unsupported("sessions.downloads", "file downloads cannot be retrieved through the API");
|
|
215
|
-
this.uploads = unsupported("sessions.uploads", "use your automation client's setInputFiles");
|
|
216
305
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.c.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
id: data.sessionId,
|
|
230
|
-
status: "active",
|
|
231
|
-
createdAt: new Date().toISOString(),
|
|
232
|
-
...(data.expiresAt ? { expiresAt: data.expiresAt } : {}),
|
|
233
|
-
...(data.userMetadata ? { userMetadata: data.userMetadata } : {}),
|
|
234
|
-
}, { keepAlive: params?.keepAlive === true, ...(ctx ? { contextId: ctx } : {}), status: "RUNNING" });
|
|
235
|
-
const out = withConnect(s, data.cdpEndpoint);
|
|
236
|
-
if (ctx) {
|
|
237
|
-
let state = data.storageState ?? null;
|
|
238
|
-
if (!state && data.storageStateUrl?.url) {
|
|
239
|
-
const r = await this.c.fetchPublic(data.storageStateUrl.url).catch(() => undefined);
|
|
240
|
-
if (r?.ok)
|
|
241
|
-
state = (await r.json().catch(() => null));
|
|
242
|
-
else
|
|
243
|
-
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)}`);
|
|
244
318
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
+
});
|
|
248
343
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
});
|
|
252
350
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
+
});
|
|
257
378
|
}
|
|
258
379
|
/** `{ status: "REQUEST_RELEASE" }` releases the session (saving the context first when it was created with persist: true). */
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
});
|
|
269
396
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
+
});
|
|
275
406
|
}
|
|
276
407
|
}
|
|
277
408
|
class Recording {
|
|
278
409
|
c;
|
|
410
|
+
downloads = unsupportedMethods("sessions.recording.downloads", "Solari records DOM events, not video; use sessions.recording.retrieve", ["create", "list"]);
|
|
279
411
|
constructor(c) {
|
|
280
412
|
this.c = c;
|
|
281
413
|
}
|
|
282
414
|
/** The session's DOM recording as an array of rrweb events. Needs `browserSettings: { recordSession: true }`; available a few seconds after release. */
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
buf =
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
+
});
|
|
298
433
|
}
|
|
299
434
|
}
|
|
300
435
|
class Contexts {
|
|
@@ -302,22 +437,35 @@ class Contexts {
|
|
|
302
437
|
constructor(c) {
|
|
303
438
|
this.c = c;
|
|
304
439
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
+
});
|
|
309
450
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
+
});
|
|
316
461
|
}
|
|
317
|
-
update() {
|
|
462
|
+
update(..._a) {
|
|
318
463
|
throw new SolariCompatUnsupported("contexts.update", "contexts are saved when a persist:true session is released");
|
|
319
464
|
}
|
|
320
|
-
|
|
321
|
-
|
|
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
|
+
});
|
|
322
470
|
}
|
|
323
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;
|