@effect-agent/platform-cloudflare 0.1.0-beta.122 → 0.1.0-beta.124

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 (34) hide show
  1. package/dist/BrowserCredentials.d.mts +91 -0
  2. package/dist/BrowserCredentials.mjs +120 -0
  3. package/dist/BrowserCredentials.mjs.map +1 -0
  4. package/dist/BrowserSession-DuOUcC6G.mjs +442 -0
  5. package/dist/BrowserSession-DuOUcC6G.mjs.map +1 -0
  6. package/dist/BrowserSession-DxCN-5F9.d.mts +109 -0
  7. package/dist/BrowserSession.d.mts +3 -0
  8. package/dist/BrowserSession.mjs +4 -0
  9. package/dist/CloudflareThreadClient.d.mts +48 -48
  10. package/dist/{InteractiveBrowser-Cm4xgiGy.d.mts → InteractiveBrowser-BnqY_Y-F.d.mts} +2 -2
  11. package/dist/{InteractiveBrowser-DXwmvWdA.mjs → InteractiveBrowser-DeOlcu-2.mjs} +4 -5
  12. package/dist/InteractiveBrowser-DeOlcu-2.mjs.map +1 -0
  13. package/dist/InteractiveBrowser.d.mts +1 -1
  14. package/dist/InteractiveBrowser.mjs +1 -1
  15. package/dist/{ThreadObject-mt0uxvZZ.d.mts → ThreadObject-QBlTByAB.d.mts} +32 -32
  16. package/dist/ThreadObject.d.mts +1 -1
  17. package/dist/index.d.mts +4 -2
  18. package/dist/index.mjs +3 -1
  19. package/package.json +1 -1
  20. package/src/BrowserCredentials.ts +146 -0
  21. package/src/BrowserSession.ts +589 -0
  22. package/src/index.ts +2 -0
  23. package/src/internal/browser-binding.ts +3 -3
  24. package/src/internal/browser-credentials.ts +362 -0
  25. package/dist/InteractiveBrowser-DXwmvWdA.mjs.map +0 -1
  26. package/dist/ProtectedBrowser.d.mts +0 -142
  27. package/dist/ProtectedBrowser.mjs +0 -1242
  28. package/dist/ProtectedBrowser.mjs.map +0 -1
  29. package/src/ProtectedBrowser.ts +0 -9
  30. package/src/protected-browser/binding.ts +0 -445
  31. package/src/protected-browser/host.ts +0 -398
  32. package/src/protected-browser/inspect-frame.ts +0 -140
  33. package/src/protected-browser/native.ts +0 -483
  34. package/src/protected-browser/policy.ts +0 -872
