@effect-agent/platform-cloudflare 0.1.0-beta.121 → 0.1.0-beta.123

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 (35) 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 +50 -50
  10. package/dist/{InteractiveBrowser-Cm4xgiGy.d.mts → InteractiveBrowser-BnqY_Y-F.d.mts} +2 -2
  11. package/dist/{InteractiveBrowser-DhOkdUx9.mjs → InteractiveBrowser-DeOlcu-2.mjs} +190 -50
  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-BTo1jqab.d.mts} +33 -33
  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/InteractiveBrowser.ts +1 -1
  23. package/src/index.ts +2 -0
  24. package/src/internal/browser-binding.ts +245 -57
  25. package/src/internal/browser-credentials.ts +362 -0
  26. package/dist/InteractiveBrowser-DhOkdUx9.mjs.map +0 -1
  27. package/dist/ProtectedBrowser.d.mts +0 -128
  28. package/dist/ProtectedBrowser.mjs +0 -1201
  29. package/dist/ProtectedBrowser.mjs.map +0 -1
  30. package/src/ProtectedBrowser.ts +0 -9
  31. package/src/protected-browser/binding.ts +0 -366
  32. package/src/protected-browser/host.ts +0 -382
  33. package/src/protected-browser/inspect-frame.ts +0 -140
  34. package/src/protected-browser/native.ts +0 -483
  35. package/src/protected-browser/policy.ts +0 -872
