effect-browserbase 0.3.0-beta.5 → 0.3.0-beta.7

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 CHANGED
@@ -266,10 +266,12 @@ under the real `BrowserbaseClient`, so the client and `Browserbase` run as they
266
266
  Browserbase, for free. Sessions run until released or until their timeout on the Effect `Clock`, so
267
267
  `TestClock` ends them. A `Script` loses a create, whose session it can list only `listedAfter` a
268
268
  while, leaves a release pending, refuses it, or fails status reads, in turn. `connectUrl` gives
269
- each session a DevTools address, such as a local Chromium's. It keeps sessions and stored contexts
270
- only, and fails a test that calls anything else; `sessions` tells each session's status, user
271
- metadata, releases asked, whether it was kept alive, and the stored context it loaded and whether
272
- it saves to it. Its ids are UUIDs, as Browserbase's are, and it answers each id shape as Browserbase
269
+ each session a DevTools address, such as a local Chromium's. It keeps sessions and stored contexts,
270
+ and supplies synthetic Live View links for known session ids, with an empty page list. It does not
271
+ model link expiry or Browserbase's Live View behavior after a session ends. Other endpoints fail
272
+ the test. `sessions` tells each session's status, its original create `options` without provider
273
+ defaults, user metadata, releases asked, whether it was kept alive, and the stored context it
274
+ loaded and whether it saves to it. Its ids are UUIDs, as Browserbase's are, and it answers each id shape as Browserbase
273
275
  does: it refuses a session id of any other shape, where it answers an unknown well-formed one as
274
276
  not found. `BrowserbaseContract.checks` are what this package relies on Browserbase to do, each an
275
277
  Effect over `BrowserbaseClient`; the fake passes them. Its answers also have the shapes
@@ -1,5 +1,5 @@
1
1
  import { t as BrowserbaseError } from "../BrowserbaseError-D1EOrLNc.mjs";
2
- import { n as BrowserbaseClient } from "../BrowserbaseClient-CtAQ2rg4.mjs";
2
+ import { n as BrowserbaseClient, p as SessionOptions } from "../BrowserbaseClient-CtAQ2rg4.mjs";
3
3
  import { Context, Effect, Layer, Schema } from "effect";
4
4
  import { HttpClient } from "effect/http";
