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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +250 -317
  2. package/dist/Browserbase-FDltiz36.mjs +542 -0
  3. package/dist/Browserbase-FDltiz36.mjs.map +1 -0
  4. package/dist/Browserbase.d.mts +105 -0
  5. package/dist/Browserbase.mjs +3 -0
  6. package/dist/BrowserbaseClient-CtAQ2rg4.d.mts +195 -0
  7. package/dist/BrowserbaseClient.d.mts +2 -0
  8. package/dist/BrowserbaseClient.mjs +190 -0
  9. package/dist/BrowserbaseClient.mjs.map +1 -0
  10. package/dist/BrowserbaseError-D1EOrLNc.d.mts +79 -0
  11. package/dist/BrowserbaseError.d.mts +2 -0
  12. package/dist/BrowserbaseError.mjs +101 -0
  13. package/dist/BrowserbaseError.mjs.map +1 -0
  14. package/dist/ContextLease.d.mts +42 -0
  15. package/dist/ContextLease.mjs +80 -0
  16. package/dist/ContextLease.mjs.map +1 -0
  17. package/dist/index.d.mts +5 -27
  18. package/dist/index.mjs +5 -26
  19. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  20. package/dist/testing/index.d.mts +134 -0
  21. package/dist/testing/index.mjs +382 -0
  22. package/dist/testing/index.mjs.map +1 -0
  23. package/package.json +51 -121
  24. package/src/Browserbase.ts +698 -0
  25. package/src/BrowserbaseClient.ts +559 -0
  26. package/src/BrowserbaseError.ts +108 -0
  27. package/src/ContextLease.ts +118 -0
  28. package/src/index.ts +7 -0
  29. package/src/internal/captureConnection.ts +214 -0
  30. package/src/testing/BrowserbaseContract.ts +170 -0
  31. package/src/testing/TestBrowserbase.ts +445 -0
  32. package/src/testing/index.ts +7 -0
  33. package/dist/Account.d.mts +0 -32
  34. package/dist/Account.mjs +0 -36
  35. package/dist/Account.mjs.map +0 -1
  36. package/dist/Acquisition-BOABu_qJ.mjs +0 -244
  37. package/dist/Acquisition-BOABu_qJ.mjs.map +0 -1
  38. package/dist/Agents-CPaTOH_g.d.mts +0 -156
  39. package/dist/Agents.d.mts +0 -3
  40. package/dist/Agents.mjs +0 -292
  41. package/dist/Agents.mjs.map +0 -1
  42. package/dist/Allocation.d.mts +0 -97
  43. package/dist/Allocation.mjs +0 -27
  44. package/dist/Allocation.mjs.map +0 -1
  45. package/dist/Authorization-Ll5f_XNM.mjs +0 -12
  46. package/dist/Authorization-Ll5f_XNM.mjs.map +0 -1
  47. package/dist/Browser-ByrWJ0ss.mjs +0 -274
  48. package/dist/Browser-ByrWJ0ss.mjs.map +0 -1
  49. package/dist/Browser.d.mts +0 -83
  50. package/dist/Browser.mjs +0 -3
  51. package/dist/BrowserBinding.d.mts +0 -13
  52. package/dist/BrowserBinding.mjs +0 -31
  53. package/dist/BrowserBinding.mjs.map +0 -1
  54. package/dist/Certificates-D4NQmyeA.d.mts +0 -34
  55. package/dist/Certificates.d.mts +0 -3
  56. package/dist/Certificates.mjs +0 -85
  57. package/dist/Certificates.mjs.map +0 -1
  58. package/dist/Cleanup-DMTDBir8.d.mts +0 -23
  59. package/dist/Cleanup.d.mts +0 -3
  60. package/dist/Cleanup.mjs +0 -43
  61. package/dist/Cleanup.mjs.map +0 -1
  62. package/dist/Client-BEC4Q8ZS.mjs +0 -564
  63. package/dist/Client-BEC4Q8ZS.mjs.map +0 -1
  64. package/dist/Client-B_f6bBLT.d.mts +0 -52
  65. package/dist/Client.d.mts +0 -3
  66. package/dist/Client.mjs +0 -2
  67. package/dist/Connection-sxyxtwlk.mjs +0 -56
  68. package/dist/Connection-sxyxtwlk.mjs.map +0 -1
  69. package/dist/ContextCoordination.d.mts +0 -29
  70. package/dist/ContextCoordination.mjs +0 -94
  71. package/dist/ContextCoordination.mjs.map +0 -1
  72. package/dist/ContextWriter-CiEma24z.mjs +0 -204
  73. package/dist/ContextWriter-CiEma24z.mjs.map +0 -1
  74. package/dist/Contexts-DqdL7cyX.d.mts +0 -31
  75. package/dist/Contexts.d.mts +0 -3
  76. package/dist/Contexts.mjs +0 -97
  77. package/dist/Contexts.mjs.map +0 -1
  78. package/dist/Deadline-CrViaogr.mjs +0 -13
  79. package/dist/Deadline-CrViaogr.mjs.map +0 -1
  80. package/dist/Downloads-CX52GOFJ.d.mts +0 -43
  81. package/dist/Downloads.d.mts +0 -3
  82. package/dist/Downloads.mjs +0 -137
  83. package/dist/Downloads.mjs.map +0 -1
  84. package/dist/Errors.d.mts +0 -100
  85. package/dist/Errors.mjs +0 -194
  86. package/dist/Errors.mjs.map +0 -1
  87. package/dist/Extensions-BTUn9zhK.mjs +0 -231
  88. package/dist/Extensions-BTUn9zhK.mjs.map +0 -1
  89. package/dist/Extensions-DBXuVM1f.d.mts +0 -43
  90. package/dist/Extensions.d.mts +0 -3
  91. package/dist/Extensions.mjs +0 -2
  92. package/dist/Functions-CsKMMUuZ.d.mts +0 -226
  93. package/dist/Functions.d.mts +0 -3
  94. package/dist/Functions.mjs +0 -318
  95. package/dist/Functions.mjs.map +0 -1
  96. package/dist/Issued-DejqoMYL.mjs +0 -15
  97. package/dist/Issued-DejqoMYL.mjs.map +0 -1
  98. package/dist/Launch-cAdE2rrZ.mjs +0 -115
  99. package/dist/Launch-cAdE2rrZ.mjs.map +0 -1
  100. package/dist/Launch.d.mts +0 -127
  101. package/dist/Launch.mjs +0 -97
  102. package/dist/Launch.mjs.map +0 -1
  103. package/dist/PageFetch-CjAM_0DS.d.mts +0 -40
  104. package/dist/PageFetch.d.mts +0 -3
  105. package/dist/PageFetch.mjs +0 -59
  106. package/dist/PageFetch.mjs.map +0 -1
  107. package/dist/Projects-tm5bTDH0.d.mts +0 -38
  108. package/dist/Projects.d.mts +0 -3
  109. package/dist/Projects.mjs +0 -65
  110. package/dist/Projects.mjs.map +0 -1
  111. package/dist/Recordings-B9k5aODB.d.mts +0 -27
  112. package/dist/Recordings.d.mts +0 -3
  113. package/dist/Recordings.mjs +0 -144
  114. package/dist/Recordings.mjs.map +0 -1
  115. package/dist/References-DOH3Wu44.d.mts +0 -34
  116. package/dist/References.d.mts +0 -3
  117. package/dist/References.mjs +0 -33
  118. package/dist/References.mjs.map +0 -1
  119. package/dist/Replays-mV93WGtx.d.mts +0 -27
  120. package/dist/Replays.d.mts +0 -3
  121. package/dist/Replays.mjs +0 -107
  122. package/dist/Replays.mjs.map +0 -1
  123. package/dist/Resource-J7Kkk5mu.mjs +0 -54
  124. package/dist/Resource-J7Kkk5mu.mjs.map +0 -1
  125. package/dist/Search-B64e7g-h.d.mts +0 -37
  126. package/dist/Search.d.mts +0 -3
  127. package/dist/Search.mjs +0 -46
  128. package/dist/Search.mjs.map +0 -1
  129. package/dist/SessionData-5xYZDshJ.d.mts +0 -38
  130. package/dist/SessionData.d.mts +0 -3
  131. package/dist/SessionData.mjs +0 -48
  132. package/dist/SessionData.mjs.map +0 -1
  133. package/dist/Sessions-D2H5m6rF.mjs +0 -260
  134. package/dist/Sessions-D2H5m6rF.mjs.map +0 -1
  135. package/dist/Sessions-oJEUmZth.d.mts +0 -69
  136. package/dist/Sessions.d.mts +0 -4
  137. package/dist/Sessions.mjs +0 -3
  138. package/dist/Testing.d.mts +0 -119
  139. package/dist/Testing.mjs +0 -206
  140. package/dist/Testing.mjs.map +0 -1
  141. package/dist/TransferPolicy-DQ06i7SZ.mjs +0 -25
  142. package/dist/TransferPolicy-DQ06i7SZ.mjs.map +0 -1
  143. package/dist/Transfers-GVqzrDWl.d.mts +0 -89
  144. package/dist/Transfers.d.mts +0 -3
  145. package/dist/Transfers.mjs +0 -85
  146. package/dist/Transfers.mjs.map +0 -1
  147. package/dist/Uploads-C2_bACgv.d.mts +0 -28
  148. package/dist/Uploads.d.mts +0 -3
  149. package/dist/Uploads.mjs +0 -105
  150. package/dist/Uploads.mjs.map +0 -1
  151. package/dist/Webhooks-BoeRAxzG.d.mts +0 -59
  152. package/dist/Webhooks.d.mts +0 -3
  153. package/dist/Webhooks.mjs +0 -134
  154. package/dist/Webhooks.mjs.map +0 -1
  155. package/dist/WriterFacts-BBJRDZYY.d.mts +0 -54