@@ -0,0 +1,589 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ import {
4
+ Cause,
5
+ Clock,
6
+ Context,
7
+ Effect,
8
+ Exit,
9
+ Layer,
10
+ Redacted,
11
+ Schema,
12
+ Semaphore,
13
+ Scope,
14
+ } from "effect";
15
+ import type { Page } from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
16
+
17
+ import {
18
+ CredentialFillError,
19
+ type BrowserCredentialAccess,
20
+ type CredentialFillResult,
21
+ type FillCredentialRequest,
22
+ } from "./BrowserCredentials.ts";
23
+ import {
24
+ BrowserRunHandoffRequest,
25
+ BrowserRunHandoffResult,
26
+ BrowserRunHandoffState,
27
+ BrowserRunLiveViewRequest,
28
+ BrowserRunLiveViewResult,
29
+ } from "./InteractiveBrowser.ts";
30
+ import { BrowserRunBinding, type BrowserRunAttachment } from "./internal/browser-binding.ts";
31
+ import { BrowserSessionPage, fillCredential } from "./internal/browser-credentials.ts";
32
+ import { reportBrowserCause, reportedBrowserError } from "./internal/browser-failure.ts";
33
+ import {
34
+ BrowserRunSessionLifecycle,
35
+ type BrowserRunLifecycleOptions,
36
+ } from "./internal/browser-session-lifecycle.ts";
37
+
38
+ export {
39
+ BrowserRunHandoffRequest,
40
+ BrowserRunHandoffResult,
41
+ BrowserRunHandoffState,
42
+ BrowserRunLiveViewRequest,
43
+ BrowserRunLiveViewResult,
44
+ };
45
+
46
+ const PositiveMillis = Schema.Int.check(
47
+ Schema.isGreaterThan(0),
48
+ Schema.isLessThanOrEqualTo(Number.MAX_SAFE_INTEGER),
49
+ );
50
+
51
+ const SessionId = Schema.Redacted(Schema.String.check(Schema.isUUID()));
52
+ const ProviderId = Schema.Redacted(Schema.NonEmptyString.check(Schema.isMaxLength(256)));
53
+
54
+ /** Private host record, not a model capability. Persist together with the owner's controller state. */
55
+ export class BrowserSessionReference extends Schema.Class<BrowserSessionReference>(
56
+ "BrowserSessionReference",
57
+ )({
58
+ version: Schema.Literal(1),
59
+ sessionId: SessionId,
60
+ contextId: ProviderId,
61
+ targetId: ProviderId,
62
+ expiresAt: PositiveMillis,
63
+ commandTimeoutMillis: PositiveMillis,
64
+ }) {}
65
+
66
+ export class BrowserSessionOptions extends Schema.Class<BrowserSessionOptions>(
67
+ "BrowserSessionOptions",
68
+ )({
69
+ maxElapsedMillis: PositiveMillis,
70
+ keepAliveMillis: Schema.optionalKey(PositiveMillis.check(Schema.isLessThanOrEqualTo(600_000))),
71
+ commandTimeoutMillis: Schema.optionalKey(PositiveMillis),
72
+ }) {}
73
+
74
+ /** No SDK text, selectors, page data, or credential values are retained in public failures. */
75
+ export class BrowserSessionError extends Schema.TaggedError<BrowserSessionError>()(
76
+ "BrowserSessionError",
77
+ {
78
+ reason: Schema.Literals([
79
+ "invalid",
80
+ "expired",
81
+ "missing-page",
82
+ "busy",
83
+ "closed",
84
+ "provider",
85
+ "timeout",
86
+ "cleanup",
87
+ ]),
88
+ dispatch: Schema.Literals(["not-dispatched", "possibly-dispatched"]),
89
+ cleanup: Schema.Literals(["not-requested", "confirmed", "unconfirmed"]),
90
+ },
91
+ ) {}
92
+
93
+ /**
94
+ * One local attachment to an application-owned browser. Scope release disconnects; it never
95
+ * transfers ownership or closes a healthy remote session. The owner serializes attachments,
96
+ * fences old attempts, authorizes human access, and calls BrowserSessions.close on expiry/stop.
97
+ * Native callbacks are trusted host code: never retain SDK handles or start unawaited work.
98
+ */
99
+ export interface BrowserSession {
100
+ readonly reference: BrowserSessionReference;
101
+ /**
102
+ * Check current authority under the lock before native dispatch. A settled SDK rejection leaves
103
+ * the session available for inspection, but may have changed the website; never retry blindly.
104
+ * Timeout/interruption fences outstanding SDK work and terminates the exact browser.
105
+ */
106
+ readonly run: <A, E, R>(
107
+ authorize: Effect.Effect<void, E, R>,
108
+ action: (page: Page) => Promise<A>,
109
+ ) => Effect.Effect<A, E | BrowserSessionError, R>;
110
+ /**
111
+ * Resolves host-owned material under fresh credential grants. Never submits or retries a write.
112
+ * Credential timeouts retain acknowledged writes and dispatch evidence alongside cleanup.
113
+ */
114
+ readonly fillCredential: (
115
+ request: FillCredentialRequest,
116
+ ) => Effect.Effect<
117
+ CredentialFillResult,
118
+ CredentialFillError | BrowserSessionError,
119
+ BrowserCredentialAccess
120
+ >;
121
+ readonly handoff: <E, R>(
122
+ authorize: Effect.Effect<void, E, R>,
123
+ request: BrowserRunHandoffRequest,
124
+ ) => Effect.Effect<BrowserRunHandoffResult, E | BrowserSessionError, R>;
125
+ readonly getLiveView: <E, R>(
126
+ authorize: Effect.Effect<void, E, R>,
127
+ request: BrowserRunLiveViewRequest,
128
+ ) => Effect.Effect<BrowserRunLiveViewResult, E | BrowserSessionError, R>;
129
+ readonly getHandoffState: <E, R>(
130
+ authorize: Effect.Effect<void, E, R>,
131
+ ) => Effect.Effect<BrowserRunHandoffState, E | BrowserSessionError, R>;
132
+ }
133
+
134
+ const failure = (
135
+ reason: BrowserSessionError["reason"],
136
+ dispatch: BrowserSessionError["dispatch"] = "not-dispatched",
137
+ cleanup: BrowserSessionError["cleanup"] = "not-requested",
138
+ ) => new BrowserSessionError({ reason, dispatch, cleanup });
139
+
140
+ const decode = <A>(schema: Schema.Codec<A>, input: unknown) =>
141
+ Schema.decodeUnknownEffect(schema, { onExcessProperty: "error" })(input).pipe(
142
+ Effect.mapError(() => failure("invalid")),
143
+ );
144
+
145
+ // Catch inside the Promise before workerd can report an unhandled foreign diagnostic.
146
+ const native = Effect.fnUntraced(function* <A>(
147
+ operation: string,
148
+ action: (signal: AbortSignal) => Promise<A>,
149
+ ) {
150
+ const runReport = Effect.runPromiseWith(yield* Effect.context<never>());
151
+
152
+ return yield* Effect.tryPromise({
153
+ try: async (signal) => {
154
+ try {
155
+ return { ok: true as const, value: await action(signal) };
156
+ } catch (cause) {
157
+ if (!signal.aborted || cause !== signal.reason)
158
+ await runReport(reportBrowserCause(operation, Cause.fail(cause)));
159
+
160
+ return { ok: false as const };
161
+ }
162
+ },
163
+ catch: () => failure("provider"),
164
+ }).pipe(
165
+ Effect.flatMap((result) =>
166
+ result.ok
167
+ ? Effect.succeed(result.value)
168
+ : Effect.fail(reportedBrowserError(failure("provider"))),
169
+ ),
170
+ Effect.withTracerEnabled(false),
171
+ );
172
+ });
173
+
174
+ const targetId = (page: Page) =>
175
+ native("session.target", async () => {
176
+ const cdp = await page.createCDPSession();
177
+
178
+ try {
179
+ return (await cdp.send("Target.getTargetInfo")).targetInfo.targetId;
180
+ } finally {
181
+ await cdp.detach();
182
+ }
183
+ });
184
+
185
+ export interface BrowserSessionsOptions extends BrowserRunLifecycleOptions {
186
+ readonly browser: Pick<BrowserRun, "fetch">;
187
+ }
188
+
189
+ /** Native Cloudflare sessions; no framework browser workflow, checkpoint transfer, or observation policy. */
190
+ export class BrowserSessions extends Context.Service<
191
+ BrowserSessions,
192
+ {
193
+ /**
194
+ * Allocate once and commit its private reference in retain. Failed/uncommitted creation closes
195
+ * the allocation. After retain succeeds the application owns remote cleanup, independent of
196
+ * attempt Scopes. A lost allocation reply can remain indeterminate until provider idle expiry.
197
+ */
198
+ readonly create: <E, R>(
199
+ options: BrowserSessionOptions,
200
+ retain: (reference: BrowserSessionReference) => Effect.Effect<void, E, R>,
201
+ ) => Effect.Effect<BrowserSessionReference, E | BrowserSessionError, R>;
202
+ /** Attach only the exact saved context/page. Never recreate missing/expired state. */
203
+ readonly attach: (
204
+ reference: BrowserSessionReference,
205
+ ) => Effect.Effect<BrowserSession, BrowserSessionError, Scope.Scope>;
206
+ /** Refresh provider inactivity only; the owner must still enforce its expiresAt. */
207
+ readonly keepAlive: (
208
+ sessionId: Redacted.Redacted<string>,
209
+ ) => Effect.Effect<void, BrowserSessionError>;
210
+ readonly close: (
211
+ sessionId: Redacted.Redacted<string>,
212
+ ) => Effect.Effect<void, BrowserSessionError>;
213
+ }
214
+ >()("@effect-agent/platform-cloudflare/BrowserSessions") {
215
+ static readonly layerNoDeps = Layer.effect(
216
+ this,
217
+ Effect.gen(function* () {
218
+ const binding = yield* BrowserRunBinding;
219
+ const lifecycle = yield* BrowserRunSessionLifecycle;
220
+
221
+ const close = (sessionId: Redacted.Redacted<string>) =>
222
+ decode(SessionId, sessionId).pipe(
223
+ Effect.flatMap((id) =>
224
+ lifecycle
225
+ .close(id)
226
+ .pipe(Effect.mapError(() => failure("cleanup", "not-dispatched", "unconfirmed"))),
227
+ ),
228
+ );
229
+
230
+ const cleanup = (sessionId: Redacted.Redacted<string>) =>
231
+ close(sessionId).pipe(
232
+ Effect.as("confirmed" as const),
233
+ Effect.catchCause((cause) =>
234
+ reportBrowserCause("session.close", cause).pipe(Effect.as("unconfirmed" as const)),
235
+ ),
236
+ );
237
+
238
+ const connect = Effect.fnUntraced(function* (sessionId: Redacted.Redacted<string>) {
239
+ let attachment: BrowserRunAttachment | undefined;
240
+
241
+ const retire = Effect.suspend(() =>
242
+ attachment === undefined
243
+ ? Effect.void
244
+ : attachment.retire.pipe(
245
+ Effect.timeoutOrElse({
246
+ duration: "1 second",
247
+ orElse: () => Effect.fail(failure("cleanup")),
248
+ }),
249
+ Effect.catchCause((cause) =>
250
+ reportBrowserCause("session.disconnect", cause).pipe(
251
+ Effect.andThen(Effect.die(failure("cleanup", "not-dispatched", "unconfirmed"))),
252
+ ),
253
+ ),
254
+ ),
255
+ );
256
+
257
+ yield* Effect.addFinalizer(() => retire);
258
+
259
+ const browser = yield* native("session.connect", (signal) => {
260
+ attachment = binding.connect(Redacted.value(sessionId), "session.connect", signal);
261
+
262
+ return attachment.browser;
263
+ });
264
+
265
+ return { browser, retire };
266
+ });
267
+
268
+ const create = <E, R>(
269
+ input: BrowserSessionOptions,
270
+ retain: (reference: BrowserSessionReference) => Effect.Effect<void, E, R>,
271
+ ) =>
272
+ Effect.gen(function* () {
273
+ const options = yield* decode(BrowserSessionOptions, input);
274
+ const expiresAt = (yield* Clock.currentTimeMillis) + options.maxElapsedMillis;
275
+
276
+ if (!Number.isSafeInteger(expiresAt)) return yield* failure("invalid");
277
+ let sessionId: Redacted.Redacted<string> | undefined;
278
+ let retained = false;
279
+ const runCleanup = Effect.runPromiseWith(yield* Effect.context<never>());
280
+
281
+ yield* Effect.addFinalizer(() =>
282
+ sessionId === undefined || retained ? Effect.void : cleanup(sessionId),
283
+ );
284
+ sessionId = yield* native("session.acquire", async (signal) => {
285
+ const id = Redacted.make(
286
+ await binding.acquire(options.keepAliveMillis ?? 600_000, "session.acquire"),
287
+ );
288
+
289
+ if (signal.aborted) {
290
+ await runCleanup(cleanup(id));
291
+ signal.throwIfAborted();
292
+ }
293
+
294
+ return id;
295
+ });
296
+ const { browser } = yield* connect(sessionId);
297
+
298
+ const page = yield* native("session.page", async () => {
299
+ const context = await browser.createBrowserContext();
300
+
301
+ return await context.newPage();
302
+ });
303
+
304
+ const reference = yield* decode(BrowserSessionReference, {
305
+ version: 1,
306
+ sessionId,
307
+ contextId: Redacted.make(page.browserContext().id ?? ""),
308
+ targetId: Redacted.make(yield* targetId(page)),
309
+ expiresAt,
310
+ commandTimeoutMillis: options.commandTimeoutMillis ?? 30_000,
311
+ });
312
+
313
+ yield* Effect.uninterruptibleMask((restore) =>
314
+ restore(retain(reference)).pipe(
315
+ Effect.tap(() =>
316
+ Effect.sync(() => {
317
+ retained = true;
318
+ }),
319
+ ),
320
+ ),
321
+ );
322
+
323
+ return reference;
324
+ }).pipe(
325
+ Effect.scoped,
326
+ Effect.timeoutOrElse({
327
+ duration: "30 seconds",
328
+ orElse: () => Effect.fail(failure("timeout")),
329
+ }),
330
+ Effect.withTracerEnabled(false),
331
+ );
332
+
333
+ const attachPage = Effect.fnUntraced(function* (input: BrowserSessionReference) {
334
+ const reference = yield* decode(BrowserSessionReference, input);
335
+
336
+ if (reference.expiresAt <= (yield* Clock.currentTimeMillis))
337
+ return yield* failure("expired");
338
+ const scope = yield* Effect.scope;
339
+ const { browser, retire } = yield* connect(reference.sessionId);
340
+
341
+ const context = browser
342
+ .browserContexts()
343
+ .find((value) => value.id === Redacted.value(reference.contextId));
344
+
345
+ if (context === undefined) return yield* failure("missing-page");
346
+ let page: Page | undefined;
347
+
348
+ for (const candidate of yield* native("session.pages", () => context.pages())) {
349
+ if ((yield* targetId(candidate)) === Redacted.value(reference.targetId)) {
350
+ page = candidate;
351
+ break;
352
+ }
353
+ }
354
+ if (page === undefined) return yield* failure("missing-page");
355
+ const currentPage = page;
356
+ const lock = yield* Semaphore.make(1);
357
+ let invalid = false;
358
+
359
+ const terminate = Effect.suspend(() => {
360
+ invalid = true;
361
+
362
+ return retire.pipe(
363
+ Effect.catchCause((cause) => reportBrowserCause("session.disconnect", cause)),
364
+ Effect.andThen(cleanup(reference.sessionId)),
365
+ );
366
+ });
367
+
368
+ const runEffect = <A, E, R>(
369
+ action: (commandTimeoutMillis: number) => Effect.Effect<A, E, R>,
370
+ ) =>
371
+ lock
372
+ .withPermitsIfAvailable(1)(
373
+ Effect.gen(function* () {
374
+ if (invalid || scope.state._tag === "Closed" || !browser.isConnected())
375
+ return yield* failure("closed");
376
+ const remaining = reference.expiresAt - (yield* Clock.currentTimeMillis);
377
+
378
+ if (remaining <= 0) {
379
+ return yield* failure("expired", "not-dispatched", yield* terminate);
380
+ }
381
+
382
+ return yield* action(Math.min(remaining, reference.commandTimeoutMillis)).pipe(
383
+ Effect.catchDefect((defect) =>
384
+ reportBrowserCause("session.command", Cause.die(defect)).pipe(
385
+ Effect.andThen(terminate),
386
+ Effect.flatMap((closed) =>
387
+ Effect.fail(failure("provider", "possibly-dispatched", closed)),
388
+ ),
389
+ ),
390
+ ),
391
+ Effect.onInterrupt(() => terminate),
392
+ );
393
+ }),
394
+ )
395
+ .pipe(
396
+ Effect.flatMap(Effect.fromOption(() => failure("busy"))),
397
+ Effect.withTracerEnabled(false),
398
+ );
399
+
400
+ const run = <A, E, R>(
401
+ authorize: Effect.Effect<void, E, R>,
402
+ action: (page: Page) => Promise<A>,
403
+ terminateOnError = false,
404
+ ) =>
405
+ runEffect((commandTimeoutMillis) =>
406
+ authorize.pipe(
407
+ Effect.andThen(() =>
408
+ native("session.command", () => action(currentPage)).pipe(
409
+ Effect.catch((error) =>
410
+ Effect.gen(function* () {
411
+ const closed = terminateOnError ? yield* terminate : "not-requested";
412
+
413
+ return yield* reportedBrowserError(
414
+ failure(error.reason, "possibly-dispatched", closed),
415
+ );
416
+ }),
417
+ ),
418
+ ),
419
+ ),
420
+ Effect.timeoutOrElse({
421
+ duration: commandTimeoutMillis,
422
+ orElse: () =>
423
+ terminate.pipe(
424
+ Effect.flatMap((closed) =>
425
+ Effect.fail(failure("timeout", "possibly-dispatched", closed)),
426
+ ),
427
+ ),
428
+ }),
429
+ ),
430
+ );
431
+
432
+ const command = <A, E, R>(
433
+ authorize: Effect.Effect<void, E, R>,
434
+ method: string,
435
+ parameters: Record<string, string | number>,
436
+ schema: Schema.Codec<A>,
437
+ ) =>
438
+ run(
439
+ authorize,
440
+ async (page) => {
441
+ const cdp = await page.createCDPSession();
442
+
443
+ try {
444
+ const result: unknown = await Reflect.apply(cdp.send, cdp, [method, parameters]);
445
+
446
+ return Schema.decodeUnknownSync(schema)(result);
447
+ } finally {
448
+ await cdp.detach();
449
+ }
450
+ },
451
+ method === "Cloudflare.handoff",
452
+ );
453
+
454
+ const session: BrowserSession = {
455
+ reference,
456
+ run,
457
+ fillCredential: (request) =>
458
+ runEffect((commandTimeoutMillis) =>
459
+ fillCredential(request).pipe(
460
+ Effect.provideService(BrowserSessionPage, {
461
+ page: currentPage,
462
+ commandTimeoutMillis,
463
+ }),
464
+ Effect.catchIf(
465
+ (error) => error.reason === "timeout" || error.dispatch === "possibly-dispatched",
466
+ (error) =>
467
+ terminate.pipe(
468
+ Effect.flatMap((cleanup) =>
469
+ Effect.fail(CredentialFillError.make({ ...error, cleanup })),
470
+ ),
471
+ ),
472
+ ),
473
+ ),
474
+ ),
475
+ handoff: (authorize, request) =>
476
+ decode(BrowserRunHandoffRequest, request).pipe(
477
+ Effect.flatMap((value) =>
478
+ command(
479
+ authorize,
480
+ "Cloudflare.handoff",
481
+ { instructions: value.instructions, timeout: value.timeout },
482
+ Schema.Struct({
483
+ handoffId: Schema.NonEmptyString.check(Schema.isMaxLength(1024)),
484
+ }),
485
+ ),
486
+ ),
487
+ Effect.map((value) =>
488
+ BrowserRunHandoffResult.make({ handoffId: Redacted.make(value.handoffId) }),
489
+ ),
490
+ ),
491
+ getLiveView: (authorize, request) =>
492
+ decode(BrowserRunLiveViewRequest, request).pipe(
493
+ Effect.flatMap((value) =>
494
+ command(
495
+ authorize,
496
+ "Cloudflare.getLiveView",
497
+ { mode: value.mode, expiresInMs: value.expiresInMs },
498
+ Schema.Struct({ devtoolsFrontendUrl: Schema.String }),
499
+ ),
500
+ ),
501
+ Effect.flatMap((value) =>
502
+ decode(BrowserRunLiveViewResult, {
503
+ devtoolsFrontendUrl: Redacted.make(value.devtoolsFrontendUrl),
504
+ }),
505
+ ),
506
+ ),
507
+ getHandoffState: (authorize) =>
508
+ command(
509
+ authorize,
510
+ "Cloudflare.getHandoffState",
511
+ {},
512
+ Schema.Struct({
513
+ active: Schema.Boolean,
514
+ handoffId: Schema.optionalKey(
515
+ Schema.NonEmptyString.check(Schema.isMaxLength(1024)),
516
+ ),
517
+ durationMs: Schema.optionalKey(Schema.Natural),
518
+ }),
519
+ ).pipe(
520
+ Effect.map((value) =>
521
+ BrowserRunHandoffState.make({
522
+ active: value.active,
523
+ ...(value.handoffId === undefined
524
+ ? {}
525
+ : { handoffId: Redacted.make(value.handoffId) }),
526
+ ...(value.durationMs === undefined ? {} : { durationMs: value.durationMs }),
527
+ }),
528
+ ),
529
+ ),
530
+ };
531
+
532
+ return session;
533
+ });
534
+
535
+ const attach = Effect.fnUntraced(function* (input: BrowserSessionReference) {
536
+ const reference = yield* decode(BrowserSessionReference, input);
537
+ const scope = yield* Scope.fork(yield* Effect.scope);
538
+
539
+ return yield* attachPage(reference).pipe(
540
+ Effect.provideService(Scope.Scope, scope),
541
+ Effect.timeoutOrElse({
542
+ duration: reference.commandTimeoutMillis,
543
+ orElse: () =>
544
+ reportBrowserCause("session.connect", Cause.fail(failure("timeout"))).pipe(
545
+ Effect.andThen(Effect.fail(reportedBrowserError(failure("timeout")))),
546
+ ),
547
+ }),
548
+ Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void)),
549
+ );
550
+ });
551
+
552
+ return BrowserSessions.of({
553
+ create,
554
+ attach,
555
+ close,
556
+ keepAlive: (sessionId) =>
557
+ decode(SessionId, sessionId).pipe(
558
+ Effect.flatMap((id) =>
559
+ binding.keepAlive(Redacted.value(id)).pipe(
560
+ Effect.timeoutOrElse({
561
+ duration: "10 seconds",
562
+ orElse: () => Effect.fail(failure("timeout")),
563
+ }),
564
+ Effect.catch((error) =>
565
+ reportBrowserCause("session.keepAlive", Cause.fail(error)).pipe(
566
+ Effect.andThen(
567
+ Effect.fail(
568
+ reportedBrowserError(
569
+ failure(error.reason === "timeout" ? "timeout" : "provider"),
570
+ ),
571
+ ),
572
+ ),
573
+ ),
574
+ ),
575
+ ),
576
+ ),
577
+ Effect.withTracerEnabled(false),
578
+ ),
579
+ });
580
+ }),
581
+ );
582
+
583
+ static layer(options: BrowserSessionsOptions) {
584
+ return this.layerNoDeps.pipe(
585
+ Layer.provide(BrowserRunBinding.layer(options.browser)),
586
+ Layer.provide(BrowserRunSessionLifecycle.layer(options)),
587
+ );
588
+ }
589
+ }
package/src/index.ts CHANGED
@@ -10,3 +10,5 @@ export * as CloudflareThreadClient from "./CloudflareThreadClient.ts";
10
10
  export * as ThreadObject from "./ThreadObject.ts";
