@effect-agent/platform-cloudflare 0.1.0-beta.106 → 0.1.0-beta.108

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.
@@ -1,6 +1,6 @@
1
1
  /// <reference types="@cloudflare/workers-types" />
2
2
  import type { Browser, Frame, JSHandle, Page } from "@cloudflare/puppeteer";
3
- import { Clock, Context, Crypto, Effect, Redacted, Schema, type Scope } from "effect";
3
+ import { Cause, Clock, Context, Crypto, Effect, Redacted, Schema, type Scope } from "effect";
4
4
  import { type InteractiveBrowserPolicy } from "effect-agent/interactive-browser";
5
5
  import {
6
6
  CredentialOrigin,
@@ -8,6 +8,11 @@ import {
8
8
  ProtectedBrowserControl,
9
9
  } from "effect-agent/protected-browser";
10
10
 
11
+ import {
12
+ browserFailure,
13
+ reportBrowserCause,
14
+ reportedBrowserError,
15
+ } from "../internal/browser-failure.ts";
11
16
  import { inspectFrame, maxAttributeLength } from "./inspect-frame.ts";
12
17
  import {
13
18
  ProtectedBrowserDispatch,
@@ -65,22 +70,33 @@ const transportError = (reason: ProtectedTransportError["reason"]) =>
65
70
  new ProtectedTransportError({ reason });
66
71
 
67
72
  const decode = <A>(schema: Schema.Codec<A>, value: unknown) =>
68
- Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => transportError("provider")));
73
+ Schema.decodeUnknownEffect(schema)(value).pipe(
74
+ Effect.catch((error) =>
75
+ reportBrowserCause("protected.decode", Cause.fail(error)).pipe(
76
+ Effect.andThen(Effect.fail(reportedBrowserError(transportError("provider")))),
77
+ ),
78
+ ),
79
+ );
69
80
 
70
81
  // Attach rejection handling inside the SDK callback before workerd can report foreign diagnostics.
71
- const remote = <A>(run: (signal: AbortSignal) => Promise<A>) =>
82
+ const remote = <A>(operation: string, run: (signal: AbortSignal) => Promise<A>) =>
72
83
  Effect.tryPromise({
73
84
  try: async (signal) => {
74
85
  try {
75
86
  return { ok: true as const, value: await run(signal) };
76
- } catch {
77
- return { ok: false as const };
87
+ } catch (cause) {
88
+ return { ok: false as const, failure: browserFailure(operation, cause) };
78
89
  }
79
90
  },
80
- catch: () => transportError("provider"),
91
+ catch: (cause) => browserFailure(operation, cause),
81
92
  }).pipe(
82
93
  Effect.flatMap((result) =>
83
- result.ok ? Effect.succeed(result.value) : Effect.fail(transportError("provider")),
94
+ result.ok ? Effect.succeed(result.value) : Effect.fail(result.failure),
95
+ ),
96
+ Effect.catch((failure) =>
97
+ reportBrowserCause(operation, Cause.fail(failure)).pipe(
98
+ Effect.andThen(Effect.fail(reportedBrowserError(transportError("provider")))),
99
+ ),
84
100
  ),
85
101
  );
86
102
 
@@ -96,7 +112,15 @@ export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.
96
112
  const { browser, page } = session;
97
113
  const crypto = yield* Crypto.Crypto;
98
114
  const clock = yield* Clock.Clock;
99
- const uuid = crypto.randomUUIDv4.pipe(Effect.mapError(() => transportError("provider")));
115
+
116
+ const uuid = crypto.randomUUIDv4.pipe(
117
+ Effect.catch((error) =>
118
+ reportBrowserCause("protected.identity", Cause.fail(error)).pipe(
119
+ Effect.andThen(Effect.fail(reportedBrowserError(transportError("provider")))),
120
+ ),
121
+ ),
122
+ );
123
+
100
124
  let closed = false;
101
125
  let violation = false;
102
126
  let documentRef: string | undefined;