package/README.md CHANGED
@@ -1,359 +1,292 @@
1
- # Browserbase for Effect
1
+ # effect-browserbase
2
2
 
3
- `effect-browserbase` supplies Browserbase resources and hosted lifetimes to the shared browser runtime: account identity and transport, session, context and extension resources, one owned browser over Playwright/CDP — allocated or borrowed — with trusted registrations, modeled file selection, bounded live capture, explicit page holds, the provider's recordings, replays, uploads and website downloads, and the platform APIs outside a browser session (projects, certificates, Search, Fetch, Agents, Functions and webhooks).
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
- Its host supplies the required `effect-browser` peer, at the same coordinated `0.2.0-beta.9` version; it has no Effect Agent dependency. Install both packages explicitly so the provider and application share one runtime instance. Playwright is an optional peer of the shared runtime and is loaded only when a browser connects; artifact-only consumers do not need it. The Effect peer is exactly `4.0.0-rc.117`, the tested version: later Effect releases moved the `effect/unstable/*` modules this package imports. Releases through `0.2.0-beta.8` declared a caret range; with one of them, install exactly the Effect it was tested with (`4.0.0-rc.117` from `0.2.0-beta.3`, `4.0.0-rc.115` before). The package targets trusted Node and Bun hosts. It is published as a beta, on the `beta` dist-tag: hosted provider behavior requires separate validation, and ordinary tests never create a paid Browserbase session or invoke a paid model.
6
-
7
- ## One account, then resources
6
+ ```sh
7
+ npm install effect-browserbase@beta effect-browser@beta effect playwright-core
8
+ ```
8
9
 