11
11
  export * as WakeScheduler from "./WakeScheduler.ts";
12
12
  export * as CloudflareAiGateway from "./CloudflareAiGateway.ts";
13
+ export * as BrowserSession from "./BrowserSession.ts";
14
+ export * as BrowserCredentials from "./BrowserCredentials.ts";
@@ -40,7 +40,7 @@ export class BrowserRunBinding extends Context.Service<
40
40
  {
41
41
  readonly acquire: (
42
42
  keepAliveMillis: number,
43
- operation: "protected.acquire" | "interactive.acquire",
43
+ operation: "interactive.acquire" | "session.acquire",
44
44
  ) => Promise<string>;
45
45
  readonly connect: (
46
46
  sessionId: string,
@@ -110,7 +110,7 @@ export class BrowserRunBinding extends Context.Service<
110
110
  let retirementFailure: BrowserRunFailure | undefined;
111
111
 
112
112
  const disconnectOperation =
113
- operation === "protected.connect" ? "protected.disconnect" : "interactive.disconnect";
113
+ operation === "session.connect" ? "session.disconnect" : "interactive.disconnect";
114
114
 
115
115
  const transport: ConnectionTransport = {
116
116
  send: (message) => {
@@ -231,7 +231,7 @@ export class BrowserRunBinding extends Context.Service<
231
231
  };
232
232
  },
233
233
  keepAlive: Effect.fnUntraced(function* (sessionId: string) {
234
- const operation = "protected.keepAlive";
234
+ const operation = "session.keepAlive";
235
235
  const runReport = Effect.runPromiseWith(yield* Effect.context<never>());
236
236
 
237
237
  const release = (socket: WebSocket) =>