effect-browserbase 0.2.0-beta.9 → 0.3.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -319
- package/dist/Browserbase-CaPKwEY9.mjs +544 -0
- package/dist/Browserbase-CaPKwEY9.mjs.map +1 -0
- package/dist/Browserbase.d.mts +110 -0
- package/dist/Browserbase.mjs +3 -0
- package/dist/BrowserbaseClient-CtAQ2rg4.d.mts +195 -0
- package/dist/BrowserbaseClient.d.mts +2 -0
- package/dist/BrowserbaseClient.mjs +190 -0
- package/dist/BrowserbaseClient.mjs.map +1 -0
- package/dist/BrowserbaseError-D1EOrLNc.d.mts +79 -0
- package/dist/BrowserbaseError.d.mts +2 -0
- package/dist/BrowserbaseError.mjs +101 -0
- package/dist/BrowserbaseError.mjs.map +1 -0
- package/dist/ContextLease.d.mts +42 -0
- package/dist/ContextLease.mjs +80 -0
- package/dist/ContextLease.mjs.map +1 -0
- package/dist/index.d.mts +5 -27
- package/dist/index.mjs +5 -26
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/testing/index.d.mts +136 -0
- package/dist/testing/index.mjs +440 -0
- package/dist/testing/index.mjs.map +1 -0
- package/package.json +51 -121
- package/src/Browserbase.ts +729 -0
- package/src/BrowserbaseClient.ts +559 -0
- package/src/BrowserbaseError.ts +108 -0
- package/src/ContextLease.ts +118 -0
- package/src/index.ts +7 -0
- package/src/internal/captureConnection.ts +214 -0
- package/src/testing/BrowserbaseContract.ts +170 -0
- package/src/testing/TestBrowserbase.ts +509 -0
- package/src/testing/index.ts +7 -0
- package/dist/Account.d.mts +0 -32
- package/dist/Account.mjs +0 -36
- package/dist/Account.mjs.map +0 -1
- package/dist/Acquisition-BOABu_qJ.mjs +0 -244
- package/dist/Acquisition-BOABu_qJ.mjs.map +0 -1
- package/dist/Agents-CPaTOH_g.d.mts +0 -156
- package/dist/Agents.d.mts +0 -3
- package/dist/Agents.mjs +0 -292
- package/dist/Agents.mjs.map +0 -1
- package/dist/Allocation.d.mts +0 -97
- package/dist/Allocation.mjs +0 -27
- package/dist/Allocation.mjs.map +0 -1
- package/dist/Authorization-Ll5f_XNM.mjs +0 -12
- package/dist/Authorization-Ll5f_XNM.mjs.map +0 -1
- package/dist/Browser-ByrWJ0ss.mjs +0 -274
- package/dist/Browser-ByrWJ0ss.mjs.map +0 -1
- package/dist/Browser.d.mts +0 -83
- package/dist/Browser.mjs +0 -3
- package/dist/BrowserBinding.d.mts +0 -13
- package/dist/BrowserBinding.mjs +0 -31
- package/dist/BrowserBinding.mjs.map +0 -1
- package/dist/Certificates-D4NQmyeA.d.mts +0 -34
- package/dist/Certificates.d.mts +0 -3
- package/dist/Certificates.mjs +0 -85
- package/dist/Certificates.mjs.map +0 -1
- package/dist/Cleanup-DMTDBir8.d.mts +0 -23
- package/dist/Cleanup.d.mts +0 -3
- package/dist/Cleanup.mjs +0 -43
- package/dist/Cleanup.mjs.map +0 -1
- package/dist/Client-BEC4Q8ZS.mjs +0 -564
- package/dist/Client-BEC4Q8ZS.mjs.map +0 -1
- package/dist/Client-B_f6bBLT.d.mts +0 -52
- package/dist/Client.d.mts +0 -3
- package/dist/Client.mjs +0 -2
- package/dist/Connection-sxyxtwlk.mjs +0 -56
- package/dist/Connection-sxyxtwlk.mjs.map +0 -1
- package/dist/ContextCoordination.d.mts +0 -29
- package/dist/ContextCoordination.mjs +0 -94
- package/dist/ContextCoordination.mjs.map +0 -1
- package/dist/ContextWriter-CiEma24z.mjs +0 -204
- package/dist/ContextWriter-CiEma24z.mjs.map +0 -1
- package/dist/Contexts-DqdL7cyX.d.mts +0 -31
- package/dist/Contexts.d.mts +0 -3
- package/dist/Contexts.mjs +0 -97
- package/dist/Contexts.mjs.map +0 -1
- package/dist/Deadline-CrViaogr.mjs +0 -13
- package/dist/Deadline-CrViaogr.mjs.map +0 -1
- package/dist/Downloads-CX52GOFJ.d.mts +0 -43
- package/dist/Downloads.d.mts +0 -3
- package/dist/Downloads.mjs +0 -137
- package/dist/Downloads.mjs.map +0 -1
- package/dist/Errors.d.mts +0 -100
- package/dist/Errors.mjs +0 -194
- package/dist/Errors.mjs.map +0 -1
- package/dist/Extensions-BTUn9zhK.mjs +0 -231
- package/dist/Extensions-BTUn9zhK.mjs.map +0 -1
- package/dist/Extensions-DBXuVM1f.d.mts +0 -43
- package/dist/Extensions.d.mts +0 -3
- package/dist/Extensions.mjs +0 -2
- package/dist/Functions-CsKMMUuZ.d.mts +0 -226
- package/dist/Functions.d.mts +0 -3
- package/dist/Functions.mjs +0 -318
- package/dist/Functions.mjs.map +0 -1
- package/dist/Issued-DejqoMYL.mjs +0 -15
- package/dist/Issued-DejqoMYL.mjs.map +0 -1
- package/dist/Launch-cAdE2rrZ.mjs +0 -115
- package/dist/Launch-cAdE2rrZ.mjs.map +0 -1
- package/dist/Launch.d.mts +0 -127
- package/dist/Launch.mjs +0 -97
- package/dist/Launch.mjs.map +0 -1
- package/dist/PageFetch-CjAM_0DS.d.mts +0 -40
- package/dist/PageFetch.d.mts +0 -3
- package/dist/PageFetch.mjs +0 -59
- package/dist/PageFetch.mjs.map +0 -1
- package/dist/Projects-tm5bTDH0.d.mts +0 -38
- package/dist/Projects.d.mts +0 -3
- package/dist/Projects.mjs +0 -65
- package/dist/Projects.mjs.map +0 -1
- package/dist/Recordings-B9k5aODB.d.mts +0 -27
- package/dist/Recordings.d.mts +0 -3
- package/dist/Recordings.mjs +0 -144
- package/dist/Recordings.mjs.map +0 -1
- package/dist/References-DOH3Wu44.d.mts +0 -34
- package/dist/References.d.mts +0 -3
- package/dist/References.mjs +0 -33
- package/dist/References.mjs.map +0 -1
- package/dist/Replays-mV93WGtx.d.mts +0 -27
- package/dist/Replays.d.mts +0 -3
- package/dist/Replays.mjs +0 -107
- package/dist/Replays.mjs.map +0 -1
- package/dist/Resource-J7Kkk5mu.mjs +0 -54
- package/dist/Resource-J7Kkk5mu.mjs.map +0 -1
- package/dist/Search-B64e7g-h.d.mts +0 -37
- package/dist/Search.d.mts +0 -3
- package/dist/Search.mjs +0 -46
- package/dist/Search.mjs.map +0 -1
- package/dist/SessionData-5xYZDshJ.d.mts +0 -38
- package/dist/SessionData.d.mts +0 -3
- package/dist/SessionData.mjs +0 -48
- package/dist/SessionData.mjs.map +0 -1
- package/dist/Sessions-D2H5m6rF.mjs +0 -260
- package/dist/Sessions-D2H5m6rF.mjs.map +0 -1
- package/dist/Sessions-oJEUmZth.d.mts +0 -69
- package/dist/Sessions.d.mts +0 -4
- package/dist/Sessions.mjs +0 -3
- package/dist/Testing.d.mts +0 -119
- package/dist/Testing.mjs +0 -206
- package/dist/Testing.mjs.map +0 -1
- package/dist/TransferPolicy-DQ06i7SZ.mjs +0 -25
- package/dist/TransferPolicy-DQ06i7SZ.mjs.map +0 -1
- package/dist/Transfers-GVqzrDWl.d.mts +0 -89
- package/dist/Transfers.d.mts +0 -3
- package/dist/Transfers.mjs +0 -85
- package/dist/Transfers.mjs.map +0 -1
- package/dist/Uploads-C2_bACgv.d.mts +0 -28
- package/dist/Uploads.d.mts +0 -3
- package/dist/Uploads.mjs +0 -105
- package/dist/Uploads.mjs.map +0 -1
- package/dist/Webhooks-BoeRAxzG.d.mts +0 -59
- package/dist/Webhooks.d.mts +0 -3
- package/dist/Webhooks.mjs +0 -134
- package/dist/Webhooks.mjs.map +0 -1
- package/dist/WriterFacts-BBJRDZYY.d.mts +0 -54
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small client for the Browserbase REST API: sessions, stored contexts, extensions, web search
|
|
3
|
+
* and page fetch.
|
|
4
|
+
*
|
|
5
|
+
* It runs on the `HttpClient` the application provides, such as `FetchHttpClient.layer`. The API
|
|
6
|
+
* key travels only in the `X-BB-API-Key` header and is redacted from logs and traces. Redirects
|
|
7
|
+
* are refused rather than followed, because following one would send the key wherever it points.
|
|
8
|
+
*
|
|
9
|
+
* Every request has a deadline, so a provider that never answers cannot hold a caller's scope open.
|
|
10
|
+
* Reads are retried twice on transient failures. Writes are not: a session create that fails in
|
|
11
|
+
* transit or misses its deadline may still have created the session, which then ends at its timeout.
|
|
12
|
+
*
|
|
13
|
+
* @since 0.3.0
|
|
14
|
+
*/
|
|
15
|
+
import {
|
|
16
|
+
Config,
|
|
17
|
+
Context,
|
|
18
|
+
Duration,
|
|
19
|
+
Effect,
|
|
20
|
+
Exit,
|
|
21
|
+
flow,
|
|
22
|
+
identity,
|
|
23
|
+
Layer,
|
|
24
|
+
Option,
|
|
25
|
+
Redacted,
|
|
26
|
+
Schedule,
|
|
27
|
+
Schema,
|
|
28
|
+
} from "effect";
|
|
29
|
+
import {
|
|
30
|
+
FetchHttpClient,
|
|
31
|
+
Headers,
|
|
32
|
+
HttpClient,
|
|
33
|
+
HttpClientRequest,
|
|
34
|
+
HttpClientResponse,
|
|
35
|
+
} from "effect/http";
|
|
36
|
+
|
|
37
|
+
import {
|
|
38
|
+
BrowserbaseError,
|
|
39
|
+
Decode,
|
|
40
|
+
InvalidRequest,
|
|
41
|
+
isTransient,
|
|
42
|
+
NotFound,
|
|
43
|
+
RateLimited,
|
|
44
|
+
type Reason,
|
|
45
|
+
Status,
|
|
46
|
+
Transport,
|
|
47
|
+
Unauthorized,
|
|
48
|
+
} from "./BrowserbaseError.ts";
|
|
49
|
+
|
|
50
|
+
export const defaultBaseUrl = "https://api.browserbase.com";
|
|
51
|
+
|
|
52
|
+
export class Session extends Schema.Class<Session>("effect-browserbase/Session")({
|
|
53
|
+
id: Schema.String,
|
|
54
|
+
/** `PENDING`, `RUNNING`, `ERROR`, `TIMED_OUT` or `COMPLETED`, kept open for new values. */
|
|
55
|
+
status: Schema.String,
|
|
56
|
+
region: Schema.String,
|
|
57
|
+
keepAlive: Schema.Boolean,
|
|
58
|
+
createdAt: Schema.String,
|
|
59
|
+
expiresAt: Schema.String,
|
|
60
|
+
endedAt: Schema.optional(Schema.String),
|
|
61
|
+
contextId: Schema.optional(Schema.String),
|
|
62
|
+
proxyBytes: Schema.optional(Schema.Finite),
|
|
63
|
+
userMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
64
|
+
/** The CDP WebSocket address, a credential. Only create and get return it. */
|
|
65
|
+
connectUrl: Schema.optional(Schema.RedactedFromValue(Schema.String)),
|
|
66
|
+
}) {}
|
|
67
|
+
|
|
68
|
+
/** Links that let a person watch and control a running session. They are credentials. */
|
|
69
|
+
export class LiveView extends Schema.Class<LiveView>("effect-browserbase/LiveView")({
|
|
70
|
+
/** The active tab, without browser chrome: suited to an iframe. */
|
|
71
|
+
debuggerFullscreenUrl: Schema.RedactedFromValue(Schema.String),
|
|
72
|
+
debuggerUrl: Schema.RedactedFromValue(Schema.String),
|
|
73
|
+
pages: Schema.Array(
|
|
74
|
+
Schema.Struct({
|
|
75
|
+
id: Schema.String,
|
|
76
|
+
url: Schema.String,
|
|
77
|
+
title: Schema.String,
|
|
78
|
+
debuggerFullscreenUrl: Schema.RedactedFromValue(Schema.String),
|
|
79
|
+
}),
|
|
80
|
+
),
|
|
81
|
+
}) {}
|
|
82
|
+
|
|
83
|
+
/** Browser state (cookies, storage, cache) kept by Browserbase for sessions to load and save. */
|
|
84
|
+
export class StoredContext extends Schema.Class<StoredContext>("effect-browserbase/StoredContext")({
|
|
85
|
+
id: Schema.String,
|
|
86
|
+
name: Schema.optional(Schema.String),
|
|
87
|
+
/** `getContext` returns the timestamps; `createContext` does not. */
|
|
88
|
+
createdAt: Schema.optional(Schema.String),
|
|
89
|
+
updatedAt: Schema.optional(Schema.String),
|
|
90
|
+
}) {}
|
|
91
|
+
|
|
92
|
+
/** A Chrome extension uploaded to the project, which sessions load when created with its id. */
|
|
93
|
+
export class Extension extends Schema.Class<Extension>("effect-browserbase/Extension")({
|
|
94
|
+
id: Schema.String,
|
|
95
|
+
fileName: Schema.String,
|
|
96
|
+
createdAt: Schema.String,
|
|
97
|
+
updatedAt: Schema.String,
|
|
98
|
+
}) {}
|
|
99
|
+
|
|
100
|
+
export class SearchResult extends Schema.Class<SearchResult>("effect-browserbase/SearchResult")({
|
|
101
|
+
url: Schema.String,
|
|
102
|
+
title: Schema.String,
|
|
103
|
+
author: Schema.optional(Schema.String),
|
|
104
|
+
publishedDate: Schema.optional(Schema.String),
|
|
105
|
+
}) {}
|
|
106
|
+
|
|
107
|
+
/** A page fetched without a browser. */
|
|
108
|
+
export class FetchedPage extends Schema.Class<FetchedPage>("effect-browserbase/FetchedPage")({
|
|
109
|
+
statusCode: Schema.Finite,
|
|
110
|
+
contentType: Schema.String,
|
|
111
|
+
headers: Schema.Record(Schema.String, Schema.String),
|
|
112
|
+
/** Text for the `raw` and `markdown` formats; the extracted object for `json`. */
|
|
113
|
+
content: Schema.Union([Schema.String, Schema.Record(Schema.String, Schema.Unknown)]),
|
|
114
|
+
}) {}
|
|
115
|
+
|
|
116
|
+
/** A session create request, as Browserbase's API reference describes it. */
|
|
117
|
+
export interface SessionOptions {
|
|
118
|
+
/** Defaults to the API key's project. */
|
|
119
|
+
readonly projectId?: string | undefined;
|
|
120
|
+
/** An uploaded extension for the browser to load. It makes the session slower to start. */
|
|
121
|
+
readonly extensionId?: string | undefined;
|
|
122
|
+
/** Seconds until Browserbase ends the session, from 60 to 21,600. Defaults to the project's. */
|
|
123
|
+
readonly timeout?: number | undefined;
|
|
124
|
+
/** Keep the session running after every client disconnects, until released or timed out. */
|
|
125
|
+
readonly keepAlive?: boolean | undefined;
|
|
126
|
+
readonly region?: "us-west-2" | "us-east-1" | "eu-central-1" | "ap-southeast-1" | undefined;
|
|
127
|
+
/** `true` for Browserbase's proxies, or rules per domain. */
|
|
128
|
+
readonly proxies?: boolean | ReadonlyArray<ProxyRule> | undefined;
|
|
129
|
+
/** Labels that `listSessions` can query. */
|
|
130
|
+
readonly userMetadata?: Readonly<Record<string, unknown>> | undefined;
|
|
131
|
+
readonly browserSettings?: BrowserSettings | undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Browser settings. Left unset, each takes Browserbase's default. */
|
|
135
|
+
export interface BrowserSettings {
|
|
136
|
+
/**
|
|
137
|
+
* Load a stored context. With `persist`, Browserbase saves the session's changes back to it once
|
|
138
|
+
* the session ends; `Browserbase.open` lets one such session at a time write to each context.
|
|
139
|
+
*/
|
|
140
|
+
readonly context?: { readonly id: string; readonly persist?: boolean | undefined } | undefined;
|
|
141
|
+
readonly viewport?: { readonly width: number; readonly height: number } | undefined;
|
|
142
|
+
readonly blockAds?: boolean | undefined;
|
|
143
|
+
/** Browserbase solves captchas unless this is false. */
|
|
144
|
+
readonly solveCaptchas?: boolean | undefined;
|
|
145
|
+
/** Browserbase records the session for replay unless this is false. */
|
|
146
|
+
readonly recordSession?: boolean | undefined;
|
|
147
|
+
/** Browserbase keeps the session's logs unless this is false. */
|
|
148
|
+
readonly logSession?: boolean | undefined;
|
|
149
|
+
/** Verified browser mode, on plans that include it. */
|
|
150
|
+
readonly verified?: boolean | undefined;
|
|
151
|
+
readonly os?: "windows" | "mac" | "linux" | "mobile" | "tablet" | undefined;
|
|
152
|
+
/** Restrict top-level navigation to these domains and their subdomains. */
|
|
153
|
+
readonly allowedDomains?: ReadonlyArray<string> | undefined;
|
|
154
|
+
readonly ignoreCertificateErrors?: boolean | undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type ProxyRule =
|
|
158
|
+
| {
|
|
159
|
+
readonly type: "browserbase";
|
|
160
|
+
readonly geolocation?:
|
|
161
|
+
| {
|
|
162
|
+
readonly country: string;
|
|
163
|
+
readonly state?: string | undefined;
|
|
164
|
+
readonly city?: string | undefined;
|
|
165
|
+
}
|
|
166
|
+
| undefined;
|
|
167
|
+
readonly domainPattern?: string | undefined;
|
|
168
|
+
}
|
|
169
|
+
| {
|
|
170
|
+
readonly type: "external";
|
|
171
|
+
readonly server: string;
|
|
172
|
+
readonly username?: string | undefined;
|
|
173
|
+
readonly password?: string | undefined;
|
|
174
|
+
readonly domainPattern?: string | undefined;
|
|
175
|
+
}
|
|
176
|
+
| { readonly type: "none"; readonly domainPattern?: string | undefined };
|
|
177
|
+
|
|
178
|
+
export interface FetchOptions {
|
|
179
|
+
/** `raw` (the default) returns the body as is; `json` needs `schema`. */
|
|
180
|
+
readonly format?: "raw" | "markdown" | "json" | undefined;
|
|
181
|
+
/** JSON Schema of the object to extract with the `json` format. */
|
|
182
|
+
readonly schema?: Readonly<Record<string, unknown>> | undefined;
|
|
183
|
+
readonly allowRedirects?: boolean | undefined;
|
|
184
|
+
readonly allowInsecureSsl?: boolean | undefined;
|
|
185
|
+
readonly proxies?: boolean | undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface Service {
|
|
189
|
+
readonly createSession: (options?: SessionOptions) => Effect.Effect<Session, BrowserbaseError>;
|
|
190
|
+
readonly getSession: (id: string) => Effect.Effect<Session, BrowserbaseError>;
|
|
191
|
+
/** Sessions with the given status, or all. `query` filters on user metadata, in Browserbase's syntax. */
|
|
192
|
+
readonly listSessions: (options?: {
|
|
193
|
+
readonly status?: string | undefined;
|
|
194
|
+
readonly query?: string | undefined;
|
|
195
|
+
}) => Effect.Effect<ReadonlyArray<Session>, BrowserbaseError>;
|
|
196
|
+
/** End the session now, which stops its billing. Ending an ended session succeeds. */
|
|
197
|
+
readonly releaseSession: (id: string) => Effect.Effect<void, BrowserbaseError>;
|
|
198
|
+
/** Live view links. They last `expiresInSeconds`, or as long as the session. */
|
|
199
|
+
readonly liveView: (
|
|
200
|
+
id: string,
|
|
201
|
+
options?: { readonly expiresInSeconds?: number | undefined },
|
|
202
|
+
) => Effect.Effect<LiveView, BrowserbaseError>;
|
|
203
|
+
readonly createContext: (options?: {
|
|
204
|
+
readonly name?: string | undefined;
|
|
205
|
+
}) => Effect.Effect<StoredContext, BrowserbaseError>;
|
|
206
|
+
readonly getContext: (id: string) => Effect.Effect<StoredContext, BrowserbaseError>;
|
|
207
|
+
/** Delete a stored context for good. Sessions can no longer load it. */
|
|
208
|
+
readonly deleteContext: (id: string) => Effect.Effect<void, BrowserbaseError>;
|
|
209
|
+
/**
|
|
210
|
+
* Upload a Chrome extension: a zip of at most 100 MB with `manifest.json` at its root. Create a
|
|
211
|
+
* session with the returned id as `extensionId` to load it.
|
|
212
|
+
*/
|
|
213
|
+
readonly uploadExtension: (
|
|
214
|
+
archive: Uint8Array,
|
|
215
|
+
options?: { readonly fileName?: string | undefined },
|
|
216
|
+
) => Effect.Effect<Extension, BrowserbaseError>;
|
|
217
|
+
readonly getExtension: (id: string) => Effect.Effect<Extension, BrowserbaseError>;
|
|
218
|
+
readonly deleteExtension: (id: string) => Effect.Effect<void, BrowserbaseError>;
|
|
219
|
+
/** Search the web for up to `results` hits, from 1 to 25. Defaults to 10. */
|
|
220
|
+
readonly search: (
|
|
221
|
+
query: string,
|
|
222
|
+
options?: { readonly results?: number | undefined },
|
|
223
|
+
) => Effect.Effect<ReadonlyArray<SearchResult>, BrowserbaseError>;
|
|
224
|
+
readonly fetch: (
|
|
225
|
+
url: string,
|
|
226
|
+
options?: FetchOptions,
|
|
227
|
+
) => Effect.Effect<FetchedPage, BrowserbaseError>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export class BrowserbaseClient extends Context.Service<BrowserbaseClient, Service>()(
|
|
231
|
+
"effect-browserbase/BrowserbaseClient",
|
|
232
|
+
) {}
|
|
233
|
+
|
|
234
|
+
export interface Options {
|
|
235
|
+
readonly apiKey: Redacted.Redacted<string>;
|
|
236
|
+
/** Defaults to `https://api.browserbase.com`. */
|
|
237
|
+
readonly baseUrl?: string | undefined;
|
|
238
|
+
/** Bound on each request attempt, including reading its answer. Defaults to 60 seconds. */
|
|
239
|
+
readonly requestTimeout?: Duration.Input | undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const ids = /^[A-Za-z0-9_-]{1,128}$/;
|
|
243
|
+
const maxExtensionBytes = 100 * 1024 * 1024;
|
|
244
|
+
const ErrorBody = Schema.fromJsonString(Schema.Struct({ message: Schema.String }));
|
|
245
|
+
const SearchResponse = Schema.Struct({ results: Schema.Array(SearchResult) });
|
|
246
|
+
const CreatedId = Schema.Struct({ id: Schema.String.check(Schema.isPattern(ids)) });
|
|
247
|
+
|
|
248
|
+
const statusReason = (status: number, detail: string): Reason => {
|
|
249
|
+
if (status < 200 || (status >= 300 && status < 400)) {
|
|
250
|
+
return new Status({
|
|
251
|
+
status,
|
|
252
|
+
detail: "redirected; redirects are refused so the API key stays with Browserbase",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
if (status === 400) return new InvalidRequest({ detail });
|
|
256
|
+
if (status === 401 || status === 403) return new Unauthorized({ detail });
|
|
257
|
+
if (status === 404) return new NotFound({ detail });
|
|
258
|
+
if (status === 429) return new RateLimited({ detail });
|
|
259
|
+
|
|
260
|
+
return new Status({ status, detail });
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** A create's answer that did not decode though it named session `id`, released or not since. */
|
|
264
|
+
const undecoded = (detail: string, id: string, released: boolean) =>
|
|
265
|
+
new Decode({
|
|
266
|
+
detail: `${detail}; session ${id} ${released ? "was released" : "could not be released and ends at its timeout"}`,
|
|
267
|
+
released,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
/** Run the request with fetch's redirect mode set to manual, keeping any other fetch options. */
|
|
271
|
+
const refuseRedirects = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
272
|
+
Effect.contextWith((context: Context.Context<never>) =>
|
|
273
|
+
Effect.provideService(effect, FetchHttpClient.RequestInit, {
|
|
274
|
+
...Context.getOrUndefined(context, FetchHttpClient.RequestInit),
|
|
275
|
+
redirect: "manual",
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
export const make = Effect.fnUntraced(function* (options: Options) {
|
|
280
|
+
const http = (yield* HttpClient.HttpClient).pipe(
|
|
281
|
+
HttpClient.mapRequest(
|
|
282
|
+
flow(
|
|
283
|
+
HttpClientRequest.prependUrl(options.baseUrl ?? defaultBaseUrl),
|
|
284
|
+
HttpClientRequest.setHeader("x-bb-api-key", Redacted.value(options.apiKey)),
|
|
285
|
+
HttpClientRequest.acceptJson,
|
|
286
|
+
),
|
|
287
|
+
),
|
|
288
|
+
HttpClient.transformResponse(
|
|
289
|
+
flow(
|
|
290
|
+
refuseRedirects,
|
|
291
|
+
Effect.updateService(Headers.CurrentRedactedNames, (names) => [...names, "x-bb-api-key"]),
|
|
292
|
+
),
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const failure = (operation: string, reason: Reason) =>
|
|
297
|
+
new BrowserbaseError({ operation, reason });
|
|
298
|
+
|
|
299
|
+
const requestTimeout = Duration.fromInputUnsafe(options.requestTimeout ?? Duration.seconds(60));
|
|
300
|
+
|
|
301
|
+
/** Send one request; on a success status, read the answer with `read`. */
|
|
302
|
+
const send = <A>(
|
|
303
|
+
operation: string,
|
|
304
|
+
request: HttpClientRequest.HttpClientRequest,
|
|
305
|
+
read: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect<A, BrowserbaseError>,
|
|
306
|
+
): Effect.Effect<A, BrowserbaseError> =>
|
|
307
|
+
http.execute(request).pipe(
|
|
308
|
+
Effect.mapError((error) => failure(operation, new Transport({ detail: error.message }))),
|
|
309
|
+
Effect.flatMap((response) =>
|
|
310
|
+
response.status >= 200 && response.status < 300
|
|
311
|
+
? read(response)
|
|
312
|
+
: response.text.pipe(
|
|
313
|
+
Effect.map((text) =>
|
|
314
|
+
Option.getOrElse(
|
|
315
|
+
Option.map(Schema.decodeOption(ErrorBody)(text), (body) => body.message),
|
|
316
|
+
() => text.slice(0, 300),
|
|
317
|
+
),
|
|
318
|
+
),
|
|
319
|
+
Effect.orElseSucceed(() => ""),
|
|
320
|
+
Effect.flatMap((detail) =>
|
|
321
|
+
Effect.fail(failure(operation, statusReason(response.status, detail))),
|
|
322
|
+
),
|
|
323
|
+
),
|
|
324
|
+
),
|
|
325
|
+
// The deadline also applies inside a caller's uninterruptible acquisition, such as the
|
|
326
|
+
// session create in Browserbase.open, so that acquisition always settles.
|
|
327
|
+
Effect.timeoutOrElse({
|
|
328
|
+
duration: requestTimeout,
|
|
329
|
+
orElse: () =>
|
|
330
|
+
Effect.fail(
|
|
331
|
+
failure(
|
|
332
|
+
operation,
|
|
333
|
+
new Transport({
|
|
334
|
+
detail: `no answer within ${Duration.format(requestTimeout)}`,
|
|
335
|
+
}),
|
|
336
|
+
),
|
|
337
|
+
),
|
|
338
|
+
}),
|
|
339
|
+
request.method === "GET"
|
|
340
|
+
? Effect.retry({
|
|
341
|
+
while: isTransient,
|
|
342
|
+
times: 2,
|
|
343
|
+
schedule: Schedule.exponential("250 millis").pipe(Schedule.jittered),
|
|
344
|
+
})
|
|
345
|
+
: identity,
|
|
346
|
+
Effect.withSpan(`Browserbase.${operation}`),
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const json =
|
|
350
|
+
<A>(operation: string, schema: Schema.Decoder<A>) =>
|
|
351
|
+
(response: HttpClientResponse.HttpClientResponse) =>
|
|
352
|
+
HttpClientResponse.schemaBodyJson(schema)(response).pipe(
|
|
353
|
+
Effect.mapError((error) =>
|
|
354
|
+
failure(operation, new Decode({ detail: error.message.split("\n")[0] ?? "" })),
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const ignoreBody = () => Effect.void;
|
|
359
|
+
|
|
360
|
+
const withBody = (
|
|
361
|
+
operation: string,
|
|
362
|
+
request: HttpClientRequest.HttpClientRequest,
|
|
363
|
+
body: unknown,
|
|
364
|
+
) =>
|
|
365
|
+
HttpClientRequest.bodyJson(request, body).pipe(
|
|
366
|
+
Effect.mapError((error) => failure(operation, new InvalidRequest({ detail: error.message }))),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const checkId = (operation: string, id: string): Effect.Effect<string, BrowserbaseError> =>
|
|
370
|
+
ids.test(id)
|
|
371
|
+
? Effect.succeed(id)
|
|
372
|
+
: Effect.fail(
|
|
373
|
+
failure(
|
|
374
|
+
operation,
|
|
375
|
+
new InvalidRequest({ detail: `${JSON.stringify(id)} is not a Browserbase id` }),
|
|
376
|
+
),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const releaseSession = (id: string) =>
|
|
380
|
+
checkId("releaseSession", id).pipe(
|
|
381
|
+
Effect.flatMap(() =>
|
|
382
|
+
withBody("releaseSession", HttpClientRequest.post(`/v1/sessions/${id}`), {
|
|
383
|
+
status: "REQUEST_RELEASE",
|
|
384
|
+
}),
|
|
385
|
+
),
|
|
386
|
+
Effect.flatMap((request) => send("releaseSession", request, ignoreBody)),
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// A success answer means the session exists even if the rest of it does not decode. Only an id
|
|
390
|
+
// lets anyone end it early, so release it here before reporting the unexpected answer.
|
|
391
|
+
const createSession = (body: SessionOptions) =>
|
|
392
|
+
withBody("createSession", HttpClientRequest.post("/v1/sessions"), body).pipe(
|
|
393
|
+
Effect.flatMap((request) =>
|
|
394
|
+
send("createSession", request, json("createSession", Schema.Unknown)),
|
|
395
|
+
),
|
|
396
|
+
Effect.flatMap((answer) =>
|
|
397
|
+
Schema.decodeUnknownEffect(Session)(answer).pipe(
|
|
398
|
+
Effect.catch((error) => {
|
|
399
|
+
const detail = error.message.split("\n")[0] ?? "";
|
|
400
|
+
|
|
401
|
+
return Option.match(Schema.decodeUnknownOption(CreatedId)(answer), {
|
|
402
|
+
onNone: () => Effect.fail(failure("createSession", new Decode({ detail }))),
|
|
403
|
+
onSome: ({ id }) =>
|
|
404
|
+
releaseSession(id).pipe(
|
|
405
|
+
Effect.exit,
|
|
406
|
+
Effect.flatMap((release) =>
|
|
407
|
+
Effect.fail(
|
|
408
|
+
failure("createSession", undecoded(detail, id, Exit.isSuccess(release))),
|
|
409
|
+
),
|
|
410
|
+
),
|
|
411
|
+
),
|
|
412
|
+
});
|
|
413
|
+
}),
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
return BrowserbaseClient.of({
|
|
419
|
+
createSession: (body = {}) => createSession(body),
|
|
420
|
+
getSession: (id) =>
|
|
421
|
+
checkId("getSession", id).pipe(
|
|
422
|
+
Effect.flatMap(() =>
|
|
423
|
+
send(
|
|
424
|
+
"getSession",
|
|
425
|
+
HttpClientRequest.get(`/v1/sessions/${id}`),
|
|
426
|
+
json("getSession", Session),
|
|
427
|
+
),
|
|
428
|
+
),
|
|
429
|
+
),
|
|
430
|
+
listSessions: (filter = {}) =>
|
|
431
|
+
send(
|
|
432
|
+
"listSessions",
|
|
433
|
+
HttpClientRequest.get("/v1/sessions", {
|
|
434
|
+
urlParams: {
|
|
435
|
+
...(filter.status === undefined ? {} : { status: filter.status }),
|
|
436
|
+
...(filter.query === undefined ? {} : { q: filter.query }),
|
|
437
|
+
},
|
|
438
|
+
}),
|
|
439
|
+
json("listSessions", Schema.Array(Session)),
|
|
440
|
+
),
|
|
441
|
+
releaseSession,
|
|
442
|
+
liveView: (id, options = {}) =>
|
|
443
|
+
checkId("liveView", id).pipe(
|
|
444
|
+
Effect.flatMap(() =>
|
|
445
|
+
send(
|
|
446
|
+
"liveView",
|
|
447
|
+
HttpClientRequest.get(`/v1/sessions/${id}/debug`, {
|
|
448
|
+
urlParams:
|
|
449
|
+
options.expiresInSeconds === undefined
|
|
450
|
+
? {}
|
|
451
|
+
: { expiresIn: options.expiresInSeconds },
|
|
452
|
+
}),
|
|
453
|
+
json("liveView", LiveView),
|
|
454
|
+
),
|
|
455
|
+
),
|
|
456
|
+
),
|
|
457
|
+
createContext: (body = {}) =>
|
|
458
|
+
withBody("createContext", HttpClientRequest.post("/v1/contexts"), body).pipe(
|
|
459
|
+
Effect.flatMap((request) =>
|
|
460
|
+
send("createContext", request, json("createContext", StoredContext)),
|
|
461
|
+
),
|
|
462
|
+
),
|
|
463
|
+
getContext: (id) =>
|
|
464
|
+
checkId("getContext", id).pipe(
|
|
465
|
+
Effect.flatMap(() =>
|
|
466
|
+
send(
|
|
467
|
+
"getContext",
|
|
468
|
+
HttpClientRequest.get(`/v1/contexts/${id}`),
|
|
469
|
+
json("getContext", StoredContext),
|
|
470
|
+
),
|
|
471
|
+
),
|
|
472
|
+
),
|
|
473
|
+
deleteContext: (id) =>
|
|
474
|
+
checkId("deleteContext", id).pipe(
|
|
475
|
+
Effect.flatMap(() =>
|
|
476
|
+
send("deleteContext", HttpClientRequest.delete(`/v1/contexts/${id}`), ignoreBody),
|
|
477
|
+
),
|
|
478
|
+
),
|
|
479
|
+
uploadExtension: (archive, options = {}) =>
|
|
480
|
+
archive.byteLength === 0 || archive.byteLength > maxExtensionBytes
|
|
481
|
+
? Effect.fail(
|
|
482
|
+
failure(
|
|
483
|
+
"uploadExtension",
|
|
484
|
+
new InvalidRequest({
|
|
485
|
+
detail: `the archive has ${archive.byteLength} bytes; Browserbase takes 1 byte to 100 MB`,
|
|
486
|
+
}),
|
|
487
|
+
),
|
|
488
|
+
)
|
|
489
|
+
: Effect.suspend(() => {
|
|
490
|
+
const form = new FormData();
|
|
491
|
+
|
|
492
|
+
form.append(
|
|
493
|
+
"file",
|
|
494
|
+
new Blob([new Uint8Array(archive)], { type: "application/zip" }),
|
|
495
|
+
options.fileName ?? "extension.zip",
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
return send(
|
|
499
|
+
"uploadExtension",
|
|
500
|
+
HttpClientRequest.post("/v1/extensions").pipe(HttpClientRequest.bodyFormData(form)),
|
|
501
|
+
json("uploadExtension", Extension),
|
|
502
|
+
);
|
|
503
|
+
}),
|
|
504
|
+
getExtension: (id) =>
|
|
505
|
+
checkId("getExtension", id).pipe(
|
|
506
|
+
Effect.flatMap(() =>
|
|
507
|
+
send(
|
|
508
|
+
"getExtension",
|
|
509
|
+
HttpClientRequest.get(`/v1/extensions/${id}`),
|
|
510
|
+
json("getExtension", Extension),
|
|
511
|
+
),
|
|
512
|
+
),
|
|
513
|
+
),
|
|
514
|
+
deleteExtension: (id) =>
|
|
515
|
+
checkId("deleteExtension", id).pipe(
|
|
516
|
+
Effect.flatMap(() =>
|
|
517
|
+
send("deleteExtension", HttpClientRequest.delete(`/v1/extensions/${id}`), ignoreBody),
|
|
518
|
+
),
|
|
519
|
+
),
|
|
520
|
+
search: (query, options = {}) =>
|
|
521
|
+
withBody("search", HttpClientRequest.post("/v1/search"), {
|
|
522
|
+
query,
|
|
523
|
+
...(options.results === undefined ? {} : { numResults: options.results }),
|
|
524
|
+
}).pipe(
|
|
525
|
+
Effect.flatMap((request) => send("search", request, json("search", SearchResponse))),
|
|
526
|
+
Effect.map((response) => response.results),
|
|
527
|
+
),
|
|
528
|
+
fetch: (url, options = {}) =>
|
|
529
|
+
withBody("fetch", HttpClientRequest.post("/v1/fetch"), { url, ...options }).pipe(
|
|
530
|
+
Effect.flatMap((request) => send("fetch", request, json("fetch", FetchedPage))),
|
|
531
|
+
),
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
export const layer = (
|
|
536
|
+
options: Options,
|
|
537
|
+
): Layer.Layer<BrowserbaseClient, never, HttpClient.HttpClient> =>
|
|
538
|
+
Layer.effect(BrowserbaseClient, make(options));
|
|
539
|
+
|
|
540
|
+
/** The client configured from `BROWSERBASE_API_KEY` and, optionally, `BROWSERBASE_BASE_URL`. */
|
|
541
|
+
export const layerConfig = (
|
|
542
|
+
options: {
|
|
543
|
+
readonly apiKey?: Config.Config<Redacted.Redacted<string>> | undefined;
|
|
544
|
+
readonly baseUrl?: Config.Config<string> | undefined;
|
|
545
|
+
} = {},
|
|
546
|
+
): Layer.Layer<BrowserbaseClient, Config.ConfigError, HttpClient.HttpClient> =>
|
|
547
|
+
Layer.effect(
|
|
548
|
+
BrowserbaseClient,
|
|
549
|
+
Effect.gen(function* () {
|
|
550
|
+
const apiKey = yield* options.apiKey ?? Config.Redacted("BROWSERBASE_API_KEY");
|
|
551
|
+
|
|
552
|
+
const baseUrl = yield* (
|
|
553
|
+
options.baseUrl ??
|
|
554
|
+
Config.String("BROWSERBASE_BASE_URL").pipe(Config.withDefault(defaultBaseUrl))
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
return yield* make({ apiKey, baseUrl });
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one error every Browserbase API call fails with. `reason` says what went wrong.
|
|
3
|
+
*
|
|
4
|
+
* @since 0.3.0
|
|
5
|
+
*/
|
|
6
|
+
import { Schema } from "effect";
|
|
7
|
+
|
|
8
|
+
/** The API key is missing, wrong or not allowed to do this (401 or 403). */
|
|
9
|
+
export class Unauthorized extends Schema.TaggedError<Unauthorized>()("Unauthorized", {
|
|
10
|
+
detail: Schema.String,
|
|
11
|
+
}) {
|
|
12
|
+
override get message() {
|
|
13
|
+
return `unauthorized: ${this.detail}`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** No session, context or other resource has that id (404). */
|
|
18
|
+
export class NotFound extends Schema.TaggedError<NotFound>()("NotFound", {
|
|
19
|
+
detail: Schema.String,
|
|
20
|
+
}) {
|
|
21
|
+
override get message() {
|
|
22
|
+
return `not found: ${this.detail}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Too many concurrent sessions or requests for the plan (429). Wait, then try again. */
|
|
27
|
+
export class RateLimited extends Schema.TaggedError<RateLimited>()("RateLimited", {
|
|
28
|
+
detail: Schema.String,
|
|
29
|
+
}) {
|
|
30
|
+
override get message() {
|
|
31
|
+
return `rate limited: ${this.detail}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Any other answer that is not a success, including a redirect, which is never followed. */
|
|
36
|
+
export class Status extends Schema.TaggedError<Status>()("Status", {
|
|
37
|
+
status: Schema.Finite,
|
|
38
|
+
detail: Schema.String,
|
|
39
|
+
}) {
|
|
40
|
+
override get message() {
|
|
41
|
+
return `status ${this.status}: ${this.detail}`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* No answer arrived: the request could not be sent, or the connection failed. A request that
|
|
47
|
+
* creates something may still have created it.
|
|
48
|
+
*/
|
|
49
|
+
export class Transport extends Schema.TaggedError<Transport>()("Transport", {
|
|
50
|
+
detail: Schema.String,
|
|
51
|
+
}) {
|
|
52
|
+
override get message() {
|
|
53
|
+
return this.detail;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The answer did not have the expected shape. For a create whose answer named its session,
|
|
59
|
+
* `released` says whether the client then released that session; without it, a session may be
|
|
60
|
+
* left running.
|
|
61
|
+
*/
|
|
62
|
+
export class Decode extends Schema.TaggedError<Decode>()("Decode", {
|
|
63
|
+
detail: Schema.String,
|
|
64
|
+
released: Schema.optional(Schema.Boolean),
|
|
65
|
+
}) {
|
|
66
|
+
override get message() {
|
|
67
|
+
return `unexpected response: ${this.detail}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Not a request Browserbase takes, such as an id that is not one: Browserbase refused it (400), or
|
|
73
|
+
* it was refused before sending.
|
|
74
|
+
*/
|
|
75
|
+
export class InvalidRequest extends Schema.TaggedError<InvalidRequest>()("InvalidRequest", {
|
|
76
|
+
detail: Schema.String,
|
|
77
|
+
}) {
|
|
78
|
+
override get message() {
|
|
79
|
+
return this.detail;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const Reason = Schema.Union([
|
|
84
|
+
Unauthorized,
|
|
85
|
+
NotFound,
|
|
86
|
+
RateLimited,
|
|
87
|
+
Status,
|
|
88
|
+
Transport,
|
|
89
|
+
Decode,
|
|
90
|
+
InvalidRequest,
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
export type Reason = typeof Reason.Type;
|
|
94
|
+
|
|
95
|
+
export class BrowserbaseError extends Schema.TaggedError<BrowserbaseError>()("BrowserbaseError", {
|
|
96
|
+
operation: Schema.String,
|
|
97
|
+
reason: Reason,
|
|
98
|
+
}) {
|
|
99
|
+
override get message() {
|
|
100
|
+
return `Browserbase ${this.operation} failed: ${this.reason.message}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** True for failures worth retrying after a pause: no answer, a rate limit or a server error. */
|
|
105
|
+
export const isTransient = (error: BrowserbaseError): boolean =>
|
|
106
|
+
error.reason._tag === "Transport" ||
|
|
107
|
+
error.reason._tag === "RateLimited" ||
|
|
108
|
+
(error.reason._tag === "Status" && (error.reason.status >= 500 || error.reason.status === 408));
|