9
- Construct one account Layer and provide it to everything else. Credentials, approved artifact origins and request bounds live there and nowhere else; a browser recipe cannot carry them.
10
+ - `Browserbase`: `open` and `layer` create a session and release it when their scope closes, so
11
+ billing stops then rather than at the session's timeout, and closing it waits for the release.
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
- import * as Account from "effect-browserbase/account";
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
+ );
36
+ ```
13
37
 
14
- // Reads BROWSERBASE_PROJECT_ID and a redacted BROWSERBASE_API_KEY from the active
15
- // ConfigProvider (the environment by default) when the Layer is built.
16
- const account = Account.layerConfig({
17
- // Your project's own recording-delivery origin; see below for how to find it.
18
- artifactOrigins: ["https://recording-delivery.example"],
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`. A lost session is published at once, `Lost` with its cause, and replaced on the
83
+ `reopen` schedule; the next session opens `rotateBefore` ahead of the current browser's
84
+ `expiresAt`, or on `rotate`, before the current one is released. `states` reports each session's release outcome. Sessions that
85
+ persist to a stored context are exclusive: a rotation releases the current session, and lets its
86
+ save settle, before the next one opens. An open Browserbase refused, as for a bad key or an invalid
87
+ request, is `Down` at once with its cause, since no new try mends it; everything else, a held
88
+ context included, is tried again on the schedule.
89
+
90
+ ```ts
91
+ const program = Effect.gen(function* () {
92
+ const sessions = yield* Browserbase.supervise({
93
+ session: { timeout: 6 * 60 * 60 },
94
+ reopen: Schedule.exponential("1 second").pipe(Schedule.upTo({ duration: "5 minutes" })),
95
+ rotateBefore: "20 minutes",
96
+ });
97
+ const browser = yield* sessions.browser;
98
+ // `retire` releases every session; so does the scope's close.
19
99
  });
20
100
  ```