@@ -181,7 +205,7 @@ export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.
181
205
  const isCurrent = Effect.fn("ProtectedNativeTransport.isCurrent")(function* (state: FrameState) {
182
206
  if (state.frame.detached) return false;
183
207
 
184
- return yield* remote(() =>
208
+ return yield* remote("protected.validate-document", () =>
185
209
  state.handle.evaluate((held) => {
186
210
  if (typeof held !== "object" || held === null) return false;
187
211
 
@@ -203,7 +227,7 @@ export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.
203
227
  )
204
228
  return yield* transportError("stale-reference");
205
229
 
206
- const current = yield* remote(() =>
230
+ const current = yield* remote("protected.validate-control", () =>
207
231
  state.frame.handle.evaluate((held, index) => {
208
232
  if (typeof held !== "object" || held === null) return null;
209
233
 
@@ -292,7 +316,7 @@ export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.
292
316
  yield* check;
293
317
  yield* origin(url);
294
318
  clear();
295
- yield* remote(() => page.goto(url, { waitUntil: "load" }));
319
+ yield* remote("protected.navigate", () => page.goto(url, { waitUntil: "load" }));
296
320
  yield* context;
297
321
  }),
298
322
  target: (ref) => get(ref).pipe(Effect.map((state) => state.control)),
@@ -300,7 +324,8 @@ export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.
300
324
  const before = yield* context;
301
325
 
302
326
  controls.clear();
303
- for (const state of frames.values()) yield* remote(() => state.handle.dispose());
327
+ for (const state of frames.values())
328
+ yield* remote("protected.dispose", () => state.handle.dispose());
304
329
  frames.clear();
305
330
  const discovered: Array<ProtectedBrowserControl> = [];
306
331
  let text = "";
@@ -313,7 +338,10 @@ export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.
313
338
  if (observationOrigins !== undefined && !observationOrigins.has(frameOrigin)) continue;
314
339
  frameOrigins.add(frameOrigin);
315
340
  if (typeof frame.isolatedRealm !== "function") return yield* transportError("unsupported");
316
- const handle = yield* remote(() => frame.isolatedRealm().evaluateHandle(inspectFrame));
341
+
342
+ const handle = yield* remote("protected.discover", () =>
343
+ frame.isolatedRealm().evaluateHandle(inspectFrame),
344
+ );
317
345
 
318
346
  const state: FrameState = {
319
347
  frame,
@@ -325,7 +353,7 @@ export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.
325
353
 
326
354
  frames.set(frame, state);
327
355
 
328
- const raw = yield* remote(() =>
356
+ const raw = yield* remote("protected.describe-controls", () =>
329
357
  handle.evaluate((held) => {
330
358
  if (typeof held !== "object" || held === null) return null;
331
359
 
@@ -379,7 +407,7 @@ export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.
379
407
  discovered.push(control);
380
408
  }
381
409
 
382
- const rawText = yield* remote(() =>
410
+ const rawText = yield* remote("protected.read-text", () =>
383
411
  handle.evaluate((held) => {
384
412
  if (typeof held !== "object" || held === null) return null;
385
413
 
@@ -411,7 +439,7 @@ export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.
411
439
  // CDP dispatch may mutate before its reply is lost, so uncertainty starts here.
412
440
  yield* dispatch.mark;
413
441
 
414
- const filled = yield* remote(() =>
442
+ const filled = yield* remote("protected.fill", () =>
415
443
  state.frame.handle.evaluate(
416
444
  (held, index, expectedRole, secret) => {
417
445
  if (typeof held !== "object" || held === null) return false;
@@ -430,7 +458,7 @@ export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.
430
458
  click: Effect.fn("ProtectedNativeTransport.click")(function* (ref) {
431
459
  const state = yield* get(ref);
432
460
 
433
- const clicked = yield* remote(() =>
461
+ const clicked = yield* remote("protected.click", () =>
434
462
  state.frame.handle.evaluate((held, index) => {
435
463
  if (typeof held !== "object" || held === null) return false;
436
464
 
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Cause,
2
3
  Clock,
3
4
  Context,
4
5
  Crypto,
@@ -41,6 +42,13 @@ import {
41
42
  type ProtectedObservationState,
42
43
  } from "effect-agent/protected-browser";
43
44
 
45
+ import {
46
+ BrowserRunFailure,
47
+ inheritBrowserReport,
48
+ reportBrowserCause,
49
+ reportedBrowserError,
50
+ } from "../internal/browser-failure.ts";
51
+
44
52
  export class ProtectedTransportError extends Schema.TaggedError<ProtectedTransportError>()(
45
53
  "ProtectedTransportError",
46
54
  { reason: Schema.Literals(["stale-reference", "needs-attention", "unsupported", "provider"]) },
@@ -230,9 +238,14 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
230
238
  Effect.interruptible,
231
239
  Effect.timeoutOrElse({
232
240
  duration: "10 seconds",
233
- orElse: () => Effect.succeed("unconfirmed" as const),
241
+ orElse: () =>
242
+ Effect.fail(
243
+ new BrowserRunFailure({ operation: "protected.close", reason: "timeout" }),
244
+ ),
234
245
  }),
235
- Effect.catchCause(() => Effect.succeed("unconfirmed" as const)),
246
+ Effect.catchCause((cause) =>
247
+ reportBrowserCause("protected.close", cause).pipe(Effect.as("unconfirmed" as const)),
248
+ ),
236
249
  );
237
250
 
238
251
  return cleanup;
@@ -240,26 +253,30 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
240
253
  ),
241
254
  );
242
255
 
243
- yield* Effect.addFinalizer(() =>
244
- detached
245
- ? Effect.void
246
- : close.pipe(
247
- Effect.flatMap((result) =>
248
- result === "unconfirmed"
249
- ? Effect.logWarning("Protected browser exact-session closure unconfirmed")
250
- : Effect.void,
251
- ),
252
- ),
253
- );
256
+ yield* Effect.addFinalizer(() => (detached ? Effect.void : close));
254
257
 
255
- const remote = <A, R>(effect: Effect.Effect<A, ProtectedTransportError, R>) =>
256
- effect.pipe(Effect.mapError((error) => fail(error.reason)));
258
+ const publicFailure = <A, E extends { readonly reason: ProtectedBrowserError["reason"] }, R>(
259
+ effect: Effect.Effect<A, E, R>,
260
+ ) =>
261
+ effect.pipe(
262
+ Effect.catchCause((cause) =>
263
+ Effect.failCause(
264
+ Cause.map(cause, (error) => inheritBrowserReport(error, fail(error.reason))),
265
+ ),
266
+ ),
267
+ );
257
268
 
258
269
  const decode = <A>(schema: Schema.Codec<A>, value: unknown) =>
259
- Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => fail("provider")));
270
+ Schema.decodeUnknownEffect(schema)(value).pipe(
271
+ Effect.catch((error) =>
272
+ reportBrowserCause("protected.policy.decode", Cause.fail(error)).pipe(
273
+ Effect.andThen(Effect.fail(reportedBrowserError(fail("provider")))),
274
+ ),
275
+ ),
276
+ );
260
277
 
261
- const caller = access.caller.pipe(Effect.mapError((error) => fail(error.reason)));
262
- const pageContext = remote(driver.context);
278
+ const caller = access.caller.pipe(publicFailure);
279
+ const pageContext = publicFailure(driver.context);
263
280
 
264
281
  const permitObservation = Effect.gen(function* () {
265
282
  const context = yield* pageContext;
@@ -277,7 +294,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
277
294
  humanExposure,
278
295
  humanOrigins,
279
296
  })
280
- .pipe(Effect.mapError((error) => fail(error.reason)));
297
+ .pipe(publicFailure);
281
298
 
282
299
  if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
283
300
 
@@ -292,7 +309,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
292
309
  }
293
310
  observation = "approved-after-exposure";
294
311
  }
295
- yield* remote(driver.restrictObservation(origins));
312
+ yield* publicFailure(driver.restrictObservation(origins));
296
313
 
297
314
  return {
298
315
  ...context,
@@ -302,7 +319,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
302
319
  };
303
320
  });
304
321
 
305
- const target = (ref: string) => remote(driver.target(ref));
322
+ const target = (ref: string) => publicFailure(driver.target(ref));
306
323
 
307
324
  const authorizeAction = Effect.fn("ProtectedBrowser.authorizeAction")(function* (
308
325
  action: ProtectedBrowserAction,
@@ -316,7 +333,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
316
333
 
317
334
  yield* access
318
335
  .authorizeAction({ caller: principal, action, exposures: [...exposures] })
319
- .pipe(Effect.mapError((error) => fail(error.reason)));
336
+ .pipe(publicFailure);
320
337
  if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
321
338
  if (action._tag !== "Navigate") {
322
339
  const current = yield* target(action.ref);
@@ -362,14 +379,25 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
362
379
  }),
363
380
  Effect.catchCause((cause) =>
364
381
  Effect.gen(function* () {
365
- // No foreign exception or defect is retained. Interrupted operations still finalize.
382
+ // Expected rejections cannot hide a concurrent defect. Preserve safe diagnostics first.
383
+ const unexpected = cause.reasons.filter(
384
+ (reason) =>
385
+ reason._tag === "Die" ||
386
+ (reason._tag === "Fail" && !Schema.is(ProtectedBrowserError)(reason.error)),
387
+ );
388
+
389
+ if (unexpected.length > 0)
390
+ yield* reportBrowserCause("protected.operation", Cause.fromReasons(unexpected));
391
+
366
392
  const error = cause.reasons.find(
367
393
  (reason) =>
368
394
  reason._tag === "Fail" && Schema.is(ProtectedBrowserError)(reason.error),
369
395
  );
370
396
 
371
397
  const reason =
372
- error?._tag === "Fail" && Schema.is(ProtectedBrowserError)(error.error)
398
+ unexpected.length === 0 &&
399
+ error?._tag === "Fail" &&
400
+ Schema.is(ProtectedBrowserError)(error.error)
373
401
  ? error.error.reason
374
402
  : "provider";
375
403
 
@@ -384,11 +412,15 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
384
412
  yield* close;
385
413
  if (interrupt) return yield* Effect.interrupt;
386
414
 
387
- return yield* fail(
415
+ const outcome = fail(
388
416
  dispatch === "possibly-dispatched" && reason === "provider"
389
417
  ? "outcome-unknown"
390
418
  : reason,
391
419
  );
420
+
421
+ return yield* unexpected.length > 0
422
+ ? reportedBrowserError(outcome)
423
+ : inheritBrowserReport(error?._tag === "Fail" ? error.error : undefined, outcome);
392
424
  }),
393
425
  ),
394
426
  Effect.onInterrupt(() => close),
@@ -439,7 +471,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
439
471
  yield* authorizeAction({ _tag: "Navigate", url: decoded.url });
440
472
  offers.clear();
441
473
  dispatch = "possibly-dispatched";
442
- yield* remote(driver.navigate(decoded.url));
474
+ yield* publicFailure(driver.navigate(decoded.url));
443
475
  dispatch = "dispatched";
444
476
  yield* permitObservation;
445
477
  }),
@@ -447,7 +479,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
447
479
  observe: run(
448
480
  Effect.gen(function* () {
449
481
  const before = yield* permitObservation;
450
- const result = yield* remote(driver.discover);
482
+ const result = yield* publicFailure(driver.discover);
451
483
  const after = yield* permitObservation;
452
484
 
453
485
  if (
@@ -514,7 +546,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
514
546
  }
515
547
  yield* authorizeAction(action);
516
548
  dispatch = "possibly-dispatched";
517
- yield* remote(driver.click(decoded.ref)).pipe(
549
+ yield* publicFailure(driver.click(decoded.ref)).pipe(
518
550
  Effect.catch((error) => {
519
551
  if (error.reason === "needs-attention") dispatch = "not-dispatched";
520
552
 
@@ -544,7 +576,9 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
544
576
  target: control.target,
545
577
  role: control.role,
546
578
  });
547
- yield* remote(driver.fill(decoded.ref, control.role, Redacted.make(decoded.value))).pipe(
579
+ yield* publicFailure(
580
+ driver.fill(decoded.ref, control.role, Redacted.make(decoded.value)),
581
+ ).pipe(
548
582
  Effect.provideService(ProtectedBrowserDispatch, {
549
583
  mark: Effect.sync(() => {
550
584
  dispatch = "possibly-dispatched";
@@ -571,7 +605,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
571
605
 
572
606
  const candidates = yield* access
573
607
  .list({ caller: principal, kind: decoded.kind, target: control.target })
574
- .pipe(Effect.mapError((error) => fail(error.reason)));
608
+ .pipe(publicFailure);
575
609
 
576
610
  const now = yield* Clock.currentTimeMillis;
577
611
 
@@ -587,7 +621,13 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
587
621
  for (const candidate of candidates) {
588
622
  const metadata = yield* decode(CredentialOfferMetadata, candidate.metadata);
589
623
 
590
- const ref = yield* crypto.randomUUIDv4.pipe(Effect.mapError(() => fail("provider")));
624
+ const ref = yield* crypto.randomUUIDv4.pipe(
625
+ Effect.catch((error) =>
626
+ reportBrowserCause("protected.identity", Cause.fail(error)).pipe(
627
+ Effect.andThen(Effect.fail(reportedBrowserError(fail("provider")))),
628
+ ),
629
+ ),
630
+ );
591
631
 
592
632
  pending.set(ref, {
593
633
  caller: principal,
@@ -661,9 +701,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
661
701
  const authorize = Effect.gen(function* () {
662
702
  if (Redacted.value(yield* caller) !== Redacted.value(principal))
663
703
  return yield* fail("denied");
664
- yield* access
665
- .authorize(authorization)
666
- .pipe(Effect.mapError((error) => fail(error.reason)));
704
+ yield* access.authorize(authorization).pipe(publicFailure);
667
705
  if (Redacted.value(yield* caller) !== Redacted.value(principal))
668
706
  return yield* fail("denied");
669
707
  });
@@ -671,9 +709,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
671
709
  yield* authorize;
672
710
  yield* validate;
673
711
 
674
- const raw = yield* access
675
- .resolve(authorization)
676
- .pipe(Effect.mapError((error) => fail(error.reason)));
712
+ const raw = yield* access.resolve(authorization).pipe(publicFailure);
677
713
 
678
714
  const material = yield* Schema.decodeEffect(
679
715
  offer.kind === "login" ? LoginCredential : CardCredential,
@@ -690,7 +726,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
690
726
 
691
727
  if (value === undefined) return yield* fail("missing-credential");
692
728
  observation = "protected";
693
- yield* remote(driver.fill(field.ref, field.role, value)).pipe(
729
+ yield* publicFailure(driver.fill(field.ref, field.role, value)).pipe(
694
730
  Effect.provideService(ProtectedBrowserDispatch, {
695
731
  mark: Effect.sync(() => {
696
732
  dispatch = "possibly-dispatched";
@@ -709,7 +745,7 @@ export const makeProtectedBrowserPolicy = Effect.fn("ProtectedBrowser.open")(fun
709
745
  const ref = decoded.submit;
710
746
 
711
747
  dispatch = "possibly-dispatched";
712
- yield* remote(driver.click(ref)).pipe(
748
+ yield* publicFailure(driver.click(ref)).pipe(
713
749
  Effect.catch((error) => {
714
750
  if (error.reason === "needs-attention") dispatch = "dispatched";
715
751