effect-browserbase 0.2.0-beta.8 → 0.3.0-beta.5

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.
Files changed (155) hide show
  1. package/README.md +250 -313
  2. package/dist/Browserbase-FDltiz36.mjs +542 -0
  3. package/dist/Browserbase-FDltiz36.mjs.map +1 -0
  4. package/dist/Browserbase.d.mts +105 -0
  5. package/dist/Browserbase.mjs +3 -0
  6. package/dist/BrowserbaseClient-CtAQ2rg4.d.mts +195 -0
  7. package/dist/BrowserbaseClient.d.mts +2 -0
  8. package/dist/BrowserbaseClient.mjs +190 -0
  9. package/dist/BrowserbaseClient.mjs.map +1 -0
  10. package/dist/BrowserbaseError-D1EOrLNc.d.mts +79 -0
  11. package/dist/BrowserbaseError.d.mts +2 -0
  12. package/dist/BrowserbaseError.mjs +101 -0
  13. package/dist/BrowserbaseError.mjs.map +1 -0
  14. package/dist/ContextLease.d.mts +42 -0
  15. package/dist/ContextLease.mjs +80 -0
  16. package/dist/ContextLease.mjs.map +1 -0
  17. package/dist/index.d.mts +5 -27
  18. package/dist/index.mjs +5 -26
  19. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  20. package/dist/testing/index.d.mts +134 -0
  21. package/dist/testing/index.mjs +382 -0
  22. package/dist/testing/index.mjs.map +1 -0
  23. package/package.json +51 -121
  24. package/src/Browserbase.ts +698 -0
  25. package/src/BrowserbaseClient.ts +559 -0
  26. package/src/BrowserbaseError.ts +108 -0
  27. package/src/ContextLease.ts +118 -0
  28. package/src/index.ts +7 -0
  29. package/src/internal/captureConnection.ts +214 -0
  30. package/src/testing/BrowserbaseContract.ts +170 -0
  31. package/src/testing/TestBrowserbase.ts +445 -0
  32. package/src/testing/index.ts +7 -0
  33. package/dist/Account.d.mts +0 -32
  34. package/dist/Account.mjs +0 -36
  35. package/dist/Account.mjs.map +0 -1
  36. package/dist/Acquisition-BOABu_qJ.mjs +0 -244
  37. package/dist/Acquisition-BOABu_qJ.mjs.map +0 -1
  38. package/dist/Agents-CPaTOH_g.d.mts +0 -156
  39. package/dist/Agents.d.mts +0 -3
  40. package/dist/Agents.mjs +0 -292
  41. package/dist/Agents.mjs.map +0 -1
  42. package/dist/Allocation.d.mts +0 -97
  43. package/dist/Allocation.mjs +0 -27
  44. package/dist/Allocation.mjs.map +0 -1
  45. package/dist/Authorization-Ll5f_XNM.mjs +0 -12
  46. package/dist/Authorization-Ll5f_XNM.mjs.map +0 -1
  47. package/dist/Browser-ByrWJ0ss.mjs +0 -274
  48. package/dist/Browser-ByrWJ0ss.mjs.map +0 -1
  49. package/dist/Browser.d.mts +0 -83
  50. package/dist/Browser.mjs +0 -3
  51. package/dist/BrowserBinding.d.mts +0 -13
  52. package/dist/BrowserBinding.mjs +0 -31
  53. package/dist/BrowserBinding.mjs.map +0 -1
  54. package/dist/Certificates-D4NQmyeA.d.mts +0 -34
  55. package/dist/Certificates.d.mts +0 -3
  56. package/dist/Certificates.mjs +0 -85
  57. package/dist/Certificates.mjs.map +0 -1
  58. package/dist/Cleanup-DMTDBir8.d.mts +0 -23
  59. package/dist/Cleanup.d.mts +0 -3
  60. package/dist/Cleanup.mjs +0 -43
  61. package/dist/Cleanup.mjs.map +0 -1
  62. package/dist/Client-BEC4Q8ZS.mjs +0 -564
  63. package/dist/Client-BEC4Q8ZS.mjs.map +0 -1
  64. package/dist/Client-B_f6bBLT.d.mts +0 -52
  65. package/dist/Client.d.mts +0 -3
  66. package/dist/Client.mjs +0 -2
  67. package/dist/Connection-sxyxtwlk.mjs +0 -56
  68. package/dist/Connection-sxyxtwlk.mjs.map +0 -1
  69. package/dist/ContextCoordination.d.mts +0 -29
  70. package/dist/ContextCoordination.mjs +0 -94
  71. package/dist/ContextCoordination.mjs.map +0 -1
  72. package/dist/ContextWriter-CiEma24z.mjs +0 -204
  73. package/dist/ContextWriter-CiEma24z.mjs.map +0 -1
  74. package/dist/Contexts-DqdL7cyX.d.mts +0 -31
  75. package/dist/Contexts.d.mts +0 -3
  76. package/dist/Contexts.mjs +0 -97
  77. package/dist/Contexts.mjs.map +0 -1
  78. package/dist/Deadline-CrViaogr.mjs +0 -13
  79. package/dist/Deadline-CrViaogr.mjs.map +0 -1
  80. package/dist/Downloads-CX52GOFJ.d.mts +0 -43
  81. package/dist/Downloads.d.mts +0 -3
  82. package/dist/Downloads.mjs +0 -137
  83. package/dist/Downloads.mjs.map +0 -1
  84. package/dist/Errors.d.mts +0 -100
  85. package/dist/Errors.mjs +0 -194
  86. package/dist/Errors.mjs.map +0 -1
  87. package/dist/Extensions-BTUn9zhK.mjs +0 -231
  88. package/dist/Extensions-BTUn9zhK.mjs.map +0 -1
  89. package/dist/Extensions-DBXuVM1f.d.mts +0 -43
  90. package/dist/Extensions.d.mts +0 -3
  91. package/dist/Extensions.mjs +0 -2
  92. package/dist/Functions-CsKMMUuZ.d.mts +0 -226
  93. package/dist/Functions.d.mts +0 -3
  94. package/dist/Functions.mjs +0 -318
  95. package/dist/Functions.mjs.map +0 -1
  96. package/dist/Issued-DejqoMYL.mjs +0 -15
  97. package/dist/Issued-DejqoMYL.mjs.map +0 -1
  98. package/dist/Launch-cAdE2rrZ.mjs +0 -115
  99. package/dist/Launch-cAdE2rrZ.mjs.map +0 -1
  100. package/dist/Launch.d.mts +0 -127
  101. package/dist/Launch.mjs +0 -97
  102. package/dist/Launch.mjs.map +0 -1
  103. package/dist/PageFetch-CjAM_0DS.d.mts +0 -40
  104. package/dist/PageFetch.d.mts +0 -3
  105. package/dist/PageFetch.mjs +0 -59
  106. package/dist/PageFetch.mjs.map +0 -1
  107. package/dist/Projects-tm5bTDH0.d.mts +0 -38
  108. package/dist/Projects.d.mts +0 -3
  109. package/dist/Projects.mjs +0 -65
  110. package/dist/Projects.mjs.map +0 -1
  111. package/dist/Recordings-B9k5aODB.d.mts +0 -27
  112. package/dist/Recordings.d.mts +0 -3
  113. package/dist/Recordings.mjs +0 -144
  114. package/dist/Recordings.mjs.map +0 -1
  115. package/dist/References-DOH3Wu44.d.mts +0 -34
  116. package/dist/References.d.mts +0 -3
  117. package/dist/References.mjs +0 -33
  118. package/dist/References.mjs.map +0 -1
  119. package/dist/Replays-mV93WGtx.d.mts +0 -27
  120. package/dist/Replays.d.mts +0 -3
  121. package/dist/Replays.mjs +0 -107
  122. package/dist/Replays.mjs.map +0 -1
  123. package/dist/Resource-J7Kkk5mu.mjs +0 -54
  124. package/dist/Resource-J7Kkk5mu.mjs.map +0 -1
  125. package/dist/Search-B64e7g-h.d.mts +0 -37
  126. package/dist/Search.d.mts +0 -3
  127. package/dist/Search.mjs +0 -46
  128. package/dist/Search.mjs.map +0 -1
  129. package/dist/SessionData-5xYZDshJ.d.mts +0 -38
  130. package/dist/SessionData.d.mts +0 -3
  131. package/dist/SessionData.mjs +0 -48
  132. package/dist/SessionData.mjs.map +0 -1
  133. package/dist/Sessions-D2H5m6rF.mjs +0 -260
  134. package/dist/Sessions-D2H5m6rF.mjs.map +0 -1
  135. package/dist/Sessions-oJEUmZth.d.mts +0 -69
  136. package/dist/Sessions.d.mts +0 -4
  137. package/dist/Sessions.mjs +0 -3
  138. package/dist/Testing.d.mts +0 -119
  139. package/dist/Testing.mjs +0 -206
  140. package/dist/Testing.mjs.map +0 -1
  141. package/dist/TransferPolicy-DQ06i7SZ.mjs +0 -25
  142. package/dist/TransferPolicy-DQ06i7SZ.mjs.map +0 -1
  143. package/dist/Transfers-GVqzrDWl.d.mts +0 -89
  144. package/dist/Transfers.d.mts +0 -3
  145. package/dist/Transfers.mjs +0 -85
  146. package/dist/Transfers.mjs.map +0 -1
  147. package/dist/Uploads-C2_bACgv.d.mts +0 -28
  148. package/dist/Uploads.d.mts +0 -3
  149. package/dist/Uploads.mjs +0 -105
  150. package/dist/Uploads.mjs.map +0 -1
  151. package/dist/Webhooks-BoeRAxzG.d.mts +0 -59
  152. package/dist/Webhooks.d.mts +0 -3
  153. package/dist/Webhooks.mjs +0 -134
  154. package/dist/Webhooks.mjs.map +0 -1
  155. package/dist/WriterFacts-BBJRDZYY.d.mts +0 -54