21
101
 
22
- `Account.layer({ projectId, apiKey: Redacted.make(key), ... })` takes the same authority explicitly. Either one bundles every resource service on a single Client, so credentials are composed once instead of per service; each service still exports its own `layer` when you want a narrower set. `BrowserbaseBrowser.layer` stays separate, because a browser also fixes budgets and a connection lifetime that an account does not. It also requires Effect's `Crypto` from the host platform (`NodeServices.layer`, `BunServices.layer` or `NodeCrypto.layer`), which supplies allocation attempt ids, connection ids and handoff tokens; the account services need no `Crypto`. The Client uses Effect's `FetchHttpClient.Fetch` reference, which already defaults to `globalThis.fetch`; provide a different `fetch` only when you need one.
102
+ ### Keeping a session past the scope
23
103
 
24
- `artifactOrigins` is the exact set of HTTPS origins this client will fetch provider media from, and it is deliberately empty by default: recording downloads and replay media are refused with `unsafe-url` until you approve a host. Browserbase documents only a "signed CDN URL" and does not publish that origin, so discover your own rather than copying anyone's — request a recording for a completed session, read `downloadUrl` from `GET /v1/sessions/{id}/recording/downloads`, and approve exactly its origin. A hosted run on 21 September 2026 confirmed that a completed recording then downloads through this check rather than around it. The origin is an observation and not a contract: it may differ by project or region, the provider can re-point it without notice, and a BYOS project returns no signed URL at all. Treat a later `unsafe-url` as the delivery host having moved, not as a defect.
104
+ With `keep`, a name, closing the supervisor's scope leaves its current session running instead of
105
+ releasing it, published as `Kept`, and the first generation of the next `supervise` under the same
106
+ name adopts that session, its pages and their ids with it, rather than creating one. So a deploy or
107
+ a restart of the process that owns the browser keeps its prepared pages and its signed-in state.
108
+ It is off by default, and opt-in, because a kept session bills until something adopts and releases
109
+ it, or until its own `timeout` ends it: set one that bounds what a forgotten session costs.
25
110
 
26
- `sessions.retrieve`, `list`, `waitUntilRunning` and `waitForTerminal` are passive. `sessions.requestRelease` is an explicit remote mutation and is never issued as a side effect of reading. Context create/retrieve/delete are separate resource operations; deleting a Context is never a browser finalizer. Mutating control-plane requests are never retried automatically, and a rejected request stays distinguishable from one whose effect is unknown.
111
+ - Each session is created with `keepAlive`, so it outlives its connection, and labelled
112
+ `keptAs: <name>` in its user metadata, since Browserbase sets user metadata only at the create.
113
+ A name is 1 to 64 letters, digits, `_`, `.`, `:` or `-`; another is refused, `Down` at once.
114
+ - Adopting takes the newest session kept under the name that saves to the same stored context, or
115
+ to none; any other session kept under the name is ended, as nothing else adopts it.
116
+ - `retire` always releases, `keep` or not. Only the first generation adopts: a later one, after a
117
+ loss or a rotation, is a new session.
118
+ - One supervisor at a time per name: two at once would adopt one session together.
27
119
 
28
- `extensions.register` is the same kind of explicit resource operation. The archive is inspected before it is sent — a root `manifest.json`, no rooted or `..` members, bounded entries, names and declared size — and nothing is decompressed or written; the provider remains the loader of record. A launch recipe then selects the provisioned `extension` by reference. That qualified reference is the only spelling — the compiler alone projects it onto the provider's `extensionId` — so an unqualified identifier cannot be passed through unchecked and a foreign project is refused before allocation.
120
+ ```ts
121
+ const sessions = yield * Browserbase.supervise({ keep: "on-air", session: { timeout: 60 * 60 } });
122
+ const browser = yield * sessions.browser; // the session the last process kept, if one runs
123
+ ```
29
124
 
30
- `ContextCoordination.withWriter` accepts a consumer-owned distributed lease backend and preserves the consumer's Effect error and environment types. A persisting allocation authenticates that live permit and reports its exact attempt, session and cleanup receipt before settlement. Unknown writers and unconfirmed persistence are quarantined. A terminal session does not establish that Context data finished synchronizing.
125
+ ## Session settings
31
126
 
