effect-browserbase 0.2.0-beta.9 → 0.3.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -319
- package/dist/Browserbase-CaPKwEY9.mjs +544 -0
- package/dist/Browserbase-CaPKwEY9.mjs.map +1 -0
- package/dist/Browserbase.d.mts +110 -0
- package/dist/Browserbase.mjs +3 -0
- package/dist/BrowserbaseClient-CtAQ2rg4.d.mts +195 -0
- package/dist/BrowserbaseClient.d.mts +2 -0
- package/dist/BrowserbaseClient.mjs +190 -0
- package/dist/BrowserbaseClient.mjs.map +1 -0
- package/dist/BrowserbaseError-D1EOrLNc.d.mts +79 -0
- package/dist/BrowserbaseError.d.mts +2 -0
- package/dist/BrowserbaseError.mjs +101 -0
- package/dist/BrowserbaseError.mjs.map +1 -0
- package/dist/ContextLease.d.mts +42 -0
- package/dist/ContextLease.mjs +80 -0
- package/dist/ContextLease.mjs.map +1 -0
- package/dist/index.d.mts +5 -27
- package/dist/index.mjs +5 -26
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/testing/index.d.mts +136 -0
- package/dist/testing/index.mjs +440 -0
- package/dist/testing/index.mjs.map +1 -0
- package/package.json +51 -121
- package/src/Browserbase.ts +729 -0
- package/src/BrowserbaseClient.ts +559 -0
- package/src/BrowserbaseError.ts +108 -0
- package/src/ContextLease.ts +118 -0
- package/src/index.ts +7 -0
- package/src/internal/captureConnection.ts +214 -0
- package/src/testing/BrowserbaseContract.ts +170 -0
- package/src/testing/TestBrowserbase.ts +509 -0
- package/src/testing/index.ts +7 -0
- package/dist/Account.d.mts +0 -32
- package/dist/Account.mjs +0 -36
- package/dist/Account.mjs.map +0 -1
- package/dist/Acquisition-BOABu_qJ.mjs +0 -244
- package/dist/Acquisition-BOABu_qJ.mjs.map +0 -1
- package/dist/Agents-CPaTOH_g.d.mts +0 -156
- package/dist/Agents.d.mts +0 -3
- package/dist/Agents.mjs +0 -292
- package/dist/Agents.mjs.map +0 -1
- package/dist/Allocation.d.mts +0 -97
- package/dist/Allocation.mjs +0 -27
- package/dist/Allocation.mjs.map +0 -1
- package/dist/Authorization-Ll5f_XNM.mjs +0 -12
- package/dist/Authorization-Ll5f_XNM.mjs.map +0 -1
- package/dist/Browser-ByrWJ0ss.mjs +0 -274
- package/dist/Browser-ByrWJ0ss.mjs.map +0 -1
- package/dist/Browser.d.mts +0 -83
- package/dist/Browser.mjs +0 -3
- package/dist/BrowserBinding.d.mts +0 -13
- package/dist/BrowserBinding.mjs +0 -31
- package/dist/BrowserBinding.mjs.map +0 -1
- package/dist/Certificates-D4NQmyeA.d.mts +0 -34
- package/dist/Certificates.d.mts +0 -3
- package/dist/Certificates.mjs +0 -85
- package/dist/Certificates.mjs.map +0 -1
- package/dist/Cleanup-DMTDBir8.d.mts +0 -23
- package/dist/Cleanup.d.mts +0 -3
- package/dist/Cleanup.mjs +0 -43
- package/dist/Cleanup.mjs.map +0 -1
- package/dist/Client-BEC4Q8ZS.mjs +0 -564
- package/dist/Client-BEC4Q8ZS.mjs.map +0 -1
- package/dist/Client-B_f6bBLT.d.mts +0 -52
- package/dist/Client.d.mts +0 -3
- package/dist/Client.mjs +0 -2
- package/dist/Connection-sxyxtwlk.mjs +0 -56
- package/dist/Connection-sxyxtwlk.mjs.map +0 -1
- package/dist/ContextCoordination.d.mts +0 -29
- package/dist/ContextCoordination.mjs +0 -94
- package/dist/ContextCoordination.mjs.map +0 -1
- package/dist/ContextWriter-CiEma24z.mjs +0 -204
- package/dist/ContextWriter-CiEma24z.mjs.map +0 -1
- package/dist/Contexts-DqdL7cyX.d.mts +0 -31
- package/dist/Contexts.d.mts +0 -3
- package/dist/Contexts.mjs +0 -97
- package/dist/Contexts.mjs.map +0 -1
- package/dist/Deadline-CrViaogr.mjs +0 -13
- package/dist/Deadline-CrViaogr.mjs.map +0 -1
- package/dist/Downloads-CX52GOFJ.d.mts +0 -43
- package/dist/Downloads.d.mts +0 -3
- package/dist/Downloads.mjs +0 -137
- package/dist/Downloads.mjs.map +0 -1
- package/dist/Errors.d.mts +0 -100
- package/dist/Errors.mjs +0 -194
- package/dist/Errors.mjs.map +0 -1
- package/dist/Extensions-BTUn9zhK.mjs +0 -231
- package/dist/Extensions-BTUn9zhK.mjs.map +0 -1
- package/dist/Extensions-DBXuVM1f.d.mts +0 -43
- package/dist/Extensions.d.mts +0 -3
- package/dist/Extensions.mjs +0 -2
- package/dist/Functions-CsKMMUuZ.d.mts +0 -226
- package/dist/Functions.d.mts +0 -3
- package/dist/Functions.mjs +0 -318
- package/dist/Functions.mjs.map +0 -1
- package/dist/Issued-DejqoMYL.mjs +0 -15
- package/dist/Issued-DejqoMYL.mjs.map +0 -1
- package/dist/Launch-cAdE2rrZ.mjs +0 -115
- package/dist/Launch-cAdE2rrZ.mjs.map +0 -1
- package/dist/Launch.d.mts +0 -127
- package/dist/Launch.mjs +0 -97
- package/dist/Launch.mjs.map +0 -1
- package/dist/PageFetch-CjAM_0DS.d.mts +0 -40
- package/dist/PageFetch.d.mts +0 -3
- package/dist/PageFetch.mjs +0 -59
- package/dist/PageFetch.mjs.map +0 -1
- package/dist/Projects-tm5bTDH0.d.mts +0 -38
- package/dist/Projects.d.mts +0 -3
- package/dist/Projects.mjs +0 -65
- package/dist/Projects.mjs.map +0 -1
- package/dist/Recordings-B9k5aODB.d.mts +0 -27
- package/dist/Recordings.d.mts +0 -3
- package/dist/Recordings.mjs +0 -144
- package/dist/Recordings.mjs.map +0 -1
- package/dist/References-DOH3Wu44.d.mts +0 -34
- package/dist/References.d.mts +0 -3
- package/dist/References.mjs +0 -33
- package/dist/References.mjs.map +0 -1
- package/dist/Replays-mV93WGtx.d.mts +0 -27
- package/dist/Replays.d.mts +0 -3
- package/dist/Replays.mjs +0 -107
- package/dist/Replays.mjs.map +0 -1
- package/dist/Resource-J7Kkk5mu.mjs +0 -54
- package/dist/Resource-J7Kkk5mu.mjs.map +0 -1
- package/dist/Search-B64e7g-h.d.mts +0 -37
- package/dist/Search.d.mts +0 -3
- package/dist/Search.mjs +0 -46
- package/dist/Search.mjs.map +0 -1
- package/dist/SessionData-5xYZDshJ.d.mts +0 -38
- package/dist/SessionData.d.mts +0 -3
- package/dist/SessionData.mjs +0 -48
- package/dist/SessionData.mjs.map +0 -1
- package/dist/Sessions-D2H5m6rF.mjs +0 -260
- package/dist/Sessions-D2H5m6rF.mjs.map +0 -1
- package/dist/Sessions-oJEUmZth.d.mts +0 -69
- package/dist/Sessions.d.mts +0 -4
- package/dist/Sessions.mjs +0 -3
- package/dist/Testing.d.mts +0 -119
- package/dist/Testing.mjs +0 -206
- package/dist/Testing.mjs.map +0 -1
- package/dist/TransferPolicy-DQ06i7SZ.mjs +0 -25
- package/dist/TransferPolicy-DQ06i7SZ.mjs.map +0 -1
- package/dist/Transfers-GVqzrDWl.d.mts +0 -89
- package/dist/Transfers.d.mts +0 -3
- package/dist/Transfers.mjs +0 -85
- package/dist/Transfers.mjs.map +0 -1
- package/dist/Uploads-C2_bACgv.d.mts +0 -28
- package/dist/Uploads.d.mts +0 -3
- package/dist/Uploads.mjs +0 -105
- package/dist/Uploads.mjs.map +0 -1
- package/dist/Webhooks-BoeRAxzG.d.mts +0 -59
- package/dist/Webhooks.d.mts +0 -3
- package/dist/Webhooks.mjs +0 -134
- package/dist/Webhooks.mjs.map +0 -1
- package/dist/WriterFacts-BBJRDZYY.d.mts +0 -54
package/README.md
CHANGED
|
@@ -1,359 +1,317 @@
|
|
|
1
|
-
#
|
|
1
|
+
# effect-browserbase
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Browserbase](https://www.browserbase.com) for [effect-browser](../browser): hosted sessions as a
|
|
4
|
+
`Browser` layer, and an Effect client for the Browserbase REST API.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
```sh
|
|
7
|
+
npm install effect-browserbase@beta effect-browser@beta effect playwright-core
|
|
8
|
+
```
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
- `Browserbase`: `open` and `layer` create a session and request its release when their scope
|
|
11
|
+
closes. Closing waits for the outcome; an `Unconfirmed` session may still run and bill.
|
|
12
|
+
`attach` connects to a running session without taking ownership of it, as from another process:
|
|
13
|
+
that is resume, of a session created with `keepAlive`. `supervise` keeps
|
|
14
|
+
sessions open across losses and session ends, and with `keep`, past its own scope. `reconcile`
|
|
15
|
+
ends a stored context's sessions, and `verifyContext` reads a stored context back.
|
|
16
|
+
- `ContextLease`: who may write a stored context. `ContextLease.layer` lets one writer at a time
|
|
17
|
+
hold each context in this process; an application provides its own to exclude writers across
|
|
18
|
+
processes. A persisting open that will not write to a context whose sessions may still save to
|
|
19
|
+
it fails with `ContextHeld`.
|
|
20
|
+
- `BrowserbaseClient`: sessions, Live View, stored contexts, extensions, Search and Fetch.
|
|
21
|
+
`layerConfig()` reads `BROWSERBASE_API_KEY` and, optionally, `BROWSERBASE_BASE_URL`.
|
|
22
|
+
- `BrowserbaseError`: one error with a reason: `Unauthorized`, `NotFound`, `RateLimited`, `Status`,
|
|
23
|
+
`Transport`, `Decode` or `InvalidRequest`. A request Browserbase refuses as malformed, such as a
|
|
24
|
+
session id that is not a UUID, is `InvalidRequest`, as is one this client refuses before sending.
|
|
25
|
+
A create whose answer named its session but did not decode is `Decode` with `released`, whether
|
|
26
|
+
the client released that session.
|
|
27
|
+
|
|
28
|
+
`open`, `layer`, `supervise`, `reconcile` and `verifyContext` need a `ContextLease`, and the client:
|
|
10
29
|
|
|
11
30
|
```ts
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// Your project's own recording-delivery origin; see below for how to find it.
|
|
18
|
-
artifactOrigins: ["https://recording-delivery.example"],
|
|
19
|
-
});
|
|
31
|
+
const Hosted = Browserbase.layer({ session: { region: "us-west-2" } }).pipe(
|
|
32
|
+
Layer.provide(BrowserbaseClient.layerConfig()),
|
|
33
|
+
Layer.provide(ContextLease.layer),
|
|
34
|
+
Layer.provide(FetchHttpClient.layer),
|
|
35
|
+
);
|
|
20
36
|
```
|
|
21
37
|
|
|
22
|
-
|
|
38
|
+
Each page keeps its CDP target id as its `id` across connections, so `attach` to the same session,
|
|
39
|
+
after a dropped connection or from another process, finds a page stored before with
|
|
40
|
+
`browser.page(id)`. `attach` refuses a session that has ended, `Closed` by the session, before it
|
|
41
|
+
connects. The browser carries the session's `expiresAt`, announces it as a `SessionEnding` event,
|
|
42
|
+
and reports a loss at or after it as the session's end (`Disconnected` with cause `session`), any
|
|
43
|
+
other drop as `connection`.
|
|
44
|
+
|
|
45
|
+
The API key travels only in the `x-bb-api-key` header, which logs and traces redact, and the client
|
|
46
|
+
refuses redirects so the key never follows one. Each request attempt has a deadline
|
|
47
|
+
(`requestTimeout`, 60 seconds by default), so a session create that never answers still settles
|
|
48
|
+
and its caller can stop. Only `GET` requests are retried, twice, after a transient failure; a
|
|
49
|
+
create that fails in transit is not resent, because it may have created the session. Connect URLs and the Live View debugger URL are `Redacted`: keep them away from
|
|
50
|
+
models and logs. A failed connect names only the connect URL's scheme, host and port.
|
|
51
|
+
|
|
52
|
+
Each create `open` sends carries a nonce of its own in the session's user metadata, `createNonce`.
|
|
53
|
+
A create whose answer was lost, or that Browserbase answered 5xx or 408, may still have made a
|
|
54
|
+
session that nobody can see, and that bills until its timeout: `open` looks for it by its nonce,
|
|
55
|
+
again and again for up to 30 seconds, since Browserbase can list a new session late, ends it, and
|
|
56
|
+
then fails with the create's error. A `supervise` that tries again after such failures leaves no
|
|
57
|
+
session behind each try.
|
|
58
|
+
|
|
59
|
+
`open` and `attach` record the session's id and region on their spans, so a trace finds its
|
|
60
|
+
session in Browserbase's dashboard (`https://browserbase.com/sessions/<id>`).
|
|
61
|
+
|
|
62
|
+
## Releases
|
|
63
|
+
|
|
64
|
+
`open` and `attach` give a `Hosted`: the `browser`, Browserbase's `session`, and `release`. A
|
|
65
|
+
release first disconnects the browser, which is lost as `released`, so a call in flight or a
|
|
66
|
+
capture's reader says the session was released, not that the connection Browserbase then cuts was
|
|
67
|
+
lost. It asks Browserbase to end the session and then reads it until Browserbase reports it ended,
|
|
68
|
+
trying both again a second apart for up to a minute, and says how it went: `Settled`, or
|
|
69
|
+
`Unconfirmed` when the session still ran, or Browserbase could not be asked, at the deadline. An
|
|
70
|
+
unconfirmed session may still bill until its timeout. `open`'s scope releases its session as it
|
|
71
|
+
closes, so closing the scope waits for the release: up to a minute while Browserbase confirms the
|
|
72
|
+
end, and for a session that saves to a stored context `contextSettle` longer, 10 seconds by
|
|
73
|
+
default. `release` does it sooner and gives the outcome as a value, and the scope then asks
|
|
74
|
+
Browserbase nothing more. Closing `attach`'s scope only disconnects; its `release` ends the
|
|
75
|
+
session. Browserbase ends a session whose last connection closes unless it was created with
|
|
76
|
+
`keepAlive`, which `supervise({ keep })` sets and `session.keepAlive` asks for, so only such a
|
|
77
|
+
session outlives the process that opened it, for another to attach to.
|
|
78
|
+
|
|
79
|
+
## Supervised sessions
|
|
80
|
+
|
|
81
|
+
`supervise` keeps a browser on Browserbase as `Supervisor` generations, each a new session made
|
|
82
|
+
from `session`. Use `sessions.use` to hold a generation while working: its lease gives the
|
|
83
|
+
`browser` and `value`, which is the `Session` unless `build` supplies an application value.
|
|
84
|
+
`build(hosted)` runs in the generation's scope on every acquisition, including adoption; failed
|
|
85
|
+
or cancelled construction closes that scope and releases the acquired session before retry.
|
|
86
|
+
The [generic supervisor contract](../browser/README.md#supervising-browser-generations) describes
|
|
87
|
+
`use`, transferable `retain` scopes, cancellation and stale-work refusal.
|
|
88
|
+
|
|
89
|
+
Outgoing holds drain for `handover`, 30 seconds by default. A lost session revokes immediately and
|
|
90
|
+
is replaced on `reopen`; `rotateBefore` and `rotate` keep their normal schedule while older
|
|
91
|
+
nonexclusive generations drain. This can overlap more than two hosted sessions. The consumer
|
|
92
|
+
owns its overlap and billing policy. `states` reports each generation's release outcome;
|
|
93
|
+
`Unconfirmed` means the old session might still run even after local ownership ends.
|
|
94
|
+
|
|
95
|
+
Sessions that persist to a stored context are exclusive: rotation first drains the current
|
|
96
|
+
generation, releases its session and lets its save settle. If release is unconfirmed, the next
|
|
97
|
+
writer reconciles the context and fails `ContextHeld` while the old writer cannot be confirmed
|
|
98
|
+
ended. It does not silently create another writer. An open Browserbase refused, as for a bad key
|
|
99
|
+
or an invalid request, is `Down` at once; everything else, a held context included, is tried again
|
|
100
|
+
on the schedule.
|
|
23
101
|
|
|
24
|
-
|
|
102
|
+
```ts
|
|
103
|
+
const program = Effect.gen(function* () {
|
|
104
|
+
const sessions = yield* Browserbase.supervise({
|
|
105
|
+
session: { timeout: 6 * 60 * 60 },
|
|
106
|
+
reopen: Schedule.exponential("1 second").pipe(Schedule.upTo({ duration: "5 minutes" })),
|
|
107
|
+
rotateBefore: "20 minutes",
|
|
108
|
+
build: ({ browser, session }) =>
|
|
109
|
+
browser.firstPage.pipe(Effect.map((page) => ({ page, session }))),
|
|
110
|
+
});
|
|
111
|
+
// `retire` releases every session; so does the scope's close.
|
|
112
|
+
return yield* sessions.use(({ value }) => value.page.text());
|
|
113
|
+
});
|
|
114
|
+
```
|
|
25
115
|
|
|
26
|
-
|
|
116
|
+
Keep pages that must close before provider release in a `use` or retained scope. Resources
|
|
117
|
+
acquired by `build` live in the generation's scope, which closes after provider release.
|
|
118
|
+
Generation `hostTime` uses the owning Effect clock; provider `createdAt` and `endedAt` are separate
|
|
119
|
+
wall-clock evidence. Fake-provider ordering tests do not prove hosted context-save timing or
|
|
120
|
+
billing. The included-hours-only [H2 recipe](../../docs/HOSTED_CHECKS.md) remains a hosted check to
|
|
121
|
+
run after its allowance is verified.
|
|
122
|
+
|
|
123
|
+
### Keeping a session past the scope
|
|
124
|
+
|
|
125
|
+
With `keep`, a name, closing the supervisor's scope leaves its current session running instead of
|
|
126
|
+
releasing it, published as `Kept`, and the first generation of the next `supervise` under the same
|
|
127
|
+
name adopts that session, its pages and their ids with it, rather than creating one. So a deploy or
|
|
128
|
+
a restart of the process that owns the browser keeps its prepared pages and its signed-in state.
|
|
129
|
+
It is off by default, and opt-in, because a kept session bills until something adopts and releases
|
|
130
|
+
it, or until its own `timeout` ends it: set one that bounds what a forgotten session costs.
|
|
131
|
+
Keeping revokes active work and closes retained and generation application scopes. The next
|
|
132
|
+
supervisor rebuilds its value when it adopts the session; only the provider's browser state stays.
|
|
133
|
+
|
|
134
|
+
- Each session is created with `keepAlive`, so it outlives its connection, and labelled
|
|
135
|
+
`keptAs: <name>` in its user metadata, since Browserbase sets user metadata only at the create.
|
|
136
|
+
A name is 1 to 64 letters, digits, `_`, `.`, `:` or `-`; another is refused, `Down` at once.
|
|
137
|
+
- Adopting takes the newest session kept under the name that saves to the same stored context, or
|
|
138
|
+
to none; any other session kept under the name is ended, as nothing else adopts it.
|
|
139
|
+
- `retire` always releases, `keep` or not. Only the first generation adopts: a later one, after a
|
|
140
|
+
loss or a rotation, is a new session.
|
|
141
|
+
- One supervisor at a time per name: two at once would adopt one session together.
|
|
27
142
|
|
|
28
|
-
|
|
143
|
+
```ts
|
|
144
|
+
const sessions = yield * Browserbase.supervise({ keep: "on-air", session: { timeout: 60 * 60 } });
|
|
145
|
+
const browser = yield * sessions.browser; // the session the last process kept, if one runs
|
|
146
|
+
```
|
|
29
147
|
|
|
30
|
-
|
|
148
|
+
## Session settings
|
|
31
149
|
|
|
32
|
-
|
|
150
|
+
`session` takes Browserbase's session create request. Anything left unset takes Browserbase's
|
|
151
|
+
default, and this package changes none of them. Browserbase records every session for replay, keeps
|
|
152
|
+
its logs and solves captchas unless told not to:
|
|
33
153
|
|
|
34
|
-
|
|
154
|
+
```ts
|
|
155
|
+
Browserbase.layer({
|
|
156
|
+
session: { browserSettings: { recordSession: false, logSession: false, solveCaptchas: false } },
|
|
157
|
+
});
|
|
158
|
+
```
|
|
35
159
|
|
|
36
|
-
|
|
160
|
+
Pages open at Browserbase's default viewport, which was 2560×1440 on 7 October 2026: four times
|
|
161
|
+
the pixels of `Chromium.layer`'s 1280×720. `browserSettings.viewport` sets another.
|
|
162
|
+
|
|
163
|
+
## The capture connection
|
|
164
|
+
|
|
165
|
+
A page's screencast runs on a second connection to the session that carries nothing else. On the
|
|
166
|
+
connection Playwright drives the pages over, a frame and its acknowledgement would wait behind any
|
|
167
|
+
large message, such as an upload or a read's answer, because Browserbase refuses compression: on a
|
|
168
|
+
hosted session at 1280×720, while another tab read and uploaded, the on-air page's longest wait
|
|
169
|
+
between frames was 1,802 ms on that connection and 352 ms on its own (8 October 2026).
|
|
170
|
+
|
|
171
|
+
The connection is raw CDP, with no Playwright, and read-only: a page's session on it starts,
|
|
172
|
+
acknowledges and stops the screencast, and turns on the Page domain and reads the frame tree, so
|
|
173
|
+
that each frame carries the document its own connection saw commit, numbered as `Navigated`
|
|
174
|
+
numbers them. A page's first capture there costs three round trips, run beside the control
|
|
175
|
+
connection's own: the attach, then the Page domain and the frame tree together, then the start.
|
|
176
|
+
The browser's first capture also opens the connection. A later capture of the page costs the start
|
|
177
|
+
alone, and on the control connection a capture costs one call fewer. The page's own session keeps the focus
|
|
178
|
+
emulation that keeps a tab behind painting: a session that held it too would blur the page as it
|
|
179
|
+
went. The connection opens with the first capture and closes with the browser. If it fails alone,
|
|
180
|
+
the capture ends with `Failed` two seconds on, the page and its own session stay as they were, and
|
|
181
|
+
the next capture opens another. If the browser is lost too, the capture's readers are told the
|
|
182
|
+
browser's loss, `Closed` by its cause, whichever connection hears the end first. A wait for a
|
|
183
|
+
still screen, `ready({ quietMillis })`, ends with one round trip on this connection,
|
|
184
|
+
`Page.getFrameTree`, whose answer comes behind any frame still on its way, so a stall here is not
|
|
185
|
+
read as stillness; it costs the control connection nothing. `captureConnection: false` keeps
|
|
186
|
+
captures on each page's own session; so does a DevTools server's `http:` address, such as a local
|
|
187
|
+
Chromium's in tests.
|
|
188
|
+
|
|
189
|
+
## Stored contexts
|
|
190
|
+
|
|
191
|
+
A context keeps cookies, storage and cache between sessions. A session that loads it with
|
|
192
|
+
`persist: true` saves its browser state back to it when the session ends; without `persist` it
|
|
193
|
+
only reads.
|
|
37
194
|
|
|
38
195
|
```ts
|
|
39
|
-
import { NodeServices } from "@effect/platform-node";
|
|
40
|
-
import { BrowserbaseBrowser } from "effect-browserbase/browser";
|
|
41
|
-
import * as Browser from "effect-browser/browser";
|
|
42
|
-
import { BrowserPolicy, NavigateRequest } from "effect-browser/browser-data";
|
|
43
|
-
import { recipe } from "effect-browserbase/launch";
|
|
44
|
-
import { BrowserbaseRecordings } from "effect-browserbase/recordings";
|
|
45
|
-
import { RecordingPageReference } from "effect-browserbase/transfers";
|
|
46
|
-
import { Effect, Layer, Stream } from "effect";
|
|
47
|
-
|
|
48
|
-
// Five-minute provider lifetime and a provider-managed viewport unless overridden.
|
|
49
|
-
const launch = recipe({ provider: { browserSettings: { recordSession: true } } });
|
|
50
|
-
|
|
51
|
-
// 100 actions, five minutes, 2 MiB returned unless overridden.
|
|
52
|
-
const policy = BrowserPolicy.unrestricted();
|
|
53
|
-
|
|
54
|
-
const layers = Layer.merge(BrowserbaseBrowser.layer({ launch }), BrowserbaseRecordings.layer).pipe(
|
|
55
|
-
Layer.provide(account),
|
|
56
|
-
Layer.provide(NodeServices.layer),
|
|
57
|
-
);
|
|
58
|
-
|
|
59
196
|
const program = Effect.gen(function* () {
|
|
60
|
-
const
|
|
197
|
+
const client = yield* BrowserbaseClient.BrowserbaseClient;
|
|
198
|
+
const { id } = yield* client.createContext({ name: "signed-in" });
|
|
61
199
|
|
|
62
|
-
|
|
63
|
-
Effect.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// Browser.scoped checks the owner's release before returning this durable identity.
|
|
67
|
-
return session.reference;
|
|
68
|
-
}),
|
|
200
|
+
yield* signIn.pipe(
|
|
201
|
+
Effect.provide(
|
|
202
|
+
Browserbase.layer({ session: { browserSettings: { context: { id, persist: true } } } }),
|
|
203
|
+
),
|
|
69
204
|
);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const batch = yield* recordings.wait(reference, { timeoutMillis: 120_000 });
|
|
73
|
-
const page = batch.pages.find((item) => item.delivery === "download");
|
|
74
|
-
|
|
75
|
-
if (page === undefined) return batch;
|
|
76
|
-
|
|
77
|
-
const bytes = yield* recordings
|
|
78
|
-
.download(RecordingPageReference.make({ session: reference, pageId: page.pageId }), {
|
|
79
|
-
maxBytes: 512 * 1024 * 1024,
|
|
80
|
-
timeoutMillis: 120_000,
|
|
81
|
-
})
|
|
82
|
-
.pipe(Stream.runCollect);
|
|
83
|
-
|
|
84
|
-
return { batch, bytes };
|
|
85
|
-
}).pipe(Effect.provide(layers));
|
|
205
|
+
// Later sessions on the same context start signed in.
|
|
206
|
+
});
|
|
86
207
|
```
|
|
87
208
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
## Choose a video path
|
|
99
|
-
|
|
100
|
-
| | Provider recording (`recordings` / `replays`) | Live `capture` |
|
|
101
|
-
| ---------- | --------------------------------------------- | --------------------------------------------------- |
|
|
102
|
-
| Encoding | Browserbase | caller-owned |
|
|
103
|
-
| Available | after the session ends | while the session is live |
|
|
104
|
-
| Enablement | `recordSession: true` at allocation | independent of provider recording |
|
|
105
|
-
| Output | per-page MP4 download or validated HLS replay | bounded JPEG frames |
|
|
106
|
-
| Control | provider-owned encoding | JPEG quality/source-fit bounds plus caller encoding |
|
|
107
|
-
| Audio | none: measured, see below | none; the screencast frame seam has no audio source |
|
|
108
|
-
|
|
109
|
-
A bounded hosted check ran a page producing a known 440 Hz tone two independent ways, an `<audio>` element and a WebAudio oscillator started under real user activation, and confirmed from inside the page that it was playing (`AudioContext.state: "running"`, the element unpaused, `currentTime` advancing). The assembled provider MP4 decoded to a single h264 video stream with no audio track, in each of three recordings. That follows from the architecture: the provider records a CDP screencast of viewport screenshots, and its MP4 and HLS renditions draw from that same data. This package does not synthesize a silent track or infer audio support from a container; real audio needs control of the browser host process, which a hosted session does not give.
|
|
110
|
-
|
|
111
|
-
Prefer provider recording when post-session MP4/HLS is enough. Use `capture` when you need frames during the session, need to transform or encode them yourself, did not enable provider recording, are testing locally without a paid session, or need a live-frame path distinct from Browserbase's post-session artifact lifetime. BYOS recording completion is reported explicitly even when Browserbase does not return a download URL.
|
|
112
|
-
|
|
113
|
-
`Capture.start(page)` requires an issued `Page`, such as `session.initialPage` or `yield* session.page(info)` for exact metadata from `session.listPages()`. The interval belongs to that Page independently of display selection; later selection changes do not move or stop it. An agent can drive a scout tab while the stage page keeps recording. Distinct pages may capture concurrently, with one interval per native target, at most four active or quarantined intervals, and 64 MiB of aggregate reserved buffering per session. The maintained Playwright screencast callback is JPEG-only here and the public API does not expose an FPS cap or lossless format.
|
|
114
|
-
|
|
115
|
-
`Capture.start(page, { size: { width: 640, height: 360 } })` requests source frames fitted within those pixel bounds without resizing the live viewport. Both dimensions must be integers from 1 through 16,384; the existing 33,554,432-pixel and byte limits still apply to actual frames. The request is validated and copied before native work or capture-budget admission. Omitting `size` preserves Playwright's default (the viewport fitted within 800×800). An already-active native screencast can override a requested size, so the package independently checks actual JPEG dimensions and ends the affected interval with a `limit` error before delivering an oversized frame. It does not silently resize bytes, invent dimensions, alter timestamps, or end a sibling page's capture. A size request is not a frame-rate limit or a guarantee about bytes produced before they reach the package.
|
|
116
|
-
|
|
117
|
-
## Session, reference, and ownership
|
|
118
|
-
|
|
119
|
-
`BrowserbaseSession` is the live capability. It belongs to the enclosing Effect `Scope`, owns the Playwright-over-CDP connection, and cannot outlive that scope. `SessionReference` is credential-free durable identity; it can be retained after the browser is gone and is what the independent `recordings`, `replays` and cleanup APIs use.
|
|
120
|
-
|
|
121
|
-
That split is why an agent can use one browser for many turns while provider artifacts remain accessible after the interactive scope ends. Importing a Layer does not allocate a browser.
|
|
122
|
-
|
|
123
|
-
`Allocation.scoped(recipe)` allocates a session without connecting a browser, for a caller that drives the remote session with its own automation client. It is the same allocation path the browser owner uses, so the creation request is never retried, an unknown outcome is still reported exactly once, a persisting Context still needs a live writer permit, and no native peer is loaded. The scope owns the release: closing it releases the session whether or not anything ever connected. A reply naming another project is refused, and because a foreign reference is never acted on, the caller is handed that reference rather than a mutation attempted against it. The attempt id comes from Effect's `Crypto`, so `Allocation.scoped` requires that service beside the account.
|
|
124
|
-
|
|
125
|
-
The shared browser runtime delegates allocation and release to this provider lifetime. It supplies the local half of cleanup — fence, capture stop, initialization teardown, disconnect — and the canonical control plane owns the release request and the terminal status observation. A local disconnect, an accepted provider release request and provider-confirmed termination therefore stay distinct facts in one `CleanupResult`, together with the exact steps that failed.
|
|
126
|
-
|
|
127
|
-
`onCleanup` runs once after the canonical receipt and any Context-writer settlement are recorded.
|
|
128
|
-
Its construction throws, defects, self-interruption and two-second cooperative timeout are
|
|
129
|
-
contained separately from the built-in diagnostics reporter. A failed reporter cannot suppress
|
|
130
|
-
the sink, and a failed sink cannot change cleanup facts or repeat release. The receipt remains
|
|
131
|
-
readable through `cleanupResult`; `closeChecked` still fails when its actual confirmation predicate
|
|
132
|
-
fails. An outer race can discard that checked error, so keep any required host receipt sink outside
|
|
133
|
-
the raced workflow. The shared guide's [Layer and receipt examples](../browser/README.md#a-long-lived-session-in-a-layer)
|
|
134
|
-
describe sharing one bounded session and the supervision that a Layer alone does not provide.
|
|
135
|
-
|
|
136
|
-
Each Page serializes its operations with its frames; operations on independent Pages can proceed concurrently. An observation identifies retained native nodes only until the next invalidating event; it is not a DOM snapshot version. Replaced or detached nodes fail instead of silently resolving to replacements. An action interrupted or timed out after native dispatch has an **unknown** outcome and is never automatically replayed. Unresolved control fences the affected Page until confirmed closure; unsuccessful containment fences the session. The shared runtime's main-frame navigation recovery can preserve usability after one acknowledged stop while still reporting `Timeout/unknown`. Recovery is bounded by the loading deadline plus at most three seconds and the existing lifetime. Child-frame timeouts retain conservative containment. `undispatched` is used only when the package established that native mutation dispatch did not occur.
|
|
137
|
-
|
|
138
|
-
## Shared browser operations
|
|
139
|
-
|
|
140
|
-
Navigation, observation, native input, typed bootstrap bindings, live capture and page holds are implemented by [`effect-browser`](../browser/README.md). Import their contracts from that package. Session owns lifecycle and inventory; its issued Pages and Frames own browser operations. `session.initialPage` keeps its original identity when display selection changes, and `page.frame(info)` issues exact Frame authority. Capture and page holds require an issued Page. The [`effect-agent-browser`](../agent-browser/README.md) adapter and maintained tools accept both this session and self-managed Chromium without a second connection.
|
|
141
|
-
|
|
142
|
-
The shared session also provides passive `status` and bounded `diagnostics`, including after
|
|
143
|
-
closure. Known expiry, callback failure and policy pressure are not automatically labeled
|
|
144
|
-
uncertain; unresolved native dispatch remains a separate fact. Owned provider termination can
|
|
145
|
-
retire native control, whereas borrowed disconnection alone cannot. These snapshots do not replace
|
|
146
|
-
the provider's canonical cleanup receipt or make business outcomes known. `maxHostReads` is a
|
|
147
|
-
browser Layer option (default 10,000, maximum 1,000,000) for checkpoint/control-facts sampling;
|
|
148
|
-
it is independent of model action accounting and never a tool parameter.
|
|
149
|
-
|
|
150
|
-
## Files in and out
|
|
151
|
-
|
|
152
|
-
Small selection needs no provisioning: `session.selectFiles` attaches in-memory bytes the caller already holds, and `session.clickForFileSelection` registers the chooser observation before the single click that opens it and attaches exactly once.
|
|
153
|
-
|
|
154
|
-
`session.selectFiles(page, request)`, `session.clickForFileSelection(page, request)` and `session.clickForDownload(page, request)` take an issued `Page` from that session first, as `Capture.start(page)` and `PageControl` do, such as `session.initialPage` or `yield* session.page(info)`. Transfers stay on that Page through the original connection while display selection moves. Cloned, foreign and retired Pages fail before transfer dispatch.
|
|
155
|
-
|
|
156
|
-
Each transfer accepts trailing host-only `OperationOptions`: `session.selectFiles(page, request, { timeoutMillis: 5000, admission: { queue: "1 second" } })`. Queue omission or zero fails immediately when conflicting work is active; a positive finite duration allows bounded FIFO waiting. Waiting counts toward the operation deadline, and neither option extends the browser's policy or lifetime. `QueueFull` and `QueueExpired` distinguish capacity refusal and expired waiting from immediate `Busy`. Live View, handoff, resume and reconnect also accept trailing operation options; cleanup effects retain their owned lifetime.
|
|
157
|
-
|
|
158
|
-
Larger files use `BrowserbaseUploads.create`, which places bytes for the exact running session and returns a receipt. A stored file is named to the browser process, which opens the path itself; only in-memory bytes are streamed from this client, and the two mechanisms are never mixed. Attachment authority is the identity of a receipt this package issued for that session, so a value that merely has the right shape carries none: no caller, and no model, turns a server pathname into an attached file. The receipt reports the provider's remote path only when the provider returned one; when it does not, attachment by path is refused rather than guessed. Hosted H6 remains the check for real provider upload identity and routing.
|
|
159
|
-
|
|
160
|
-
## Borrowed attachment
|
|
161
|
-
|
|
162
|
-
`BrowserbaseBrowser.attach(reference, { policy, target })` takes control of a session this process did not allocate. It reads status first, so project authority is checked before anything is borrowed; a terminal session is refused because that needs a fresh allocation rather than a reattachment, and a starting session is admitted only within a bounded `pendingWaitMillis`. The requested target is resolved explicitly — there is no positional first-tab fallback — and connection credentials are fetched fresh, because expiry races the caller's own status read.
|
|
163
|
-
|
|
164
|
-
`session.closeChecked` accepts confirmed owned cleanup or complete borrowed disconnection according to the actual ownership, then returns the same frozen `CleanupResult` cached by `close`. The concrete success value replaces the former `undefined`; failed confirmation remains a typed `BrowserError`. A borrowed scope disconnects locally and reports `ownership: "borrowed"` with `remote: "not-owned"`. It never requests release and never claims Context-writer authority: whoever allocated the session keeps both. It also does not detach and reattach inside itself; attaching again is the cross-process path, and it revalidates the session instead of assuming it is still there. A prior uncertain business mutation is still yours to reconcile before the next one; nothing is replayed automatically.
|
|
209
|
+
Two sessions saving to one context at once can lose one's changes, and sites may sign a session out
|
|
210
|
+
when another uses its login. So a persisting `open` holds the context through the `ContextLease`,
|
|
211
|
+
and another writer waits. It takes the hold before it creates the session, and the writer's release
|
|
212
|
+
keeps it until Browserbase reports the session ended and then `contextSettle` (10 seconds by
|
|
213
|
+
default) longer, because the save lands after the session ends and Browserbase does not
|
|
214
|
+
acknowledge it. `ContextLease.layer` excludes writers in this process. To exclude writers in other
|
|
215
|
+
processes, provide a `ContextLease` of your own, such as an advisory lock in your database: `hold`
|
|
216
|
+
holds a context for a scope, waiting while another holds it, and passes on how the writer before
|
|
217
|
+
left it. `attach` never holds a context. `deleteContext` is permanent.
|
|
165
218
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
| Reuse old page/frame metadata after reconnect | In the same known browser lifetime, read fresh pages and match exactly one saved `targetId`; then read fresh frames. No fallback to order, old ID, URL or title |
|
|
173
|
-
| `Adapter.fromSession(session)` or implicit selection | Supply the owning session and issued Page to `yield* Adapter.fromSession(session, page)`; its lifecycle remains on the original browser owner |
|
|
174
|
-
| `BrowserError.reason` string and optional outcome | Tagged reason and required outcome. Use `Effect.catchReason`; factual limit/status/retry fields belong to their reason |
|
|
175
|
-
| All host reason names reach model output | Thirteen tool failure reasons; original structured fields and bounded call IDs remain in `ToolHost.toolFailures` |
|
|
176
|
-
| `closeChecked` success is `undefined` | Concrete receipt success after the existing ownership check; `close` and `cleanupResult` remain available |
|
|
177
|
-
| Capture `dropped` | `discarded = overflow + late + duplicates + rejected`; no change to default capacity or unknown upstream loss |
|
|
178
|
-
| Non-supervising helper takes default `BrowserSession` | Use `AnySession`; keep supervising helpers generic in callback error or concrete session |
|
|
179
|
-
| Session readiness, target and frame shortcuts | Use `page.ready()`, immutable `page.identity`, `page.listFrames()` and `page.frame(info)`; Session retains `listPages()` and `createPage()` |
|
|
180
|
-
| Transfers called as `(request, page, options)` | Pass the issued Page first: `selectFiles(page, request, options)`, `clickForFileSelection(page, request, options)` and `clickForDownload(page, request, options)` |
|
|
181
|
-
|
|
182
|
-
`resume(token, operatorReleasedControl)` and `reconnect(operatorReleasedControl)` return a
|
|
183
|
-
bounded `Inventory` containing the new owner generation and fresh Page metadata. They do not
|
|
184
|
-
read page content or issue observed references. Acquire an issued Page from `inventory.pages`
|
|
185
|
-
and call `page.observe()` explicitly. A handoff retires Page authority although the connection
|
|
186
|
-
stays open: every Page and Frame issued before `beginHandoff`, `session.initialPage` included,
|
|
187
|
-
belongs to the paused generation, and after `resume` its operations fail `Stale` and
|
|
188
|
-
`undispatched`. Issue each Page again with `session.page(info)` from `inventory.pages`, and its
|
|
189
|
-
Frames with `page.frame(info)`. `detach` returns its pre-disconnection inventory and exact
|
|
190
|
-
selected native `targetId`, preserving bounded target identities for deliberate attachment;
|
|
191
|
-
its Page IDs are old authority and cannot be reused after reconnect. Page metadata is a sequence
|
|
192
|
-
of native reads and is non-atomic.
|
|
193
|
-
|
|
194
|
-
Binding omission defaults are one concurrent call, 64 KiB each direction, 10 seconds and
|
|
195
|
-
`reject-call`; explicit values remain validated. Combined readiness uses the most conservative
|
|
196
|
-
`existingDocuments` policy. Common navigation now accepts optional `timeoutMillis` with the same
|
|
197
|
-
bounds as `startNavigation`; issued Page and Frame operations share it. Observations add
|
|
198
|
-
bounded checked/selected/inputType/required state without field values or destinations. These
|
|
199
|
-
contracts live in the [common browser guide](../browser/README.md).
|
|
200
|
-
|
|
201
|
-
Explicit generic applications of curried `Browser.scoped` use four outer parameters and three
|
|
202
|
-
returned parameters; ordinary inferred syntax is unchanged. Provider resource errors such as
|
|
203
|
-
`SessionError` keep their independent reason schemas. When a control-plane read becomes a browser
|
|
204
|
-
operation failure, the bridge preserves available facts; a resource limit with no measured bound
|
|
205
|
-
becomes `Provider` rather than fabricating `Limit` values. A native connection whose setup fails
|
|
206
|
-
after connection work began carries `unknown` outcome.
|
|
207
|
-
|
|
208
|
-
`test/native/handoff-process.test.ts` exercises that path with a real second process: it starts a fresh runtime that receives only the reference, a target id and fixture addresses, attaches, changes the page and closes as a borrower, and the allocating process then drives the same page and releases it. That is local CDP evidence, not a hosted handoff.
|
|
209
|
-
|
|
210
|
-
## The native engine
|
|
211
|
-
|
|
212
|
-
`browser-binding` names the engine an owned or borrowed browser connects through. The default is Playwright over CDP with the provider's own address, so nothing has to be provided; `BrowserBinding.layer(binding)` supplies another to every browser Layer built beneath it. A binding is opaque and issued by the common runtime, so a value that merely has its shape is refused before anything is allocated.
|
|
213
|
-
|
|
214
|
-
`BrowserBinding.playwright({ resolveEndpoint, onConnected })` is trusted host configuration for running the unmodified Playwright engine somewhere else, such as a local Chromium. `resolveEndpoint` runs only after the provider-issued address passed the default checks, and chooses the CDP endpoint actually connected to; `onConnected` sees the engine's own browser object before the owner drives it. Neither is model-facing, and a local engine reached this way says nothing about how a hosted connection behaves. This package's own native fixtures use exactly this, rather than replacing `chromium.connectOverCDP` globally.
|
|
215
|
-
|
|
216
|
-
## Network policy
|
|
217
|
-
|
|
218
|
-
`Unrestricted` is the only supported `BrowserPolicy.network`, and only when selected by trusted host policy. The adapter package refuses Effect Agent's `ExactHosts` before allocation because Browserbase's `allowedDomains` setting does not prove exact-host containment for redirects, frames, subresources, popups and service workers, and refuses `PublicWeb` because request interception cannot establish connection-time public-address containment. These modes are deliberately not weakened to make them appear supported.
|
|
219
|
+
```ts
|
|
220
|
+
const Lease = Layer.succeed(ContextLease.ContextLease, {
|
|
221
|
+
// Take the row's lock for the scope; say `unsettled` when its holder left it so, or never said.
|
|
222
|
+
hold: (context) => lockRow(context), // Effect<ContextLease.Hold, ContextHeld, Scope>
|
|
223
|
+
});
|
|
224
|
+
```
|
|
219
225
|
|
|
220
|
-
|
|
226
|
+
Three things leave a session that may still be saving to the context: a release left `Unconfirmed`;
|
|
227
|
+
a create whose answer was lost, which may have made a session nobody can see; and a session kept
|
|
228
|
+
past its supervisor's scope. Each leaves the context unsettled, through the lease, and the next
|
|
229
|
+
writer clears it before it writes: it ends the context's sessions, which `open` labels with
|
|
230
|
+
`persistsContext: <context id>` in Browserbase's user metadata, confirms they ended, waits
|
|
231
|
+
`contextSettle` and goes on. While they cannot be confirmed ended it fails with `ContextHeld`, which
|
|
232
|
+
leaves the context unsettled, so the open after it tries again. A lost create's own open ends that
|
|
233
|
+
session, found by its create's nonce, waits `contextSettle`, and then fails with the create's error.
|
|
234
|
+
A lease that outlives its holders' processes reads a holder that ended without saying, as when its
|
|
235
|
+
process was killed, as unsettled too. No context is ever held with no way out, and no two sessions
|
|
236
|
+
write to one context.
|
|
237
|
+
|
|
238
|
+
`reconcile(contextId)` does that clearing without opening a session: the way to end sessions another
|
|
239
|
+
process left running. It holds the context through the lease, so it waits for a writer, and a
|
|
240
|
+
session still running at its deadline makes it `Unconfirmed` and leaves the context unsettled.
|
|
241
|
+
|
|
242
|
+
`verifyContext(contextId, check)` reads a context back, as a login: a session that loads the
|
|
243
|
+
context and saves nothing runs `check` on its browser and is released, and `verifyContext` gives
|
|
244
|
+
what `check` gave. It holds the context meanwhile, so it reads what the last writer saved, once
|
|
245
|
+
that save has settled, and never runs beside a writer that a site could sign out; a session a
|
|
246
|
+
writer before may have left saving to the context is ended first.
|
|
221
247
|
|
|
222
|
-
|
|
248
|
+
```ts
|
|
249
|
+
const signedIn =
|
|
250
|
+
yield *
|
|
251
|
+
Browserbase.verifyContext(contextId, (browser) =>
|
|
252
|
+
Effect.gen(function* () {
|
|
253
|
+
const page = yield* browser.firstPage;
|
|
223
254
|
|
|
224
|
-
|
|
225
|
-
- A paused request the engine cannot match to a network request, or to a frame or service worker it knows, is continued without being offered.
|
|
226
|
-
- Turning routing on disables the HTTP cache for every page it covers, so the policy would change what a page loads, and what a recording of it shows.
|
|
255
|
+
yield* page.goto("https://example.com/account");
|
|
227
256
|
|
|
228
|
-
|
|
257
|
+
return (yield* page.text()).text.includes("Sign out");
|
|
258
|
+
}),
|
|
259
|
+
);
|
|
260
|
+
```
|
|
229
261
|
|
|
230
|
-
|
|
262
|
+
A supervised persisting session reopens through the same path, so a held context is tried again on
|
|
263
|
+
the `reopen` schedule. An open that Browserbase refused outright, as for a bad key, is `Down` at
|
|
264
|
+
once instead, with its cause: a schedule would only hide a configuration error. A supervisor that
|
|
265
|
+
keeps a persisting session leaves its context unsettled, so any other writer ends that session
|
|
266
|
+
first; the next supervisor under its name adopts it without ending it. Under `ContextLease.layer`,
|
|
267
|
+
a later process's other writers can't know it runs: keep one where the next writer is the
|
|
268
|
+
supervisor that adopts it, or under a lease that outlives the process.
|
|
231
269
|
|
|
232
|
-
|
|
270
|
+
## Extensions
|
|
233
271
|
|
|
234
|
-
|
|
272
|
+
Upload a zipped Chrome extension, with `manifest.json` at its root and at most 100 MB, and pass
|
|
273
|
+
its id when creating a session. Loading one makes a session slower to start.
|
|
235
274
|
|
|
236
275
|
```ts
|
|
237
|
-
const
|
|
238
|
-
|
|
276
|
+
const program = Effect.gen(function* () {
|
|
277
|
+
const client = yield* BrowserbaseClient.BrowserbaseClient;
|
|
278
|
+
const extension = yield* client.uploadExtension(archive, { fileName: "my-extension.zip" });
|
|
279
|
+
|
|
280
|
+
yield* work.pipe(Effect.provide(Browserbase.layer({ session: { extensionId: extension.id } })));
|
|
281
|
+
// getExtension reads its name and timestamps; deleteExtension removes it from the project.
|
|
239
282
|
});
|
|
240
283
|
```
|
|
241
284
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
285
|
+
## Testing
|
|
286
|
+
|
|
287
|
+
`effect-browserbase/testing` holds `TestBrowserbase`, the Browserbase API in memory: an `HttpClient`
|
|
288
|
+
under the real `BrowserbaseClient`, so the client and `Browserbase` run as they would against
|
|
289
|
+
Browserbase, for free. Sessions run until released or until their timeout on the Effect `Clock`, so
|
|
290
|
+
`TestClock` ends them. A `Script` loses a create, whose session it can list only `listedAfter` a
|
|
291
|
+
while, leaves a release pending, refuses it, or fails status reads, in turn. `connectUrl` gives
|
|
292
|
+
each session a DevTools address, such as a local Chromium's. It keeps sessions and stored contexts,
|
|
293
|
+
and supplies synthetic Live View links for known session ids, with an empty page list. It does not
|
|
294
|
+
model link expiry or Browserbase's Live View behavior after a session ends. Other endpoints fail
|
|
295
|
+
the test. `sessions` tells each session's status, its original create `options` without provider
|
|
296
|
+
defaults, user metadata, releases asked, whether it was kept alive, and the stored context it
|
|
297
|
+
loaded and whether it saves to it. Its ids are UUIDs, as Browserbase's are, and it answers each id shape as Browserbase
|
|
298
|
+
does: it refuses a session id of any other shape, where it answers an unknown well-formed one as
|
|
299
|
+
not found. `BrowserbaseContract.checks` are what this package relies on Browserbase to do, each an
|
|
300
|
+
Effect over `BrowserbaseClient`; the fake passes them. Its answers also have the shapes
|
|
301
|
+
Browserbase's published API reference gives them, which the package's tests check against a copy
|
|
302
|
+
of it, except that a session has a `connectUrl` only when the script gives one. Give each test a
|
|
303
|
+
`ContextLease.layer` of its own, so no test inherits a context another left unsettled.
|
|
253
304
|
|
|
254
305
|
```ts
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
it.effect("release is requested once and confirmed", () =>
|
|
263
|
-
Effect.gen(function* () {
|
|
264
|
-
const scripted = yield* Testing.ScriptedBrowserbase;
|
|
265
|
-
|
|
266
|
-
const receipt = yield* Browser.scoped(
|
|
267
|
-
BrowserbaseBrowser.open(BrowserPolicy.unrestricted()),
|
|
268
|
-
(session) =>
|
|
269
|
-
Effect.gen(function* () {
|
|
270
|
-
yield* session.initialPage.navigate({ url: "https://shop.test/" });
|
|
271
|
-
|
|
272
|
-
return yield* session.closeChecked;
|
|
273
|
-
}),
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
expect(receipt).toMatchObject({ releaseRequested: true, remote: "confirmed", local: "closed" });
|
|
277
|
-
expect(yield* scripted.provider.sessions).toMatchObject([
|
|
278
|
-
{ id: "session-1", status: "COMPLETED", releaseRequests: 1 },
|
|
279
|
-
]);
|
|
280
|
-
}).pipe(Effect.provide(Testing.layer({ browser: shop }))),
|
|
306
|
+
it.effect("leaves the context unsettled while a release is pending", () =>
|
|
307
|
+
work.pipe(
|
|
308
|
+
Effect.provide(
|
|
309
|
+
Layer.merge(TestBrowserbase.layer({ releases: [{ _tag: "Pending" }] }), ContextLease.layer),
|
|
310
|
+
),
|
|
311
|
+
),
|
|
281
312
|
);
|
|
282
313
|
```
|
|
283
314
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
What a scripted control plane answers is what this repository has read of the provider API, replayed. It establishes that this package handles those answers correctly; it establishes nothing about what Browserbase answers today, which only the hosted checks in [Status](../../docs/STATUS.md) record.
|
|
287
|
-
|
|
288
|
-
## Beyond the browser session
|
|
289
|
-
|
|
290
|
-
These services share the Client and its rules: strict input decoding, identity-checked replies, 1 MiB reply bounds, mutations that are never retried, and typed failures whose `outcome` says whether a request was sent. They are host APIs; none of them is exposed to a model by the adapter package.
|
|
291
|
-
|
|
292
|
-
- `sessions.logs(reference, { includePayloads? })` returns CDP log entries. Protocol payloads are omitted unless requested, because they carry page content, cookies and typed input.
|
|
293
|
-
- `sessions.liveUrls(reference, ttl?)` issues redacted Live View URLs for any session in the project. These URLs are bearer capabilities: Browserbase documents that a holder can watch and control the session. Issuing one does not pause automation; use `beginHandoff` when the owner must suspend its own operations for an operator. See Browserbase's [Session Live URLs API](https://docs.browserbase.com/reference/api/session-live-urls) and [Session Live View guide](https://docs.browserbase.com/platform/browser/observability/session-live-view).
|
|
294
|
-
- `downloads.list(reference, query)` filters by filename, MIME type, size and creation time at the provider. `downloads.delete` first proves that the file belongs to the session.
|
|
295
|
-
- `projects` reads the Client's project and its usage. `certificates` registers proxy CA certificates for `proxySettings.caCertificates`.
|
|
296
|
-
- `search` and `page-fetch` call Browserbase's Search and Fetch APIs. `agents` starts, observes and stops hosted agent runs. `functions` invokes and observes deployed Functions; deployment stays with Browserbase's CLI. `webhooks` manages endpoints and returns signing secrets as `Redacted`.
|
|
297
|
-
|
|
298
|
-
The Live View `ttl` is a requested TTL in seconds. This client accepts positive safe integers from 1 through 21,600 and reports the request as `requestedTtlSeconds`; that field is not a provider-issued expiration receipt. The pinned SDK documents `expiresIn` as optional with a 21,600-second maximum, and says omission lets the URLs expire with the session. The published debug-route schema currently lists the session ID path parameter but omits `expiresIn`. Neither source establishes a provider minimum, so the session-create `timeout` minimum does not apply here and this package does not claim that a one-second request is accepted or honored.
|
|
299
|
-
|
|
300
|
-
Browserbase's read-only embed example applies `pointer-events: none` to the iframe. That disables pointer interaction through that embed; it does not turn the Live View URL into a read-only capability. Keep the URL private to the intended operator. The provider's `debuggerUrl` is its bordered Live View variant, with the Live View navbar shown by default; its `navbar=false` option hides that provider UI, not native browser chrome.
|
|
301
|
-
|
|
302
|
-
An agent run or Function invocation that persists a Context writes it from Browserbase's side, outside `ContextCoordination.withWriter`. It is refused while a local writer holds that Context, but nothing stops a later local writer from racing the hosted run.
|
|
303
|
-
|
|
304
|
-
## Usage observations and allowances
|
|
305
|
-
|
|
306
|
-
`BrowserbaseProjects.usage` returns the Client project's `browserMinutes` and `proxyBytes`, with no billing period or as-of time. The [usage observation example](examples/usage-observation.ts) timestamps each sample with the Effect Clock **after** the decoded reply: “observed by this host,” not “current in Browserbase's ledger.” Reporting lag, adjustments and resets are unspecified, and session times and proxy bytes do not establish billable units.
|
|
307
|
-
|
|
308
|
-
| Resource | Unit and scope | Available source | Period, rounding and remaining unknowns |
|
|
309
|
-
| ----------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
310
|
-
| Browser time | Minutes, one project | `BrowserbaseProjects.usage.browserMinutes`; session timestamps are diagnostic | The API supplies no period or as-of time. [Plan billing](https://docs.browserbase.com/account/billing/plans) rounds per session with a one-minute minimum; whether the API value is already rounded is unspecified. Reporting lag and paid-plan reset date are unknown. |
|
|
311
|
-
| Proxy traffic | Bytes, one project or session | Project usage and `SessionMetadata.proxyBytes` | The API supplies no period or as-of time. Billing rounds MB with a one-MB minimum per session; the mapping from reported bytes to billed units and reporting lag are unspecified. |
|
|
312
|
-
| Search | Calls, plan/account allowance | Host observation of `BrowserbaseSearch.web` only | The [Project Usage API](https://docs.browserbase.com/reference/api/get-project-usage) has no Search counter or billing period. Other writers, failure billing, reporting lag and paid-plan reset date are unknown. |
|
|
313
|
-
| Fetch and Extract | Calls, plan/account allowance; format and proxy choice matter | Host observation of `BrowserbasePageFetch.fetch` only | The Project Usage API has no Fetch or Extract counter or billing period. Provider classification and billing of failed or uncertain calls, other writers, reporting lag and reset date are unknown. |
|
|
314
|
-
| Agents | Calls, plan/account allowance | Host observation of `BrowserbaseAgents.run`; its reply can represent a pending run | The Project Usage API has no Agent counter. Run completion, inference charges, failure billing, other writers, period and reset date are not established by a run reply. |
|
|
315
|
-
| Capacity | Concurrent sessions, session duration, creation rate, Search/Fetch requests per second | Project metadata and [plan limits](https://docs.browserbase.com/account/billing/plans) | These are operational limits, not a consumed monthly allowance or a price. `effect-browser`'s action budget is likewise local policy. |
|
|
316
|
-
|
|
317
|
-
The example's `observe(attempt, journal)` wraps the host's own Search, Fetch or Agent-run Effect, such as `search.web(query)`. Through `Effect.acquireUseRelease` it commits a journal intent with its Clock start time before the POST and settles it on exit; `Effect.onExit` alone cannot make the intent durable. Both writes are bounded at two seconds: a failed or late `begin` stops the call with its error or a `Cause.TimeoutError`, a failed or late `finish` logs a fixed warning, and an unsettled intent stays `unknown`. The deadlines are cooperative, and `finish` must be idempotent. One intent covers one attempt: retrying or recovering inside the wrapped Effect would hide POSTs or mislabel a failure.
|
|
318
|
-
|
|
319
|
-
`undispatched` means not sent, `api-reply` means a decoded reply arrived, and `rejected` and `unknown` mean possibly billed; interruptions and defects are `unknown`. `api-reply` replaces the issue's `confirmed`, which reads as a billing confirmation. Observations store only the project, operation, Fetch's requested format and proxy flags, time and outcome. Spans can export journal failure causes, so the host sanitizes them. Metrics may carry the finite operation and outcome values but are not a ledger.
|
|
320
|
-
|
|
321
|
-
No documented Browserbase API reports allowance consumption, so `assessAllowance` returns `Unknown` unless the host supplies a provider total for the same scope, meter and billing period, or when the period is unknown: 720 observed Fetch replies do not mean “280 of 1,000 left.” The host decodes both at its boundary and verifies a total's provenance through its own billing channel. For an owner's browser-minutes alert, `observedUsageChange` reports the host-observed change between two samples of one project and whether it reached a decoded `BrowserMinutesThreshold`, or `Unknown` after a counter reset.
|
|
322
|
-
|
|
323
|
-
Rates are outside this package's runtime contract. As reviewed on 25 September 2026, the [Startup pricing card](https://www.browserbase.com/pricing) says $1 per 1,000 unproxied Fetch calls after the included quantity, while its comparison table and the [plan guide](https://docs.browserbase.com/account/billing/plans) say $0.50. The repository maintainer should compare those two sources at least monthly and when the [provider changelog](https://www.browserbase.com/changelog) announces a change, notify maintainers through an issue when they disagree or change materially, and check account billing before updating an example. A webpage is not an automatic rate feed; no price or USD calculator is shipped here.
|
|
324
|
-
|
|
325
|
-
## Public entry points
|
|
326
|
-
|
|
327
|
-
- `client` — one immutable account, transport and approved artifact origins.
|
|
328
|
-
- `account` — every resource service on one Client, so credentials are composed once.
|
|
329
|
-
- `allocation` — a scoped session with no browser connected, released by its own scope.
|
|
330
|
-
- `sessions`, `contexts`, `context-coordination` — passive inspection, explicit release, context resources and writer settlement.
|
|
331
|
-
- `extensions` — provisioning a Chrome extension archive once as a durable project resource, and selecting it by reference at launch.
|
|
332
|
-
- `uploads` — placing a file where the running session can already reach it, and the receipt that authorizes attaching it.
|
|
333
|
-
- `launch`, `references`, `session-data`, `cleanup`, `transfers`, `errors` — credential-free schemas and typed expected errors.
|
|
334
|
-
- `browser-binding` — the trusted, opaque native engine a browser connects through: Playwright by default, or Playwright routed to a host-resolved endpoint.
|
|
335
|
-
- `browser` — scoped allocation, borrowed attachment to a running session, deterministic page control, real pointer, wheel and key input, host-only tabs/frames/viewport, modeled file selection, Live View handoff, keep-alive detach and explicit reconnect.
|
|
336
|
-
- `recordings` — post-session Browserbase MP4 assembly, status and bounded retrieval. Stable identity is session + recording page; signed URLs are refreshed and are not durable identity.
|
|
337
|
-
- `replays` — host-authorized replay metadata and validated HLS media proxy material. It is playback access to a recording, not another recording.
|
|
338
|
-
- `downloads` — ordinary website download metadata, provider-side filters, bounded byte streams and deletion, separate from provider recordings.
|
|
339
|
-
- `projects`, `certificates` — project inspection and usage; proxy CA certificate administration.
|
|
340
|
-
- `search`, `page-fetch`, `agents`, `functions`, `webhooks` — the Browserbase platform APIs outside a browser session.
|
|
341
|
-
- `testing` — a scripted control plane as `fetch`, and the real account and browser Layers composed over it and over the scripted engine from `effect-browser/testing`.
|
|
342
|
-
|
|
343
|
-
Every expected failure says three things. `operation` is what you asked for, from a closed vocabulary per error class: `BrowserError` names the browser operations, `SessionError` only session calls, and so on, so you can match on them exhaustively and a misspelling is a type error, not a string that happens to compile. `reason` is why it failed. `outcome`, when present, is the package's dispatch evidence: `undispatched` proves this package did not send the mutation and is safe to retry, `rejected` means a provider response refused it, and `unknown` means dispatch or acceptance cannot be ruled out. None is a billing decision. Mutations with `unknown` outcome are never replayed for you. Why an extension archive was refused is a `reason` (`limit`, `unsafe-filename`, `configuration`) of the one `extension-archive` operation. A native step's own name never appears: the driver raises a private failure, and the owner stamps the operation it admitted.
|
|
344
|
-
|
|
345
|
-
Everything under `src/internal/` is private; the common runtime offers a modeled integration constructor, while no consumer CDP seam is exported; `browser-binding` chooses the engine and where it connects, never what runs over the connection. The driver does hold a CDP session; exposing it, or the ownership internals, would place actions outside their Page's admission and outside the fencing that makes an uncertain outcome detectable. Opening a second debugger connection beside this one has the same effect and is equally unsupported. An unmodeled need is a request for a modeled entry point, not a reason to reach around the boundary.
|
|
346
|
-
|
|
347
|
-
## Artifact and capture guarantees
|
|
348
|
-
|
|
349
|
-
Provider recordings have an independent post-session lifetime. Assembly POST is a mutation; an uncertain POST is reconciled with status before any deliberate retry. Polling is bounded and preserves per-page partial success/failure. BYOS completion without a Browserbase download URL is reported explicitly.
|
|
350
|
-
|
|
351
|
-
Website downloads retain their provider download ID, safe filename, MIME type and declared size. Streaming enforces configured MIME, byte and deadline bounds and verifies the completed byte count. A click result alone is never reported as a completed file. Provider download enumeration is session-wide; concurrent clicks on different Pages do not establish an automatic association between a native download event and a provider download ID.
|
|
352
|
-
|
|
353
|
-
Replay playlists reject arbitrary URI-bearing tags and proxy only indexed media from the validated playlist. API credentials are never returned to a browser client.
|
|
354
|
-
|
|
355
|
-
Live frames and page holds follow the common [capture guarantees](../browser/README.md#live-capture-and-presentation). The provider's recording/replay services retain their independent lifetime after the browser scope ends. A downstream encoder consumes public frame streams; it does not own another browser.
|
|
356
|
-
|
|
357
|
-
## Development and evidence
|
|
315
|
+
## More
|
|
358
316
|
|
|
359
|
-
|
|
317
|
+
The [repository README](https://github.com/mannyc2/effect-agent-browserbase#readme) has an example.
|