5
5
  declare namespace BrowserbaseContract_d_exports {
@@ -66,6 +66,8 @@ type Script = typeof Script.Type;
66
66
  interface Kept {
67
67
  readonly id: string;
68
68
  readonly status: string;
69
+ /** The create options as sent, without provider defaults. Only the test handle exposes them. */
70
+ readonly options: SessionOptions;
69
71
  readonly userMetadata: Readonly<Record<string, unknown>> | undefined;
70
72
  readonly releases: number;
71
73
  /** Whether it was created to outlive its connections, until released or timed out. */
@@ -122,9 +122,9 @@ const checks = [
122
122
  * so `TestClock` drives their end. A `Script` decides how creates, releases and session reads
123
123
  * answer, in turn, so a test can lose a create, leave a release pending or fail status reads.
124
124
  *
125
- * It keeps sessions and stored contexts and nothing else: any other endpoint fails the test with a
126
- * defect. It doesn't model regions, quotas or billing, and a context is only a record, with no
127
- * browser state. Every session hands out the script's `connectUrl`, such as the DevTools address
125
+ * It keeps sessions and stored contexts, and supplies synthetic Live View links: any other endpoint
126
+ * fails the test with a defect. It doesn't model quotas or billing, and a context is only a record,
127
+ * with no browser state. Every session hands out the script's `connectUrl`, such as the DevTools address
128
128
  * of a local Chromium. Its ids are UUIDs, as Browserbase's are, and as Browserbase does, it refuses
129
129
  * a session or context id of any other shape as invalid, where it answers an unknown one as not
130
130
  * found.
@@ -184,13 +184,63 @@ var TestBrowserbase = class extends Context.Service()("effect-browserbase/testin
184
184
  /** The API key the fake takes; any other is unauthorized. */
185
185
  const apiKey = "test-browserbase-key";
186
186
  const CreateBody = Schema.fromJsonString(Schema.Struct({
187
+ projectId: Schema.optional(Schema.String),
188
+ extensionId: Schema.optional(Schema.String),
187
189
  timeout: Schema.optional(Schema.Finite),
188
190
  keepAlive: Schema.optional(Schema.Boolean),
191
+ region: Schema.optional(Schema.Literals([
192
+ "us-west-2",
193
+ "us-east-1",
194
+ "eu-central-1",
195
+ "ap-southeast-1"
196
+ ])),
197
+ proxies: Schema.optional(Schema.Union([Schema.Boolean, Schema.Array(Schema.Union([
198
+ Schema.Struct({
199
+ type: Schema.Literal("browserbase"),
200
+ geolocation: Schema.optional(Schema.Struct({
201
+ country: Schema.String,
202
+ state: Schema.optional(Schema.String),
203
+ city: Schema.optional(Schema.String)
204
+ })),
205
+ domainPattern: Schema.optional(Schema.String)
206
+ }),
207
+ Schema.Struct({
208
+ type: Schema.Literal("external"),
209
+ server: Schema.String,
210
+ username: Schema.optional(Schema.String),
211
+ password: Schema.optional(Schema.String),
212
+ domainPattern: Schema.optional(Schema.String)
213
+ }),
214
+ Schema.Struct({
215
+ type: Schema.Literal("none"),
216
+ domainPattern: Schema.optional(Schema.String)
217
+ })
218
+ ]))])),
189
219
  userMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
190
- browserSettings: Schema.optional(Schema.Struct({ context: Schema.optional(Schema.Struct({
191
- id: Schema.String,
192
- persist: Schema.optional(Schema.Boolean)
193
- })) }))
220
+ browserSettings: Schema.optional(Schema.Struct({
221
+ context: Schema.optional(Schema.Struct({
222
+ id: Schema.String,
223
+ persist: Schema.optional(Schema.Boolean)
224
+ })),
225
+ viewport: Schema.optional(Schema.Struct({
226
+ width: Schema.Finite,
227
+ height: Schema.Finite
228
+ })),
229
+ blockAds: Schema.optional(Schema.Boolean),
230
+ solveCaptchas: Schema.optional(Schema.Boolean),
231
+ recordSession: Schema.optional(Schema.Boolean),
232
+ logSession: Schema.optional(Schema.Boolean),
233
+ verified: Schema.optional(Schema.Boolean),
234
+ os: Schema.optional(Schema.Literals([
235
+ "windows",
236
+ "mac",
237
+ "linux",
238
+ "mobile",
239
+ "tablet"
240
+ ])),
241
+ allowedDomains: Schema.optional(Schema.Array(Schema.String)),
242
+ ignoreCertificateErrors: Schema.optional(Schema.Boolean)
243
+ }))
194
244
  }));
195
245
  const ReleaseBody = Schema.fromJsonString(Schema.Struct({ status: Schema.Literal("REQUEST_RELEASE") }));
196
246
  const iso = (millis) => DateTime.formatIso(DateTime.makeUnsafe(millis));
@@ -214,11 +264,11 @@ const describe = (fake, row, connect) => {
214
264
  updatedAt: iso(end?.at ?? row.createdAt),
215
265
  startedAt: iso(row.createdAt),
216
266
  expiresAt: iso(row.expiresAt),
217
- projectId: "test-project",
267
+ projectId: row.options.projectId ?? "test-project",
218
268
  status: end?.status ?? "RUNNING",
219
269
  proxyBytes: 0,
220
270
  keepAlive: row.keepAlive,
221
- region: "us-west-2",
271
+ region: row.options.region ?? "us-west-2",
222
272
  ...end === void 0 ? {} : { endedAt: iso(end.at) },
223
273
  ...row.context === void 0 ? {} : { contextId: row.context.id },
224
274
  ...row.userMetadata === void 0 ? {} : { userMetadata: row.userMetadata },
@@ -263,15 +313,17 @@ const serve = (fake, request, url) => {
263
313
  const reply = fake.pending.creates.shift() ?? { _tag: "Accept" };
264
314
  if (reply._tag === "Reject") return answer(reply.status, { message: "refused, as the script said" }, reply.retryAfterSeconds === void 0 ? {} : { "retry-after": `${reply.retryAfterSeconds}` });
265
315
  const body = Option.getOrUndefined(Schema.decodeOption(CreateBody)(bodyText(request)));
266
- const loaded = body?.browserSettings?.context;
316
+ if (body === void 0) return answer(400, { message: "Invalid session settings" });
317
+ const loaded = body.browserSettings?.context;
267
318
  const createdAt = fake.now();
268
319
  const created = {
269
320
  id: nextId(fake),
321
+ options: body,
270
322
  createdAt,
271
323
  listedAt: createdAt + (reply._tag === "Lost" ? reply.listedAfter ?? 0 : 0),
272
- expiresAt: createdAt + (body?.timeout ?? 300) * 1e3,
273
- keepAlive: body?.keepAlive ?? false,
274
- userMetadata: body?.userMetadata,
324
+ expiresAt: createdAt + (body.timeout ?? 300) * 1e3,
325
+ keepAlive: body.keepAlive ?? false,
326
+ userMetadata: body.userMetadata,
275
327
  context: loaded === void 0 ? void 0 : {
276
328
  id: loaded.id,
277
329
  persist: loaded.persist ?? false
@@ -296,6 +348,11 @@ const serve = (fake, request, url) => {
296
348
  if (reply._tag === "Lost") return lost;
297
349
  return row === void 0 ? answer(404, { message: "Session not found" }) : answer(200, describe(fake, row, true));
298
350
  }
351
+ case "GET sessions id debug": return row === void 0 ? answer(404, { message: "Session not found" }) : answer(200, {
352
+ debuggerFullscreenUrl: `https://live.browserbase.test/${row.id}/fullscreen`,
353
+ debuggerUrl: `https://live.browserbase.test/${row.id}`,
354
+ pages: []
355
+ });
299
356
  case "POST sessions id": {
300
357
  if (row === void 0) return answer(404, { message: "Session not found" });
301
358
  if (Option.isNone(Schema.decodeOption(ReleaseBody)(bodyText(request)))) return answer(400, { message: "status must be REQUEST_RELEASE" });
@@ -352,6 +409,7 @@ const make = Effect.fnUntraced(function* (script = {}) {
352
409
  requests: Effect.sync(() => [...fake.requests]),
353
410
  sessions: Effect.sync(() => [...fake.sessions.values()].map((row) => ({
354
411
  id: row.id,
412
+ options: row.options,
355
413
  status: endOf(fake, row)?.status ?? "RUNNING",
356
414
  userMetadata: row.userMetadata,
357
415
  releases: row.releases,
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["clientLayer"],"sources":["../../src/testing/BrowserbaseContract.ts","../../src/testing/TestBrowserbase.ts"],"sourcesContent":["/**\n * What this package relies on the Browserbase API to do, as named checks over\n * `BrowserbaseClient`, each an Effect with no test runner around it. A package test runs them\n * against `TestBrowserbase`, which keeps the fake to them; run against Browserbase itself, they\n * show whether the fake is still faithful, at the cost of a few one-minute sessions. Checks that\n * move the test clock run only against the fake.\n *\n * @category testing\n * @since 0.3.0\n */\nimport { Clock, Effect, Schedule, Schema } from \"effect\";\nimport { TestClock } from \"effect/testing\";\n\nimport { BrowserbaseClient, type Session } from \"../BrowserbaseClient.ts\";\nimport type { BrowserbaseError } from \"../BrowserbaseError.ts\";\n\n/** A check found the API behaving otherwise. */\nexport class Broken extends Schema.TaggedError<Broken>()(\"Broken\", {\n check: Schema.String,\n detail: Schema.String,\n}) {\n override get message() {\n return `${this.check}: ${this.detail}`;\n }\n}\n\nexport interface Check {\n readonly name: string;\n /** It moves the test clock, so only the fake can run it. */\n readonly clock: boolean;\n readonly run: Effect.Effect<void, Broken | BrowserbaseError, BrowserbaseClient>;\n}\n\nconst check = (\n name: string,\n run: (\n expect: (holds: boolean, detail: string) => Effect.Effect<void, Broken>,\n ) => Effect.Effect<void, Broken | BrowserbaseError, BrowserbaseClient>,\n clock = false,\n): Check => ({\n name,\n clock,\n run: run((holds, detail) =>\n holds ? Effect.void : Effect.fail(new Broken({ check: name, detail })),\n ).pipe(Effect.withSpan(\"BrowserbaseContract.check\", { attributes: { check: name } })),\n});\n\nconst running = (session: Session) => session.status === \"PENDING\" || session.status === \"RUNNING\";\n\n/** What the call failed with, or `nothing` when it succeeded. */\nconst failure = <A, R>(call: Effect.Effect<A, BrowserbaseError, R>) =>\n Effect.match(call, { onFailure: (error) => error.reason._tag, onSuccess: () => \"nothing\" });\n\n/** Read the session until it has ended, for at most 10 seconds. */\nconst ended = (client: BrowserbaseClient[\"Service\"], id: string) =>\n client.getSession(id).pipe(\n Effect.repeat({\n until: (session) => !running(session),\n schedule: Schedule.spaced(\"1 second\").pipe(Schedule.upTo({ times: 10 })),\n }),\n );\n\nexport const checks: ReadonlyArray<Check> = [\n check(\"a new session runs, and ends once released\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const session = yield* client.createSession({ timeout: 60 });\n\n yield* expect(running(session), `a new session is ${session.status}`);\n yield* client.releaseSession(session.id);\n const after = yield* ended(client, session.id);\n\n yield* expect(after.status === \"COMPLETED\", `a released session is ${after.status}`);\n // Releasing a session that has ended succeeds and changes nothing.\n yield* client.releaseSession(session.id);\n const again = yield* client.getSession(session.id);\n\n yield* expect(again.status === \"COMPLETED\", `released twice, it is ${again.status}`);\n }),\n ),\n check(\"a well-formed id no session has is not found\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getSession(\"00000000-0000-4000-8000-000000000000\"));\n\n yield* expect(read === \"NotFound\", `reading it failed with ${read}`);\n }),\n ),\n check(\"a session id of another shape is refused as invalid\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getSession(\"not-a-session-of-this-project\"));\n\n yield* expect(read === \"InvalidRequest\", `reading it failed with ${read}`);\n }),\n ),\n check(\"sessions are found by status and user metadata\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const label = `contract-${yield* Clock.currentTimeMillis}`;\n const session = yield* client.createSession({ timeout: 60, userMetadata: { label } });\n const query = (value: string) => `user_metadata['label']:'${value}'`;\n\n // A new session is PENDING or RUNNING, and may change between the two queries, so the\n // package looks under both statuses; a check of one alone would fail a compliant API.\n const running = (value: string) =>\n Effect.map(\n Effect.forEach([\"PENDING\", \"RUNNING\"], (status) =>\n client.listSessions({ status, query: query(value) }),\n ),\n (found) => new Set(found.flat().map(({ id }) => id)),\n );\n\n const found = yield* running(label);\n const other = yield* running(`${label}-x`);\n\n yield* client.releaseSession(session.id);\n yield* ended(client, session.id);\n const gone = yield* running(label);\n // With no status, a search finds sessions in every state, as a lost create's search needs.\n const any = yield* client.listSessions({ query: query(label) });\n\n yield* expect(\n found.size === 1 && found.has(session.id),\n `${found.size} unended sessions had its label`,\n );\n yield* expect(other.size === 0, `${other.size} sessions had another label`);\n yield* expect(gone.size === 0, `${gone.size} still ran once it had ended`);\n yield* expect(\n any.some(({ id, status }) => id === session.id && status === \"COMPLETED\"),\n `with no status, a search found ${any.length} sessions, not it ended`,\n );\n }),\n ),\n check(\"a context id of another shape is refused as invalid\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getContext(\"not-a-context-of-this-project\"));\n\n yield* expect(read === \"InvalidRequest\", `reading it failed with ${read}`);\n }),\n ),\n check(\"a stored context reads back, and is gone once deleted\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const created = yield* client.createContext();\n const read = yield* client.getContext(created.id);\n\n yield* client.deleteContext(created.id);\n const gone = yield* failure(client.getContext(created.id));\n\n yield* expect(read.id === created.id, `it read back as ${read.id}`);\n yield* expect(gone === \"NotFound\", `once deleted, reading it failed with ${gone}`);\n }),\n ),\n check(\n \"a session ends at its timeout\",\n (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const session = yield* client.createSession({ timeout: 60 });\n\n yield* TestClock.adjust(\"61 seconds\");\n const after = yield* client.getSession(session.id);\n\n yield* expect(after.status === \"TIMED_OUT\", `after its timeout it is ${after.status}`);\n }),\n true,\n ),\n];\n","/**\n * An in-memory Browserbase API for tests. It is the `HttpClient` the real `BrowserbaseClient` runs\n * on, so the client and the provider above it run as they would against Browserbase, and nothing\n * leaves the process. Sessions run until released, or until their timeout on the Effect `Clock`,\n * so `TestClock` drives their end. A `Script` decides how creates, releases and session reads\n * answer, in turn, so a test can lose a create, leave a release pending or fail status reads.\n *\n * It keeps sessions and stored contexts and nothing else: any other endpoint fails the test with a\n * defect. It doesn't model regions, quotas or billing, and a context is only a record, with no\n * browser state. Every session hands out the script's `connectUrl`, such as the DevTools address\n * of a local Chromium. Its ids are UUIDs, as Browserbase's are, and as Browserbase does, it refuses\n * a session or context id of any other shape as invalid, where it answers an unknown one as not\n * found.\n *\n * @category testing\n * @since 0.3.0\n */\nimport {\n Clock,\n Context,\n DateTime,\n Effect,\n Layer,\n Option,\n Predicate,\n Redacted,\n Schema,\n} from \"effect\";\nimport {\n HttpClient,\n HttpClientError,\n type HttpClientRequest,\n HttpClientResponse,\n} from \"effect/http\";\n\nimport { type BrowserbaseClient, layer as clientLayer } from \"../BrowserbaseClient.ts\";\n\nconst Refusal = Schema.Int.check(Schema.isBetween({ minimum: 400, maximum: 599 }));\n\n/** How a session create answers. */\nexport const Create = Schema.Union([\n /** The session is made, and runs. */\n Schema.TaggedStruct(\"Accept\", {}),\n /** Refused with `status`, and nothing is made. */\n Schema.TaggedStruct(\"Reject\", {\n status: Refusal,\n retryAfterSeconds: Schema.optional(Schema.Int),\n }),\n /** The session is made, but the answer holds only its id. */\n Schema.TaggedStruct(\"Malformed\", {}),\n /**\n * The session is made, but no answer arrives, and Browserbase lists it only `listedAfter`\n * milliseconds later, as a search can trail what was made.\n */\n Schema.TaggedStruct(\"Lost\", { listedAfter: Schema.optional(Schema.Finite) }),\n]);\n\n/** How a release answers. */\nexport const Release = Schema.Union([\n /** The session ends at once. */\n Schema.TaggedStruct(\"Ends\", {}),\n /** Accepted, but the session runs on for `millis`, or without them until its timeout. */\n Schema.TaggedStruct(\"Pending\", { millis: Schema.optional(Schema.Finite) }),\n /** Refused with `status`; the session runs on. */\n Schema.TaggedStruct(\"Refused\", { status: Refusal }),\n /** The session ends, but no answer arrives. */\n Schema.TaggedStruct(\"Lost\", {}),\n]);\n\n/** How a session read answers. */\nexport const Read = Schema.Union([\n Schema.TaggedStruct(\"Answer\", {}),\n Schema.TaggedStruct(\"Refused\", { status: Refusal }),\n Schema.TaggedStruct(\"Lost\", {}),\n]);\n\nexport const Script = Schema.Struct({\n /** How creates answer, in turn; once these run out, each is accepted. */\n creates: Schema.optional(Schema.Array(Create)),\n /** How releases answer, in turn; once these run out, each ends its session. */\n releases: Schema.optional(Schema.Array(Release)),\n /** How session reads answer, in turn; once these run out, each answers. */\n reads: Schema.optional(Schema.Array(Read)),\n /** The DevTools address each session hands out. Without it, sessions have none. */\n connectUrl: Schema.optional(Schema.String),\n});\n\nexport type Script = typeof Script.Type;\n\n/** A session as the fake keeps it, with how many releases it was asked. */\nexport interface Kept {\n readonly id: string;\n readonly status: string;\n readonly userMetadata: Readonly<Record<string, unknown>> | undefined;\n readonly releases: number;\n /** Whether it was created to outlive its connections, until released or timed out. */\n readonly keepAlive: boolean;\n /** The stored context it loaded, and whether it saves to it as it ends. */\n readonly context: { readonly id: string; readonly persist: boolean } | undefined;\n}\n\nexport interface Handle {\n /** Every request, oldest first, as `METHOD /path?query`. */\n readonly requests: Effect.Effect<ReadonlyArray<string>>;\n /** Each session as Browserbase would describe it now, oldest first. */\n readonly sessions: Effect.Effect<ReadonlyArray<Kept>>;\n /** End a session as Browserbase does on its own, as when its browser fails. */\n readonly end: (id: string) => Effect.Effect<void>;\n}\n\n/** The fake's handle, beside the `BrowserbaseClient` that `layer` runs on it. */\nexport class TestBrowserbase extends Context.Service<TestBrowserbase, Handle>()(\n \"effect-browserbase/testing/TestBrowserbase\",\n) {}\n\n/** The API key the fake takes; any other is unauthorized. */\nexport const apiKey = \"test-browserbase-key\";\n\nconst CreateBody = Schema.fromJsonString(\n Schema.Struct({\n timeout: Schema.optional(Schema.Finite),\n keepAlive: Schema.optional(Schema.Boolean),\n userMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),\n browserSettings: Schema.optional(\n Schema.Struct({\n context: Schema.optional(\n Schema.Struct({ id: Schema.String, persist: Schema.optional(Schema.Boolean) }),\n ),\n }),\n ),\n }),\n);\n\nconst ReleaseBody = Schema.fromJsonString(\n Schema.Struct({ status: Schema.Literal(\"REQUEST_RELEASE\") }),\n);\n\ninterface Row {\n readonly id: string;\n readonly createdAt: number;\n /** When a list first shows it. */\n readonly listedAt: number;\n readonly expiresAt: number;\n readonly keepAlive: boolean;\n readonly userMetadata: Readonly<Record<string, unknown>> | undefined;\n readonly context: { readonly id: string; readonly persist: boolean } | undefined;\n /** How and when it ends, unless its timeout comes first. */\n ends: { readonly status: string; readonly at: number } | undefined;\n releases: number;\n}\n\n/** Everything one fake keeps. */\ninterface Fake {\n readonly now: () => number;\n readonly connectUrl: string | undefined;\n readonly pending: {\n readonly creates: Array<typeof Create.Type>;\n readonly releases: Array<typeof Release.Type>;\n readonly reads: Array<typeof Read.Type>;\n };\n readonly requests: Array<string>;\n readonly sessions: Map<string, Row>;\n readonly contexts: Map<string, number>;\n /** How many sessions and contexts it has made, together, for their ids. */\n readonly made: { count: number };\n}\n\nconst iso = (millis: number) => DateTime.formatIso(DateTime.makeUnsafe(millis));\n\nconst uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** The next id, a UUID as Browserbase's are, counted so that every run hands out the same. */\nconst nextId = (fake: Fake) =>\n `00000000-0000-4000-8000-${(++fake.made.count).toString(16).padStart(12, \"0\")}`;\n\n/** How the session ended by now, or undefined while it runs. */\nconst endOf = (fake: Fake, row: Row) => {\n const end =\n row.ends !== undefined && row.ends.at <= row.expiresAt\n ? row.ends\n : { status: \"TIMED_OUT\", at: row.expiresAt };\n\n return end.at <= fake.now() ? end : undefined;\n};\n\n/** The session as Browserbase answers it; only creates and reads hand out its address. */\nconst describe = (fake: Fake, row: Row, connect: boolean) => {\n const end = endOf(fake, row);\n\n return {\n id: row.id,\n createdAt: iso(row.createdAt),\n updatedAt: iso(end?.at ?? row.createdAt),\n startedAt: iso(row.createdAt),\n expiresAt: iso(row.expiresAt),\n projectId: \"test-project\",\n status: end?.status ?? \"RUNNING\",\n proxyBytes: 0,\n keepAlive: row.keepAlive,\n region: \"us-west-2\",\n ...(end === undefined ? {} : { endedAt: iso(end.at) }),\n ...(row.context === undefined ? {} : { contextId: row.context.id }),\n ...(row.userMetadata === undefined ? {} : { userMetadata: row.userMetadata }),\n ...(connect && fake.connectUrl !== undefined ? { connectUrl: fake.connectUrl } : {}),\n ...(connect\n ? {\n seleniumRemoteUrl: `https://connect.browserbase.test/webdriver/${row.id}`,\n signingKey: `signing-key-${row.id}`,\n }\n : {}),\n };\n};\n\nconst bodyText = (request: HttpClientRequest.HttpClientRequest) =>\n request.body._tag === \"Uint8Array\"\n ? (request.body.text ?? new TextDecoder().decode(request.body.body))\n : \"\";\n\n/** A metadata query in Browserbase's one form, `user_metadata['a']['b']:'value'`, as a test. */\nconst metadataQuery = (query: string) => {\n const parsed = /^user_metadata((?:\\['[^']*'\\])+):'([^']*)'$/.exec(query);\n\n if (parsed === null) return undefined;\n const path = [...(parsed[1] ?? \"\").matchAll(/\\['([^']*)'\\]/g)].map((part) => part[1] ?? \"\");\n\n return (metadata: unknown) =>\n path.reduce<unknown>(\n (value, key) => (Predicate.isObject(value) ? Reflect.get(value, key) : undefined),\n metadata,\n ) === parsed[2];\n};\n\nconst unsupported = (detail: string) => Effect.die(new Error(`TestBrowserbase ${detail}`));\n\n/** Answer one request as Browserbase would, after the script's say. */\nconst serve = (fake: Fake, request: HttpClientRequest.HttpClientRequest, url: URL) => {\n const answer = (status: number, body?: unknown, headers: Record<string, string> = {}) =>\n Effect.succeed(\n HttpClientResponse.fromWeb(\n request,\n new Response(body === undefined ? null : JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\", ...headers },\n }),\n ),\n );\n\n const lost = Effect.fail(\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n description: \"TestBrowserbase lost this answer, as its script said\",\n }),\n }),\n );\n\n const refused = (status: number) => answer(status, { message: \"refused, as the script said\" });\n const [, version, kind, id, rest] = url.pathname.split(\"/\");\n const row = id === undefined ? undefined : fake.sessions.get(id);\n\n fake.requests.push(`${request.method} ${url.pathname}${url.search}`);\n if (request.headers[\"x-bb-api-key\"] !== apiKey)\n return answer(401, { message: \"Invalid API key\" });\n if (version !== \"v1\") return unsupported(`serves only /v1, not ${url.pathname}`);\n // Browserbase refuses a session or context id that is not a UUID before it looks for it.\n if ((kind === \"sessions\" || kind === \"contexts\") && id !== undefined && !uuid.test(id))\n return answer(400, { message: `Invalid ${kind === \"sessions\" ? \"Session\" : \"Context\"} ID` });\n\n switch (\n `${request.method} ${kind}${id === undefined ? \"\" : \" id\"}${rest === undefined ? \"\" : ` ${rest}`}`\n ) {\n case \"POST sessions\": {\n const reply = fake.pending.creates.shift() ?? { _tag: \"Accept\" };\n\n if (reply._tag === \"Reject\")\n return answer(\n reply.status,\n { message: \"refused, as the script said\" },\n reply.retryAfterSeconds === undefined\n ? {}\n : { \"retry-after\": `${reply.retryAfterSeconds}` },\n );\n const body = Option.getOrUndefined(Schema.decodeOption(CreateBody)(bodyText(request)));\n const loaded = body?.browserSettings?.context;\n const createdAt = fake.now();\n\n const created: Row = {\n id: nextId(fake),\n createdAt,\n listedAt: createdAt + (reply._tag === \"Lost\" ? (reply.listedAfter ?? 0) : 0),\n expiresAt: createdAt + (body?.timeout ?? 300) * 1000,\n keepAlive: body?.keepAlive ?? false,\n userMetadata: body?.userMetadata,\n context:\n loaded === undefined ? undefined : { id: loaded.id, persist: loaded.persist ?? false },\n ends: undefined,\n releases: 0,\n };\n\n fake.sessions.set(created.id, created);\n if (reply._tag === \"Lost\") return lost;\n\n return answer(\n 201,\n reply._tag === \"Malformed\" ? { id: created.id } : describe(fake, created, true),\n );\n }\n case \"GET sessions\": {\n const status = url.searchParams.get(\"status\");\n const query = url.searchParams.get(\"q\");\n const matches = query === null ? () => true : metadataQuery(query);\n\n if (matches === undefined) return unsupported(`cannot read the query ${query}`);\n\n return answer(\n 200,\n [...fake.sessions.values()]\n .filter(\n (kept) =>\n kept.listedAt <= fake.now() &&\n (status === null || (endOf(fake, kept)?.status ?? \"RUNNING\") === status) &&\n matches(kept.userMetadata),\n )\n .map((kept) => describe(fake, kept, false)),\n );\n }\n case \"GET sessions id\": {\n const reply = fake.pending.reads.shift() ?? { _tag: \"Answer\" };\n\n if (reply._tag === \"Refused\") return refused(reply.status);\n if (reply._tag === \"Lost\") return lost;\n\n return row === undefined\n ? answer(404, { message: \"Session not found\" })\n : answer(200, describe(fake, row, true));\n }\n case \"POST sessions id\": {\n if (row === undefined) return answer(404, { message: \"Session not found\" });\n if (Option.isNone(Schema.decodeOption(ReleaseBody)(bodyText(request))))\n return answer(400, { message: \"status must be REQUEST_RELEASE\" });\n row.releases += 1;\n const reply = fake.pending.releases.shift() ?? { _tag: \"Ends\" };\n\n if (reply._tag === \"Refused\") return refused(reply.status);\n // Releasing a session that has ended changes nothing, and succeeds.\n if (\n endOf(fake, row) === undefined &&\n (reply._tag !== \"Pending\" || reply.millis !== undefined)\n )\n row.ends = {\n status: \"COMPLETED\",\n at: fake.now() + (reply._tag === \"Pending\" ? (reply.millis ?? 0) : 0),\n };\n\n return reply._tag === \"Lost\" ? lost : answer(200, describe(fake, row, false));\n }\n case \"POST contexts\": {\n const created = nextId(fake);\n\n fake.contexts.set(created, fake.now());\n\n return answer(201, {\n id: created,\n publicKey: \"test-public-key\",\n cipherAlgorithm: \"AES-256-CBC\",\n initializationVectorSize: 16,\n });\n }\n case \"GET contexts id\": {\n const createdAt = id === undefined ? undefined : fake.contexts.get(id);\n\n return createdAt === undefined\n ? answer(404, { message: \"Context not found\" })\n : answer(200, {\n id,\n createdAt: iso(createdAt),\n updatedAt: iso(createdAt),\n projectId: \"test-project\",\n });\n }\n case \"DELETE contexts id\":\n return id !== undefined && fake.contexts.delete(id)\n ? answer(204)\n : answer(404, { message: \"Context not found\" });\n default:\n return unsupported(`does not implement ${request.method} ${url.pathname}`);\n }\n};\n\n/** The fake as an `HttpClient`, with its handle. */\nexport const make = Effect.fnUntraced(function* (script: Script = {}) {\n const clock = yield* Clock.Clock;\n const decoded = yield* Effect.orDie(Schema.decodeEffect(Script)(script));\n\n const fake: Fake = {\n now: () => clock.currentTimeMillisUnsafe(),\n connectUrl: decoded.connectUrl,\n pending: {\n creates: [...(decoded.creates ?? [])],\n releases: [...(decoded.releases ?? [])],\n reads: [...(decoded.reads ?? [])],\n },\n requests: [],\n sessions: new Map(),\n contexts: new Map(),\n made: { count: 0 },\n };\n\n const handle: Handle = {\n requests: Effect.sync(() => [...fake.requests]),\n sessions: Effect.sync(() =>\n [...fake.sessions.values()].map((row) => ({\n id: row.id,\n status: endOf(fake, row)?.status ?? \"RUNNING\",\n userMetadata: row.userMetadata,\n releases: row.releases,\n keepAlive: row.keepAlive,\n context: row.context,\n })),\n ),\n end: (id) =>\n Effect.sync(() => {\n const row = fake.sessions.get(id);\n\n if (row !== undefined && endOf(fake, row) === undefined)\n row.ends = { status: \"ERROR\", at: fake.now() };\n }),\n };\n\n return { http: HttpClient.make((request, url) => serve(fake, request, url)), handle };\n});\n\n/** The real `BrowserbaseClient` over the fake, and the fake's handle. */\nexport const layer = (script: Script = {}): Layer.Layer<BrowserbaseClient | TestBrowserbase> =>\n Layer.unwrap(\n Effect.map(make(script), ({ http, handle }) =>\n Layer.merge(\n clientLayer({\n apiKey: Redacted.make(apiKey),\n baseUrl: \"https://api.browserbase.test\",\n }).pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, http))),\n Layer.succeed(TestBrowserbase, handle),\n ),\n ),\n );\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["clientLayer"],"sources":["../../src/testing/BrowserbaseContract.ts","../../src/testing/TestBrowserbase.ts"],"sourcesContent":["/**\n * What this package relies on the Browserbase API to do, as named checks over\n * `BrowserbaseClient`, each an Effect with no test runner around it. A package test runs them\n * against `TestBrowserbase`, which keeps the fake to them; run against Browserbase itself, they\n * show whether the fake is still faithful, at the cost of a few one-minute sessions. Checks that\n * move the test clock run only against the fake.\n *\n * @category testing\n * @since 0.3.0\n */\nimport { Clock, Effect, Schedule, Schema } from \"effect\";\nimport { TestClock } from \"effect/testing\";\n\nimport { BrowserbaseClient, type Session } from \"../BrowserbaseClient.ts\";\nimport type { BrowserbaseError } from \"../BrowserbaseError.ts\";\n\n/** A check found the API behaving otherwise. */\nexport class Broken extends Schema.TaggedError<Broken>()(\"Broken\", {\n check: Schema.String,\n detail: Schema.String,\n}) {\n override get message() {\n return `${this.check}: ${this.detail}`;\n }\n}\n\nexport interface Check {\n readonly name: string;\n /** It moves the test clock, so only the fake can run it. */\n readonly clock: boolean;\n readonly run: Effect.Effect<void, Broken | BrowserbaseError, BrowserbaseClient>;\n}\n\nconst check = (\n name: string,\n run: (\n expect: (holds: boolean, detail: string) => Effect.Effect<void, Broken>,\n ) => Effect.Effect<void, Broken | BrowserbaseError, BrowserbaseClient>,\n clock = false,\n): Check => ({\n name,\n clock,\n run: run((holds, detail) =>\n holds ? Effect.void : Effect.fail(new Broken({ check: name, detail })),\n ).pipe(Effect.withSpan(\"BrowserbaseContract.check\", { attributes: { check: name } })),\n});\n\nconst running = (session: Session) => session.status === \"PENDING\" || session.status === \"RUNNING\";\n\n/** What the call failed with, or `nothing` when it succeeded. */\nconst failure = <A, R>(call: Effect.Effect<A, BrowserbaseError, R>) =>\n Effect.match(call, { onFailure: (error) => error.reason._tag, onSuccess: () => \"nothing\" });\n\n/** Read the session until it has ended, for at most 10 seconds. */\nconst ended = (client: BrowserbaseClient[\"Service\"], id: string) =>\n client.getSession(id).pipe(\n Effect.repeat({\n until: (session) => !running(session),\n schedule: Schedule.spaced(\"1 second\").pipe(Schedule.upTo({ times: 10 })),\n }),\n );\n\nexport const checks: ReadonlyArray<Check> = [\n check(\"a new session runs, and ends once released\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const session = yield* client.createSession({ timeout: 60 });\n\n yield* expect(running(session), `a new session is ${session.status}`);\n yield* client.releaseSession(session.id);\n const after = yield* ended(client, session.id);\n\n yield* expect(after.status === \"COMPLETED\", `a released session is ${after.status}`);\n // Releasing a session that has ended succeeds and changes nothing.\n yield* client.releaseSession(session.id);\n const again = yield* client.getSession(session.id);\n\n yield* expect(again.status === \"COMPLETED\", `released twice, it is ${again.status}`);\n }),\n ),\n check(\"a well-formed id no session has is not found\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getSession(\"00000000-0000-4000-8000-000000000000\"));\n\n yield* expect(read === \"NotFound\", `reading it failed with ${read}`);\n }),\n ),\n check(\"a session id of another shape is refused as invalid\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getSession(\"not-a-session-of-this-project\"));\n\n yield* expect(read === \"InvalidRequest\", `reading it failed with ${read}`);\n }),\n ),\n check(\"sessions are found by status and user metadata\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const label = `contract-${yield* Clock.currentTimeMillis}`;\n const session = yield* client.createSession({ timeout: 60, userMetadata: { label } });\n const query = (value: string) => `user_metadata['label']:'${value}'`;\n\n // A new session is PENDING or RUNNING, and may change between the two queries, so the\n // package looks under both statuses; a check of one alone would fail a compliant API.\n const running = (value: string) =>\n Effect.map(\n Effect.forEach([\"PENDING\", \"RUNNING\"], (status) =>\n client.listSessions({ status, query: query(value) }),\n ),\n (found) => new Set(found.flat().map(({ id }) => id)),\n );\n\n const found = yield* running(label);\n const other = yield* running(`${label}-x`);\n\n yield* client.releaseSession(session.id);\n yield* ended(client, session.id);\n const gone = yield* running(label);\n // With no status, a search finds sessions in every state, as a lost create's search needs.\n const any = yield* client.listSessions({ query: query(label) });\n\n yield* expect(\n found.size === 1 && found.has(session.id),\n `${found.size} unended sessions had its label`,\n );\n yield* expect(other.size === 0, `${other.size} sessions had another label`);\n yield* expect(gone.size === 0, `${gone.size} still ran once it had ended`);\n yield* expect(\n any.some(({ id, status }) => id === session.id && status === \"COMPLETED\"),\n `with no status, a search found ${any.length} sessions, not it ended`,\n );\n }),\n ),\n check(\"a context id of another shape is refused as invalid\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const read = yield* failure(client.getContext(\"not-a-context-of-this-project\"));\n\n yield* expect(read === \"InvalidRequest\", `reading it failed with ${read}`);\n }),\n ),\n check(\"a stored context reads back, and is gone once deleted\", (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const created = yield* client.createContext();\n const read = yield* client.getContext(created.id);\n\n yield* client.deleteContext(created.id);\n const gone = yield* failure(client.getContext(created.id));\n\n yield* expect(read.id === created.id, `it read back as ${read.id}`);\n yield* expect(gone === \"NotFound\", `once deleted, reading it failed with ${gone}`);\n }),\n ),\n check(\n \"a session ends at its timeout\",\n (expect) =>\n Effect.gen(function* () {\n const client = yield* BrowserbaseClient;\n const session = yield* client.createSession({ timeout: 60 });\n\n yield* TestClock.adjust(\"61 seconds\");\n const after = yield* client.getSession(session.id);\n\n yield* expect(after.status === \"TIMED_OUT\", `after its timeout it is ${after.status}`);\n }),\n true,\n ),\n];\n","/**\n * An in-memory Browserbase API for tests. It is the `HttpClient` the real `BrowserbaseClient` runs\n * on, so the client and the provider above it run as they would against Browserbase, and nothing\n * leaves the process. Sessions run until released, or until their timeout on the Effect `Clock`,\n * so `TestClock` drives their end. A `Script` decides how creates, releases and session reads\n * answer, in turn, so a test can lose a create, leave a release pending or fail status reads.\n *\n * It keeps sessions and stored contexts, and supplies synthetic Live View links: any other endpoint\n * fails the test with a defect. It doesn't model quotas or billing, and a context is only a record,\n * with no browser state. Every session hands out the script's `connectUrl`, such as the DevTools address\n * of a local Chromium. Its ids are UUIDs, as Browserbase's are, and as Browserbase does, it refuses\n * a session or context id of any other shape as invalid, where it answers an unknown one as not\n * found.\n *\n * @category testing\n * @since 0.3.0\n */\nimport {\n Clock,\n Context,\n DateTime,\n Effect,\n Layer,\n Option,\n Predicate,\n Redacted,\n Schema,\n} from \"effect\";\nimport {\n HttpClient,\n HttpClientError,\n type HttpClientRequest,\n HttpClientResponse,\n} from \"effect/http\";\n\nimport {\n type BrowserbaseClient,\n layer as clientLayer,\n type SessionOptions,\n} from \"../BrowserbaseClient.ts\";\n\nconst Refusal = Schema.Int.check(Schema.isBetween({ minimum: 400, maximum: 599 }));\n\n/** How a session create answers. */\nexport const Create = Schema.Union([\n /** The session is made, and runs. */\n Schema.TaggedStruct(\"Accept\", {}),\n /** Refused with `status`, and nothing is made. */\n Schema.TaggedStruct(\"Reject\", {\n status: Refusal,\n retryAfterSeconds: Schema.optional(Schema.Int),\n }),\n /** The session is made, but the answer holds only its id. */\n Schema.TaggedStruct(\"Malformed\", {}),\n /**\n * The session is made, but no answer arrives, and Browserbase lists it only `listedAfter`\n * milliseconds later, as a search can trail what was made.\n */\n Schema.TaggedStruct(\"Lost\", { listedAfter: Schema.optional(Schema.Finite) }),\n]);\n\n/** How a release answers. */\nexport const Release = Schema.Union([\n /** The session ends at once. */\n Schema.TaggedStruct(\"Ends\", {}),\n /** Accepted, but the session runs on for `millis`, or without them until its timeout. */\n Schema.TaggedStruct(\"Pending\", { millis: Schema.optional(Schema.Finite) }),\n /** Refused with `status`; the session runs on. */\n Schema.TaggedStruct(\"Refused\", { status: Refusal }),\n /** The session ends, but no answer arrives. */\n Schema.TaggedStruct(\"Lost\", {}),\n]);\n\n/** How a session read answers. */\nexport const Read = Schema.Union([\n Schema.TaggedStruct(\"Answer\", {}),\n Schema.TaggedStruct(\"Refused\", { status: Refusal }),\n Schema.TaggedStruct(\"Lost\", {}),\n]);\n\nexport const Script = Schema.Struct({\n /** How creates answer, in turn; once these run out, each is accepted. */\n creates: Schema.optional(Schema.Array(Create)),\n /** How releases answer, in turn; once these run out, each ends its session. */\n releases: Schema.optional(Schema.Array(Release)),\n /** How session reads answer, in turn; once these run out, each answers. */\n reads: Schema.optional(Schema.Array(Read)),\n /** The DevTools address each session hands out. Without it, sessions have none. */\n connectUrl: Schema.optional(Schema.String),\n});\n\nexport type Script = typeof Script.Type;\n\n/** A session as the fake keeps it, with how many releases it was asked. */\nexport interface Kept {\n readonly id: string;\n readonly status: string;\n /** The create options as sent, without provider defaults. Only the test handle exposes them. */\n readonly options: SessionOptions;\n readonly userMetadata: Readonly<Record<string, unknown>> | undefined;\n readonly releases: number;\n /** Whether it was created to outlive its connections, until released or timed out. */\n readonly keepAlive: boolean;\n /** The stored context it loaded, and whether it saves to it as it ends. */\n readonly context: { readonly id: string; readonly persist: boolean } | undefined;\n}\n\nexport interface Handle {\n /** Every request, oldest first, as `METHOD /path?query`. */\n readonly requests: Effect.Effect<ReadonlyArray<string>>;\n /** Each session as Browserbase would describe it now, oldest first. */\n readonly sessions: Effect.Effect<ReadonlyArray<Kept>>;\n /** End a session as Browserbase does on its own, as when its browser fails. */\n readonly end: (id: string) => Effect.Effect<void>;\n}\n\n/** The fake's handle, beside the `BrowserbaseClient` that `layer` runs on it. */\nexport class TestBrowserbase extends Context.Service<TestBrowserbase, Handle>()(\n \"effect-browserbase/testing/TestBrowserbase\",\n) {}\n\n/** The API key the fake takes; any other is unauthorized. */\nexport const apiKey = \"test-browserbase-key\";\n\nconst CreateBody = Schema.fromJsonString(\n Schema.Struct({\n projectId: Schema.optional(Schema.String),\n extensionId: Schema.optional(Schema.String),\n timeout: Schema.optional(Schema.Finite),\n keepAlive: Schema.optional(Schema.Boolean),\n region: Schema.optional(\n Schema.Literals([\"us-west-2\", \"us-east-1\", \"eu-central-1\", \"ap-southeast-1\"]),\n ),\n proxies: Schema.optional(\n Schema.Union([\n Schema.Boolean,\n Schema.Array(\n Schema.Union([\n Schema.Struct({\n type: Schema.Literal(\"browserbase\"),\n geolocation: Schema.optional(\n Schema.Struct({\n country: Schema.String,\n state: Schema.optional(Schema.String),\n city: Schema.optional(Schema.String),\n }),\n ),\n domainPattern: Schema.optional(Schema.String),\n }),\n Schema.Struct({\n type: Schema.Literal(\"external\"),\n server: Schema.String,\n username: Schema.optional(Schema.String),\n password: Schema.optional(Schema.String),\n domainPattern: Schema.optional(Schema.String),\n }),\n Schema.Struct({\n type: Schema.Literal(\"none\"),\n domainPattern: Schema.optional(Schema.String),\n }),\n ]),\n ),\n ]),\n ),\n userMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),\n browserSettings: Schema.optional(\n Schema.Struct({\n context: Schema.optional(\n Schema.Struct({ id: Schema.String, persist: Schema.optional(Schema.Boolean) }),\n ),\n viewport: Schema.optional(Schema.Struct({ width: Schema.Finite, height: Schema.Finite })),\n blockAds: Schema.optional(Schema.Boolean),\n solveCaptchas: Schema.optional(Schema.Boolean),\n recordSession: Schema.optional(Schema.Boolean),\n logSession: Schema.optional(Schema.Boolean),\n verified: Schema.optional(Schema.Boolean),\n os: Schema.optional(Schema.Literals([\"windows\", \"mac\", \"linux\", \"mobile\", \"tablet\"])),\n allowedDomains: Schema.optional(Schema.Array(Schema.String)),\n ignoreCertificateErrors: Schema.optional(Schema.Boolean),\n }),\n ),\n }),\n);\n\nconst ReleaseBody = Schema.fromJsonString(\n Schema.Struct({ status: Schema.Literal(\"REQUEST_RELEASE\") }),\n);\n\ninterface Row {\n readonly id: string;\n readonly options: SessionOptions;\n readonly createdAt: number;\n /** When a list first shows it. */\n readonly listedAt: number;\n readonly expiresAt: number;\n readonly keepAlive: boolean;\n readonly userMetadata: Readonly<Record<string, unknown>> | undefined;\n readonly context: { readonly id: string; readonly persist: boolean } | undefined;\n /** How and when it ends, unless its timeout comes first. */\n ends: { readonly status: string; readonly at: number } | undefined;\n releases: number;\n}\n\n/** Everything one fake keeps. */\ninterface Fake {\n readonly now: () => number;\n readonly connectUrl: string | undefined;\n readonly pending: {\n readonly creates: Array<typeof Create.Type>;\n readonly releases: Array<typeof Release.Type>;\n readonly reads: Array<typeof Read.Type>;\n };\n readonly requests: Array<string>;\n readonly sessions: Map<string, Row>;\n readonly contexts: Map<string, number>;\n /** How many sessions and contexts it has made, together, for their ids. */\n readonly made: { count: number };\n}\n\nconst iso = (millis: number) => DateTime.formatIso(DateTime.makeUnsafe(millis));\n\nconst uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** The next id, a UUID as Browserbase's are, counted so that every run hands out the same. */\nconst nextId = (fake: Fake) =>\n `00000000-0000-4000-8000-${(++fake.made.count).toString(16).padStart(12, \"0\")}`;\n\n/** How the session ended by now, or undefined while it runs. */\nconst endOf = (fake: Fake, row: Row) => {\n const end =\n row.ends !== undefined && row.ends.at <= row.expiresAt\n ? row.ends\n : { status: \"TIMED_OUT\", at: row.expiresAt };\n\n return end.at <= fake.now() ? end : undefined;\n};\n\n/** The session as Browserbase answers it; only creates and reads hand out its address. */\nconst describe = (fake: Fake, row: Row, connect: boolean) => {\n const end = endOf(fake, row);\n\n return {\n id: row.id,\n createdAt: iso(row.createdAt),\n updatedAt: iso(end?.at ?? row.createdAt),\n startedAt: iso(row.createdAt),\n expiresAt: iso(row.expiresAt),\n projectId: row.options.projectId ?? \"test-project\",\n status: end?.status ?? \"RUNNING\",\n proxyBytes: 0,\n keepAlive: row.keepAlive,\n region: row.options.region ?? \"us-west-2\",\n ...(end === undefined ? {} : { endedAt: iso(end.at) }),\n ...(row.context === undefined ? {} : { contextId: row.context.id }),\n ...(row.userMetadata === undefined ? {} : { userMetadata: row.userMetadata }),\n ...(connect && fake.connectUrl !== undefined ? { connectUrl: fake.connectUrl } : {}),\n ...(connect\n ? {\n seleniumRemoteUrl: `https://connect.browserbase.test/webdriver/${row.id}`,\n signingKey: `signing-key-${row.id}`,\n }\n : {}),\n };\n};\n\nconst bodyText = (request: HttpClientRequest.HttpClientRequest) =>\n request.body._tag === \"Uint8Array\"\n ? (request.body.text ?? new TextDecoder().decode(request.body.body))\n : \"\";\n\n/** A metadata query in Browserbase's one form, `user_metadata['a']['b']:'value'`, as a test. */\nconst metadataQuery = (query: string) => {\n const parsed = /^user_metadata((?:\\['[^']*'\\])+):'([^']*)'$/.exec(query);\n\n if (parsed === null) return undefined;\n const path = [...(parsed[1] ?? \"\").matchAll(/\\['([^']*)'\\]/g)].map((part) => part[1] ?? \"\");\n\n return (metadata: unknown) =>\n path.reduce<unknown>(\n (value, key) => (Predicate.isObject(value) ? Reflect.get(value, key) : undefined),\n metadata,\n ) === parsed[2];\n};\n\nconst unsupported = (detail: string) => Effect.die(new Error(`TestBrowserbase ${detail}`));\n\n/** Answer one request as Browserbase would, after the script's say. */\nconst serve = (fake: Fake, request: HttpClientRequest.HttpClientRequest, url: URL) => {\n const answer = (status: number, body?: unknown, headers: Record<string, string> = {}) =>\n Effect.succeed(\n HttpClientResponse.fromWeb(\n request,\n new Response(body === undefined ? null : JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\", ...headers },\n }),\n ),\n );\n\n const lost = Effect.fail(\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n description: \"TestBrowserbase lost this answer, as its script said\",\n }),\n }),\n );\n\n const refused = (status: number) => answer(status, { message: \"refused, as the script said\" });\n const [, version, kind, id, rest] = url.pathname.split(\"/\");\n const row = id === undefined ? undefined : fake.sessions.get(id);\n\n fake.requests.push(`${request.method} ${url.pathname}${url.search}`);\n if (request.headers[\"x-bb-api-key\"] !== apiKey)\n return answer(401, { message: \"Invalid API key\" });\n if (version !== \"v1\") return unsupported(`serves only /v1, not ${url.pathname}`);\n // Browserbase refuses a session or context id that is not a UUID before it looks for it.\n if ((kind === \"sessions\" || kind === \"contexts\") && id !== undefined && !uuid.test(id))\n return answer(400, { message: `Invalid ${kind === \"sessions\" ? \"Session\" : \"Context\"} ID` });\n\n switch (\n `${request.method} ${kind}${id === undefined ? \"\" : \" id\"}${rest === undefined ? \"\" : ` ${rest}`}`\n ) {\n case \"POST sessions\": {\n const reply = fake.pending.creates.shift() ?? { _tag: \"Accept\" };\n\n if (reply._tag === \"Reject\")\n return answer(\n reply.status,\n { message: \"refused, as the script said\" },\n reply.retryAfterSeconds === undefined\n ? {}\n : { \"retry-after\": `${reply.retryAfterSeconds}` },\n );\n const body = Option.getOrUndefined(Schema.decodeOption(CreateBody)(bodyText(request)));\n\n if (body === undefined) return answer(400, { message: \"Invalid session settings\" });\n const loaded = body.browserSettings?.context;\n const createdAt = fake.now();\n\n const created: Row = {\n id: nextId(fake),\n options: body,\n createdAt,\n listedAt: createdAt + (reply._tag === \"Lost\" ? (reply.listedAfter ?? 0) : 0),\n expiresAt: createdAt + (body.timeout ?? 300) * 1000,\n keepAlive: body.keepAlive ?? false,\n userMetadata: body.userMetadata,\n context:\n loaded === undefined ? undefined : { id: loaded.id, persist: loaded.persist ?? false },\n ends: undefined,\n releases: 0,\n };\n\n fake.sessions.set(created.id, created);\n if (reply._tag === \"Lost\") return lost;\n\n return answer(\n 201,\n reply._tag === \"Malformed\" ? { id: created.id } : describe(fake, created, true),\n );\n }\n case \"GET sessions\": {\n const status = url.searchParams.get(\"status\");\n const query = url.searchParams.get(\"q\");\n const matches = query === null ? () => true : metadataQuery(query);\n\n if (matches === undefined) return unsupported(`cannot read the query ${query}`);\n\n return answer(\n 200,\n [...fake.sessions.values()]\n .filter(\n (kept) =>\n kept.listedAt <= fake.now() &&\n (status === null || (endOf(fake, kept)?.status ?? \"RUNNING\") === status) &&\n matches(kept.userMetadata),\n )\n .map((kept) => describe(fake, kept, false)),\n );\n }\n case \"GET sessions id\": {\n const reply = fake.pending.reads.shift() ?? { _tag: \"Answer\" };\n\n if (reply._tag === \"Refused\") return refused(reply.status);\n if (reply._tag === \"Lost\") return lost;\n\n return row === undefined\n ? answer(404, { message: \"Session not found\" })\n : answer(200, describe(fake, row, true));\n }\n case \"GET sessions id debug\":\n return row === undefined\n ? answer(404, { message: \"Session not found\" })\n : answer(200, {\n debuggerFullscreenUrl: `https://live.browserbase.test/${row.id}/fullscreen`,\n debuggerUrl: `https://live.browserbase.test/${row.id}`,\n pages: [],\n });\n case \"POST sessions id\": {\n if (row === undefined) return answer(404, { message: \"Session not found\" });\n if (Option.isNone(Schema.decodeOption(ReleaseBody)(bodyText(request))))\n return answer(400, { message: \"status must be REQUEST_RELEASE\" });\n row.releases += 1;\n const reply = fake.pending.releases.shift() ?? { _tag: \"Ends\" };\n\n if (reply._tag === \"Refused\") return refused(reply.status);\n // Releasing a session that has ended changes nothing, and succeeds.\n if (\n endOf(fake, row) === undefined &&\n (reply._tag !== \"Pending\" || reply.millis !== undefined)\n )\n row.ends = {\n status: \"COMPLETED\",\n at: fake.now() + (reply._tag === \"Pending\" ? (reply.millis ?? 0) : 0),\n };\n\n return reply._tag === \"Lost\" ? lost : answer(200, describe(fake, row, false));\n }\n case \"POST contexts\": {\n const created = nextId(fake);\n\n fake.contexts.set(created, fake.now());\n\n return answer(201, {\n id: created,\n publicKey: \"test-public-key\",\n cipherAlgorithm: \"AES-256-CBC\",\n initializationVectorSize: 16,\n });\n }\n case \"GET contexts id\": {\n const createdAt = id === undefined ? undefined : fake.contexts.get(id);\n\n return createdAt === undefined\n ? answer(404, { message: \"Context not found\" })\n : answer(200, {\n id,\n createdAt: iso(createdAt),\n updatedAt: iso(createdAt),\n projectId: \"test-project\",\n });\n }\n case \"DELETE contexts id\":\n return id !== undefined && fake.contexts.delete(id)\n ? answer(204)\n : answer(404, { message: \"Context not found\" });\n default:\n return unsupported(`does not implement ${request.method} ${url.pathname}`);\n }\n};\n\n/** The fake as an `HttpClient`, with its handle. */\nexport const make = Effect.fnUntraced(function* (script: Script = {}) {\n const clock = yield* Clock.Clock;\n const decoded = yield* Effect.orDie(Schema.decodeEffect(Script)(script));\n\n const fake: Fake = {\n now: () => clock.currentTimeMillisUnsafe(),\n connectUrl: decoded.connectUrl,\n pending: {\n creates: [...(decoded.creates ?? [])],\n releases: [...(decoded.releases ?? [])],\n reads: [...(decoded.reads ?? [])],\n },\n requests: [],\n sessions: new Map(),\n contexts: new Map(),\n made: { count: 0 },\n };\n\n const handle: Handle = {\n requests: Effect.sync(() => [...fake.requests]),\n sessions: Effect.sync(() =>\n [...fake.sessions.values()].map((row) => ({\n id: row.id,\n options: row.options,\n status: endOf(fake, row)?.status ?? \"RUNNING\",\n userMetadata: row.userMetadata,\n releases: row.releases,\n keepAlive: row.keepAlive,\n context: row.context,\n })),\n ),\n end: (id) =>\n Effect.sync(() => {\n const row = fake.sessions.get(id);\n\n if (row !== undefined && endOf(fake, row) === undefined)\n row.ends = { status: \"ERROR\", at: fake.now() };\n }),\n };\n\n return { http: HttpClient.make((request, url) => serve(fake, request, url)), handle };\n});\n\n/** The real `BrowserbaseClient` over the fake, and the fake's handle. */\nexport const layer = (script: Script = {}): Layer.Layer<BrowserbaseClient | TestBrowserbase> =>\n Layer.unwrap(\n Effect.map(make(script), ({ http, handle }) =>\n Layer.merge(\n clientLayer({\n apiKey: Redacted.make(apiKey),\n baseUrl: \"https://api.browserbase.test\",\n }).pipe(Layer.provide(Layer.succeed(HttpClient.HttpClient, http))),\n Layer.succeed(TestBrowserbase, handle),\n ),\n ),\n );\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-browserbase",
3
- "version": "0.3.0-beta.5",
3
+ "version": "0.3.0-beta.7",
4
4
  "description": "Browserbase for effect-browser: hosted browser sessions, contexts, web search and page fetch.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -58,7 +58,7 @@
58
58
  "@effect/vitest": "4.0.0",
59
59
  "@types/node": "26.1.2",
60
60
  "effect": "4.0.0",
61
- "effect-browser": "0.3.0-beta.5",
61
+ "effect-browser": "0.3.0-beta.7",
62
62
  "playwright-core": "1.63.0",
63
63
  "typescript": "7.0.2",
64
64
  "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
@@ -67,7 +67,7 @@
67
67
  },
68
68
  "peerDependencies": {
69
69
  "effect": "~4.0.0",
70
- "effect-browser": "^0.3.0-beta.4",
70
+ "effect-browser": "^0.3.0-beta.7",
71
71
  "playwright-core": "^1.63.0"
72
72
  },
73
73
  "engines": {
@@ -5,9 +5,9 @@
5
5
  * so `TestClock` drives their end. A `Script` decides how creates, releases and session reads
6
6
  * answer, in turn, so a test can lose a create, leave a release pending or fail status reads.
7
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
8
+ * It keeps sessions and stored contexts, and supplies synthetic Live View links: any other endpoint
9
+ * fails the test with a defect. It doesn't model quotas or billing, and a context is only a record,
10
+ * with no browser state. Every session hands out the script's `connectUrl`, such as the DevTools address
11
11
  * of a local Chromium. Its ids are UUIDs, as Browserbase's are, and as Browserbase does, it refuses
12
12
  * a session or context id of any other shape as invalid, where it answers an unknown one as not
13
13
  * found.
@@ -33,7 +33,11 @@ import {
33
33
  HttpClientResponse,
34
34
  } from "effect/http";
35
35
 
36
- import { type BrowserbaseClient, layer as clientLayer } from "../BrowserbaseClient.ts";
36
+ import {
37
+ type BrowserbaseClient,
38
+ layer as clientLayer,
39
+ type SessionOptions,
40
+ } from "../BrowserbaseClient.ts";
37
41
 
38
42
  const Refusal = Schema.Int.check(Schema.isBetween({ minimum: 400, maximum: 599 }));
39
43
 
@@ -91,6 +95,8 @@ export type Script = typeof Script.Type;
91
95
  export interface Kept {
92
96
  readonly id: string;
93
97
  readonly status: string;
98
+ /** The create options as sent, without provider defaults. Only the test handle exposes them. */
99
+ readonly options: SessionOptions;
94
100
  readonly userMetadata: Readonly<Record<string, unknown>> | undefined;
95
101
  readonly releases: number;
96
102
  /** Whether it was created to outlive its connections, until released or timed out. */
@@ -118,14 +124,59 @@ export const apiKey = "test-browserbase-key";
118
124
 
119
125
  const CreateBody = Schema.fromJsonString(
120
126
  Schema.Struct({
127
+ projectId: Schema.optional(Schema.String),
128
+ extensionId: Schema.optional(Schema.String),
121
129
  timeout: Schema.optional(Schema.Finite),
122
130
  keepAlive: Schema.optional(Schema.Boolean),
131
+ region: Schema.optional(
132
+ Schema.Literals(["us-west-2", "us-east-1", "eu-central-1", "ap-southeast-1"]),
133
+ ),
134
+ proxies: Schema.optional(
135
+ Schema.Union([
136
+ Schema.Boolean,
137
+ Schema.Array(
138
+ Schema.Union([
139
+ Schema.Struct({
140
+ type: Schema.Literal("browserbase"),
141
+ geolocation: Schema.optional(
142
+ Schema.Struct({
143
+ country: Schema.String,
144
+ state: Schema.optional(Schema.String),
145
+ city: Schema.optional(Schema.String),
146
+ }),
147
+ ),
148
+ domainPattern: Schema.optional(Schema.String),
149
+ }),
150
+ Schema.Struct({
151
+ type: Schema.Literal("external"),
152
+ server: Schema.String,
153
+ username: Schema.optional(Schema.String),
154
+ password: Schema.optional(Schema.String),
155
+ domainPattern: Schema.optional(Schema.String),
156
+ }),
157
+ Schema.Struct({
158
+ type: Schema.Literal("none"),
159
+ domainPattern: Schema.optional(Schema.String),
160
+ }),
161
+ ]),
162
+ ),
163
+ ]),
164
+ ),
123
165
  userMetadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
124
166
  browserSettings: Schema.optional(
125
167
  Schema.Struct({
126
168
  context: Schema.optional(
127
169
  Schema.Struct({ id: Schema.String, persist: Schema.optional(Schema.Boolean) }),
128
170
  ),
171
+ viewport: Schema.optional(Schema.Struct({ width: Schema.Finite, height: Schema.Finite })),
172
+ blockAds: Schema.optional(Schema.Boolean),
173
+ solveCaptchas: Schema.optional(Schema.Boolean),
174
+ recordSession: Schema.optional(Schema.Boolean),
175
+ logSession: Schema.optional(Schema.Boolean),
176
+ verified: Schema.optional(Schema.Boolean),
177
+ os: Schema.optional(Schema.Literals(["windows", "mac", "linux", "mobile", "tablet"])),
178
+ allowedDomains: Schema.optional(Schema.Array(Schema.String)),
179
+ ignoreCertificateErrors: Schema.optional(Schema.Boolean),
129
180
  }),
130
181
  ),
131
182
  }),
@@ -137,6 +188,7 @@ const ReleaseBody = Schema.fromJsonString(
137
188
 
138
189
  interface Row {
139
190
  readonly id: string;
191
+ readonly options: SessionOptions;
140
192
  readonly createdAt: number;
141
193
  /** When a list first shows it. */
142
194
  readonly listedAt: number;
@@ -193,11 +245,11 @@ const describe = (fake: Fake, row: Row, connect: boolean) => {
193
245
  updatedAt: iso(end?.at ?? row.createdAt),
194
246
  startedAt: iso(row.createdAt),
195
247
  expiresAt: iso(row.expiresAt),
196
- projectId: "test-project",
248
+ projectId: row.options.projectId ?? "test-project",
197
249
  status: end?.status ?? "RUNNING",
198
250
  proxyBytes: 0,
199
251
  keepAlive: row.keepAlive,
200
- region: "us-west-2",
252
+ region: row.options.region ?? "us-west-2",
201
253
  ...(end === undefined ? {} : { endedAt: iso(end.at) }),
202
254
  ...(row.context === undefined ? {} : { contextId: row.context.id }),
203
255
  ...(row.userMetadata === undefined ? {} : { userMetadata: row.userMetadata }),
@@ -281,16 +333,19 @@ const serve = (fake: Fake, request: HttpClientRequest.HttpClientRequest, url: UR
281
333
  : { "retry-after": `${reply.retryAfterSeconds}` },
282
334
  );
283
335
  const body = Option.getOrUndefined(Schema.decodeOption(CreateBody)(bodyText(request)));
284
- const loaded = body?.browserSettings?.context;
336
+
337
+ if (body === undefined) return answer(400, { message: "Invalid session settings" });
338
+ const loaded = body.browserSettings?.context;
285
339
  const createdAt = fake.now();
286
340
 
287
341
  const created: Row = {
288
342
  id: nextId(fake),
343
+ options: body,
289
344
  createdAt,
290
345
  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,
346
+ expiresAt: createdAt + (body.timeout ?? 300) * 1000,
347
+ keepAlive: body.keepAlive ?? false,
348
+ userMetadata: body.userMetadata,
294
349
  context:
295
350
  loaded === undefined ? undefined : { id: loaded.id, persist: loaded.persist ?? false },
296
351
  ends: undefined,
@@ -334,6 +389,14 @@ const serve = (fake: Fake, request: HttpClientRequest.HttpClientRequest, url: UR
334
389
  ? answer(404, { message: "Session not found" })
335
390
  : answer(200, describe(fake, row, true));
336
391
  }
392
+ case "GET sessions id debug":
393
+ return row === undefined
394
+ ? answer(404, { message: "Session not found" })
395
+ : answer(200, {
396
+ debuggerFullscreenUrl: `https://live.browserbase.test/${row.id}/fullscreen`,
397
+ debuggerUrl: `https://live.browserbase.test/${row.id}`,
398
+ pages: [],
399
+ });
337
400
  case "POST sessions id": {
338
401
  if (row === undefined) return answer(404, { message: "Session not found" });
339
402
  if (Option.isNone(Schema.decodeOption(ReleaseBody)(bodyText(request))))
@@ -411,6 +474,7 @@ export const make = Effect.fnUntraced(function* (script: Script = {}) {
411
474
  sessions: Effect.sync(() =>
412
475
  [...fake.sessions.values()].map((row) => ({
413
476
  id: row.id,
477
+ options: row.options,
414
478
  status: endOf(fake, row)?.status ?? "RUNNING",
415
479
  userMetadata: row.userMetadata,
416
480
  releases: row.releases,