32
- A quarantined Context stays refused in the process until `ContextCoordination.reconcile(reference, readback)` succeeds. It reads the provider's status for every session the quarantined writer recorded, requires each to be terminal, then runs your readback of the Context, and only then admits a writer again. It refuses a live writer, and no writer is admitted while it runs. It never ends a session: request the release yourself, wait for it to end, then reconcile. An allocation whose outcome stayed unknown names no session to check, so its Context stays refused until the process ends. Report the reconciliation to your backend afterwards: its own quarantine is separate. A lease `acquire` that fails leaves the Context free locally, because no allocation was attempted; the backend resolves whatever that failure left uncertain.
127
+ `session` takes Browserbase's session create request. Anything left unset takes Browserbase's
128
+ default, and this package changes none of them. Browserbase records every session for replay, keeps
129
+ its logs and solves captchas unless told not to:
33
130
 
34
- ## Start with the result you need
131
+ ```ts
132
+ Browserbase.layer({
133
+ session: { browserSettings: { recordSession: false, logSession: false, solveCaptchas: false } },
134
+ });
135
+ ```
35
136
 
36
- For the simplest post-session video path, opt in to Browserbase recording in the launch recipe:
137
+ Pages open at Browserbase's default viewport, which was 2560×1440 on 7 October 2026: four times
138
+ the pixels of `Chromium.layer`'s 1280×720. `browserSettings.viewport` sets another.
139
+
140
+ ## The capture connection
141
+
142
+ A page's screencast runs on a second connection to the session that carries nothing else. On the
143
+ connection Playwright drives the pages over, a frame and its acknowledgement would wait behind any
144
+ large message, such as an upload or a read's answer, because Browserbase refuses compression: on a
145
+ hosted session at 1280×720, while another tab read and uploaded, the on-air page's longest wait
146
+ between frames was 1,802 ms on that connection and 352 ms on its own (8 October 2026).
147
+
148
+ The connection is raw CDP, with no Playwright, and read-only: a page's session on it starts,
149
+ acknowledges and stops the screencast, and turns on the Page domain and reads the frame tree, so
150
+ that each frame carries the document its own connection saw commit, numbered as `Navigated`
151
+ numbers them. A page's first capture there costs three round trips, run beside the control
152
+ connection's own: the attach, then the Page domain and the frame tree together, then the start.
153
+ The browser's first capture also opens the connection. A later capture of the page costs the start
154
+ alone, and on the control connection a capture costs one call fewer. The page's own session keeps the focus
155
+ emulation that keeps a tab behind painting: a session that held it too would blur the page as it
156
+ went. The connection opens with the first capture and closes with the browser. If it fails alone,
157
+ the capture ends with `Failed` two seconds on, the page and its own session stay as they were, and
158
+ the next capture opens another. If the browser is lost too, the capture's readers are told the
159
+ browser's loss, `Closed` by its cause, whichever connection hears the end first. A wait for a
160
+ still screen, `ready({ quietMillis })`, ends with one round trip on this connection,
161
+ `Page.getFrameTree`, whose answer comes behind any frame still on its way, so a stall here is not
162
+ read as stillness; it costs the control connection nothing. `captureConnection: false` keeps
163
+ captures on each page's own session; so does a DevTools server's `http:` address, such as a local
164
+ Chromium's in tests.
165
+
166
+ ## Stored contexts
167
+
168
+ A context keeps cookies, storage and cache between sessions. A session that loads it with
169
+ `persist: true` saves its browser state back to it when the session ends; without `persist` it
170
+ only reads.
37
171
 
38
172
  ```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
173
  const program = Effect.gen(function* () {
60
- const recordings = yield* BrowserbaseRecordings;
61
-
62
- const reference = yield* Browser.scoped(BrowserbaseBrowser.open(policy), (session) =>
63
- Effect.gen(function* () {
64
- yield* session.initialPage.navigate(NavigateRequest.make({ url: "https://example.com" }));
174
+ const client = yield* BrowserbaseClient.BrowserbaseClient;
175
+ const { id } = yield* client.createContext({ name: "signed-in" });
65
176
 
66
- // Browser.scoped checks the owner's release before returning this durable identity.
67
- return session.reference;
68
- }),
177
+ yield* signIn.pipe(
178
+ Effect.provide(
179
+ Browserbase.layer({ session: { browserSettings: { context: { id, persist: true } } } }),
180
+ ),
69
181
  );
70
-
71
- yield* recordings.request(reference);
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));
182
+ // Later sessions on the same context start signed in.
183
+ });
86
184
  ```
