@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.
- package/dist/browser-quick-action.mjs.map +1 -1
- package/dist/browser-rest-capture.mjs.map +1 -1
- package/dist/browser-rest-crawl.mjs.map +1 -1
- package/dist/browser-session-lifecycle-DqntvG-Y.d.mts +21 -0
- package/dist/browser-session-lifecycle-ZGgb3pnK.mjs +84 -0
- package/dist/browser-session-lifecycle-ZGgb3pnK.mjs.map +1 -0
- package/dist/index.d.mts +47 -34
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/interactive-browser.d.mts +1 -18
- package/dist/interactive-browser.mjs +2 -81
- package/dist/interactive-browser.mjs.map +1 -1
- package/dist/{prepared-admission-BKp_Upw2.mjs → prepared-admission-BhW2eT_a.mjs} +3 -3
- package/dist/prepared-admission-BhW2eT_a.mjs.map +1 -0
- package/dist/protected-browser.d.mts +62 -0
- package/dist/protected-browser.mjs +714 -0
- package/dist/protected-browser.mjs.map +1 -0
- package/dist/scheduling.mjs +1 -1
- package/dist/scheduling.mjs.map +1 -1
- package/dist/subscriptions.mjs +1 -1
- package/dist/subscriptions.mjs.map +1 -1
- package/package.json +1 -81
- package/src/alarm.ts +277 -242
- package/src/bindings.ts +5 -0
- package/src/boundary.ts +4 -0
- package/src/browser-quick-action.ts +52 -0
- package/src/browser-rest-capture.ts +30 -0
- package/src/browser-rest-crawl.ts +43 -0
- package/src/browser-session-lifecycle.ts +18 -0
- package/src/client.ts +40 -0
- package/src/code-mode-executor.ts +50 -0
- package/src/interactive-browser.ts +176 -0
- package/src/layers.ts +13 -3
- package/src/memory.ts +42 -3
- package/src/prepared-admission.ts +1 -0
- package/src/progress-wait.ts +14 -0
- package/src/protected-browser/binding.ts +185 -0
- package/src/protected-browser/inspect-frame.ts +82 -0
- package/src/protected-browser/native.ts +384 -0
- package/src/protected-browser/policy.ts +594 -0
- package/src/protected-browser.ts +2 -0
- package/src/scheduling.ts +34 -0
- package/src/subscriptions.ts +50 -0
- package/src/thread-object.ts +55 -1
- package/src/transport.ts +1 -0
- package/src/wake-scheduler.ts +1 -0
- package/dist/prepared-admission-BKp_Upw2.mjs.map +0 -1
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
import type { Browser, Frame, JSHandle, Page } from "@cloudflare/puppeteer";
|
|
3
|
+
import {
|
|
4
|
+
CredentialOrigin,
|
|
5
|
+
CredentialTarget,
|
|
6
|
+
ProtectedBrowserControl,
|
|
7
|
+
type InteractiveBrowserPolicy,
|
|
8
|
+
} from "@effect-agent/sandbox";
|
|
9
|
+
import { Clock, Context, Crypto, Effect, Redacted, Schema, type Scope } from "effect";
|
|
10
|
+
|
|
11
|
+
import { inspectFrame, maxAttributeLength } from "./inspect-frame.ts";
|
|
12
|
+
import {
|
|
13
|
+
ProtectedBrowserDispatch,
|
|
14
|
+
ProtectedTransportError,
|
|
15
|
+
ProtectedDiscovery,
|
|
16
|
+
ProtectedPageContext,
|
|
17
|
+
type ProtectedBrowserTransport,
|
|
18
|
+
} from "./policy.ts";
|
|
19
|
+
|
|
20
|
+
// The pinned SDK implements this method but strips its internal declaration from lib/types.d.ts.
|
|
21
|
+
// Keep the declaration narrow and refuse runtimes without it. No main-world fallback is safe.
|
|
22
|
+
declare module "@cloudflare/puppeteer" {
|
|
23
|
+
interface Frame {
|
|
24
|
+
isolatedRealm(): { evaluateHandle(source: string): Promise<JSHandle<unknown>> };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Description = Schema.Struct({
|
|
29
|
+
role: ProtectedBrowserControl.fields.role,
|
|
30
|
+
formIndex: Schema.Natural,
|
|
31
|
+
action: Schema.String.check(Schema.isMaxLength(maxAttributeLength)),
|
|
32
|
+
label: Schema.String.check(Schema.isMaxLength(200)),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const Descriptions = Schema.Array(Schema.NullOr(Description)).check(Schema.isMaxLength(65));
|
|
36
|
+
|
|
37
|
+
interface FrameState {
|
|
38
|
+
readonly frame: Frame;
|
|
39
|
+
readonly handle: JSHandle<unknown>;
|
|
40
|
+
readonly ref: string;
|
|
41
|
+
readonly document: string;
|
|
42
|
+
readonly forms: Map<number, string>;
|
|
43
|
+
}
|
|
44
|
+
interface ControlState {
|
|
45
|
+
readonly frame: FrameState;
|
|
46
|
+
readonly index: number;
|
|
47
|
+
readonly control: ProtectedBrowserControl;
|
|
48
|
+
readonly expires: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One host-private acquired session. The SDK handles and exact-session cleanup have one owner. */
|
|
52
|
+
export class ProtectedNativeSession extends Context.Service<
|
|
53
|
+
ProtectedNativeSession,
|
|
54
|
+
{
|
|
55
|
+
readonly browser: Browser;
|
|
56
|
+
readonly page: Page;
|
|
57
|
+
readonly close: Effect.Effect<"confirmed" | "unconfirmed">;
|
|
58
|
+
}
|
|
59
|
+
>()("@effect-agent/platform-cloudflare/ProtectedNativeSession") {}
|
|
60
|
+
|
|
61
|
+
const transportError = (reason: ProtectedTransportError["reason"]) =>
|
|
62
|
+
new ProtectedTransportError({ reason });
|
|
63
|
+
|
|
64
|
+
const decode = <A>(schema: Schema.Codec<A>, value: unknown) =>
|
|
65
|
+
Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => transportError("provider")));
|
|
66
|
+
|
|
67
|
+
// Attach rejection handling inside the SDK callback before workerd can report foreign diagnostics.
|
|
68
|
+
const remote = <A>(run: (signal: AbortSignal) => Promise<A>) =>
|
|
69
|
+
Effect.tryPromise({
|
|
70
|
+
try: async (signal) => {
|
|
71
|
+
try {
|
|
72
|
+
return { ok: true as const, value: await run(signal) };
|
|
73
|
+
} catch {
|
|
74
|
+
return { ok: false as const };
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
catch: () => transportError("provider"),
|
|
78
|
+
}).pipe(
|
|
79
|
+
Effect.flatMap((result) =>
|
|
80
|
+
result.ok ? Effect.succeed(result.value) : Effect.fail(transportError("provider")),
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/** Host-private scoped transport. Session capabilities are requirements; policy is per-pass data. */
|
|
85
|
+
export const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(function* (
|
|
86
|
+
policy: InteractiveBrowserPolicy,
|
|
87
|
+
): Effect.fn.Return<
|
|
88
|
+
ProtectedBrowserTransport,
|
|
89
|
+
never,
|
|
90
|
+
ProtectedNativeSession | Crypto.Crypto | Scope.Scope
|
|
91
|
+
> {
|
|
92
|
+
const session = yield* ProtectedNativeSession;
|
|
93
|
+
const { browser, page } = session;
|
|
94
|
+
const crypto = yield* Crypto.Crypto;
|
|
95
|
+
const clock = yield* Clock.Clock;
|
|
96
|
+
const uuid = crypto.randomUUIDv4.pipe(Effect.mapError(() => transportError("provider")));
|
|
97
|
+
let closed = false;
|
|
98
|
+
let violation = false;
|
|
99
|
+
let documentRef: string | undefined;
|
|
100
|
+
const frames = new Map<Frame, FrameState>();
|
|
101
|
+
const controls = new Map<string, ControlState>();
|
|
102
|
+
|
|
103
|
+
const origin = (value: string) =>
|
|
104
|
+
Effect.try({
|
|
105
|
+
try: () => {
|
|
106
|
+
const url = new URL(value);
|
|
107
|
+
|
|
108
|
+
if (url.username || url.password) throw transportError("stale-reference");
|
|
109
|
+
if (!Schema.is(CredentialOrigin)(url.origin)) throw transportError("unsupported");
|
|
110
|
+
if (policy.network._tag === "ExactHosts" && !policy.network.allowedHosts.includes(url.host))
|
|
111
|
+
throw transportError("stale-reference");
|
|
112
|
+
|
|
113
|
+
return url.origin;
|
|
114
|
+
},
|
|
115
|
+
catch: (cause) =>
|
|
116
|
+
Schema.is(ProtectedTransportError)(cause) ? cause : transportError("provider"),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const check = Effect.suspend(() =>
|
|
120
|
+
closed || violation || !browser.isConnected()
|
|
121
|
+
? Effect.fail(transportError("stale-reference"))
|
|
122
|
+
: Effect.void,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const clear = () => {
|
|
126
|
+
controls.clear();
|
|
127
|
+
// Browser event callbacks only invalidate. The next Effect operation allocates the new identity.
|
|
128
|
+
documentRef = undefined;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const onNavigated = () => {
|
|
132
|
+
clear();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const onTarget = (target: { type(): string }) => {
|
|
136
|
+
if (target.type() !== "page") return;
|
|
137
|
+
// No popup or additional page may inherit an observation channel.
|
|
138
|
+
violation = true;
|
|
139
|
+
clear();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const invalidate = () => {
|
|
143
|
+
closed = true;
|
|
144
|
+
clear();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const context = Effect.gen(function* () {
|
|
148
|
+
yield* check;
|
|
149
|
+
const list = page.frames();
|
|
150
|
+
|
|
151
|
+
if (list.length > 16) return yield* transportError("unsupported");
|
|
152
|
+
const topOrigin = yield* origin(page.url());
|
|
153
|
+
const frameOrigins = yield* Effect.forEach(list, (frame) => origin(frame.url()));
|
|
154
|
+
|
|
155
|
+
if (documentRef === undefined) documentRef = yield* uuid;
|
|
156
|
+
|
|
157
|
+
return yield* decode(ProtectedPageContext, { document: documentRef, topOrigin, frameOrigins });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const isCurrent = Effect.fn("ProtectedNativeTransport.isCurrent")(function* (state: FrameState) {
|
|
161
|
+
if (state.frame.detached) return false;
|
|
162
|
+
|
|
163
|
+
return yield* remote(() =>
|
|
164
|
+
state.handle.evaluate((held) => {
|
|
165
|
+
if (typeof held !== "object" || held === null) return false;
|
|
166
|
+
|
|
167
|
+
return Reflect.get(held, "doc") === Reflect.get(globalThis, "document");
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const get = Effect.fn("ProtectedNativeTransport.get")(function* (ref: string) {
|
|
173
|
+
yield* check;
|
|
174
|
+
const state = controls.get(ref);
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
!state ||
|
|
178
|
+
state.expires <= (yield* clock.currentTimeMillis) ||
|
|
179
|
+
!(yield* isCurrent(state.frame))
|
|
180
|
+
)
|
|
181
|
+
return yield* transportError("stale-reference");
|
|
182
|
+
|
|
183
|
+
const current = yield* remote(() =>
|
|
184
|
+
state.frame.handle.evaluate((held, index) => {
|
|
185
|
+
if (typeof held !== "object" || held === null) return null;
|
|
186
|
+
|
|
187
|
+
return Reflect.apply(Reflect.get(held, "validate"), held, [index]);
|
|
188
|
+
}, state.index),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const description = yield* decode(Schema.NullOr(Description), current);
|
|
192
|
+
|
|
193
|
+
if (description === null) return yield* transportError("stale-reference");
|
|
194
|
+
const ctx = yield* context;
|
|
195
|
+
|
|
196
|
+
if (
|
|
197
|
+
ctx.topOrigin !== state.control.target.topOrigin ||
|
|
198
|
+
(yield* origin(state.frame.frame.url())) !== state.control.target.frameOrigin ||
|
|
199
|
+
(yield* origin(description.action)) !== state.control.target.recipientOrigin ||
|
|
200
|
+
description.role !== state.control.role
|
|
201
|
+
)
|
|
202
|
+
return yield* transportError("stale-reference");
|
|
203
|
+
|
|
204
|
+
return state;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const close = yield* Effect.cached(
|
|
208
|
+
Effect.gen(function* () {
|
|
209
|
+
invalidate();
|
|
210
|
+
|
|
211
|
+
return yield* session.close;
|
|
212
|
+
}).pipe(
|
|
213
|
+
Effect.ensuring(
|
|
214
|
+
Effect.sync(() => {
|
|
215
|
+
page.off("framenavigated", onNavigated);
|
|
216
|
+
page.off("framedetached", onNavigated);
|
|
217
|
+
browser.off("targetcreated", onTarget);
|
|
218
|
+
// Handles die with the exact session. Do not block remote termination on local disposal.
|
|
219
|
+
frames.clear();
|
|
220
|
+
}),
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
yield* Effect.acquireRelease(
|
|
226
|
+
Effect.sync(() => {
|
|
227
|
+
page.on("framenavigated", onNavigated);
|
|
228
|
+
page.on("framedetached", onNavigated);
|
|
229
|
+
browser.on("targetcreated", onTarget);
|
|
230
|
+
}),
|
|
231
|
+
() => close,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
context,
|
|
236
|
+
invalidate,
|
|
237
|
+
close,
|
|
238
|
+
navigate: Effect.fn("ProtectedNativeTransport.navigate")(function* (url) {
|
|
239
|
+
yield* check;
|
|
240
|
+
yield* origin(url);
|
|
241
|
+
clear();
|
|
242
|
+
yield* remote(() => page.goto(url, { waitUntil: "load" }));
|
|
243
|
+
yield* context;
|
|
244
|
+
}),
|
|
245
|
+
target: (ref) => get(ref).pipe(Effect.map((state) => state.control)),
|
|
246
|
+
discover: Effect.gen(function* () {
|
|
247
|
+
const before = yield* context;
|
|
248
|
+
|
|
249
|
+
controls.clear();
|
|
250
|
+
for (const state of frames.values()) yield* remote(() => state.handle.dispose());
|
|
251
|
+
frames.clear();
|
|
252
|
+
const discovered: Array<ProtectedBrowserControl> = [];
|
|
253
|
+
let text = "";
|
|
254
|
+
let truncated = false;
|
|
255
|
+
|
|
256
|
+
for (const frame of page.frames()) {
|
|
257
|
+
if (typeof frame.isolatedRealm !== "function") return yield* transportError("unsupported");
|
|
258
|
+
const handle = yield* remote(() => frame.isolatedRealm().evaluateHandle(inspectFrame));
|
|
259
|
+
|
|
260
|
+
const state: FrameState = {
|
|
261
|
+
frame,
|
|
262
|
+
handle,
|
|
263
|
+
ref: yield* uuid,
|
|
264
|
+
document: yield* uuid,
|
|
265
|
+
forms: new Map(),
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
frames.set(frame, state);
|
|
269
|
+
|
|
270
|
+
const raw = yield* remote(() =>
|
|
271
|
+
handle.evaluate((held) => {
|
|
272
|
+
if (typeof held !== "object" || held === null) return null;
|
|
273
|
+
|
|
274
|
+
return Reflect.get(held, "original");
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const descriptions = yield* decode(Descriptions, raw);
|
|
279
|
+
|
|
280
|
+
for (let index = 0; index < descriptions.length; index++) {
|
|
281
|
+
const desc = descriptions[index];
|
|
282
|
+
|
|
283
|
+
if (!desc) continue;
|
|
284
|
+
if (discovered.length === 64) {
|
|
285
|
+
truncated = true;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
let form = state.forms.get(desc.formIndex);
|
|
289
|
+
|
|
290
|
+
if (form === undefined) {
|
|
291
|
+
form = yield* uuid;
|
|
292
|
+
state.forms.set(desc.formIndex, form);
|
|
293
|
+
}
|
|
294
|
+
// Unsupported destinations/controls are not offered as credential targets.
|
|
295
|
+
const recipient = yield* origin(desc.action).pipe(Effect.result);
|
|
296
|
+
|
|
297
|
+
if (recipient._tag === "Failure") continue;
|
|
298
|
+
|
|
299
|
+
const control = ProtectedBrowserControl.make({
|
|
300
|
+
ref: yield* uuid,
|
|
301
|
+
role: desc.role,
|
|
302
|
+
label: desc.label,
|
|
303
|
+
target: CredentialTarget.make({
|
|
304
|
+
topOrigin: before.topOrigin,
|
|
305
|
+
frameOrigin: yield* origin(frame.url()),
|
|
306
|
+
recipientOrigin: recipient.success,
|
|
307
|
+
document: state.document,
|
|
308
|
+
frame: state.ref,
|
|
309
|
+
form,
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
controls.set(control.ref, {
|
|
314
|
+
frame: state,
|
|
315
|
+
index,
|
|
316
|
+
control,
|
|
317
|
+
expires: (yield* clock.currentTimeMillis) + 60_000,
|
|
318
|
+
});
|
|
319
|
+
discovered.push(control);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const rawText = yield* remote(() =>
|
|
323
|
+
handle.evaluate((held) => {
|
|
324
|
+
if (typeof held !== "object" || held === null) return null;
|
|
325
|
+
|
|
326
|
+
return Reflect.apply(Reflect.get(held, "text"), held, []);
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
text += yield* decode(Schema.String.check(Schema.isMaxLength(65536)), rawText);
|
|
331
|
+
if (text.length > 65536) {
|
|
332
|
+
text = text.slice(0, 65536);
|
|
333
|
+
truncated = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if ((yield* context).document !== before.document)
|
|
337
|
+
return yield* transportError("stale-reference");
|
|
338
|
+
|
|
339
|
+
return yield* decode(ProtectedDiscovery, {
|
|
340
|
+
...before,
|
|
341
|
+
text,
|
|
342
|
+
controls: discovered,
|
|
343
|
+
truncated,
|
|
344
|
+
});
|
|
345
|
+
}),
|
|
346
|
+
fill: Effect.fn("ProtectedNativeTransport.fill")(function* (ref, role, value) {
|
|
347
|
+
const dispatch = yield* ProtectedBrowserDispatch;
|
|
348
|
+
const state = yield* get(ref);
|
|
349
|
+
|
|
350
|
+
// CDP dispatch may mutate before its reply is lost, so uncertainty starts here.
|
|
351
|
+
yield* dispatch.mark;
|
|
352
|
+
|
|
353
|
+
const filled = yield* remote(() =>
|
|
354
|
+
state.frame.handle.evaluate(
|
|
355
|
+
(held, index, expectedRole, secret) => {
|
|
356
|
+
if (typeof held !== "object" || held === null) return false;
|
|
357
|
+
|
|
358
|
+
return Reflect.apply(Reflect.get(held, "fill"), held, [index, expectedRole, secret]);
|
|
359
|
+
},
|
|
360
|
+
state.index,
|
|
361
|
+
role,
|
|
362
|
+
Redacted.value(value),
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
if (filled === "unsupported") return yield* transportError("unsupported");
|
|
367
|
+
if (filled !== true) return yield* transportError("stale-reference");
|
|
368
|
+
}),
|
|
369
|
+
click: Effect.fn("ProtectedNativeTransport.click")(function* (ref) {
|
|
370
|
+
const state = yield* get(ref);
|
|
371
|
+
|
|
372
|
+
const clicked = yield* remote(() =>
|
|
373
|
+
state.frame.handle.evaluate((held, index) => {
|
|
374
|
+
if (typeof held !== "object" || held === null) return false;
|
|
375
|
+
|
|
376
|
+
return Reflect.apply(Reflect.get(held, "click"), held, [index]);
|
|
377
|
+
}, state.index),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
if (clicked === "needs-attention") return yield* transportError("needs-attention");
|
|
381
|
+
if (clicked !== true) return yield* transportError("stale-reference");
|
|
382
|
+
}),
|
|
383
|
+
};
|
|
384
|
+
}, Effect.withTracerEnabled(false));
|