@effect-agent/platform-cloudflare 0.1.0-beta.42 → 0.1.0-beta.45

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 (47) hide show
  1. package/dist/browser-quick-action.mjs.map +1 -1
  2. package/dist/browser-rest-capture.mjs.map +1 -1
  3. package/dist/browser-rest-crawl.mjs.map +1 -1
  4. package/dist/browser-session-lifecycle-DqntvG-Y.d.mts +21 -0
  5. package/dist/browser-session-lifecycle-ZGgb3pnK.mjs +84 -0
  6. package/dist/browser-session-lifecycle-ZGgb3pnK.mjs.map +1 -0
  7. package/dist/index.d.mts +47 -34
  8. package/dist/index.mjs +3 -3
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/interactive-browser.d.mts +1 -18
  11. package/dist/interactive-browser.mjs +2 -81
  12. package/dist/interactive-browser.mjs.map +1 -1
  13. package/dist/{prepared-admission-BKp_Upw2.mjs → prepared-admission-BhW2eT_a.mjs} +3 -3
  14. package/dist/prepared-admission-BhW2eT_a.mjs.map +1 -0
  15. package/dist/protected-browser.d.mts +62 -0
  16. package/dist/protected-browser.mjs +714 -0
  17. package/dist/protected-browser.mjs.map +1 -0
  18. package/dist/scheduling.mjs +1 -1
  19. package/dist/scheduling.mjs.map +1 -1
  20. package/dist/subscriptions.mjs +1 -1
  21. package/dist/subscriptions.mjs.map +1 -1
  22. package/package.json +1 -81
  23. package/src/alarm.ts +277 -242
  24. package/src/bindings.ts +5 -0
  25. package/src/boundary.ts +4 -0
  26. package/src/browser-quick-action.ts +52 -0
  27. package/src/browser-rest-capture.ts +30 -0
  28. package/src/browser-rest-crawl.ts +43 -0
  29. package/src/browser-session-lifecycle.ts +18 -0
  30. package/src/client.ts +40 -0
  31. package/src/code-mode-executor.ts +50 -0
  32. package/src/interactive-browser.ts +176 -0
  33. package/src/layers.ts +13 -3
  34. package/src/memory.ts +42 -3
  35. package/src/prepared-admission.ts +1 -0
  36. package/src/progress-wait.ts +14 -0
  37. package/src/protected-browser/binding.ts +185 -0
  38. package/src/protected-browser/inspect-frame.ts +82 -0
  39. package/src/protected-browser/native.ts +384 -0
  40. package/src/protected-browser/policy.ts +594 -0
  41. package/src/protected-browser.ts +2 -0
  42. package/src/scheduling.ts +34 -0
  43. package/src/subscriptions.ts +50 -0
  44. package/src/thread-object.ts +55 -1
  45. package/src/transport.ts +1 -0
  46. package/src/wake-scheduler.ts +1 -0
  47. package/dist/prepared-admission-BKp_Upw2.mjs.map +0 -1