87
185
 
88
- The launch recipe deliberately differs from three Browserbase defaults. Set a field explicitly to get the provider's behavior; recording cannot be enabled retroactively after allocation.
89
-
90
- | Setting | Browserbase default | This package | Why |
91
- | --------------- | ------------------- | ------------ | ------------------------------------------------- |
92
- | `recordSession` | on | off | page video is sensitive; `Recordings` needs it on |
93
- | `logSession` | on | off | CDP logs carry page content and typed input |
94
- | `solveCaptchas` | on | off | solving is an action the host should choose |
95
-
96
- For model-driven control, use `effect-agent-browser`, whose tools borrow an already-owned session rather than allocating a browser per tool call. Provider credentials, CDP URLs, context choices, Live View controls and recording configuration are host decisions and are never Tool parameters.
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.
165
-
166
- ## Shared API migration
186
+ Two sessions saving to one context at once can lose one's changes, and sites may sign a session out
187
+ when another uses its login. So a persisting `open` holds the context through the `ContextLease`,
188
+ and another writer waits. It takes the hold before it creates the session, and the writer's release
189
+ keeps it until Browserbase reports the session ended and then `contextSettle` (10 seconds by
190
+ default) longer, because the save lands after the session ends and Browserbase does not
191
+ acknowledge it. `ContextLease.layer` excludes writers in this process. To exclude writers in other
192
+ processes, provide a `ContextLease` of your own, such as an advisory lock in your database: `hold`
193
+ holds a context for a scope, waiting while another holds it, and passes on how the writer before
194
+ left it. `attach` never holds a context. `deleteContext` is permanent.
167
195
 
168
- | Before | Now |
169
- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
170
- | Session action calls or retained/pinned wrappers | Use `session.initialPage` or `yield* session.page(info)` for Page operations and `yield* page.frame(info)` for Frame operations; display selection never retargets issued authority |
171
- | Bare page IDs in selection/closure | Pass an issued Page to `session.selectPage(page)` for display selection, and close exact issued authority with `page.close()` |
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.
196
+ ```ts
197
+ const Lease = Layer.succeed(ContextLease.ContextLease, {
198
+ // Take the row's lock for the scope; say `unsettled` when its holder left it so, or never said.
199
+ hold: (context) => lockRow(context), // Effect<ContextLease.Hold, ContextHeld, Scope>
200
+ });
201
+ ```
219
202
 
220
- ### Why there is no request-admission hook
203
+ Three things leave a session that may still be saving to the context: a release left `Unconfirmed`;
204
+ a create whose answer was lost, which may have made a session nobody can see; and a session kept
205
+ past its supervisor's scope. Each leaves the context unsettled, through the lease, and the next
206
+ writer clears it before it writes: it ends the context's sessions, which `open` labels with
207
+ `persistsContext: <context id>` in Browserbase's user metadata, confirms they ended, waits
208
+ `contextSettle` and goes on. While they cannot be confirmed ended it fails with `ContextHeld`, which
209
+ leaves the context unsettled, so the open after it tries again. A lost create's own open ends that
210
+ session, found by its create's nonce, waits `contextSettle`, and then fails with the create's error.
211
+ A lease that outlives its holders' processes reads a holder that ended without saying, as when its
212
+ process was killed, as unsettled too. No context is ever held with no way out, and no two sessions
213
+ write to one context.
214
+
215
+ `reconcile(contextId)` does that clearing without opening a session: the way to end sessions another
216
+ process left running. It holds the context through the lease, so it waits for a writer, and a
217
+ session still running at its deadline makes it `Unconfirmed` and leaves the context unsettled.
218
+
219
+ `verifyContext(contextId, check)` reads a context back, as a login: a session that loads the
220
+ context and saves nothing runs `check` on its browser and is released, and `verifyContext` gives
221
+ what `check` gave. It holds the context meanwhile, so it reads what the last writer saved, once
222
+ that save has settled, and never runs beside a writer that a site could sign out; a session a
223
+ writer before may have left saving to the context is ended first.
221
224
 