@@ -0,0 +1,445 @@
1
+ /**
2
+ * An in-memory Browserbase API for tests. It is the `HttpClient` the real `BrowserbaseClient` runs
3
+ * on, so the client and the provider above it run as they would against Browserbase, and nothing
4
+ * leaves the process. Sessions run until released, or until their timeout on the Effect `Clock`,
5
+ * so `TestClock` drives their end. A `Script` decides how creates, releases and session reads
6
+ * answer, in turn, so a test can lose a create, leave a release pending or fail status reads.
7
+ *
8
+ * It keeps sessions and stored contexts and nothing else: any other endpoint fails the test with a
9
+ * defect. It doesn't model regions, quotas or billing, and a context is only a record, with no
10
+ * browser state. Every session hands out the script's `connectUrl`, such as the DevTools address
11
+ * of a local Chromium. Its ids are UUIDs, as Browserbase's are, and as Browserbase does, it refuses
12
+ * a session or context id of any other shape as invalid, where it answers an unknown one as not
13
+ * found.
14
+ *
15
+ * @category testing
16
+ * @since 0.3.0
17
+ */
18
+ import {
19
+ Clock,
20
+ Context,
21
+ DateTime,
22
+ Effect,
23
+ Layer,
24
+ Option,
25
+ Predicate,
26
+ Redacted,
27
+ Schema,
28
+ } from "effect";
29
+ import {
30
+ HttpClient,
31
+ HttpClientError,
32
+ type HttpClientRequest,
33
+ HttpClientResponse,
34
+ } from "effect/http";
35
+
36
+ import { type BrowserbaseClient, layer as clientLayer } from "../BrowserbaseClient.ts";
37
+
38
+ const Refusal = Schema.Int.check(Schema.isBetween({ minimum: 400, maximum: 599 }));
39
+
40
+ /** How a session create answers. */
41
+ export const Create = Schema.Union([
42
+ /** The session is made, and runs. */
43
+ Schema.TaggedStruct("Accept", {}),
44
+ /** Refused with `status`, and nothing is made. */
45
+ Schema.TaggedStruct("Reject", {
46
+ status: Refusal,
47
+ retryAfterSeconds: Schema.optional(Schema.Int),
48
+ }),
49
+ /** The session is made, but the answer holds only its id. */
50
+ Schema.TaggedStruct("Malformed", {}),
51
+ /**
52
+ * The session is made, but no answer arrives, and Browserbase lists it only `listedAfter`
53
+ * milliseconds later, as a search can trail what was made.
54
+ */
55
+ Schema.TaggedStruct("Lost", { listedAfter: Schema.optional(Schema.Finite) }),
56
+ ]);
57
+
58
+ /** How a release answers. */
59
+ export const Release = Schema.Union([
60
+ /** The session ends at once. */
61
+ Schema.TaggedStruct("Ends", {}),
62
+ /** Accepted, but the session runs on for `millis`, or without them until its timeout. */
63
+ Schema.TaggedStruct("Pending", { millis: Schema.optional(Schema.Finite) }),
64
+ /** Refused with `status`; the session runs on. */
65
+ Schema.TaggedStruct("Refused", { status: Refusal }),
66
+ /** The session ends, but no answer arrives. */
67
+ Schema.TaggedStruct("Lost", {}),
68
+ ]);
69
+
70
+ /** How a session read answers. */
71
+ export const Read = Schema.Union([
72
+ Schema.TaggedStruct("Answer", {}),
73
+ Schema.TaggedStruct("Refused", { status: Refusal }),
74
+ Schema.TaggedStruct("Lost", {}),
75
+ ]);
76
+
77
+ export const Script = Schema.Struct({
78
+ /** How creates answer, in turn; once these run out, each is accepted. */
79
+ creates: Schema.optional(Schema.Array(Create)),
80
+ /** How releases answer, in turn; once these run out, each ends its session. */
81
+ releases: Schema.optional(Schema.Array(Release)),
82
+ /** How session reads answer, in turn; once these run out, each answers. */
83
+ reads: Schema.optional(Schema.Array(Read)),
84
+ /** The DevTools address each session hands out. Without it, sessions have none. */
85
+ connectUrl: Schema.optional(Schema.String),
86
+ });
87
+
88
+ export type Script = typeof Script.Type;
89
+
90
+ /** A session as the fake keeps it, with how many releases it was asked. */
91
+ export interface Kept {
92
+ readonly id: string;
93
+ readonly status: string;
94
+ readonly userMetadata: Readonly<Record<string, unknown>> | undefined;
95
+ readonly releases: number;
96
+ /** Whether it was created to outlive its connections, until released or timed out. */
97
+ readonly keepAlive: boolean;
98
+ /** The stored context it loaded, and whether it saves to it as it ends. */
99
+ readonly context: { readonly id: string; readonly persist: boolean } | undefined;
100
+ }
101
+
102
+ export interface Handle {
103
+ /** Every request, oldest first, as `METHOD /path?query`. */
104
+ readonly requests: Effect.Effect<ReadonlyArray<string>>;
105
+ /** Each session as Browserbase would describe it now, oldest first. */
106
+ readonly sessions: Effect.Effect<ReadonlyArray<Kept>>;
107
+ /** End a session as Browserbase does on its own, as when its browser fails. */
108
+ readonly end: (id: string) => Effect.Effect<void>;
109
+ }
110
+
111
+ /** The fake's handle, beside the `BrowserbaseClient` that `layer` runs on it. */
112
+ export class TestBrowserbase extends Context.Service<TestBrowserbase, Handle>()(
113
+ "effect-browserbase/testing/TestBrowserbase",
114
+ ) {}
115
+
116
+ /** The API key the fake takes; any other is unauthorized. */
117
+ export const apiKey = "test-browserbase-key";
118
+
119
+ const CreateBody = Schema.fromJsonString(
120
+ Schema.Struct({
121
+ timeout: Schema.optional(Schema.Finite),
122
+ keepAlive: Schema.optional(Schema.Boolean),
123
+ userMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
124
+ browserSettings: Schema.optional(
125
+ Schema.Struct({
126
+ context: Schema.optional(
127
+ Schema.Struct({ id: Schema.String, persist: Schema.optional(Schema.Boolean) }),
128
+ ),
129
+ }),
130
+ ),
131
+ }),
132
+ );
133
+
134
+ const ReleaseBody = Schema.fromJsonString(
135
+ Schema.Struct({ status: Schema.Literal("REQUEST_RELEASE") }),
136
+ );
137
+
138
+ interface Row {
139
+ readonly id: string;
140
+ readonly createdAt: number;
141
+ /** When a list first shows it. */
142
+ readonly listedAt: number;
143
+ readonly expiresAt: number;
144
+ readonly keepAlive: boolean;
145
+ readonly userMetadata: Readonly<Record<string, unknown>> | undefined;
146
+ readonly context: { readonly id: string; readonly persist: boolean } | undefined;
147
+ /** How and when it ends, unless its timeout comes first. */
148
+ ends: { readonly status: string; readonly at: number } | undefined;
149
+ releases: number;
150
+ }
151
+
152
+ /** Everything one fake keeps. */
153
+ interface Fake {
154
+ readonly now: () => number;
155
+ readonly connectUrl: string | undefined;
156
+ readonly pending: {
157
+ readonly creates: Array<typeof Create.Type>;
158
+ readonly releases: Array<typeof Release.Type>;
159
+ readonly reads: Array<typeof Read.Type>;
160
+ };
161
+ readonly requests: Array<string>;
162
+ readonly sessions: Map<string, Row>;
163
+ readonly contexts: Map<string, number>;
164
+ /** How many sessions and contexts it has made, together, for their ids. */
165
+ readonly made: { count: number };
166
+ }
167
+
168
+ const iso = (millis: number) => DateTime.formatIso(DateTime.makeUnsafe(millis));
169
+
170
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
171
+
172
+ /** The next id, a UUID as Browserbase's are, counted so that every run hands out the same. */
173
+ const nextId = (fake: Fake) =>
174
+ `00000000-0000-4000-8000-${(++fake.made.count).toString(16).padStart(12, "0")}`;
175
+
176
+ /** How the session ended by now, or undefined while it runs. */
177
+ const endOf = (fake: Fake, row: Row) => {
178
+ const end =
179
+ row.ends !== undefined && row.ends.at <= row.expiresAt
180
+ ? row.ends
181
+ : { status: "TIMED_OUT", at: row.expiresAt };
182
+
183
+ return end.at <= fake.now() ? end : undefined;
184
+ };
185
+
186
+ /** The session as Browserbase answers it; only creates and reads hand out its address. */
187
+ const describe = (fake: Fake, row: Row, connect: boolean) => {
188
+ const end = endOf(fake, row);
189
+
190
+ return {
191
+ id: row.id,
192
+ createdAt: iso(row.createdAt),
193
+ updatedAt: iso(end?.at ?? row.createdAt),
194
+ startedAt: iso(row.createdAt),
195
+ expiresAt: iso(row.expiresAt),
196
+ projectId: "test-project",
197
+ status: end?.status ?? "RUNNING",
198
+ proxyBytes: 0,
199
+ keepAlive: row.keepAlive,
200
+ region: "us-west-2",
201
+ ...(end === undefined ? {} : { endedAt: iso(end.at) }),
202
+ ...(row.context === undefined ? {} : { contextId: row.context.id }),
203
+ ...(row.userMetadata === undefined ? {} : { userMetadata: row.userMetadata }),
204
+ ...(connect && fake.connectUrl !== undefined ? { connectUrl: fake.connectUrl } : {}),
205
+ ...(connect
206
+ ? {
207
+ seleniumRemoteUrl: `https://connect.browserbase.test/webdriver/${row.id}`,
208
+ signingKey: `signing-key-${row.id}`,
209
+ }
210
+ : {}),
211
+ };
212
+ };
213
+
214
+ const bodyText = (request: HttpClientRequest.HttpClientRequest) =>
215
+ request.body._tag === "Uint8Array"
216
+ ? (request.body.text ?? new TextDecoder().decode(request.body.body))
217
+ : "";
218
+
219
+ /** A metadata query in Browserbase's one form, `user_metadata['a']['b']:'value'`, as a test. */
220
+ const metadataQuery = (query: string) => {
221
+ const parsed = /^user_metadata((?:\['[^']*'\])+):'([^']*)'$/.exec(query);
222
+
223
+ if (parsed === null) return undefined;
224
+ const path = [...(parsed[1] ?? "").matchAll(/\['([^']*)'\]/g)].map((part) => part[1] ?? "");
225
+
226
+ return (metadata: unknown) =>
227
+ path.reduce<unknown>(
228
+ (value, key) => (Predicate.isObject(value) ? Reflect.get(value, key) : undefined),
229
+ metadata,
230
+ ) === parsed[2];
231
+ };
232
+
233
+ const unsupported = (detail: string) => Effect.die(new Error(`TestBrowserbase ${detail}`));
234
+
235
+ /** Answer one request as Browserbase would, after the script's say. */
236
+ const serve = (fake: Fake, request: HttpClientRequest.HttpClientRequest, url: URL) => {
237
+ const answer = (status: number, body?: unknown, headers: Record<string, string> = {}) =>
238
+ Effect.succeed(
239
+ HttpClientResponse.fromWeb(
240
+ request,
241
+ new Response(body === undefined ? null : JSON.stringify(body), {
242
+ status,
243
+ headers: { "content-type": "application/json", ...headers },
244
+ }),
245
+ ),
246
+ );
247
+
248
+ const lost = Effect.fail(
249
+ new HttpClientError.HttpClientError({
250
+ reason: new HttpClientError.TransportError({
251
+ request,
252
+ description: "TestBrowserbase lost this answer, as its script said",
253
+ }),
254
+ }),
255
+ );
256
+
257
+ const refused = (status: number) => answer(status, { message: "refused, as the script said" });
258
+ const [, version, kind, id, rest] = url.pathname.split("/");
259
+ const row = id === undefined ? undefined : fake.sessions.get(id);
260
+
261
+ fake.requests.push(`${request.method} ${url.pathname}${url.search}`);
262
+ if (request.headers["x-bb-api-key"] !== apiKey)
263
+ return answer(401, { message: "Invalid API key" });
264
+ if (version !== "v1") return unsupported(`serves only /v1, not ${url.pathname}`);
265
+ // Browserbase refuses a session or context id that is not a UUID before it looks for it.
266
+ if ((kind === "sessions" || kind === "contexts") && id !== undefined && !uuid.test(id))
267
+ return answer(400, { message: `Invalid ${kind === "sessions" ? "Session" : "Context"} ID` });
268
+
269
+ switch (
270
+ `${request.method} ${kind}${id === undefined ? "" : " id"}${rest === undefined ? "" : ` ${rest}`}`
271
+ ) {
272
+ case "POST sessions": {
273
+ const reply = fake.pending.creates.shift() ?? { _tag: "Accept" };
274
+
275
+ if (reply._tag === "Reject")
276
+ return answer(
277
+ reply.status,
278
+ { message: "refused, as the script said" },
279
+ reply.retryAfterSeconds === undefined
280
+ ? {}
281
+ : { "retry-after": `${reply.retryAfterSeconds}` },
282
+ );
283
+ const body = Option.getOrUndefined(Schema.decodeOption(CreateBody)(bodyText(request)));
284
+ const loaded = body?.browserSettings?.context;
285
+ const createdAt = fake.now();
286
+
287
+ const created: Row = {
288
+ id: nextId(fake),
289
+ createdAt,
290
+ listedAt: createdAt + (reply._tag === "Lost" ? (reply.listedAfter ?? 0) : 0),
291
+ expiresAt: createdAt + (body?.timeout ?? 300) * 1000,
292
+ keepAlive: body?.keepAlive ?? false,
293
+ userMetadata: body?.userMetadata,
294
+ context:
295
+ loaded === undefined ? undefined : { id: loaded.id, persist: loaded.persist ?? false },
296
+ ends: undefined,
297
+ releases: 0,
298
+ };
299
+
300
+ fake.sessions.set(created.id, created);
301
+ if (reply._tag === "Lost") return lost;
302
+
303
+ return answer(
304
+ 201,
305
+ reply._tag === "Malformed" ? { id: created.id } : describe(fake, created, true),
306
+ );
307
+ }
308
+ case "GET sessions": {
309
+ const status = url.searchParams.get("status");
310
+ const query = url.searchParams.get("q");
311
+ const matches = query === null ? () => true : metadataQuery(query);
312
+
313
+ if (matches === undefined) return unsupported(`cannot read the query ${query}`);
314
+
315
+ return answer(
316
+ 200,
317
+ [...fake.sessions.values()]
318
+ .filter(
319
+ (kept) =>
320
+ kept.listedAt <= fake.now() &&
321
+ (status === null || (endOf(fake, kept)?.status ?? "RUNNING") === status) &&
322
+ matches(kept.userMetadata),
323
+ )
324
+ .map((kept) => describe(fake, kept, false)),
325
+ );
326
+ }
327
+ case "GET sessions id": {
328
+ const reply = fake.pending.reads.shift() ?? { _tag: "Answer" };
329
+
330
+ if (reply._tag === "Refused") return refused(reply.status);
331
+ if (reply._tag === "Lost") return lost;
332
+
333
+ return row === undefined
334
+ ? answer(404, { message: "Session not found" })
335
+ : answer(200, describe(fake, row, true));
336
+ }
337
+ case "POST sessions id": {
338
+ if (row === undefined) return answer(404, { message: "Session not found" });
339
+ if (Option.isNone(Schema.decodeOption(ReleaseBody)(bodyText(request))))
340
+ return answer(400, { message: "status must be REQUEST_RELEASE" });
341
+ row.releases += 1;
342
+ const reply = fake.pending.releases.shift() ?? { _tag: "Ends" };
343
+
344
+ if (reply._tag === "Refused") return refused(reply.status);
345
+ // Releasing a session that has ended changes nothing, and succeeds.
346
+ if (
347
+ endOf(fake, row) === undefined &&
348
+ (reply._tag !== "Pending" || reply.millis !== undefined)
349
+ )
350
+ row.ends = {
351
+ status: "COMPLETED",
352
+ at: fake.now() + (reply._tag === "Pending" ? (reply.millis ?? 0) : 0),
353
+ };
354
+
355
+ return reply._tag === "Lost" ? lost : answer(200, describe(fake, row, false));
356
+ }
357
+ case "POST contexts": {
358
+ const created = nextId(fake);
359
+
360
+ fake.contexts.set(created, fake.now());
361
+
362
+ return answer(201, {
363
+ id: created,
364
+ publicKey: "test-public-key",
365
+ cipherAlgorithm: "AES-256-CBC",
366
+ initializationVectorSize: 16,
367
+ });
368
+ }
369
+ case "GET contexts id": {
370
+ const createdAt = id === undefined ? undefined : fake.contexts.get(id);
371
+
372
+ return createdAt === undefined
373
+ ? answer(404, { message: "Context not found" })
374
+ : answer(200, {
375
+ id,
376
+ createdAt: iso(createdAt),
377
+ updatedAt: iso(createdAt),
378
+ projectId: "test-project",
379
+ });
380
+ }
381
+ case "DELETE contexts id":
382
+ return id !== undefined && fake.contexts.delete(id)
383
+ ? answer(204)
384
+ : answer(404, { message: "Context not found" });
385
+ default:
386
+ return unsupported(`does not implement ${request.method} ${url.pathname}`);
387
+ }
388
+ };
389
+
390
+ /** The fake as an `HttpClient`, with its handle. */
391
+ export const make = Effect.fnUntraced(function* (script: Script = {}) {
392
+ const clock = yield* Clock.Clock;
393
+ const decoded = yield* Effect.orDie(Schema.decodeEffect(Script)(script));
394
+
395
+ const fake: Fake = {
396
+ now: () => clock.currentTimeMillisUnsafe(),
397
+ connectUrl: decoded.connectUrl,
398
+ pending: {
399
+ creates: [...(decoded.creates ?? [])],
400
+ releases: [...(decoded.releases ?? [])],
401
+ reads: [...(decoded.reads ?? [])],
402
+ },
403
+ requests: [],
404
+ sessions: new Map(),
405
+ contexts: new Map(),
406
+ made: { count: 0 },
407
+ };
408
+
409
+ const handle: Handle = {
410
+ requests: Effect.sync(() => [...fake.requests]),
411
+ sessions: Effect.sync(() =>
412
+ [...fake.sessions.values()].map((row) => ({
413
+ id: row.id,
414
+ status: endOf(fake, row)?.status ?? "RUNNING",
415
+ userMetadata: row.userMetadata,
416
+ releases: row.releases,
417
+ keepAlive: row.keepAlive,
418
+ context: row.context,
419
+ })),
420
+ ),
421
+ end: (id) =>
422
+ Effect.sync(() => {
423
+ const row = fake.sessions.get(id);
424
+
425
+ if (row !== undefined && endOf(fake, row) === undefined)
426
+ row.ends = { status: "ERROR", at: fake.now() };
427
+ }),
428
+ };
429
+
430
+ return { http: HttpClient.make((request, url) => serve(fake, request, url)), handle };
431
+ });
432
+
433
+ /** The real `BrowserbaseClient` over the fake, and the fake's handle. */
434
+ export const layer = (script: Script = {}): Layer.Layer<BrowserbaseClient | TestBrowserbase> =>
435
+ Layer.unwrap(
436
+ Effect.map(make(script), ({ http, handle }) =>
437
+ Layer.merge(
438
+ clientLayer({
439
+ apiKey: Redacted.make(apiKey),
440
+ baseUrl: "https://api.browserbase.test",
441
+ }).pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, http))),
442
+ Layer.succeed(TestBrowserbase, handle),
443
+ ),
444
+ ),
445
+ );
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Test kits: the Browserbase API in memory, and the checks it is held to.
3
+ *
4
+ * @since 0.3.0
5
+ */
6
+ export * as BrowserbaseContract from "./BrowserbaseContract.ts";
7
+ export * as TestBrowserbase from "./TestBrowserbase.ts";
@@ -1,32 +0,0 @@
1
- import { ClientError } from "./Errors.mjs";
2
- import { r as ClientOptions, t as BrowserbaseClient } from "./Client-B_f6bBLT.mjs";
3
- import { p as BrowserbaseAgents } from "./Agents-CPaTOH_g.mjs";
4
- import { t as BrowserbaseCertificates } from "./Certificates-D4NQmyeA.mjs";
5
- import { t as BrowserbaseContexts } from "./Contexts-DqdL7cyX.mjs";
6
- import { t as BrowserbaseSessions } from "./Sessions-oJEUmZth.mjs";
7
- import { t as BrowserbaseDownloads } from "./Downloads-CX52GOFJ.mjs";
8
- import { t as BrowserbaseExtensions } from "./Extensions-DBXuVM1f.mjs";
9
- import { t as BrowserbaseFunctions } from "./Functions-CsKMMUuZ.mjs";
10
- import { t as BrowserbasePageFetch } from "./PageFetch-CjAM_0DS.mjs";
11
- import { t as BrowserbaseProjects } from "./Projects-tm5bTDH0.mjs";
12
- import { t as BrowserbaseRecordings } from "./Recordings-B9k5aODB.mjs";
13
- import { t as BrowserbaseReplays } from "./Replays-mV93WGtx.mjs";
14
- import { t as BrowserbaseSearch } from "./Search-B64e7g-h.mjs";
15
- import { t as BrowserbaseUploads } from "./Uploads-C2_bACgv.mjs";
16
- import { t as BrowserbaseWebhooks } from "./Webhooks-BoeRAxzG.mjs";
17
- import { Config, Layer } from "effect";
18
- //#region src/Account.d.ts
19
- /** Every resource service this package owns, on one account's authority. */
20
- export type Services = BrowserbaseAgents | BrowserbaseCertificates | BrowserbaseClient | BrowserbaseContexts | BrowserbaseDownloads | BrowserbaseExtensions | BrowserbaseFunctions | BrowserbasePageFetch | BrowserbaseProjects | BrowserbaseRecordings | BrowserbaseReplays | BrowserbaseSearch | BrowserbaseSessions | BrowserbaseUploads | BrowserbaseWebhooks;
21
- /**
22
- * One account for every resource operation, so credentials are composed once rather than
23
- * per service. Building it performs no I/O, allocates nothing and loads no native peer;
24
- * `BrowserbaseBrowser.layer` is deliberately separate, because a browser fixes budgets and
25
- * a connection lifetime that an account does not.
26
- */
27
- export declare const layer: (options: ClientOptions) => Layer.Layer<Services, ClientError>;
28
- /** The same bundle, taking account authority from the application's `ConfigProvider`. */
29
- export declare const layerConfig: (options?: Omit<ClientOptions, "projectId" | "apiKey">) => Layer.Layer<Services, ClientError | Config.ConfigError>;
30
- //#endregion
31
- export {};
32
- //# sourceMappingURL=Account.d.mts.map
package/dist/Account.mjs DELETED
@@ -1,36 +0,0 @@
1
- import { t as BrowserbaseClient } from "./Client-BEC4Q8ZS.mjs";
2
- import { BrowserbaseAgents } from "./Agents.mjs";
3
- import { BrowserbaseCertificates } from "./Certificates.mjs";
4
- import { BrowserbaseContexts } from "./Contexts.mjs";
5
- import { t as BrowserbaseSessions } from "./Sessions-D2H5m6rF.mjs";
6
- import { BrowserbaseDownloads } from "./Downloads.mjs";
7
- import { t as BrowserbaseExtensions } from "./Extensions-BTUn9zhK.mjs";
8
- import { BrowserbaseFunctions } from "./Functions.mjs";
9
- import { BrowserbasePageFetch } from "./PageFetch.mjs";
10
- import { BrowserbaseProjects } from "./Projects.mjs";
11
- import { BrowserbaseRecordings } from "./Recordings.mjs";
12
- import { BrowserbaseReplays } from "./Replays.mjs";
13
- import { BrowserbaseSearch } from "./Search.mjs";
14
- import { BrowserbaseUploads } from "./Uploads.mjs";
15
- import { BrowserbaseWebhooks } from "./Webhooks.mjs";
16
- import { Layer } from "effect";
17
- //#region src/Account.ts
18
- /**
19
- * The services that read one Client. Sessions is provided first and kept in the output,
20
- * because the artifact services read it and a caller composing this Layer should not have
21
- * to know which ones do.
22
- */
23
- const services = Layer.mergeAll(BrowserbaseAgents.layer, BrowserbaseCertificates.layer, BrowserbaseContexts.layer, BrowserbaseDownloads.layer, BrowserbaseExtensions.layer, BrowserbaseFunctions.layer, BrowserbasePageFetch.layer, BrowserbaseProjects.layer, BrowserbaseRecordings.layer, BrowserbaseReplays.layer, BrowserbaseSearch.layer, BrowserbaseUploads.layer, BrowserbaseWebhooks.layer).pipe(Layer.provideMerge(BrowserbaseSessions.layer));
24
- /**
25
- * One account for every resource operation, so credentials are composed once rather than
26
- * per service. Building it performs no I/O, allocates nothing and loads no native peer;
27
- * `BrowserbaseBrowser.layer` is deliberately separate, because a browser fixes budgets and
28
- * a connection lifetime that an account does not.
29
- */
30
- const layer = (options) => services.pipe(Layer.provideMerge(BrowserbaseClient.layer(options)));
31
- /** The same bundle, taking account authority from the application's `ConfigProvider`. */
32
- const layerConfig = (options = {}) => services.pipe(Layer.provideMerge(BrowserbaseClient.layerConfig(options)));
33
- //#endregion
34
- export { layer, layerConfig };
35
-
36
- //# sourceMappingURL=Account.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Account.mjs","names":[],"sources":["../src/Account.ts"],"sourcesContent":["import { type Config, Layer } from \"effect\";\n\nimport { BrowserbaseAgents } from \"./Agents.ts\";\nimport { BrowserbaseCertificates } from \"./Certificates.ts\";\nimport { BrowserbaseClient, type ClientOptions } from \"./Client.ts\";\nimport { BrowserbaseContexts } from \"./Contexts.ts\";\nimport { BrowserbaseDownloads } from \"./Downloads.ts\";\nimport type { ClientError } from \"./Errors.ts\";\nimport { BrowserbaseExtensions } from \"./Extensions.ts\";\nimport { BrowserbaseFunctions } from \"./Functions.ts\";\nimport { BrowserbasePageFetch } from \"./PageFetch.ts\";\nimport { BrowserbaseProjects } from \"./Projects.ts\";\nimport { BrowserbaseRecordings } from \"./Recordings.ts\";\nimport { BrowserbaseReplays } from \"./Replays.ts\";\nimport { BrowserbaseSearch } from \"./Search.ts\";\nimport { BrowserbaseSessions } from \"./Sessions.ts\";\nimport { BrowserbaseUploads } from \"./Uploads.ts\";\nimport { BrowserbaseWebhooks } from \"./Webhooks.ts\";\n\n/** Every resource service this package owns, on one account's authority. */\nexport type Services =\n | BrowserbaseAgents\n | BrowserbaseCertificates\n | BrowserbaseClient\n | BrowserbaseContexts\n | BrowserbaseDownloads\n | BrowserbaseExtensions\n | BrowserbaseFunctions\n | BrowserbasePageFetch\n | BrowserbaseProjects\n | BrowserbaseRecordings\n | BrowserbaseReplays\n | BrowserbaseSearch\n | BrowserbaseSessions\n | BrowserbaseUploads\n | BrowserbaseWebhooks;\n\n/**\n * The services that read one Client. Sessions is provided first and kept in the output,\n * because the artifact services read it and a caller composing this Layer should not have\n * to know which ones do.\n */\nconst services = Layer.mergeAll(\n BrowserbaseAgents.layer,\n BrowserbaseCertificates.layer,\n BrowserbaseContexts.layer,\n BrowserbaseDownloads.layer,\n BrowserbaseExtensions.layer,\n BrowserbaseFunctions.layer,\n BrowserbasePageFetch.layer,\n BrowserbaseProjects.layer,\n BrowserbaseRecordings.layer,\n BrowserbaseReplays.layer,\n BrowserbaseSearch.layer,\n BrowserbaseUploads.layer,\n BrowserbaseWebhooks.layer,\n).pipe(Layer.provideMerge(BrowserbaseSessions.layer));\n\n/**\n * One account for every resource operation, so credentials are composed once rather than\n * per service. Building it performs no I/O, allocates nothing and loads no native peer;\n * `BrowserbaseBrowser.layer` is deliberately separate, because a browser fixes budgets and\n * a connection lifetime that an account does not.\n */\nexport const layer = (options: ClientOptions): Layer.Layer<Services, ClientError> =>\n services.pipe(Layer.provideMerge(BrowserbaseClient.layer(options)));\n\n/** The same bundle, taking account authority from the application's `ConfigProvider`. */\nexport const layerConfig = (\n options: Omit<ClientOptions, \"projectId\" | \"apiKey\"> = {},\n): Layer.Layer<Services, ClientError | Config.ConfigError> =>\n services.pipe(Layer.provideMerge(BrowserbaseClient.layerConfig(options)));\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,WAAW,MAAM,SACrB,kBAAkB,OAClB,wBAAwB,OACxB,oBAAoB,OACpB,qBAAqB,OACrB,sBAAsB,OACtB,qBAAqB,OACrB,qBAAqB,OACrB,oBAAoB,OACpB,sBAAsB,OACtB,mBAAmB,OACnB,kBAAkB,OAClB,mBAAmB,OACnB,oBAAoB,KACtB,CAAC,CAAC,KAAK,MAAM,aAAa,oBAAoB,KAAK,CAAC;;;;;;;AAQpD,MAAa,SAAS,YACpB,SAAS,KAAK,MAAM,aAAa,kBAAkB,MAAM,OAAO,CAAC,CAAC;;AAGpE,MAAa,eACX,UAAuD,CAAC,MAExD,SAAS,KAAK,MAAM,aAAa,kBAAkB,YAAY,OAAO,CAAC,CAAC"}