@@ -0,0 +1,594 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+ import {
3
+ BrowserCredentialAccess,
4
+ type BrowserCredentialMaterial,
5
+ CardCredential,
6
+ CredentialOffer,
7
+ CredentialOfferMetadata,
8
+ CredentialOrigin,
9
+ type CredentialTarget,
10
+ CredentialUseResult,
11
+ InteractiveBrowserPolicy,
12
+ ListCredentialOffers,
13
+ LoginCredential,
14
+ ProtectedBrowser,
15
+ ProtectedBrowserClick,
16
+ ProtectedBrowserError,
17
+ ProtectedBrowserNavigate,
18
+ ProtectedBrowserObservation,
19
+ ProtectedBrowserControl,
20
+ UseCredential,
21
+ type CredentialFieldRole,
22
+ type CredentialKind,
23
+ type CredentialUseAuthorization,
24
+ type ProtectedBrowserHandle,
25
+ type ProtectedCleanup,
26
+ type ProtectedObservationState,
27
+ } from "@effect-agent/sandbox";
28
+ import {
29
+ Clock,
30
+ Context,
31
+ Crypto,
32
+ Effect,
33
+ Layer,
34
+ Option,
35
+ Redacted,
36
+ Schema,
37
+ Semaphore,
38
+ type Scope,
39
+ } from "effect";
40
+
41
+ export class ProtectedTransportError extends Schema.TaggedError<ProtectedTransportError>()(
42
+ "ProtectedTransportError",
43
+ { reason: Schema.Literals(["stale-reference", "needs-attention", "unsupported", "provider"]) },
44
+ ) {}
45
+
46
+ /** Per-write evidence authority, provided by the policy immediately around a transport fill. */
47
+ export class ProtectedBrowserDispatch extends Context.Service<
48
+ ProtectedBrowserDispatch,
49
+ { readonly mark: Effect.Effect<void> }
50
+ >()("@effect-agent/platform-cloudflare/ProtectedBrowserDispatch") {}
51
+
52
+ /** Decoded adapter boundary. SDK exceptions and page diagnostics never cross this port. */
53
+ export interface ProtectedBrowserTransport {
54
+ readonly context: Effect.Effect<typeof ProtectedPageContext.Type, ProtectedTransportError>;
55
+ readonly discover: Effect.Effect<typeof ProtectedDiscovery.Type, ProtectedTransportError>;
56
+ readonly target: (ref: string) => Effect.Effect<ProtectedBrowserControl, ProtectedTransportError>;
57
+ readonly navigate: (url: string) => Effect.Effect<void, ProtectedTransportError>;
58
+ readonly click: (ref: string) => Effect.Effect<void, ProtectedTransportError>;
59
+ readonly fill: (
60
+ ref: string,
61
+ role: typeof CredentialFieldRole.Type,
62
+ value: Redacted.Redacted<string>,
63
+ ) => Effect.Effect<void, ProtectedTransportError, ProtectedBrowserDispatch>;
64
+ /** Invalidates local references synchronously before bounded exact-session cleanup. */
65
+ readonly invalidate: () => void;
66
+ readonly close: Effect.Effect<typeof ProtectedCleanup.Type>;
67
+ }
68
+
69
+ /** Adapter-private injection seam for deterministic tests, not a model capability. */
70
+ export class BrowserRunProtectedTransport extends Context.Service<
71
+ BrowserRunProtectedTransport,
72
+ {
73
+ readonly open: (
74
+ policy: InteractiveBrowserPolicy,
75
+ ) => Effect.Effect<ProtectedBrowserTransport, ProtectedBrowserError, Scope.Scope>;
76
+ }
77
+ >()("@effect-agent/platform-cloudflare/BrowserRunProtectedTransport") {}
78
+
79
+ export const ProtectedPageContext = Schema.Struct({
80
+ document: Schema.String.check(Schema.isUUID()),
81
+ topOrigin: CredentialOrigin,
82
+ frameOrigins: Schema.Array(CredentialOrigin).check(Schema.isMaxLength(16)),
83
+ });
84
+
85
+ export const ProtectedDiscovery = Schema.Struct({
86
+ ...ProtectedPageContext.fields,
87
+ text: Schema.String.check(Schema.isMaxLength(64 * 1024)),
88
+ controls: Schema.Array(ProtectedBrowserControl).check(Schema.isMaxLength(64)),
89
+ truncated: Schema.Boolean,
90
+ });
91
+
92
+ const sameTarget = (a: CredentialTarget, b: CredentialTarget) =>
93
+ a.topOrigin === b.topOrigin &&
94
+ a.frameOrigin === b.frameOrigin &&
95
+ a.recipientOrigin === b.recipientOrigin &&
96
+ a.document === b.document &&
97
+ a.frame === b.frame &&
98
+ a.form === b.form;
99
+
100
+ const kindFor = (role: string): typeof CredentialKind.Type | undefined =>
101
+ role === "username" || role === "password"
102
+ ? "login"
103
+ : role.startsWith("card-")
104
+ ? "card"
105
+ : undefined;
106
+
107
+ const secretFor = (material: BrowserCredentialMaterial, role: typeof CredentialFieldRole.Type) => {
108
+ if (material._tag === "LoginCredential") {
109
+ return role === "username"
110
+ ? material.username
111
+ : role === "password"
112
+ ? material.password
113
+ : undefined;
114
+ }
115
+ switch (role) {
116
+ case "card-name":
117
+ return material.name;
118
+ case "card-number":
119
+ return material.number;
120
+ case "card-expiry":
121
+ return material.expiry;
122
+ case "card-expiry-month":
123
+ return material.expiryMonth;
124
+ case "card-expiry-year":
125
+ return material.expiryYear;
126
+ case "card-security-code":
127
+ return material.securityCode;
128
+ case "username":
129
+ case "password":
130
+ return undefined;
131
+ }
132
+ };
133
+
134
+ /**
135
+ * Fresh private passes only. Account administrators and Browser Rendering token holders are
136
+ * trusted operators. No viewer, handoff, raw JavaScript, screenshot or plaintext-fill API exists.
137
+ * Hosts explicitly authorize post-exposure observations for recipients they trust not to echo.
138
+ */
139
+ export const browserRunProtectedLayer = () =>
140
+ Layer.effect(ProtectedBrowser)(
141
+ Effect.gen(function* () {
142
+ const transport = yield* BrowserRunProtectedTransport;
143
+ const crypto = yield* Crypto.Crypto;
144
+
145
+ const open = Effect.fn("ProtectedBrowser.open")(function* (
146
+ input: InteractiveBrowserPolicy,
147
+ ): Effect.fn.Return<
148
+ ProtectedBrowserHandle,
149
+ ProtectedBrowserError,
150
+ Scope.Scope | BrowserCredentialAccess
151
+ > {
152
+ const initialError = (reason: ProtectedBrowserError["reason"]) =>
153
+ new ProtectedBrowserError({
154
+ reason,
155
+ dispatch: "not-dispatched",
156
+ milestone: "none",
157
+ observation: "closed",
158
+ cleanup: "not-requested",
159
+ });
160
+
161
+ const decodedPolicy = yield* Schema.decodeUnknownEffect(InteractiveBrowserPolicy)(
162
+ input,
163
+ ).pipe(Effect.mapError(() => initialError("denied")));
164
+
165
+ if (decodedPolicy.network._tag === "PublicWeb") return yield* initialError("unsupported");
166
+
167
+ const policy = InteractiveBrowserPolicy.make({
168
+ ...decodedPolicy,
169
+ network:
170
+ decodedPolicy.network._tag === "ExactHosts"
171
+ ? { _tag: "ExactHosts", allowedHosts: [...decodedPolicy.network.allowedHosts] }
172
+ : { _tag: "Unrestricted" },
173
+ });
174
+
175
+ // Capture the host access service for THIS execution, not the application singleton.
176
+ const access = yield* BrowserCredentialAccess;
177
+ const started = yield* Clock.currentTimeMillis;
178
+ const driver = yield* transport.open(policy);
179
+ const lock = yield* Semaphore.make(1);
180
+ let observation: typeof ProtectedObservationState.Type = "before-exposure";
181
+ let cleanup: typeof ProtectedCleanup.Type = "not-requested";
182
+ let actions = 0;
183
+ let dispatch: ProtectedBrowserError["dispatch"] = "not-dispatched";
184
+ let milestone: ProtectedBrowserError["milestone"] = "none";
185
+ const exposures: Array<CredentialTarget> = [];
186
+
187
+ const offers = new Map<
188
+ string,
189
+ {
190
+ caller: Redacted.Redacted<string>;
191
+ key: Redacted.Redacted<string>;
192
+ target: CredentialTarget;
193
+ targetRef: string;
194
+ kind: typeof CredentialKind.Type;
195
+ expires: number;
196
+ }
197
+ >();
198
+
199
+ const fail = (reason: ProtectedBrowserError["reason"]) =>
200
+ new ProtectedBrowserError({ reason, dispatch, milestone, observation, cleanup });
201
+
202
+ const close = yield* Effect.cached(
203
+ Effect.uninterruptible(
204
+ Effect.gen(function* () {
205
+ observation = "closed";
206
+ offers.clear();
207
+ driver.invalidate();
208
+ cleanup = yield* driver.close.pipe(
209
+ Effect.interruptible,
210
+ Effect.timeoutOrElse({
211
+ duration: "10 seconds",
212
+ orElse: () => Effect.succeed("unconfirmed" as const),
213
+ }),
214
+ Effect.catchCause(() => Effect.succeed("unconfirmed" as const)),
215
+ );
216
+
217
+ return cleanup;
218
+ }),
219
+ ),
220
+ );
221
+
222
+ yield* Effect.addFinalizer(() =>
223
+ close.pipe(
224
+ Effect.flatMap((result) =>
225
+ result === "unconfirmed"
226
+ ? Effect.logWarning("Protected browser exact-session closure unconfirmed")
227
+ : Effect.void,
228
+ ),
229
+ ),
230
+ );
231
+
232
+ const remote = <A, R>(effect: Effect.Effect<A, ProtectedTransportError, R>) =>
233
+ effect.pipe(Effect.mapError((error) => fail(error.reason)));
234
+
235
+ const decode = <A>(schema: Schema.Codec<A>, value: unknown) =>
236
+ Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => fail("provider")));
237
+
238
+ const caller = access.caller.pipe(Effect.mapError((error) => fail(error.reason)));
239
+ const pageContext = remote(driver.context);
240
+
241
+ const permitObservation = Effect.gen(function* () {
242
+ const context = yield* pageContext;
243
+
244
+ if (exposures.length > 0) {
245
+ observation = "protected";
246
+
247
+ const decision = yield* access
248
+ .observation({ ...context, caller: yield* caller, exposures: [...exposures] })
249
+ .pipe(Effect.mapError((error) => fail(error.reason)));
250
+
251
+ if (decision !== "trust-recipient-no-credential-echo")
252
+ return yield* fail("observation-blocked");
253
+ observation = "approved-after-exposure";
254
+ }
255
+
256
+ return context;
257
+ });
258
+
259
+ const target = (ref: string) => remote(driver.target(ref));
260
+
261
+ const bounded = <A>(result: A) => {
262
+ const bytes = new TextEncoder().encode(JSON.stringify(result)).byteLength;
263
+
264
+ return bytes <= policy.maxReturnedBytes
265
+ ? Effect.succeed(result)
266
+ : Effect.fail(fail("limit"));
267
+ };
268
+
269
+ const run = <A>(effect: Effect.Effect<A, ProtectedBrowserError>) =>
270
+ lock
271
+ .withPermitsIfAvailable(1)(
272
+ Effect.gen(function* () {
273
+ dispatch = "not-dispatched";
274
+ milestone = "none";
275
+ if (observation === "closed") return yield* fail("closed");
276
+ if (++actions > policy.maxActions) return yield* fail("limit");
277
+
278
+ const remaining =
279
+ policy.maxElapsedMillis - ((yield* Clock.currentTimeMillis) - started);
280
+
281
+ if (remaining <= 0) {
282
+ yield* close;
283
+
284
+ return yield* fail("timeout");
285
+ }
286
+
287
+ return yield* effect.pipe(
288
+ Effect.timeoutOrElse({
289
+ duration: remaining,
290
+ orElse: () => Effect.fail(fail("timeout")),
291
+ }),
292
+ Effect.catchCause((cause) =>
293
+ Effect.gen(function* () {
294
+ // No foreign exception or defect is retained. Interrupted operations still finalize.
295
+ const error = cause.reasons.find(
296
+ (reason) =>
297
+ reason._tag === "Fail" && Schema.is(ProtectedBrowserError)(reason.error),
298
+ );
299
+
300
+ const reason =
301
+ error?._tag === "Fail" && Schema.is(ProtectedBrowserError)(error.error)
302
+ ? error.error.reason
303
+ : "provider";
304
+
305
+ const interrupt = cause.reasons.some((reason) => reason._tag === "Interrupt");
306
+
307
+ if (
308
+ dispatch !== "not-dispatched" ||
309
+ interrupt ||
310
+ reason === "provider" ||
311
+ reason === "timeout"
312
+ )
313
+ yield* close;
314
+ if (interrupt) return yield* Effect.interrupt;
315
+
316
+ return yield* fail(
317
+ dispatch === "possibly-dispatched" && reason === "provider"
318
+ ? "outcome-unknown"
319
+ : reason,
320
+ );
321
+ }),
322
+ ),
323
+ Effect.onInterrupt(() => close),
324
+ Effect.withTracerEnabled(false),
325
+ );
326
+ }),
327
+ )
328
+ .pipe(
329
+ Effect.flatMap((result) =>
330
+ Option.isSome(result)
331
+ ? Effect.succeed(result.value)
332
+ : Effect.fail(
333
+ new ProtectedBrowserError({
334
+ reason: "busy",
335
+ dispatch: "not-dispatched",
336
+ milestone: "none",
337
+ observation,
338
+ cleanup,
339
+ }),
340
+ ),
341
+ ),
342
+ );
343
+
344
+ return {
345
+ close,
346
+ navigate: (request) =>
347
+ run(
348
+ Effect.gen(function* () {
349
+ const decoded = yield* Schema.decodeUnknownEffect(ProtectedBrowserNavigate)(
350
+ request,
351
+ ).pipe(Effect.mapError(() => fail("denied")));
352
+
353
+ let url: URL;
354
+
355
+ try {
356
+ url = new URL(decoded.url);
357
+ } catch {
358
+ return yield* fail("denied");
359
+ }
360
+ if (
361
+ url.protocol !== "https:" ||
362
+ url.username ||
363
+ url.password ||
364
+ (policy.network._tag === "ExactHosts" &&
365
+ !policy.network.allowedHosts.includes(url.host))
366
+ )
367
+ return yield* fail("denied");
368
+ if (exposures.length > 0) yield* permitObservation;
369
+ offers.clear();
370
+ dispatch = "possibly-dispatched";
371
+ yield* remote(driver.navigate(decoded.url));
372
+ dispatch = "dispatched";
373
+ yield* permitObservation;
374
+ }),
375
+ ),
376
+ observe: run(
377
+ Effect.gen(function* () {
378
+ const before = yield* permitObservation;
379
+ const result = yield* remote(driver.discover);
380
+ const after = yield* permitObservation;
381
+
382
+ if (before.document !== after.document || result.document !== after.document)
383
+ return yield* fail("stale-reference");
384
+
385
+ return yield* bounded(
386
+ ProtectedBrowserObservation.make({
387
+ ...result,
388
+ observation: exposures.length > 0 ? "approved-after-exposure" : "before-exposure",
389
+ }),
390
+ );
391
+ }),
392
+ ),
393
+ click: (request) =>
394
+ run(
395
+ Effect.gen(function* () {
396
+ const decoded = yield* Schema.decodeUnknownEffect(ProtectedBrowserClick)(
397
+ request,
398
+ ).pipe(Effect.mapError(() => fail("denied")));
399
+
400
+ yield* permitObservation;
401
+ const control = yield* target(decoded.ref);
402
+
403
+ // Credential submission goes through useCredential, never a generic click.
404
+ if (control.role !== "link" && control.role !== "button")
405
+ return yield* fail("unsupported");
406
+ dispatch = "possibly-dispatched";
407
+ yield* remote(driver.click(decoded.ref));
408
+ dispatch = "dispatched";
409
+ yield* permitObservation;
410
+ }),
411
+ ),
412
+ listCredentialOffers: (request) =>
413
+ run(
414
+ Effect.gen(function* () {
415
+ const decoded = yield* Schema.decodeUnknownEffect(ListCredentialOffers)(
416
+ request,
417
+ ).pipe(Effect.mapError(() => fail("denied")));
418
+
419
+ yield* permitObservation;
420
+ const control = yield* target(decoded.target);
421
+
422
+ if (kindFor(control.role) !== decoded.kind) return yield* fail("unsupported");
423
+ const principal = yield* caller;
424
+
425
+ const candidates = yield* access
426
+ .list({ caller: principal, kind: decoded.kind, target: control.target })
427
+ .pipe(Effect.mapError((error) => fail(error.reason)));
428
+
429
+ const now = yield* Clock.currentTimeMillis;
430
+
431
+ for (const [ref, offer] of offers) if (offer.expires <= now) offers.delete(ref);
432
+ if (candidates.length > 16 || offers.size + candidates.length > 64)
433
+ return yield* fail("limit");
434
+ if (!sameTarget(control.target, (yield* target(decoded.target)).target))
435
+ return yield* fail("stale-reference");
436
+ const expires = now + 60_000;
437
+ const result: Array<CredentialOffer> = [];
438
+ const pending: typeof offers = new Map();
439
+
440
+ for (const candidate of candidates) {
441
+ const metadata = yield* decode(CredentialOfferMetadata, candidate.metadata);
442
+
443
+ const ref = yield* crypto.randomUUIDv4.pipe(
444
+ Effect.mapError(() => fail("provider")),
445
+ );
446
+
447
+ pending.set(ref, {
448
+ caller: principal,
449
+ key: candidate.key,
450
+ target: control.target,
451
+ targetRef: decoded.target,
452
+ kind: decoded.kind,
453
+ expires,
454
+ });
455
+ result.push(CredentialOffer.make({ ref, kind: decoded.kind, metadata }));
456
+ }
457
+ yield* bounded(result);
458
+ for (const [ref, offer] of pending) offers.set(ref, offer);
459
+
460
+ return result;
461
+ }),
462
+ ),
463
+ useCredential: (request) =>
464
+ run(
465
+ Effect.gen(function* () {
466
+ const decoded = yield* Schema.decodeUnknownEffect(UseCredential)(request).pipe(
467
+ Effect.mapError(() => fail("denied")),
468
+ );
469
+
470
+ const offer = offers.get(decoded.offer);
471
+
472
+ if (offer === undefined || offer.expires <= (yield* Clock.currentTimeMillis))
473
+ return yield* fail("stale-reference");
474
+ const principal = yield* caller;
475
+
476
+ if (Redacted.value(principal) !== Redacted.value(offer.caller))
477
+ return yield* fail("denied");
478
+ if (offer.kind === "card" && decoded.submit !== undefined)
479
+ return yield* fail("unsupported");
480
+ if (
481
+ new Set(decoded.fields.map((field) => field.ref)).size !==
482
+ decoded.fields.length ||
483
+ new Set(decoded.fields.map((field) => field.role)).size !== decoded.fields.length
484
+ )
485
+ return yield* fail("denied");
486
+
487
+ const validate = Effect.gen(function* () {
488
+ if (!sameTarget(offer.target, (yield* target(offer.targetRef)).target))
489
+ return yield* fail("stale-reference");
490
+ for (const field of decoded.fields) {
491
+ const current = yield* target(field.ref);
492
+
493
+ if (
494
+ field.role !== current.role ||
495
+ kindFor(field.role) !== offer.kind ||
496
+ !sameTarget(offer.target, current.target)
497
+ )
498
+ return yield* fail("denied");
499
+ }
500
+ if (decoded.submit !== undefined) {
501
+ const submit = yield* target(decoded.submit);
502
+
503
+ if (submit.role !== "submit" || !sameTarget(offer.target, submit.target))
504
+ return yield* fail("denied");
505
+ }
506
+ });
507
+
508
+ yield* validate;
509
+
510
+ const authorization: CredentialUseAuthorization = {
511
+ caller: principal,
512
+ key: offer.key,
513
+ kind: offer.kind,
514
+ target: offer.target,
515
+ roles: decoded.fields.map((field) => field.role),
516
+ submit: decoded.submit !== undefined,
517
+ };
518
+
519
+ const authorize = Effect.gen(function* () {
520
+ if (Redacted.value(yield* caller) !== Redacted.value(principal))
521
+ return yield* fail("denied");
522
+ yield* access
523
+ .authorize(authorization)
524
+ .pipe(Effect.mapError((error) => fail(error.reason)));
525
+ });
526
+
527
+ yield* authorize;
528
+ yield* validate;
529
+
530
+ const raw = yield* access
531
+ .resolve(authorization)
532
+ .pipe(Effect.mapError((error) => fail(error.reason)));
533
+
534
+ const material = yield* Schema.decodeUnknownEffect(
535
+ offer.kind === "login" ? LoginCredential : CardCredential,
536
+ )(raw).pipe(Effect.mapError(() => fail("resolver")));
537
+
538
+ for (const field of decoded.fields)
539
+ if (secretFor(material, field.role) === undefined)
540
+ return yield* fail("missing-credential");
541
+ offers.delete(decoded.offer);
542
+ for (const field of decoded.fields) {
543
+ yield* validate;
544
+ yield* authorize;
545
+ const value = secretFor(material, field.role);
546
+
547
+ if (value === undefined) return yield* fail("missing-credential");
548
+ observation = "protected";
549
+ yield* remote(driver.fill(field.ref, field.role, value)).pipe(
550
+ Effect.provideService(ProtectedBrowserDispatch, {
551
+ mark: Effect.sync(() => {
552
+ dispatch = "possibly-dispatched";
553
+ if (!exposures.some((target) => sameTarget(target, offer.target)))
554
+ exposures.push(offer.target);
555
+ }),
556
+ }),
557
+ );
558
+ dispatch = "dispatched";
559
+ milestone = "partial-fill";
560
+ }
561
+ milestone = "filled";
562
+ if (decoded.submit !== undefined) {
563
+ yield* validate;
564
+ yield* authorize;
565
+ const ref = decoded.submit;
566
+
567
+ dispatch = "possibly-dispatched";
568
+ yield* remote(driver.click(ref)).pipe(
569
+ Effect.catch((error) => {
570
+ if (error.reason === "needs-attention") dispatch = "dispatched";
571
+
572
+ return Effect.fail(error);
573
+ }),
574
+ );
575
+ dispatch = "dispatched";
576
+ milestone = "submission-dispatched";
577
+ }
578
+ yield* permitObservation;
579
+
580
+ return CredentialUseResult.make({
581
+ dispatch,
582
+ milestone,
583
+ observation,
584
+ cleanup,
585
+ authentication: "unverified",
586
+ });
587
+ }),
588
+ ),
589
+ };
590
+ }, Effect.withTracerEnabled(false));
591
+
592
+ return { open };
593
+ }),
594
+ );
@@ -0,0 +1,2 @@
1
+ export { browserRunProtectedLayer } from "./protected-browser/policy.ts";
2
+ export { browserRunProtectedBindingLayer } from "./protected-browser/binding.ts";