222
- A hook that lets a host allow or deny each request is worth having only if it sees every request it claims to cover. The owner drives the browser through the engine's one connection, where the available interception is Playwright routing, and in the pinned Playwright 1.63.0 that does not see them all:
225
+ ```ts
226
+ const signedIn =
227
+ yield *
228
+ Browserbase.verifyContext(contextId, (browser) =>
229
+ Effect.gen(function* () {
230
+ const page = yield* browser.firstPage;
223
231
 
224
- - A redirected request is continued by the engine itself and is never offered to a route handler. A hook would decide the first hop of a chain and none of the rest.
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.
232
+ yield* page.goto("https://example.com/account");
227
233
 
228
- A second interception client on the same targets would compete with the engine for the same paused requests, and is the raw-protocol side channel the single owner exists to rule out. So this package offers no hook, rather than one that covers less than it appears to.
234
+ return (yield* page.text()).text.includes("Sign out");
235
+ }),
236
+ );
237
+ ```
229
238
 
230
- Complete URL-level admission would still not be `PublicWeb`. A URL names a host. The address is chosen afterwards, by whichever resolver the connecting browser or proxy uses, so a lookup the host makes beforehand says nothing about the connection that follows.
239
+ A supervised persisting session reopens through the same path, so a held context is tried again on
240
+ the `reopen` schedule. An open that Browserbase refused outright, as for a bad key, is `Down` at
241
+ once instead, with its cause: a schedule would only hide a configuration error. A supervisor that
242
+ keeps a persisting session leaves its context unsettled, so any other writer ends that session
243
+ first; the next supervisor under its name adopts it without ending it. Under `ContextLease.layer`,
244
+ a later process's other writers can't know it runs: keep one where the next writer is the
245
+ supervisor that adopts it, or under a lease that outlives the process.
231
246
 
232
- ### The boundary that can enforce it
247
+ ## Extensions
233
248
 
234
- Containment has to be enforced where the session's connections are made: an egress point beneath every page, frame, worker and socket, which sees each connection's real target and resolves names itself. On this provider that is a proxy the host operates, selected for the whole session at launch:
249
+ Upload a zipped Chrome extension, with `manifest.json` at its root and at most 100 MB, and pass
250
+ its id when creating a session. Loading one makes a session slower to start.
235
251
 
236
252
  ```ts
237
- const launch = recipe({
238
- provider: { proxies: [{ type: "external", server, username, password }] },
253
+ const program = Effect.gen(function* () {
254
+ const client = yield* BrowserbaseClient.BrowserbaseClient;
255
+ const extension = yield* client.uploadExtension(archive, { fileName: "my-extension.zip" });
256
+
257
+ yield* work.pipe(Effect.provide(Browserbase.layer({ session: { extensionId: extension.id } })));
258
+ // getExtension reads its name and timestamps; deleteExtension removes it from the project.
239
259
  });
240
260
  ```
241
261
 