@@ -1,483 +0,0 @@
1
- /// <reference types="@cloudflare/workers-types" />
2
-
3
- import { Cause, Clock, Context, Crypto, Effect, Redacted, Schema, type Scope } from "effect";
4
- import { type InteractiveBrowserPolicy } from "effect-agent/interactive-browser";
5
- import {
6
- CredentialOrigin,
7
- CredentialTarget,
8
- ProtectedBrowserControl,
9
- } from "effect-agent/protected-browser";
10
- import type {
11
- Browser,
12
- Frame,
13
- JSHandle,
14
- Page,
15
- } from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
16
-
17
- import {
18
- browserFailure,
19
- reportBrowserCause,
20
- reportedBrowserError,
21
- } from "../internal/browser-failure.ts";
22
- import { inspectFrame, maxAttributeLength } from "./inspect-frame.ts";
23
- import {
24
- ProtectedBrowserDispatch,
25
- ProtectedTransportError,
26
- ProtectedDiscovery,
27
- ProtectedPageContext,
28
- type ProtectedBrowserTransport,
29
- } from "./policy.ts";
30
-
31
- const Description = Schema.Struct({
32
- role: ProtectedBrowserControl.fields.role,
33
- formIndex: Schema.Natural,
34
- action: Schema.String.check(Schema.isMaxLength(maxAttributeLength)),
35
- label: Schema.String.check(Schema.isMaxLength(200)),
36
- checked: Schema.optionalKey(Schema.Boolean),
37
- options: ProtectedBrowserControl.fields.options,
38
- truncated: Schema.optionalKey(Schema.Boolean),
39
- });
40
-
41
- const Descriptions = Schema.Array(Schema.NullOr(Description)).check(Schema.isMaxLength(65));
42
- const FillResult = Schema.Literals([true, false, "unsupported-before-write", "unsupported"]);
43
-
44
- interface FrameState {
45
- readonly frame: Frame;
46
- readonly handle: JSHandle<unknown>;
47
- readonly ref: string;
48
- readonly document: string;
49
- readonly forms: Map<number, string>;
50
- }
51
- interface ControlState {
52
- readonly frame: FrameState;
53
- readonly index: number;
54
- readonly control: ProtectedBrowserControl;
55
- readonly expires: number;
56
- }
57
-
58
- /** One host-private acquired session. The SDK handles and exact-session cleanup have one owner. */
59
- export class ProtectedNativeSession extends Context.Service<
60
- ProtectedNativeSession,
61
- {
62
- readonly browser: Browser;
63
- readonly page: Page;
64
- readonly close: Effect.Effect<"confirmed" | "unconfirmed">;
65
- /** Host-owned attachment release may detach a committed suspended session. */
66
- readonly release?: Effect.Effect<void>;
67
- }
68
- >()("@effect-agent/platform-cloudflare/ProtectedNativeSession") {}
69
-
70
- const transportError = (reason: ProtectedTransportError["reason"]) =>
71
- new ProtectedTransportError({ reason });
72
-
73
- const decode = <A>(schema: Schema.Codec<A>, value: unknown) =>
74
- Schema.decodeUnknownEffect(schema)(value).pipe(
75
- Effect.catch((error) =>
76
- reportBrowserCause("protected.decode", Cause.fail(error)).pipe(
77
- Effect.andThen(Effect.fail(reportedBrowserError(transportError("provider")))),
78
- ),
79
- ),
80
- );
81
-
82
- // Attach rejection handling inside the SDK callback before workerd can report foreign diagnostics.
83
- const remote = <A>(operation: string, run: (signal: AbortSignal) => Promise<A>) =>
84
- Effect.tryPromise({
85
- try: async (signal) => {
86
- try {
87
- return { ok: true as const, value: await run(signal) };
88
- } catch (cause) {
89
- return { ok: false as const, failure: browserFailure(operation, cause) };
90
- }
91
- },
92
- catch: (cause) => browserFailure(operation, cause),
93
- }).pipe(
94
- Effect.flatMap((result) =>
95
- result.ok ? Effect.succeed(result.value) : Effect.fail(result.failure),
96
- ),
97
- Effect.catch((failure) =>
98
- reportBrowserCause(operation, Cause.fail(failure)).pipe(
99
- Effect.andThen(Effect.fail(reportedBrowserError(transportError("provider")))),
100
- ),
101
- ),
102
- );
103
-
104
- /** Host-private scoped transport. Session capabilities are requirements; policy is per-pass data. */
105
- export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(function* (
106
- policy: InteractiveBrowserPolicy,
107
- ): Effect.fn.Return<
108
- ProtectedBrowserTransport,
109
- never,
110
- ProtectedNativeSession | Crypto.Crypto | Scope.Scope
111
- > {
112
- const session = yield* ProtectedNativeSession;
113
- const { browser, page } = session;
114
- const crypto = yield* Crypto.Crypto;
115
- const clock = yield* Clock.Clock;
116
-
117
- const uuid = crypto.randomUUIDv4.pipe(
118
- Effect.catch((error) =>
119
- reportBrowserCause("protected.identity", Cause.fail(error)).pipe(
120
- Effect.andThen(Effect.fail(reportedBrowserError(transportError("provider")))),
121
- ),
122
- ),
123
- );
124
-
125
- let closed = false;
126
- let violation = false;
127
- let documentRef: string | undefined;
128
- let observationOrigins: ReadonlySet<string> | undefined;
129
- const frames = new Map<Frame, FrameState>();
130
- const controls = new Map<string, ControlState>();
131
-
132
- const origin = (value: string) =>
133
- Effect.try({
134
- try: () => {
135
- const url = new URL(value);
136
-
137
- if (url.username || url.password) throw transportError("stale-reference");
138
- if (!Schema.is(CredentialOrigin)(url.origin)) throw transportError("unsupported");
139
- if (policy.network._tag === "ExactHosts" && !policy.network.allowedHosts.includes(url.host))
140
- throw transportError("stale-reference");
141
-
142
- return url.origin;
143
- },
144
- catch: (cause) =>
145
- Schema.is(ProtectedTransportError)(cause) ? cause : transportError("provider"),
146
- });
147
-
148
- const check = Effect.suspend(() =>
149
- closed || violation || !browser.isConnected()
150
- ? Effect.fail(transportError("stale-reference"))
151
- : Effect.void,
152
- );
153
-
154
- const clear = () => {
155
- controls.clear();
156
- // Browser event callbacks only invalidate. The next Effect operation allocates the new identity.
157
- documentRef = undefined;
158
- };
159
-
160
- const onNavigated = () => {
161
- clear();
162
- };
163
-
164
- const onTarget = (target: { type(): string }) => {
165
- if (target.type() !== "page") return;
166
- // No popup or additional page may inherit an observation channel.
167
- violation = true;
168
- clear();
169
- };
170
-
171
- const invalidate = () => {
172
- closed = true;
173
- clear();
174
- };
175
-
176
- const observationFrames = Effect.gen(function* () {
177
- const list = page.frames();
178
-
179
- if (list.length > 32) return yield* transportError("unsupported");
180
-
181
- // Opaque/blank child documents have no observation channel or credential targets.
182
- // The main document and every retained HTTPS child still require strict origin validation.
183
- return yield* Effect.filter(list, (frame) =>
184
- frame === page.mainFrame()
185
- ? Effect.succeed(true)
186
- : Effect.try({
187
- try: () => frame.url() !== "" && new URL(frame.url()).protocol === "https:",
188
- catch: () => transportError("provider"),
189
- }),
190
- );
191
- });
192
-
193
- const context = Effect.gen(function* () {
194
- yield* check;
195
- const list = yield* observationFrames;
196
- const topOrigin = yield* origin(page.url());
197
- const frameOrigins = [...new Set(yield* Effect.forEach(list, (frame) => origin(frame.url())))];
198
-
199
- if (frameOrigins.length > 16) return yield* transportError("unsupported");
200
-
201
- if (documentRef === undefined) documentRef = yield* uuid;
202
-
203
- return yield* decode(ProtectedPageContext, { document: documentRef, topOrigin, frameOrigins });
204
- });
205
-
206
- const isCurrent = Effect.fn("ProtectedNativeTransport.isCurrent")(function* (state: FrameState) {
207
- if (state.frame.detached) return false;
208
-
209
- return yield* remote("protected.validate-document", () =>
210
- state.handle.evaluate((held) => {
211
- if (typeof held !== "object" || held === null) return false;
212
-
213
- return Reflect.get(held, "doc") === Reflect.get(globalThis, "document");
214
- }),
215
- );
216
- });
217
-
218
- const get = Effect.fn("ProtectedNativeTransport.get")(function* (ref: string) {
219
- yield* check;
220
- const state = controls.get(ref);
221
-
222
- if (
223
- !state ||
224
- (observationOrigins !== undefined &&
225
- !observationOrigins.has(state.control.target.frameOrigin)) ||
226
- state.expires <= (yield* clock.currentTimeMillis) ||
227
- !(yield* isCurrent(state.frame))
228
- )
229
- return yield* transportError("stale-reference");
230
-
231
- const current = yield* remote("protected.validate-control", () =>
232
- state.frame.handle.evaluate((held, index) => {
233
- if (typeof held !== "object" || held === null) return null;
234
-
235
- return Reflect.apply(Reflect.get(held, "validate"), held, [index]);
236
- }, state.index),
237
- );
238
-
239
- const description = yield* decode(Schema.NullOr(Description), current);
240
-
241
- if (description === null) return yield* transportError("stale-reference");
242
- const ctx = yield* context;
243
-
244
- if (
245
- ctx.topOrigin !== state.control.target.topOrigin ||
246
- (yield* origin(state.frame.frame.url())) !== state.control.target.frameOrigin ||
247
- (yield* origin(description.action)) !== state.control.target.recipientOrigin ||
248
- description.role !== state.control.role
249
- )
250
- return yield* transportError("stale-reference");
251
-
252
- return state;
253
- });
254
-
255
- const close = yield* Effect.cached(
256
- Effect.gen(function* () {
257
- invalidate();
258
-
259
- return yield* session.close;
260
- }).pipe(
261
- Effect.ensuring(
262
- Effect.sync(() => {
263
- page.off("framenavigated", onNavigated);
264
- page.off("framedetached", onNavigated);
265
- browser.off("targetcreated", onTarget);
266
- // Handles die with the exact session. Do not block remote termination on local disposal.
267
- frames.clear();
268
- }),
269
- ),
270
- ),
271
- );
272
-
273
- yield* Effect.acquireRelease(
274
- Effect.sync(() => {
275
- page.on("framenavigated", onNavigated);
276
- page.on("framedetached", onNavigated);
277
- browser.on("targetcreated", onTarget);
278
- }),
279
- () =>
280
- session.release === undefined
281
- ? close
282
- : session.release.pipe(
283
- Effect.ensuring(
284
- Effect.sync(() => {
285
- invalidate();
286
- page.off("framenavigated", onNavigated);
287
- page.off("framedetached", onNavigated);
288
- browser.off("targetcreated", onTarget);
289
- frames.clear();
290
- }),
291
- ),
292
- ),
293
- );
294
-
295
- return {
296
- restrictObservation: Effect.fn("ProtectedNativeTransport.restrictObservation")(
297
- function* (origins) {
298
- yield* check;
299
- observationOrigins =
300
- origins === undefined
301
- ? undefined
302
- : new Set(yield* decode(Schema.Array(CredentialOrigin), origins));
303
- // A later grant expansion must not revive previously excluded references.
304
- for (const [ref, state] of controls)
305
- if (
306
- observationOrigins !== undefined &&
307
- !observationOrigins.has(state.control.target.frameOrigin)
308
- )
309
- controls.delete(ref);
310
- },
311
- ),
312
- context,
313
- invalidate,
314
- resetReferences: clear,
315
- close,
316
- navigate: Effect.fn("ProtectedNativeTransport.navigate")(function* (url) {
317
- yield* check;
318
- yield* origin(url);
319
- clear();
320
- yield* remote("protected.navigate", () => page.goto(url, { waitUntil: "load" }));
321
- yield* context;
322
- }),
323
- target: (ref) => get(ref).pipe(Effect.map((state) => state.control)),
324
- discover: Effect.gen(function* () {
325
- const before = yield* context;
326
-
327
- controls.clear();
328
- for (const state of frames.values())
329
- yield* remote("protected.dispose", () => state.handle.dispose());
330
- frames.clear();
331
- const discovered: Array<ProtectedBrowserControl> = [];
332
- let text = "";
333
- let truncated = false;
334
- const frameOrigins = new Set<string>();
335
-
336
- for (const frame of yield* observationFrames) {
337
- const frameOrigin = yield* origin(frame.url());
338
-
339
- if (observationOrigins !== undefined && !observationOrigins.has(frameOrigin)) continue;
340
- frameOrigins.add(frameOrigin);
341
- if (typeof frame.isolatedRealm !== "function") return yield* transportError("unsupported");
342
-
343
- const handle = yield* remote("protected.discover", () =>
344
- frame.isolatedRealm().evaluateHandle(inspectFrame),
345
- );
346
-
347
- const state: FrameState = {
348
- frame,
349
- handle,
350
- ref: yield* uuid,
351
- document: yield* uuid,
352
- forms: new Map(),
353
- };
354
-
355
- frames.set(frame, state);
356
-
357
- const raw = yield* remote("protected.describe-controls", () =>
358
- handle.evaluate((held) => {
359
- if (typeof held !== "object" || held === null) return null;
360
-
361
- return Reflect.get(held, "original");
362
- }),
363
- );
364
-
365
- const descriptions = yield* decode(Descriptions, raw);
366
-
367
- for (let index = 0; index < descriptions.length; index++) {
368
- const desc = descriptions[index];
369
-
370
- if (!desc) continue;
371
- if (desc.truncated) truncated = true;
372
- if (discovered.length === 64) {
373
- truncated = true;
374
- break;
375
- }
376
- let form = state.forms.get(desc.formIndex);
377
-
378
- if (form === undefined) {
379
- form = yield* uuid;
380
- state.forms.set(desc.formIndex, form);
381
- }
382
- // Unsupported destinations/controls are not offered as credential targets.
383
- const recipient = yield* origin(desc.action).pipe(Effect.result);
384
-
385
- if (recipient._tag === "Failure") continue;
386
-
387
- const control = ProtectedBrowserControl.make({
388
- ref: yield* uuid,
389
- role: desc.role,
390
- label: desc.label,
391
- ...(desc.checked === undefined ? {} : { checked: desc.checked }),
392
- ...(desc.options === undefined ? {} : { options: desc.options }),
393
- ...(desc.role === "link" ? { url: desc.action } : {}),
394
- target: CredentialTarget.make({
395
- topOrigin: before.topOrigin,
396
- frameOrigin: yield* origin(frame.url()),
397
- recipientOrigin: recipient.success,
398
- document: state.document,
399
- frame: state.ref,
400
- form,
401
- }),
402
- });
403
-
404
- controls.set(control.ref, {
405
- frame: state,
406
- index,
407
- control,
408
- expires: (yield* clock.currentTimeMillis) + 60_000,
409
- });
410
- discovered.push(control);
411
- }
412
-
413
- const rawText = yield* remote("protected.read-text", () =>
414
- handle.evaluate((held) => {
415
- if (typeof held !== "object" || held === null) return null;
416
-
417
- return Reflect.apply(Reflect.get(held, "text"), held, []);
418
- }),
419
- );
420
-
421
- text += yield* decode(Schema.String.check(Schema.isMaxLength(65536)), rawText);
422
- if (text.length > 65536) {
423
- text = text.slice(0, 65536);
424
- truncated = true;
425
- }
426
- }
427
- if ((yield* context).document !== before.document)
428
- return yield* transportError("stale-reference");
429
-
430
- return yield* decode(ProtectedDiscovery, {
431
- ...before,
432
- frameOrigins: [...frameOrigins],
433
- text,
434
- controls: discovered,
435
- truncated,
436
- });
437
- }),
438
- fill: Effect.fn("ProtectedNativeTransport.fill")(function* (ref, role, value) {
439
- const dispatch = yield* ProtectedBrowserDispatch;
440
- const state = yield* get(ref);
441
-
442
- // CDP dispatch may mutate before its reply is lost, so uncertainty starts here.
443
- yield* dispatch.mark;
444
-
445
- const raw = yield* remote("protected.fill", () =>
446
- state.frame.handle.evaluate(
447
- (held, index, expectedRole, secret) => {
448
- if (typeof held !== "object" || held === null) return false;
449
-
450
- return Reflect.apply(Reflect.get(held, "fill"), held, [index, expectedRole, secret]);
451
- },
452
- state.index,
453
- role,
454
- Redacted.value(value),
455
- ),
456
- );
457
-
458
- const filled = yield* decode(FillResult, raw);
459
-
460
- if (filled === false || filled === "unsupported-before-write") {
461
- yield* dispatch.confirmNoWrite;
462
-
463
- return yield* transportError(filled === false ? "stale-reference" : "unsupported");
464
- }
465
-
466
- if (filled === "unsupported") return yield* transportError("unsupported");
467
- }),
468
- click: Effect.fn("ProtectedNativeTransport.click")(function* (ref) {
469
- const state = yield* get(ref);
470
-
471
- const clicked = yield* remote("protected.click", () =>
472
- state.frame.handle.evaluate((held, index) => {
473
- if (typeof held !== "object" || held === null) return false;
474
-
475
- return Reflect.apply(Reflect.get(held, "click"), held, [index]);
476
- }, state.index),
477
- );
478
-
479
- if (clicked === "needs-attention") return yield* transportError("needs-attention");
480
- if (clicked !== true) return yield* transportError("stale-reference");
481
- }),
482
- };
483
- }, Effect.withTracerEnabled(false));