@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.
- package/dist/BrowserCredentials.d.mts +91 -0
- package/dist/BrowserCredentials.mjs +120 -0
- package/dist/BrowserCredentials.mjs.map +1 -0
- package/dist/BrowserSession-DuOUcC6G.mjs +442 -0
- package/dist/BrowserSession-DuOUcC6G.mjs.map +1 -0
- package/dist/BrowserSession-DxCN-5F9.d.mts +109 -0
- package/dist/BrowserSession.d.mts +3 -0
- package/dist/BrowserSession.mjs +4 -0
- package/dist/CloudflareThreadClient.d.mts +48 -48
- package/dist/{InteractiveBrowser-Cm4xgiGy.d.mts → InteractiveBrowser-BnqY_Y-F.d.mts} +2 -2
- package/dist/{InteractiveBrowser-DXwmvWdA.mjs → InteractiveBrowser-DeOlcu-2.mjs} +4 -5
- package/dist/InteractiveBrowser-DeOlcu-2.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +1 -1
- package/dist/InteractiveBrowser.mjs +1 -1
- package/dist/{ThreadObject-mt0uxvZZ.d.mts → ThreadObject-QBlTByAB.d.mts} +32 -32
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/index.d.mts +4 -2
- package/dist/index.mjs +3 -1
- package/package.json +1 -1
- package/src/BrowserCredentials.ts +146 -0
- package/src/BrowserSession.ts +589 -0
- package/src/index.ts +2 -0
- package/src/internal/browser-binding.ts +3 -3
- package/src/internal/browser-credentials.ts +362 -0
- package/dist/InteractiveBrowser-DXwmvWdA.mjs.map +0 -1
- package/dist/ProtectedBrowser.d.mts +0 -142
- package/dist/ProtectedBrowser.mjs +0 -1242
- package/dist/ProtectedBrowser.mjs.map +0 -1
- package/src/ProtectedBrowser.ts +0 -9
- package/src/protected-browser/binding.ts +0 -445
- package/src/protected-browser/host.ts +0 -398
- package/src/protected-browser/inspect-frame.ts +0 -140
- package/src/protected-browser/native.ts +0 -483
- 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: "
|
|
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 === "
|
|
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 = "
|
|
234
|
+
const operation = "session.keepAlive";
|
|
235
235
|
const runReport = Effect.runPromiseWith(yield* Effect.context<never>());
|
|
236
236
|
|
|
237
237
|
const release = (socket: WebSocket) =>
|