242
- `username` and `password` are `Redacted`, and a `server` URL that carries credentials is refused before allocation. The rule is listed alone and has no `domainPattern`: the provider applies the first rule that matches, so a rule ahead of it, or a pattern on it, is a way round the proxy. Exact hosts, public addresses, redirects and child pages are then the proxy's decisions, made per connection.
243
-
244
- A host that does this still selects `Unrestricted` here, and the containment claim is the host's own, made at its proxy. This package keeps refusing `ExactHosts` and `PublicWeb`, because it has no evidence that every connection of a hosted session takes that proxy. The provider documents how rules are ordered, not whether WebRTC, QUIC or name resolution go through one, and no hosted run in this repository has tested it. Accepting either policy needs that evidence first: a hosted session behind one catch-all proxy, exercised through navigation, redirects, frames, subresources, popups, workers, service workers, WebSockets and non-HTTP transports, with nothing arriving anywhere but the proxy.
245
-
246
- What this package does check is input, not requests. `controlFacts` and an `admit` policy ([common browser guide](../browser/README.md#what-is-on-screen-and-what-a-host-may-know-about-it)) let a host that drives the session refuse a link or a form by its resolved destination before anything is sent. That limits what the host's own automation acts on. It does not limit what a page loads, or where it redirects.
247
-
248
- ## Testing against a scripted control plane
249
-
250
- `effect-browserbase/testing` supplies the provider side of a test the way `effect-browser/testing` supplies the browser side. `Testing.provider(script)` returns a `fetch` for `FetchHttpClient.Fetch` and a `ProviderControl` handle; the real `Client`, `Sessions`, `Uploads`, allocation and cleanup code parse its replies, apply their bounds and reconcile release against it. The script says how creation answers (`Accept`, `Reject` with a status the transport classifies and an optional `retryAfterMillis`, `Malformed`, or `Lost` after the request was sent, so the allocation outcome is unknown), whether a release request is `confirmed`, stays `pending` or fails, whether a Live View is issued, and whose identity a session's retrieval and release replies carry (`own`, `foreign-session` or `foreign-project`). Its secrets are marker strings, so evidence can be grepped for a leak, and nothing in it is a credential.
251
-
252
- `Testing.layer({ browser, provider?, launch?, options? })` composes that control plane under the real `Account.layer` and `BrowserbaseBrowser.layer`, with the scripted engine from `effect-browser/testing` as the `BrowserBinding` and its `sequentialCrypto` as the browser's `Crypto`, and adds a `ScriptedBrowserbase` service whose `provider` is the control handle and whose `browsers` are the engine control handles, one per provider session: a keep-alive reconnection or a borrowed attachment reaches the same scripted browser again, with the pages it kept. An application written against `BrowserbaseBrowser.open` runs unchanged:
262
+ ## Testing
263
+
264
+ `effect-browserbase/testing` holds `TestBrowserbase`, the Browserbase API in memory: an `HttpClient`
265
+ under the real `BrowserbaseClient`, so the client and `Browserbase` run as they would against
266
+ Browserbase, for free. Sessions run until released or until their timeout on the Effect `Clock`, so
267
+ `TestClock` ends them. A `Script` loses a create, whose session it can list only `listedAfter` a
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
273
+ does: it refuses a session id of any other shape, where it answers an unknown well-formed one as
274
+ not found. `BrowserbaseContract.checks` are what this package relies on Browserbase to do, each an
275
+ Effect over `BrowserbaseClient`; the fake passes them. Its answers also have the shapes
276
+ Browserbase's published API reference gives them, which the package's tests check against a copy
277
+ of it, except that a session has a `connectUrl` only when the script gives one. Give each test a
278
+ `ContextLease.layer` of its own, so no test inherits a context another left unsettled.
253
279
 
254
280
  ```ts
255
- import { expect, it } from "@effect/vitest";
256
- import { Effect } from "effect";
257
- import * as Browser from "effect-browser/browser";
258
- import { BrowserPolicy } from "effect-browser/browser-data";
259
- import { BrowserbaseBrowser } from "effect-browserbase/browser";
260
- import * as Testing from "effect-browserbase/testing";
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 }))),
281
+ it.effect("leaves the context unsettled while a release is pending", () =>
282
+ work.pipe(
283
+ Effect.provide(
284
+ Layer.merge(TestBrowserbase.layer({ releases: [{ _tag: "Pending" }] }), ContextLease.layer),
285
+ ),
286
+ ),
281
287
  );
282
288
  ```
283
289
 
284
- Sessions are numbered `session-1`, `session-2`, … in creation order. A `pending` release leaves the session `RUNNING`, so checked close fails `Provider` with outcome `unknown` after the real status bound elapses, and the receipt carries `remote: "pending"`; under `it.effect` a `TestClock.adjust` reaches that bound with nothing real elapsing. A `Lost` creation makes `onAllocationUncertain` fire once, as it does when a real request is cut off. A `foreign-session` or `foreign-project` identity makes connection fail `Malformed` before any page is reached, and no reply about the other session can confirm this one's release, so its receipt carries `remote: "unknown"`. Borrowed attachment through `attach` makes a second connection to the owner's scripted browser, which its control lists under `connections`, and no release request. `Allocation.scoped`, `BrowserbaseSessions` and the other resource services run over the same `fetch` with `Testing.provider()` alone, with no browser at all; `Allocation.scoped` also takes a `Crypto`, and `sequentialCrypto` from `effect-browser/testing` keeps its attempt ids predictable.
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
290
+ ## More
358
291
 
359
- Read [Contributing](../../CONTRIBUTING.md) for the frozen compatibility workspace and required checks. Provider unit tests cover HTTP/resources and release reconciliation; `test/testing.test.ts` runs the public scripted control plane and Layer through the real allocation and cleanup code. Provider regressions that need a browser, such as release reconciliation around a held teardown or a keep-alive reconnection, run over the same public entries in this package's `test/`. The `generic` and `agent-hosted` installed consumers run the maintained native suites against real Chromium with scripted provider HTTP; the `resources` consumer installs no Playwright or Effect Agent, and it also opens scripted browsers through both testing entry points from the packed tarballs. Hosted evidence remains separately recorded in [Status](../../docs/STATUS.md) and never inferred from a local pass.
292
+ The [repository README](https://github.com/mannyc2/effect-agent-browserbase#readme) has an example.