@effect-agent/platform-cloudflare 0.1.0-beta.13 → 0.1.0-beta.131
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/Alarm.d.mts +244 -0
- package/dist/Alarm.mjs +947 -0
- package/dist/Alarm.mjs.map +1 -0
- package/dist/BrowserCredentials.d.mts +91 -0
- package/dist/BrowserCredentials.mjs +120 -0
- package/dist/BrowserCredentials.mjs.map +1 -0
- package/dist/BrowserRestCapture.d.mts +34 -0
- package/dist/BrowserRestCapture.mjs +238 -0
- package/dist/BrowserRestCapture.mjs.map +1 -0
- package/dist/BrowserRestCrawl.d.mts +16 -0
- package/dist/BrowserRestCrawl.mjs +244 -0
- package/dist/BrowserRestCrawl.mjs.map +1 -0
- package/dist/BrowserSession-DP6b-cpL.d.mts +125 -0
- package/dist/BrowserSession-DWoRC_iM.mjs +505 -0
- package/dist/BrowserSession-DWoRC_iM.mjs.map +1 -0
- package/dist/BrowserSession.d.mts +3 -0
- package/dist/BrowserSession.mjs +4 -0
- package/dist/CloudflareAiGateway.d.mts +64 -0
- package/dist/CloudflareAiGateway.mjs +70 -0
- package/dist/CloudflareAiGateway.mjs.map +1 -0
- package/dist/CloudflareBindings.d.mts +115 -0
- package/dist/CloudflareBindings.mjs +113 -0
- package/dist/CloudflareBindings.mjs.map +1 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs +495 -0
- package/dist/CloudflareBrowser-DV6kya1H.mjs.map +1 -0
- package/dist/CloudflareBrowser-Doe1M7R5.d.mts +80 -0
- package/dist/CloudflareBrowser.d.mts +2 -0
- package/dist/CloudflareBrowser.mjs +2 -0
- package/dist/CloudflareCodeMode.d.mts +44 -0
- package/dist/CloudflareCodeMode.mjs +616 -0
- package/dist/CloudflareCodeMode.mjs.map +1 -0
- package/dist/CloudflareConfig-DKXGo8L3.d.mts +106 -0
- package/dist/CloudflareConfig.d.mts +2 -0
- package/dist/CloudflareConfig.mjs +128 -0
- package/dist/CloudflareConfig.mjs.map +1 -0
- package/dist/CloudflareMemory.d.mts +142 -0
- package/dist/CloudflareMemory.mjs +202 -0
- package/dist/CloudflareMemory.mjs.map +1 -0
- package/dist/CloudflareScheduling.d.mts +52 -0
- package/dist/CloudflareScheduling.mjs +396 -0
- package/dist/CloudflareScheduling.mjs.map +1 -0
- package/dist/CloudflareSubscriptions.d.mts +87 -0
- package/dist/CloudflareSubscriptions.mjs +530 -0
- package/dist/CloudflareSubscriptions.mjs.map +1 -0
- package/dist/CloudflareThreadClient.d.mts +1752 -0
- package/dist/CloudflareThreadClient.mjs +382 -0
- package/dist/CloudflareThreadClient.mjs.map +1 -0
- package/dist/InteractiveBrowser-BnqY_Y-F.d.mts +228 -0
- package/dist/InteractiveBrowser-CxFT-Nwm.mjs +1923 -0
- package/dist/InteractiveBrowser-CxFT-Nwm.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +2 -0
- package/dist/InteractiveBrowser.mjs +2 -0
- package/dist/ThreadObject-CVhZC-7l.mjs +849 -0
- package/dist/ThreadObject-CVhZC-7l.mjs.map +1 -0
- package/dist/ThreadObject-D8GzJxJB.d.mts +839 -0
- package/dist/ThreadObject.d.mts +2 -0
- package/dist/ThreadObject.mjs +3 -0
- package/dist/WakeScheduler.d.mts +23 -0
- package/dist/WakeScheduler.mjs +57 -0
- package/dist/WakeScheduler.mjs.map +1 -0
- package/dist/boundary-BguazVkh.mjs +42 -0
- package/dist/boundary-BguazVkh.mjs.map +1 -0
- package/dist/index.d.mts +15 -1550
- package/dist/index.mjs +15 -1716
- package/dist/prepared-admission-DU0_T-ev.mjs +73 -0
- package/dist/prepared-admission-DU0_T-ev.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -56
- package/src/Alarm.ts +1979 -0
- package/src/BrowserCredentials.ts +146 -0
- package/src/BrowserRestCapture.ts +477 -0
- package/src/BrowserRestCrawl.ts +540 -0
- package/src/BrowserSession.ts +624 -0
- package/src/CloudflareAiGateway.ts +170 -0
- package/src/CloudflareBindings.ts +199 -0
- package/src/CloudflareBrowser.ts +15 -0
- package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +308 -283
- package/src/{config.ts → CloudflareConfig.ts} +19 -12
- package/src/CloudflareMemory.ts +420 -0
- package/src/CloudflareScheduling.ts +747 -0
- package/src/CloudflareSubscriptions.ts +1041 -0
- package/src/CloudflareThreadClient.ts +809 -0
- package/src/InteractiveBrowser.ts +3246 -0
- package/src/ThreadObject.ts +1190 -0
- package/src/{wake-scheduler.ts → WakeScheduler.ts} +32 -26
- package/src/index.ts +14 -23
- package/src/internal/boundary.ts +55 -0
- package/src/internal/browser-binding.ts +322 -0
- package/src/internal/browser-credentials.ts +362 -0
- package/src/internal/browser-failure.ts +92 -0
- package/src/internal/browser-file-selection.ts +126 -0
- package/src/internal/browser-quick-action.ts +857 -0
- package/src/internal/browser-readonly-live-view.ts +111 -0
- package/src/internal/browser-session-lifecycle.ts +193 -0
- package/src/internal/layers.ts +773 -0
- package/src/internal/message-delivery.ts +131 -0
- package/src/internal/prepared-admission.ts +116 -0
- package/src/internal/progress-wait.ts +121 -0
- package/src/internal/transport.ts +44 -0
- package/dist/index.mjs.map +0 -1
- package/src/alarm.ts +0 -334
- package/src/bindings.ts +0 -145
- package/src/client.ts +0 -646
- package/src/conversation-object.ts +0 -768
- package/src/layers.ts +0 -376
- package/src/transport.ts +0 -38
|
@@ -0,0 +1,1923 @@
|
|
|
1
|
+
import { Cause, Clock, Context, Data, Duration, Effect, Encoding, ErrorReporter, Layer, Option, Predicate, Redacted, Schema, Scope, Semaphore, Stream } from "effect";
|
|
2
|
+
import { PageScreenshotResult } from "effect-agent/page-screenshot";
|
|
3
|
+
import { SandboxImplementation } from "effect-agent/sandbox";
|
|
4
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
5
|
+
import { BrowserActionResult, BrowserExpectedTargetState, BrowserFileSelectionResult, BrowserNavigationResult, BrowserScreenshotRequest, BrowserScrollRequest, BrowserSelectFileRequest, BrowserTextResult, InteractiveBrowser, InteractiveBrowserActionError, InteractiveBrowserBusyError, InteractiveBrowserCapacityError, InteractiveBrowserExpiredError, InteractiveBrowserLimitError, InteractiveBrowserPolicy, InteractiveBrowserPolicyDeniedError, InteractiveBrowserProtocolError, InteractiveBrowserTargetUrl, InteractiveBrowserUnsupportedError } from "effect-agent/interactive-browser";
|
|
6
|
+
import puppeteer from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
|
|
7
|
+
//#region src/internal/browser-failure.ts
|
|
8
|
+
/** Source-authored stages and classifications only; never retain SDK text or session capabilities. */
|
|
9
|
+
var BrowserRunFailure = class extends Data.TaggedError("BrowserRunFailure") {
|
|
10
|
+
[ErrorReporter.severity] = "Error";
|
|
11
|
+
};
|
|
12
|
+
/** A public projection of an already reported private failure must not produce another issue. */
|
|
13
|
+
const reportedBrowserError = (error) => Object.assign(error, { [ErrorReporter.ignore]: true });
|
|
14
|
+
const reasonSchema = Schema.Literals([
|
|
15
|
+
"provider",
|
|
16
|
+
"timeout",
|
|
17
|
+
"malformed",
|
|
18
|
+
"configuration",
|
|
19
|
+
"authorization",
|
|
20
|
+
"rate-limited",
|
|
21
|
+
"pending"
|
|
22
|
+
]);
|
|
23
|
+
const statusSchema = Schema.Int.check(Schema.isBetween({
|
|
24
|
+
minimum: 100,
|
|
25
|
+
maximum: 599
|
|
26
|
+
}));
|
|
27
|
+
const failureNames = /* @__PURE__ */ new Set([
|
|
28
|
+
"TypeError",
|
|
29
|
+
"RangeError",
|
|
30
|
+
"SyntaxError",
|
|
31
|
+
"TimeoutError",
|
|
32
|
+
"SchemaError"
|
|
33
|
+
]);
|
|
34
|
+
const browserFailure = (operation, cause) => cause instanceof BrowserRunFailure ? cause : new BrowserRunFailure({
|
|
35
|
+
operation,
|
|
36
|
+
reason: Predicate.isObject(cause) && "reason" in cause && Schema.is(reasonSchema)(cause.reason) ? cause.reason : "provider",
|
|
37
|
+
...Predicate.isObject(cause) && "status" in cause && Schema.is(statusSchema)(cause.status) ? { status: cause.status } : {},
|
|
38
|
+
...Predicate.isObject(cause) && "cause" in cause && cause.cause instanceof BrowserRunFailure ? { cause: cause.cause } : Predicate.isObject(cause) && "name" in cause && typeof cause.name === "string" && failureNames.has(cause.name) ? { cause: { name: cause.name } } : {}
|
|
39
|
+
});
|
|
40
|
+
/** Keep mixed defects and interruptions distinct while discarding all foreign diagnostic content. */
|
|
41
|
+
const reportBrowserCause = (operation, cause) => ErrorReporter.report(Cause.fromReasons(cause.reasons.filter((reason) => !Cause.isFailReason(reason) || !ErrorReporter.isIgnored(reason.error)).map((reason) => {
|
|
42
|
+
switch (reason._tag) {
|
|
43
|
+
case "Fail": return Cause.makeFailReason(browserFailure(operation, reason.error));
|
|
44
|
+
case "Die": return Cause.makeDieReason(browserFailure(operation, reason.defect));
|
|
45
|
+
case "Interrupt": return reason;
|
|
46
|
+
}
|
|
47
|
+
})));
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/internal/browser-binding.ts
|
|
50
|
+
const Acquired = Schema.Struct({ sessionId: Schema.String.check(Schema.isUUID()) });
|
|
51
|
+
const VersionReply = Schema.fromJsonString(Schema.Struct({
|
|
52
|
+
id: Schema.optionalKey(Schema.Int),
|
|
53
|
+
result: Schema.optionalKey(Schema.Unknown),
|
|
54
|
+
error: Schema.optionalKey(Schema.Unknown)
|
|
55
|
+
}));
|
|
56
|
+
const Version = Schema.Struct({
|
|
57
|
+
product: Schema.NonEmptyString,
|
|
58
|
+
protocolVersion: Schema.NonEmptyString
|
|
59
|
+
});
|
|
60
|
+
/** Native transport port shared by both scoped adapters; the Layer owns the foreign binding. */
|
|
61
|
+
var BrowserRunBinding = class extends Context.Service()("@effect-agent/platform-cloudflare/internal/BrowserRunBinding") {
|
|
62
|
+
static layer(browser) {
|
|
63
|
+
const upgrade = async (sessionId, operation, signal) => {
|
|
64
|
+
const response = await browser.fetch(`https://browser-rendering.cloudflare.com/v1/devtools/browser/${sessionId}`, {
|
|
65
|
+
headers: { Upgrade: "websocket" },
|
|
66
|
+
...signal === void 0 ? {} : { signal }
|
|
67
|
+
});
|
|
68
|
+
if (response.status !== 101) {
|
|
69
|
+
const failure = new BrowserRunFailure({
|
|
70
|
+
operation,
|
|
71
|
+
reason: "provider",
|
|
72
|
+
status: response.status
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
await response.body?.cancel();
|
|
76
|
+
} catch {}
|
|
77
|
+
throw failure;
|
|
78
|
+
}
|
|
79
|
+
const socket = response.webSocket;
|
|
80
|
+
if (socket === null) throw new BrowserRunFailure({
|
|
81
|
+
operation,
|
|
82
|
+
reason: "malformed"
|
|
83
|
+
});
|
|
84
|
+
return socket;
|
|
85
|
+
};
|
|
86
|
+
return Layer.succeed(this)({
|
|
87
|
+
acquire: async (keepAliveMillis, operation) => {
|
|
88
|
+
const response = await browser.fetch(`https://browser-rendering.cloudflare.com/v1/devtools/browser?keep_alive=${keepAliveMillis}&recording=false`, { method: "POST" });
|
|
89
|
+
if (response.status !== 200) {
|
|
90
|
+
const failure = new BrowserRunFailure({
|
|
91
|
+
operation,
|
|
92
|
+
reason: "provider",
|
|
93
|
+
status: response.status
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
await response.body?.cancel();
|
|
97
|
+
} catch {}
|
|
98
|
+
throw failure;
|
|
99
|
+
}
|
|
100
|
+
return Schema.decodeUnknownSync(Acquired)(await response.json()).sessionId;
|
|
101
|
+
},
|
|
102
|
+
connect: (sessionId, operation, signal) => {
|
|
103
|
+
let socket;
|
|
104
|
+
let retired = false;
|
|
105
|
+
let closedByAbort = false;
|
|
106
|
+
let retirementFailure;
|
|
107
|
+
const disconnectOperation = operation === "session.connect" ? "session.disconnect" : "interactive.disconnect";
|
|
108
|
+
const transport = {
|
|
109
|
+
send: (message) => {
|
|
110
|
+
if (retired || socket === void 0) throw new BrowserRunFailure({
|
|
111
|
+
operation,
|
|
112
|
+
reason: "provider"
|
|
113
|
+
});
|
|
114
|
+
socket.send(message);
|
|
115
|
+
},
|
|
116
|
+
close: () => retireNow()
|
|
117
|
+
};
|
|
118
|
+
const onMessage = (event) => {
|
|
119
|
+
if (!retired) transport.onmessage?.(event.data);
|
|
120
|
+
};
|
|
121
|
+
const fence = () => {
|
|
122
|
+
if (retired) return;
|
|
123
|
+
retired = true;
|
|
124
|
+
const notify = transport.onclose;
|
|
125
|
+
transport.onmessage = void 0;
|
|
126
|
+
transport.onclose = void 0;
|
|
127
|
+
try {
|
|
128
|
+
notify?.();
|
|
129
|
+
} finally {
|
|
130
|
+
socket?.removeEventListener("message", onMessage);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const retireNow = () => {
|
|
134
|
+
try {
|
|
135
|
+
fence();
|
|
136
|
+
} catch (cause) {
|
|
137
|
+
retirementFailure ??= browserFailure(disconnectOperation, cause);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
if (socket !== void 0 && socket.readyState < WebSocket.CLOSED) socket.close();
|
|
141
|
+
} catch (cause) {
|
|
142
|
+
retirementFailure ??= browserFailure(disconnectOperation, cause);
|
|
143
|
+
}
|
|
144
|
+
if (retirementFailure !== void 0) throw retirementFailure;
|
|
145
|
+
};
|
|
146
|
+
const abort = () => {
|
|
147
|
+
closedByAbort = socket?.readyState === WebSocket.OPEN;
|
|
148
|
+
try {
|
|
149
|
+
retireNow();
|
|
150
|
+
} catch {}
|
|
151
|
+
};
|
|
152
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
153
|
+
return {
|
|
154
|
+
browser: (async () => {
|
|
155
|
+
try {
|
|
156
|
+
signal?.throwIfAborted();
|
|
157
|
+
socket = await upgrade(sessionId, operation, signal);
|
|
158
|
+
socket.accept();
|
|
159
|
+
if (retired || signal?.aborted) {
|
|
160
|
+
retireNow();
|
|
161
|
+
signal?.throwIfAborted();
|
|
162
|
+
throw new BrowserRunFailure({
|
|
163
|
+
operation,
|
|
164
|
+
reason: "provider"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
socket.addEventListener("message", onMessage);
|
|
168
|
+
socket.addEventListener("close", () => {
|
|
169
|
+
try {
|
|
170
|
+
retireNow();
|
|
171
|
+
} catch (cause) {
|
|
172
|
+
retirementFailure ??= browserFailure(disconnectOperation, cause);
|
|
173
|
+
}
|
|
174
|
+
}, { once: true });
|
|
175
|
+
return await puppeteer.connect({
|
|
176
|
+
transport,
|
|
177
|
+
defaultViewport: null
|
|
178
|
+
});
|
|
179
|
+
} catch (cause) {
|
|
180
|
+
try {
|
|
181
|
+
retireNow();
|
|
182
|
+
} catch {}
|
|
183
|
+
const failure = browserFailure(operation, cause);
|
|
184
|
+
throw cause instanceof Error && signal?.aborted === true && (cause === signal.reason || closedByAbort && cause.name === "TargetCloseError") ? reportedBrowserError(failure) : failure;
|
|
185
|
+
} finally {
|
|
186
|
+
signal?.removeEventListener("abort", abort);
|
|
187
|
+
}
|
|
188
|
+
})(),
|
|
189
|
+
retire: Effect.callback((resume) => {
|
|
190
|
+
const connected = socket;
|
|
191
|
+
const onClose = () => resume(Effect.void);
|
|
192
|
+
try {
|
|
193
|
+
retireNow();
|
|
194
|
+
if (connected === void 0 || connected.readyState === WebSocket.CLOSED) resume(Effect.void);
|
|
195
|
+
else connected.addEventListener("close", onClose, { once: true });
|
|
196
|
+
} catch (cause) {
|
|
197
|
+
resume(Effect.fail(browserFailure(disconnectOperation, cause)));
|
|
198
|
+
}
|
|
199
|
+
return Effect.sync(() => connected?.removeEventListener("close", onClose));
|
|
200
|
+
})
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
keepAlive: Effect.fnUntraced(function* (sessionId) {
|
|
204
|
+
const operation = "session.keepAlive";
|
|
205
|
+
const runReport = Effect.runPromiseWith(yield* Effect.context());
|
|
206
|
+
const release = (socket) => Effect.sync(() => socket.close()).pipe(Effect.catchCause((cause) => reportBrowserCause(operation, cause)));
|
|
207
|
+
const socket = yield* Effect.acquireRelease(Effect.tryPromise({
|
|
208
|
+
try: async (signal) => {
|
|
209
|
+
let acquired;
|
|
210
|
+
try {
|
|
211
|
+
acquired = await upgrade(sessionId, operation, signal);
|
|
212
|
+
acquired.accept();
|
|
213
|
+
signal.throwIfAborted();
|
|
214
|
+
return acquired;
|
|
215
|
+
} catch (cause) {
|
|
216
|
+
if (acquired !== void 0) await runReport(release(acquired));
|
|
217
|
+
if (signal.aborted && cause !== signal.reason) await runReport(reportBrowserCause(operation, Cause.fail(cause)));
|
|
218
|
+
throw cause;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
catch: (cause) => browserFailure(operation, cause)
|
|
222
|
+
}), release, { interruptible: true });
|
|
223
|
+
yield* Effect.callback((resume) => {
|
|
224
|
+
const finish = (result) => {
|
|
225
|
+
removeListeners();
|
|
226
|
+
resume(result);
|
|
227
|
+
};
|
|
228
|
+
const fail = (reason) => finish(Effect.fail(new BrowserRunFailure({
|
|
229
|
+
operation,
|
|
230
|
+
reason
|
|
231
|
+
})));
|
|
232
|
+
const onMessage = (event) => {
|
|
233
|
+
try {
|
|
234
|
+
if (typeof event.data !== "string" || event.data.length > 16384) {
|
|
235
|
+
fail("malformed");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const reply = Schema.decodeSync(VersionReply)(event.data);
|
|
239
|
+
if (reply.id !== 1) return;
|
|
240
|
+
if (reply.error !== void 0) {
|
|
241
|
+
fail("provider");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
Schema.decodeUnknownSync(Version)(reply.result);
|
|
245
|
+
finish(Effect.void);
|
|
246
|
+
} catch {
|
|
247
|
+
fail("malformed");
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const onClose = () => fail("provider");
|
|
251
|
+
const removeListeners = () => {
|
|
252
|
+
socket.removeEventListener("message", onMessage);
|
|
253
|
+
socket.removeEventListener("close", onClose);
|
|
254
|
+
socket.removeEventListener("error", onClose);
|
|
255
|
+
};
|
|
256
|
+
socket.addEventListener("message", onMessage);
|
|
257
|
+
socket.addEventListener("close", onClose);
|
|
258
|
+
socket.addEventListener("error", onClose);
|
|
259
|
+
try {
|
|
260
|
+
socket.send(JSON.stringify({
|
|
261
|
+
id: 1,
|
|
262
|
+
method: "Browser.getVersion"
|
|
263
|
+
}));
|
|
264
|
+
} catch (cause) {
|
|
265
|
+
finish(Effect.fail(browserFailure(operation, cause)));
|
|
266
|
+
}
|
|
267
|
+
return Effect.sync(removeListeners);
|
|
268
|
+
});
|
|
269
|
+
}, Effect.scoped)
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/internal/browser-file-selection.ts
|
|
275
|
+
function setFileSelection(element, encoded, name, type) {
|
|
276
|
+
if (Reflect.get(element, "type") !== "file" || Reflect.get(element, "disabled") || Reflect.get(element, "webkitdirectory")) throw new Error("The target is not an enabled file input");
|
|
277
|
+
const bytes = Uint8Array.from(atob(encoded), (character) => character.charCodeAt(0));
|
|
278
|
+
const transfer = new (Reflect.get(globalThis, "DataTransfer"))();
|
|
279
|
+
transfer.items.add(new File([bytes], name, { type }));
|
|
280
|
+
Reflect.set(element, "files", transfer.files);
|
|
281
|
+
const selected = Reflect.get(element, "files");
|
|
282
|
+
if (selected?.length !== 1 || selected[0]?.size !== bytes.length || selected[0]?.name !== name || selected[0]?.type !== type) throw new Error("The input did not retain the selected file");
|
|
283
|
+
const dispatch = Reflect.get(element, "dispatchEvent");
|
|
284
|
+
Reflect.apply(dispatch, element, [new Event("input", { bubbles: true })]);
|
|
285
|
+
Reflect.apply(dispatch, element, [new Event("change", { bubbles: true })]);
|
|
286
|
+
}
|
|
287
|
+
const makeFileSelection = (page) => async (request, signal) => {
|
|
288
|
+
const encoded = Encoding.encodeBase64(request.bytes);
|
|
289
|
+
const session = request.target === "chooser" ? await page.createCDPSession() : void 0;
|
|
290
|
+
if (session !== void 0) try {
|
|
291
|
+
await session.send("Page.enable");
|
|
292
|
+
await session.send("Page.setInterceptFileChooserDialog", { enabled: true });
|
|
293
|
+
} catch (cause) {
|
|
294
|
+
await session.detach();
|
|
295
|
+
throw cause;
|
|
296
|
+
}
|
|
297
|
+
const select = async (element) => {
|
|
298
|
+
if (session === void 0) return element.evaluate(setFileSelection, encoded, request.fileName, request.mediaType);
|
|
299
|
+
let onOpened = () => {};
|
|
300
|
+
let onAbort = () => {};
|
|
301
|
+
let timer;
|
|
302
|
+
const opened = new Promise((resolve, reject) => {
|
|
303
|
+
onOpened = (event) => event.backendNodeId === void 0 ? reject(/* @__PURE__ */ new Error("The chooser has no file input")) : resolve(event.backendNodeId);
|
|
304
|
+
onAbort = () => reject(/* @__PURE__ */ new Error("File selection interrupted"));
|
|
305
|
+
session.on("Page.fileChooserOpened", onOpened);
|
|
306
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
307
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("No file chooser opened")), 5e3);
|
|
308
|
+
if (signal.aborted) onAbort();
|
|
309
|
+
});
|
|
310
|
+
try {
|
|
311
|
+
const [backendNodeId] = await Promise.all([opened, element.click()]);
|
|
312
|
+
if (signal.aborted) throw new Error("File selection interrupted");
|
|
313
|
+
const objectId = (await session.send("DOM.resolveNode", { backendNodeId })).object.objectId;
|
|
314
|
+
if (objectId === void 0) throw new Error("The chooser has no file input");
|
|
315
|
+
if (signal.aborted) throw new Error("File selection interrupted");
|
|
316
|
+
if ((await session.send("Runtime.callFunctionOn", {
|
|
317
|
+
objectId,
|
|
318
|
+
functionDeclaration: `function(encoded, name, type) { (${setFileSelection.toString()})(this, encoded, name, type); }`,
|
|
319
|
+
arguments: [
|
|
320
|
+
{ value: encoded },
|
|
321
|
+
{ value: request.fileName },
|
|
322
|
+
{ value: request.mediaType }
|
|
323
|
+
],
|
|
324
|
+
returnByValue: true
|
|
325
|
+
})).exceptionDetails !== void 0) throw new Error("The chooser did not select the file");
|
|
326
|
+
} finally {
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
session.off("Page.fileChooserOpened", onOpened);
|
|
329
|
+
signal.removeEventListener("abort", onAbort);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
validate: async (element) => request.target === "chooser" || await element.evaluate((input) => Reflect.get(input, "type") === "file" && !Reflect.get(input, "disabled") && !Reflect.get(input, "webkitdirectory")),
|
|
334
|
+
select,
|
|
335
|
+
close: async () => {
|
|
336
|
+
if (session !== void 0) try {
|
|
337
|
+
await session.send("Page.setInterceptFileChooserDialog", { enabled: false });
|
|
338
|
+
} finally {
|
|
339
|
+
await session.detach();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/internal/browser-session-lifecycle.ts
|
|
346
|
+
var BrowserRunCleanupError = class extends Schema.TaggedError()("BrowserRunCleanupError", {
|
|
347
|
+
reason: Schema.Literals([
|
|
348
|
+
"configuration",
|
|
349
|
+
"authorization",
|
|
350
|
+
"rate-limited",
|
|
351
|
+
"provider",
|
|
352
|
+
"malformed",
|
|
353
|
+
"timeout",
|
|
354
|
+
"pending"
|
|
355
|
+
]),
|
|
356
|
+
status: Schema.optionalKey(Schema.Int),
|
|
357
|
+
cause: Schema.optionalKey(Schema.Defect())
|
|
358
|
+
}) {};
|
|
359
|
+
const Identity = Schema.String.check(Schema.isUUID());
|
|
360
|
+
const Account = Schema.String.check(Schema.isPattern(/^[a-f0-9]{32}$/));
|
|
361
|
+
const Closed = Schema.Struct({ status: Schema.Literals(["closing", "closed"]) });
|
|
362
|
+
const Metadata = Schema.Struct({
|
|
363
|
+
sessionId: Identity,
|
|
364
|
+
endTime: Schema.optionalKey(Schema.Finite)
|
|
365
|
+
});
|
|
366
|
+
const Absent = Schema.Struct({ error: Schema.Literal("Session not found") });
|
|
367
|
+
var BrowserRunSessionLifecycle = class extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunSessionLifecycle") {
|
|
368
|
+
static layer(options) {
|
|
369
|
+
return Layer.effect(this, Effect.gen(function* () {
|
|
370
|
+
if (!Schema.is(Account)(options.accountId) || Redacted.value(options.apiToken).length === 0) return yield* new BrowserRunCleanupError({ reason: "configuration" });
|
|
371
|
+
const client = yield* HttpClient.HttpClient;
|
|
372
|
+
const request = Effect.fn("BrowserRunSessionLifecycle.request")(function* (method, sessionId) {
|
|
373
|
+
const path = method === "DELETE" ? "browser" : "session";
|
|
374
|
+
const response = yield* client.execute(HttpClientRequest.make(method)(`https://api.cloudflare.com/client/v4/accounts/${options.accountId}/browser-rendering/devtools/${path}/${sessionId}`).pipe(HttpClientRequest.bearerToken(options.apiToken))).pipe(Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }), Effect.mapError((cause) => new BrowserRunCleanupError({
|
|
375
|
+
reason: "provider",
|
|
376
|
+
cause: browserFailure("browser.close.request", cause)
|
|
377
|
+
})));
|
|
378
|
+
if (response.status === 401 || response.status === 403) return yield* new BrowserRunCleanupError({
|
|
379
|
+
reason: "authorization",
|
|
380
|
+
status: response.status
|
|
381
|
+
});
|
|
382
|
+
if (response.status === 429) return yield* new BrowserRunCleanupError({
|
|
383
|
+
reason: "rate-limited",
|
|
384
|
+
status: response.status
|
|
385
|
+
});
|
|
386
|
+
if (response.status !== 200 && response.status !== 404) return yield* new BrowserRunCleanupError({
|
|
387
|
+
reason: "provider",
|
|
388
|
+
status: response.status
|
|
389
|
+
});
|
|
390
|
+
if (response.headers["content-type"]?.split(";", 1)[0]?.trim() !== "application/json") return yield* new BrowserRunCleanupError({ reason: "malformed" });
|
|
391
|
+
const bytes = yield* Stream.runFoldEffect(response.stream, () => /* @__PURE__ */ new Uint8Array(), (body, chunk) => {
|
|
392
|
+
if (body.byteLength + chunk.byteLength > 16384) return Effect.fail(new BrowserRunCleanupError({ reason: "malformed" }));
|
|
393
|
+
const combined = new Uint8Array(body.byteLength + chunk.byteLength);
|
|
394
|
+
combined.set(body);
|
|
395
|
+
combined.set(chunk, body.byteLength);
|
|
396
|
+
return Effect.succeed(combined);
|
|
397
|
+
}).pipe(Effect.mapError((cause) => new BrowserRunCleanupError({
|
|
398
|
+
reason: "malformed",
|
|
399
|
+
cause: browserFailure("browser.close.response", cause)
|
|
400
|
+
})));
|
|
401
|
+
const body = yield* Effect.try({
|
|
402
|
+
try: () => new TextDecoder("utf-8", {
|
|
403
|
+
fatal: true,
|
|
404
|
+
ignoreBOM: false
|
|
405
|
+
}).decode(bytes),
|
|
406
|
+
catch: (cause) => new BrowserRunCleanupError({
|
|
407
|
+
reason: "malformed",
|
|
408
|
+
cause: browserFailure("browser.close.decode", cause)
|
|
409
|
+
})
|
|
410
|
+
});
|
|
411
|
+
if (response.status === 404) {
|
|
412
|
+
const absent = Schema.decodeOption(Schema.fromJsonString(Absent))(body, { onExcessProperty: "error" });
|
|
413
|
+
if (Option.isSome(absent)) return true;
|
|
414
|
+
return yield* new BrowserRunCleanupError({ reason: "malformed" });
|
|
415
|
+
}
|
|
416
|
+
if (method === "DELETE") return (yield* Schema.decodeEffect(Schema.fromJsonString(Closed))(body).pipe(Effect.mapError((cause) => new BrowserRunCleanupError({
|
|
417
|
+
reason: "malformed",
|
|
418
|
+
cause: browserFailure("browser.close.response", cause)
|
|
419
|
+
})))).status === "closed";
|
|
420
|
+
const result = yield* Schema.decodeEffect(Schema.fromJsonString(Metadata))(body).pipe(Effect.mapError((cause) => new BrowserRunCleanupError({
|
|
421
|
+
reason: "malformed",
|
|
422
|
+
cause: browserFailure("browser.close.response", cause)
|
|
423
|
+
})));
|
|
424
|
+
if (result.sessionId !== sessionId) return yield* new BrowserRunCleanupError({ reason: "malformed" });
|
|
425
|
+
return result.endTime !== void 0 && result.endTime > 0;
|
|
426
|
+
});
|
|
427
|
+
return { close: Effect.fn("BrowserRunSessionLifecycle.close")(function* (sessionId) {
|
|
428
|
+
const id = yield* Schema.decodeEffect(Identity)(Redacted.value(sessionId)).pipe(Effect.mapError(() => new BrowserRunCleanupError({ reason: "configuration" })));
|
|
429
|
+
if (yield* request("DELETE", id)) return;
|
|
430
|
+
for (let read = 0; read < 2; read++) if (yield* request("GET", id)) return;
|
|
431
|
+
return yield* new BrowserRunCleanupError({ reason: "pending" });
|
|
432
|
+
}, Effect.timeoutOrElse({
|
|
433
|
+
duration: "10 seconds",
|
|
434
|
+
orElse: () => Effect.fail(new BrowserRunCleanupError({ reason: "timeout" }))
|
|
435
|
+
}), Effect.withTracerEnabled(false)) };
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/InteractiveBrowser.ts
|
|
441
|
+
const browserRunInteractiveImplementation = SandboxImplementation.make({
|
|
442
|
+
isolation: "isolated",
|
|
443
|
+
identity: "cloudflare-browser-run-interactive"
|
|
444
|
+
});
|
|
445
|
+
const MIN_KEEP_ALIVE_MILLIS = 1e4;
|
|
446
|
+
const MAX_KEEP_ALIVE_MILLIS = 6e5;
|
|
447
|
+
const MAX_TEXT_LENGTH = 8388608;
|
|
448
|
+
const MAX_SCREENSHOT_BYTES = 8388608;
|
|
449
|
+
const MIN_LIVE_VIEW_EXPIRY_MILLIS = 6e4;
|
|
450
|
+
const MAX_LIVE_VIEW_EXPIRY_MILLIS = 36e5;
|
|
451
|
+
const MAX_HANDOFF_TIMEOUT_MILLIS = 18e5;
|
|
452
|
+
const MAX_HOST_TEXT_LENGTH = 8192;
|
|
453
|
+
const CLEANUP_STEP_TIMEOUT_MILLIS = 1e4;
|
|
454
|
+
const ACTION_NETWORK_QUIET_MILLIS = 200;
|
|
455
|
+
const ACTION_NETWORK_SETTLE_MILLIS = 2e3;
|
|
456
|
+
const ACTION_POST_STATE_MILLIS = 250;
|
|
457
|
+
const MAX_OBSERVED_CONTROLS = 64;
|
|
458
|
+
const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0));
|
|
459
|
+
const BoundedHostText = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(MAX_HOST_TEXT_LENGTH));
|
|
460
|
+
const BoundedRemoteText = Schema.String.check(Schema.isMaxLength(MAX_TEXT_LENGTH));
|
|
461
|
+
const TextObservation = Schema.Union([
|
|
462
|
+
Schema.Struct({
|
|
463
|
+
_tag: Schema.Literal("Text"),
|
|
464
|
+
text: BoundedRemoteText
|
|
465
|
+
}),
|
|
466
|
+
Schema.Struct({ _tag: Schema.Literal("MissingElement") }),
|
|
467
|
+
Schema.Struct({
|
|
468
|
+
_tag: Schema.Literal("OverLimit"),
|
|
469
|
+
observed: Schema.Natural
|
|
470
|
+
})
|
|
471
|
+
]);
|
|
472
|
+
const ActionTargetState = Schema.Struct({
|
|
473
|
+
matchCount: Schema.Natural,
|
|
474
|
+
invalidSelector: Schema.optionalKey(Schema.Boolean),
|
|
475
|
+
kind: Schema.optionalKey(Schema.Literals([
|
|
476
|
+
"button",
|
|
477
|
+
"checkbox",
|
|
478
|
+
"radio",
|
|
479
|
+
"select",
|
|
480
|
+
"text",
|
|
481
|
+
"link",
|
|
482
|
+
"other"
|
|
483
|
+
])),
|
|
484
|
+
checked: Schema.optionalKey(Schema.Boolean),
|
|
485
|
+
selected: Schema.optionalKey(Schema.Boolean),
|
|
486
|
+
disabled: Schema.optionalKey(Schema.Boolean),
|
|
487
|
+
required: Schema.optionalKey(Schema.Boolean),
|
|
488
|
+
valid: Schema.optionalKey(Schema.Boolean),
|
|
489
|
+
formValid: Schema.optionalKey(Schema.Boolean)
|
|
490
|
+
});
|
|
491
|
+
const ActionNetworkState = Schema.Struct({
|
|
492
|
+
total: Schema.Natural,
|
|
493
|
+
status2xx: Schema.Natural,
|
|
494
|
+
status3xx: Schema.Natural,
|
|
495
|
+
status4xx: Schema.Natural,
|
|
496
|
+
status5xx: Schema.Natural,
|
|
497
|
+
failed: Schema.Natural,
|
|
498
|
+
pending: Schema.Natural,
|
|
499
|
+
settleTimedOut: Schema.Boolean
|
|
500
|
+
});
|
|
501
|
+
const ActionObservation = Schema.Struct({
|
|
502
|
+
before: ActionTargetState,
|
|
503
|
+
after: Schema.optionalKey(ActionTargetState),
|
|
504
|
+
afterUnavailable: Schema.Boolean,
|
|
505
|
+
network: ActionNetworkState,
|
|
506
|
+
inputMillis: Schema.optionalKey(Schema.Finite),
|
|
507
|
+
observationMillis: Schema.optionalKey(Schema.Finite)
|
|
508
|
+
});
|
|
509
|
+
const BrowserRunPageObservation = Schema.fromJsonString(Schema.Struct({
|
|
510
|
+
documentId: Schema.NonEmptyString,
|
|
511
|
+
pageText: BoundedRemoteText,
|
|
512
|
+
selectorMatchCount: Schema.Natural,
|
|
513
|
+
controlsTruncated: Schema.Boolean,
|
|
514
|
+
controls: Schema.Array(Schema.Struct({
|
|
515
|
+
nodeId: Schema.NonEmptyString,
|
|
516
|
+
selector: BoundedRemoteText,
|
|
517
|
+
...BrowserExpectedTargetState.fields
|
|
518
|
+
})).check(Schema.isMaxLength(MAX_OBSERVED_CONTROLS))
|
|
519
|
+
}));
|
|
520
|
+
const PngBytes = Schema.Uint8Array.check(Schema.isMaxLength(MAX_SCREENSHOT_BYTES), Schema.makeFilter((bytes) => bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10, { title: "PNG bytes" }));
|
|
521
|
+
const BrowserRunSessionId = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256), Schema.makeFilter((value) => /^[A-Za-z0-9_-]+$/.test(value), { title: "a Browser Run session identifier" }));
|
|
522
|
+
const LiveViewUrl = Schema.String.check(Schema.isMaxLength(MAX_HOST_TEXT_LENGTH), Schema.makeFilter((value) => {
|
|
523
|
+
try {
|
|
524
|
+
const url = new URL(value);
|
|
525
|
+
return url.protocol === "https:" && url.host === "live.browser.run" && url.username === "" && url.password === "" && url.pathname === "/ui/view" && url.searchParams.get("mode") === "tab" && (url.searchParams.get("wss") ?? "").startsWith("live.browser.run/api/devtools/browser/");
|
|
526
|
+
} catch {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
}, { title: "a Cloudflare Live View HTTPS URL" }));
|
|
530
|
+
const LiveViewObservation = Schema.Struct({ devtoolsFrontendUrl: LiveViewUrl });
|
|
531
|
+
const HandoffObservation = Schema.Struct({ handoffId: BoundedHostText });
|
|
532
|
+
const HandoffDuration = Schema.Natural.check(Schema.isLessThanOrEqualTo(MAX_HANDOFF_TIMEOUT_MILLIS));
|
|
533
|
+
const HandoffStateObservation = Schema.Union([Schema.Struct({
|
|
534
|
+
active: Schema.Literal(true),
|
|
535
|
+
handoffId: BoundedHostText,
|
|
536
|
+
durationMs: HandoffDuration
|
|
537
|
+
}), Schema.Struct({
|
|
538
|
+
active: Schema.Literal(false),
|
|
539
|
+
handoffId: Schema.optionalKey(BoundedHostText),
|
|
540
|
+
durationMs: Schema.optionalKey(HandoffDuration)
|
|
541
|
+
})]);
|
|
542
|
+
/**
|
|
543
|
+
* Host presentation state in CSS pixels. Dimensions are integers in 1..2048,
|
|
544
|
+
* density is finite in 1..2 (default 1), and neither scaled dimension may exceed
|
|
545
|
+
* 2048 pixels. Mobile, touch, and orientation emulation are not supported.
|
|
546
|
+
*/
|
|
547
|
+
var BrowserRunViewport = class extends Schema.Class("BrowserRunViewport")(Schema.Struct({
|
|
548
|
+
width: PositiveInt.check(Schema.isLessThanOrEqualTo(2048)),
|
|
549
|
+
height: PositiveInt.check(Schema.isLessThanOrEqualTo(2048)),
|
|
550
|
+
deviceScaleFactor: Schema.optionalKey(Schema.Finite.check(Schema.isBetween({
|
|
551
|
+
minimum: 1,
|
|
552
|
+
maximum: 2
|
|
553
|
+
})))
|
|
554
|
+
}).check(Schema.makeFilter((viewport) => Math.max(viewport.width, viewport.height) * (viewport.deviceScaleFactor ?? 1) <= 2048, { title: "a viewport with scaled dimensions no larger than 2048 pixels" }))) {};
|
|
555
|
+
const decodeViewport = (input) => Schema.decodeEffect(BrowserRunViewport)(input, { onExcessProperty: "error" }).pipe(Effect.mapError(() => policyError("The browser viewport is malformed")), Effect.map((viewport) => ({
|
|
556
|
+
width: viewport.width,
|
|
557
|
+
height: viewport.height,
|
|
558
|
+
deviceScaleFactor: viewport.deviceScaleFactor ?? 1
|
|
559
|
+
})));
|
|
560
|
+
/** Host-only request for a redacted Cloudflare Live View URL. */
|
|
561
|
+
var BrowserRunLiveViewRequest = class extends Schema.Class("BrowserRunLiveViewRequest")({
|
|
562
|
+
mode: Schema.Literal("tab"),
|
|
563
|
+
expiresInMs: PositiveInt.check(Schema.isBetween({
|
|
564
|
+
minimum: MIN_LIVE_VIEW_EXPIRY_MILLIS,
|
|
565
|
+
maximum: MAX_LIVE_VIEW_EXPIRY_MILLIS
|
|
566
|
+
}))
|
|
567
|
+
}) {};
|
|
568
|
+
var BrowserRunLiveViewResult = class extends Schema.Class("BrowserRunLiveViewResult")({ devtoolsFrontendUrl: Schema.Redacted(LiveViewUrl) }) {};
|
|
569
|
+
/** Start one bounded handoff; controller ownership remains a consumer concern. */
|
|
570
|
+
var BrowserRunHandoffRequest = class extends Schema.Class("BrowserRunHandoffRequest")({
|
|
571
|
+
instructions: BoundedHostText.check(Schema.isMaxLength(1024)),
|
|
572
|
+
timeout: PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_HANDOFF_TIMEOUT_MILLIS))
|
|
573
|
+
}) {};
|
|
574
|
+
var BrowserRunHandoffResult = class extends Schema.Class("BrowserRunHandoffResult")({ handoffId: Schema.Redacted(BoundedHostText) }) {};
|
|
575
|
+
var BrowserRunHandoffState = class extends Schema.Class("BrowserRunHandoffState")({
|
|
576
|
+
active: Schema.Boolean,
|
|
577
|
+
handoffId: Schema.optionalKey(Schema.Redacted(BoundedHostText)),
|
|
578
|
+
durationMs: Schema.optionalKey(HandoffDuration)
|
|
579
|
+
}) {};
|
|
580
|
+
/** Host-supplied Browser Run allocation and connection transport. */
|
|
581
|
+
var BrowserRunInteractiveBinding = class BrowserRunInteractiveBinding extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunInteractiveBinding") {
|
|
582
|
+
static layer(options) {
|
|
583
|
+
return Layer.effect(BrowserRunInteractiveBinding)(Effect.gen(function* () {
|
|
584
|
+
const lifecycle = yield* BrowserRunSessionLifecycle;
|
|
585
|
+
const binding = yield* BrowserRunBinding;
|
|
586
|
+
const viewport = options.viewport === void 0 ? void 0 : yield* decodeViewport(options.viewport);
|
|
587
|
+
return {
|
|
588
|
+
acquire: async (keepAliveMillis) => binding.acquire(keepAliveMillis, "interactive.acquire"),
|
|
589
|
+
connect: async (sessionId, signal) => makeProductionBrowser(await binding.connect(sessionId, "interactive.connect", signal).browser, sessionId, viewport),
|
|
590
|
+
closeSession: (sessionId) => lifecycle.close(sessionId).pipe(Effect.mapError((cause) => actionError("close", cause)))
|
|
591
|
+
};
|
|
592
|
+
})).pipe(Layer.provide(BrowserRunBinding.layer(options.browser)));
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
/** Private target identity; never include this checkpoint in model-visible records. */
|
|
596
|
+
var BrowserRunPageIdentity = class extends Schema.Class("BrowserRunPageIdentity")({
|
|
597
|
+
contextId: BrowserRunSessionId,
|
|
598
|
+
targetId: BrowserRunSessionId
|
|
599
|
+
}) {};
|
|
600
|
+
/** Host-owned recovery data. Persist with the application's input receipts and ownership fence. */
|
|
601
|
+
var BrowserRunInteractiveCheckpoint = class extends Schema.Class("BrowserRunInteractiveCheckpoint")({
|
|
602
|
+
sessionId: Schema.RedactedFromValue(BrowserRunSessionId),
|
|
603
|
+
page: BrowserRunPageIdentity,
|
|
604
|
+
policy: InteractiveBrowserPolicy,
|
|
605
|
+
startedAt: Schema.Natural,
|
|
606
|
+
consumedActions: Schema.Natural.check(Schema.isLessThanOrEqualTo(1e3)),
|
|
607
|
+
inputState: Schema.Literals([
|
|
608
|
+
"idle",
|
|
609
|
+
"running",
|
|
610
|
+
"unknown"
|
|
611
|
+
])
|
|
612
|
+
}) {};
|
|
613
|
+
/** Cloudflare host authority kept separate from the provider-neutral browser handle. */
|
|
614
|
+
var BrowserRunInteractiveHost = class extends Context.Service()("@effect-agent/platform-cloudflare/BrowserRunInteractiveHost") {};
|
|
615
|
+
const makeProductionRequest = (request) => ({
|
|
616
|
+
url: () => request.url(),
|
|
617
|
+
abort: () => request.abort("blockedbyclient"),
|
|
618
|
+
continue: () => request.continue()
|
|
619
|
+
});
|
|
620
|
+
const makeProductionCdpSession = (session) => ({
|
|
621
|
+
send: async (command, parameters) => {
|
|
622
|
+
const send = Reflect.get(session, "send");
|
|
623
|
+
return await Reflect.apply(send, session, [command, parameters]);
|
|
624
|
+
},
|
|
625
|
+
detach: () => session.detach()
|
|
626
|
+
});
|
|
627
|
+
var BrowserRunActionUndispatched = class extends Error {
|
|
628
|
+
matchCount;
|
|
629
|
+
constructor(matchCount) {
|
|
630
|
+
super("The browser action selector did not resolve to exactly one element");
|
|
631
|
+
this.matchCount = matchCount;
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
const readActionTarget = (page, selector) => page.evaluate((requestedSelector) => {
|
|
635
|
+
const pageDocument = Reflect.get(globalThis, "document");
|
|
636
|
+
let matches;
|
|
637
|
+
try {
|
|
638
|
+
matches = Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [requestedSelector]);
|
|
639
|
+
} catch (cause) {
|
|
640
|
+
if (cause instanceof Error && cause.name === "SyntaxError") return {
|
|
641
|
+
matchCount: 0,
|
|
642
|
+
invalidSelector: true
|
|
643
|
+
};
|
|
644
|
+
throw cause;
|
|
645
|
+
}
|
|
646
|
+
if (typeof matches !== "object" || matches === null) throw new Error("Invalid query result");
|
|
647
|
+
const matchCount = Math.min(1e4, Reflect.get(matches, "length"));
|
|
648
|
+
const element = Reflect.get(matches, 0);
|
|
649
|
+
if (element === void 0) return { matchCount };
|
|
650
|
+
const associated = Reflect.get(element, "control") ?? element;
|
|
651
|
+
const tagName = String(Reflect.get(associated, "tagName") ?? "").toLowerCase();
|
|
652
|
+
const inputType = String(Reflect.get(associated, "type") ?? "").toLowerCase();
|
|
653
|
+
const role = String(Reflect.apply(Reflect.get(element, "getAttribute"), element, ["role"]) ?? "").toLowerCase();
|
|
654
|
+
const kind = tagName === "button" || role === "button" ? "button" : inputType === "checkbox" || role === "checkbox" || role === "switch" ? "checkbox" : inputType === "radio" || role === "radio" ? "radio" : tagName === "select" ? "select" : tagName === "input" || tagName === "textarea" ? "text" : tagName === "a" ? "link" : "other";
|
|
655
|
+
const checked = Reflect.get(associated, "checked");
|
|
656
|
+
const selected = tagName === "select" ? Reflect.get(associated, "selectedIndex") >= 0 : Reflect.get(associated, "selected");
|
|
657
|
+
const disabled = Reflect.get(associated, "disabled");
|
|
658
|
+
const required = Reflect.get(associated, "required");
|
|
659
|
+
const validity = Reflect.get(associated, "validity");
|
|
660
|
+
const form = Reflect.get(associated, "form");
|
|
661
|
+
const formMatches = form === null || form === void 0 ? void 0 : Reflect.get(form, "matches");
|
|
662
|
+
const ariaChecked = Reflect.apply(Reflect.get(element, "getAttribute"), element, ["aria-checked"]);
|
|
663
|
+
const ariaDisabled = Reflect.apply(Reflect.get(element, "getAttribute"), element, ["aria-disabled"]);
|
|
664
|
+
const ariaSelected = Reflect.apply(Reflect.get(element, "getAttribute"), element, ["aria-selected"]);
|
|
665
|
+
return {
|
|
666
|
+
matchCount,
|
|
667
|
+
kind,
|
|
668
|
+
...typeof checked === "boolean" ? { checked } : ariaChecked === "true" || ariaChecked === "false" ? { checked: ariaChecked === "true" } : {},
|
|
669
|
+
...typeof selected === "boolean" ? { selected } : ariaSelected === "true" || ariaSelected === "false" ? { selected: ariaSelected === "true" } : {},
|
|
670
|
+
disabled: typeof disabled === "boolean" ? disabled : ariaDisabled === "true",
|
|
671
|
+
...typeof required === "boolean" ? { required } : {},
|
|
672
|
+
...validity !== void 0 && typeof Reflect.get(validity, "valid") === "boolean" ? { valid: Reflect.get(validity, "valid") } : {},
|
|
673
|
+
...typeof formMatches === "function" ? { formValid: Reflect.apply(formMatches, form, [":valid"]) } : {}
|
|
674
|
+
};
|
|
675
|
+
}, selector);
|
|
676
|
+
const boundedBestEffort = async (promise, millis) => {
|
|
677
|
+
let timer;
|
|
678
|
+
try {
|
|
679
|
+
return await Promise.race([promise.catch(() => void 0), new Promise((resolve) => {
|
|
680
|
+
timer = setTimeout(resolve, millis);
|
|
681
|
+
})]);
|
|
682
|
+
} finally {
|
|
683
|
+
clearTimeout(timer);
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
const readActionTargetAfter = (page, selector) => boundedBestEffort(readActionTarget(page, selector).then(Schema.decodeUnknownSync(ActionTargetState)), ACTION_POST_STATE_MILLIS);
|
|
687
|
+
const makeActionRequestTracker = (page, signal) => {
|
|
688
|
+
const pending = /* @__PURE__ */ new Set();
|
|
689
|
+
let total = 0;
|
|
690
|
+
let status2xx = 0;
|
|
691
|
+
let status3xx = 0;
|
|
692
|
+
let status4xx = 0;
|
|
693
|
+
let status5xx = 0;
|
|
694
|
+
let failed = 0;
|
|
695
|
+
let lastChange = performance.now();
|
|
696
|
+
let closed = false;
|
|
697
|
+
let wake;
|
|
698
|
+
let timer;
|
|
699
|
+
const relevant = (request) => {
|
|
700
|
+
const resourceType = request.resourceType();
|
|
701
|
+
return resourceType === "fetch" || resourceType === "xhr";
|
|
702
|
+
};
|
|
703
|
+
const onRequest = (request) => {
|
|
704
|
+
if (!relevant(request)) return;
|
|
705
|
+
pending.add(request);
|
|
706
|
+
total++;
|
|
707
|
+
lastChange = performance.now();
|
|
708
|
+
};
|
|
709
|
+
const onFinished = (request) => {
|
|
710
|
+
if (!pending.delete(request)) return;
|
|
711
|
+
let status;
|
|
712
|
+
try {
|
|
713
|
+
status = request.response()?.status();
|
|
714
|
+
} catch {}
|
|
715
|
+
if (status !== void 0) {
|
|
716
|
+
if (status >= 200 && status < 300) status2xx++;
|
|
717
|
+
else if (status >= 300 && status < 400) status3xx++;
|
|
718
|
+
else if (status >= 400 && status < 500) status4xx++;
|
|
719
|
+
else if (status >= 500 && status < 600) status5xx++;
|
|
720
|
+
}
|
|
721
|
+
lastChange = performance.now();
|
|
722
|
+
};
|
|
723
|
+
const onFailed = (request) => {
|
|
724
|
+
if (!pending.delete(request)) return;
|
|
725
|
+
failed++;
|
|
726
|
+
lastChange = performance.now();
|
|
727
|
+
};
|
|
728
|
+
const close = () => {
|
|
729
|
+
if (closed) return;
|
|
730
|
+
closed = true;
|
|
731
|
+
page.off("request", onRequest);
|
|
732
|
+
page.off("requestfinished", onFinished);
|
|
733
|
+
page.off("requestfailed", onFailed);
|
|
734
|
+
signal.removeEventListener("abort", close);
|
|
735
|
+
clearTimeout(timer);
|
|
736
|
+
wake?.();
|
|
737
|
+
};
|
|
738
|
+
try {
|
|
739
|
+
page.on("request", onRequest);
|
|
740
|
+
page.on("requestfinished", onFinished);
|
|
741
|
+
page.on("requestfailed", onFailed);
|
|
742
|
+
signal.addEventListener("abort", close, { once: true });
|
|
743
|
+
if (signal.aborted) close();
|
|
744
|
+
} catch (cause) {
|
|
745
|
+
close();
|
|
746
|
+
throw cause;
|
|
747
|
+
}
|
|
748
|
+
const wait = async () => {
|
|
749
|
+
const startedAt = performance.now();
|
|
750
|
+
let settleTimedOut = false;
|
|
751
|
+
while (!closed) {
|
|
752
|
+
const now = performance.now();
|
|
753
|
+
if (pending.size === 0 && now - lastChange >= ACTION_NETWORK_QUIET_MILLIS) break;
|
|
754
|
+
if (now - startedAt >= ACTION_NETWORK_SETTLE_MILLIS) {
|
|
755
|
+
settleTimedOut = true;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
await new Promise((resolve) => {
|
|
759
|
+
wake = resolve;
|
|
760
|
+
timer = setTimeout(() => resolve(), 50);
|
|
761
|
+
});
|
|
762
|
+
wake = void 0;
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
total,
|
|
766
|
+
status2xx,
|
|
767
|
+
status3xx,
|
|
768
|
+
status4xx,
|
|
769
|
+
status5xx,
|
|
770
|
+
failed,
|
|
771
|
+
pending: pending.size,
|
|
772
|
+
settleTimedOut
|
|
773
|
+
};
|
|
774
|
+
};
|
|
775
|
+
return {
|
|
776
|
+
wait,
|
|
777
|
+
close,
|
|
778
|
+
markActionSettled: () => {
|
|
779
|
+
lastChange = performance.now();
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
};
|
|
783
|
+
const disposeActionHandles = async (handles) => {
|
|
784
|
+
await boundedBestEffort(Promise.allSettled(handles.map(async (handle) => handle.dispose())), ACTION_POST_STATE_MILLIS);
|
|
785
|
+
};
|
|
786
|
+
/** Runs entirely in the page realm for both observations and the last bound-element guard. */
|
|
787
|
+
const observePage = (target, requestedSelector, maximum, maximumControls, input) => {
|
|
788
|
+
const pageDocument = Reflect.get(globalThis, "document");
|
|
789
|
+
const matches = requestedSelector === void 0 ? void 0 : Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [requestedSelector]);
|
|
790
|
+
const selectorMatchCount = matches === void 0 || matches === null ? 1 : Math.min(1e4, Reflect.get(matches, "length"));
|
|
791
|
+
const element = target ?? (matches === void 0 || matches === null ? Reflect.get(pageDocument, "body") : Reflect.get(matches, 0));
|
|
792
|
+
if (target !== void 0 && Reflect.get(target, "isConnected") !== true) return { _tag: "MissingElement" };
|
|
793
|
+
if (element === null) return { _tag: "MissingElement" };
|
|
794
|
+
if (element === void 0) return { _tag: "MissingElement" };
|
|
795
|
+
const innerText = Reflect.get(element, "innerText");
|
|
796
|
+
const textContent = Reflect.get(element, "textContent");
|
|
797
|
+
const pageText = typeof innerText === "string" ? innerText : typeof textContent === "string" ? textContent : "";
|
|
798
|
+
const primaryControlSelector = "input,select,textarea,label,button,[role=\"checkbox\"],[role=\"radio\"],[role=\"option\"],[role=\"switch\"],[role=\"tab\"],[role=\"button\"]";
|
|
799
|
+
const optionSelector = "select option";
|
|
800
|
+
const secondaryControlSelector = "a[href]";
|
|
801
|
+
const controlSelector = `${primaryControlSelector},${optionSelector},${secondaryControlSelector}`;
|
|
802
|
+
const selectorFor = (candidate) => {
|
|
803
|
+
const parts = [];
|
|
804
|
+
let current = candidate;
|
|
805
|
+
while (current !== null && current !== void 0) {
|
|
806
|
+
const tagName = String(Reflect.get(current, "tagName") ?? "").toLowerCase();
|
|
807
|
+
if (tagName === "") break;
|
|
808
|
+
const parent = Reflect.get(current, "parentElement");
|
|
809
|
+
if (parent === null) {
|
|
810
|
+
parts.push(tagName);
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
let sibling = Reflect.get(current, "previousElementSibling");
|
|
814
|
+
let index = 1;
|
|
815
|
+
while (sibling !== null && sibling !== void 0) {
|
|
816
|
+
if (String(Reflect.get(sibling, "tagName") ?? "").toLowerCase() === tagName) index++;
|
|
817
|
+
sibling = Reflect.get(sibling, "previousElementSibling");
|
|
818
|
+
}
|
|
819
|
+
parts.push(`${tagName}:nth-of-type(${index})`);
|
|
820
|
+
current = parent;
|
|
821
|
+
}
|
|
822
|
+
return parts.reverse().join(" > ");
|
|
823
|
+
};
|
|
824
|
+
const visible = (candidate) => {
|
|
825
|
+
const hidden = Reflect.get(candidate, "hidden");
|
|
826
|
+
const ariaHidden = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-hidden"]);
|
|
827
|
+
const rects = Reflect.apply(Reflect.get(candidate, "getClientRects"), candidate, []);
|
|
828
|
+
return hidden !== true && ariaHidden !== "true" && typeof rects === "object" && rects !== null && Reflect.get(rects, "length") > 0;
|
|
829
|
+
};
|
|
830
|
+
const candidates = [];
|
|
831
|
+
let controlsTruncated = false;
|
|
832
|
+
const consider = (candidate) => {
|
|
833
|
+
const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
|
|
834
|
+
const visibilityTarget = tagName === "option" ? Reflect.apply(Reflect.get(candidate, "closest"), candidate, ["select"]) : candidate;
|
|
835
|
+
if (!(tagName !== "label" || Reflect.get(candidate, "control") !== null) || typeof visibilityTarget !== "object" || visibilityTarget === null || !visible(visibilityTarget)) return;
|
|
836
|
+
candidates.push(candidate);
|
|
837
|
+
if (candidates.length > maximumControls) controlsTruncated = true;
|
|
838
|
+
};
|
|
839
|
+
const elementMatches = Reflect.get(element, "matches");
|
|
840
|
+
if (typeof elementMatches === "function" && Reflect.apply(elementMatches, element, [controlSelector])) consider(element);
|
|
841
|
+
const considerSelector = (candidateSelector) => {
|
|
842
|
+
const descendants = Reflect.apply(Reflect.get(element, "querySelectorAll"), element, [candidateSelector]);
|
|
843
|
+
if (typeof descendants !== "object" || descendants === null) return;
|
|
844
|
+
const descendantCount = Reflect.get(descendants, "length");
|
|
845
|
+
for (let index = 0; index < descendantCount && !controlsTruncated; index++) consider(Reflect.get(descendants, index));
|
|
846
|
+
};
|
|
847
|
+
considerSelector(primaryControlSelector);
|
|
848
|
+
if (!controlsTruncated) considerSelector(optionSelector);
|
|
849
|
+
if (!controlsTruncated) considerSelector(secondaryControlSelector);
|
|
850
|
+
const identityKey = Symbol.for("@effect-agent/browser-observation");
|
|
851
|
+
let identity = Reflect.get(globalThis, identityKey);
|
|
852
|
+
if (identity === void 0 || identity.document !== pageDocument) {
|
|
853
|
+
identity = {
|
|
854
|
+
document: pageDocument,
|
|
855
|
+
documentId: crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(4)).join("-"),
|
|
856
|
+
nodes: /* @__PURE__ */ new WeakMap()
|
|
857
|
+
};
|
|
858
|
+
Reflect.set(globalThis, identityKey, identity);
|
|
859
|
+
}
|
|
860
|
+
const controls = candidates.slice(0, maximumControls).map((candidate) => {
|
|
861
|
+
let nodeId = identity.nodes.get(candidate);
|
|
862
|
+
if (nodeId === void 0) {
|
|
863
|
+
nodeId = crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(4)).join("-");
|
|
864
|
+
identity.nodes.set(candidate, nodeId);
|
|
865
|
+
}
|
|
866
|
+
const associated = Reflect.get(candidate, "control") ?? candidate;
|
|
867
|
+
const tagName = String(Reflect.get(candidate, "tagName") ?? "").toLowerCase();
|
|
868
|
+
const inputType = String(Reflect.get(associated, "type") ?? "").toLowerCase();
|
|
869
|
+
const role = String(Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["role"]) ?? "").toLowerCase();
|
|
870
|
+
const ariaLabel = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-label"]);
|
|
871
|
+
const candidateText = tagName === "textarea" || tagName === "input" || tagName === "select" ? void 0 : Reflect.get(candidate, tagName === "option" ? "label" : "innerText");
|
|
872
|
+
const associatedLabels = Reflect.get(associated, "labels");
|
|
873
|
+
const associatedLabel = associatedLabels !== void 0 && associatedLabels !== null && Reflect.get(associatedLabels, "length") > 0 ? Reflect.get(Reflect.get(associatedLabels, 0), "innerText") : void 0;
|
|
874
|
+
const label = String(typeof ariaLabel === "string" && ariaLabel !== "" ? ariaLabel : typeof candidateText === "string" && candidateText !== "" ? candidateText : associatedLabel ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
875
|
+
const checked = Reflect.get(associated, "checked");
|
|
876
|
+
const selected = tagName === "select" ? Reflect.get(associated, "selectedIndex") >= 0 : Reflect.get(associated, "selected");
|
|
877
|
+
const disabled = Reflect.get(associated, "disabled");
|
|
878
|
+
const required = Reflect.get(associated, "required");
|
|
879
|
+
const validity = Reflect.get(associated, "validity");
|
|
880
|
+
const form = Reflect.get(associated, "form");
|
|
881
|
+
const formMatches = form === null || form === void 0 ? void 0 : Reflect.get(form, "matches");
|
|
882
|
+
const ariaChecked = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-checked"]);
|
|
883
|
+
const ariaSelected = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-selected"]);
|
|
884
|
+
const ariaDisabled = Reflect.apply(Reflect.get(candidate, "getAttribute"), candidate, ["aria-disabled"]);
|
|
885
|
+
return {
|
|
886
|
+
nodeId,
|
|
887
|
+
...inputType === "" ? {} : { inputType: inputType.slice(0, 64) },
|
|
888
|
+
selector: selectorFor(candidate),
|
|
889
|
+
kind: (tagName === "label" ? `label:${inputType || "control"}` : tagName === "input" ? `input:${inputType || "text"}` : role !== "" ? `role:${role}` : tagName).slice(0, 128),
|
|
890
|
+
...label === "" ? {} : { label },
|
|
891
|
+
...typeof checked === "boolean" ? { checked } : ariaChecked === "true" || ariaChecked === "false" ? { checked: ariaChecked === "true" } : {},
|
|
892
|
+
...typeof selected === "boolean" ? { selected } : ariaSelected === "true" || ariaSelected === "false" ? { selected: ariaSelected === "true" } : {},
|
|
893
|
+
...typeof disabled === "boolean" ? { disabled } : ariaDisabled === "true" || ariaDisabled === "false" ? { disabled: ariaDisabled === "true" } : {},
|
|
894
|
+
...typeof required === "boolean" ? { required } : {},
|
|
895
|
+
...validity !== void 0 && typeof Reflect.get(validity, "valid") === "boolean" ? { valid: Reflect.get(validity, "valid") } : {},
|
|
896
|
+
...typeof formMatches === "function" ? { formValid: Reflect.apply(formMatches, form, [":valid"]) } : {}
|
|
897
|
+
};
|
|
898
|
+
});
|
|
899
|
+
if (input !== void 0) {
|
|
900
|
+
const expected = input.expectedTarget;
|
|
901
|
+
const control = controls.find((candidate) => candidate.nodeId === expected.nodeId);
|
|
902
|
+
if (target === void 0 || identity.documentId !== expected.documentId || identity.nodes.get(target) !== expected.nodeId || control === void 0 || expected.state !== void 0 && [
|
|
903
|
+
"kind",
|
|
904
|
+
"inputType",
|
|
905
|
+
"label",
|
|
906
|
+
"checked",
|
|
907
|
+
"selected",
|
|
908
|
+
"disabled",
|
|
909
|
+
"required",
|
|
910
|
+
"valid",
|
|
911
|
+
"formValid"
|
|
912
|
+
].some((field) => control[field] !== expected.state?.[field]) || Reflect.get(target, "isConnected") !== true) return { _tag: "MissingElement" };
|
|
913
|
+
if (expected.scopeSelector !== void 0) {
|
|
914
|
+
let roots;
|
|
915
|
+
try {
|
|
916
|
+
roots = Reflect.apply(Reflect.get(pageDocument, "querySelectorAll"), pageDocument, [expected.scopeSelector]);
|
|
917
|
+
} catch {
|
|
918
|
+
return { _tag: "MissingElement" };
|
|
919
|
+
}
|
|
920
|
+
if (typeof roots !== "object" || roots === null) return { _tag: "MissingElement" };
|
|
921
|
+
const root = Reflect.get(roots, 0);
|
|
922
|
+
if (Reflect.get(roots, "length") !== 1 || root === void 0 || !Reflect.apply(Reflect.get(root, "contains"), root, [target])) return { _tag: "MissingElement" };
|
|
923
|
+
}
|
|
924
|
+
if (input.mode === "click") {
|
|
925
|
+
const prototype = Reflect.get(Reflect.get(globalThis, "HTMLElement"), "prototype");
|
|
926
|
+
Reflect.apply(Reflect.get(prototype, "click"), target, []);
|
|
927
|
+
} else {
|
|
928
|
+
let prototype = Reflect.getPrototypeOf(target);
|
|
929
|
+
let setValue;
|
|
930
|
+
while (prototype !== null) {
|
|
931
|
+
const setter = Reflect.getOwnPropertyDescriptor(prototype, "value")?.set;
|
|
932
|
+
if (typeof setter === "function") {
|
|
933
|
+
setValue = setter;
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
prototype = Reflect.getPrototypeOf(prototype);
|
|
937
|
+
}
|
|
938
|
+
if (setValue === void 0 || input.value === void 0) return { _tag: "MissingElement" };
|
|
939
|
+
Reflect.apply(setValue, target, [input.value]);
|
|
940
|
+
const dispatchEvent = Reflect.get(target, "dispatchEvent");
|
|
941
|
+
Reflect.apply(dispatchEvent, target, [new Event("input", { bubbles: true })]);
|
|
942
|
+
Reflect.apply(dispatchEvent, target, [new Event("change", { bubbles: true })]);
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
_tag: "Text",
|
|
946
|
+
text: ""
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const text = JSON.stringify({
|
|
950
|
+
documentId: identity.documentId,
|
|
951
|
+
pageText,
|
|
952
|
+
selectorMatchCount,
|
|
953
|
+
controls,
|
|
954
|
+
controlsTruncated
|
|
955
|
+
});
|
|
956
|
+
const observed = new TextEncoder().encode(text).byteLength;
|
|
957
|
+
return observed > maximum ? {
|
|
958
|
+
_tag: "OverLimit",
|
|
959
|
+
observed
|
|
960
|
+
} : {
|
|
961
|
+
_tag: "Text",
|
|
962
|
+
text
|
|
963
|
+
};
|
|
964
|
+
};
|
|
965
|
+
const runObservedPageAction = async (page, selector, signal, onDispatch, action, validate, onComplete) => {
|
|
966
|
+
if (signal.aborted) throw new BrowserRunActionUndispatched(0);
|
|
967
|
+
const before = Schema.decodeUnknownSync(ActionTargetState)(await readActionTarget(page, selector));
|
|
968
|
+
if (before.matchCount !== 1 || before.invalidSelector === true) throw new BrowserRunActionUndispatched(before.matchCount);
|
|
969
|
+
const matches = await page.$$(selector);
|
|
970
|
+
if (matches.length !== 1 || matches[0] === void 0) {
|
|
971
|
+
await disposeActionHandles(matches);
|
|
972
|
+
throw new BrowserRunActionUndispatched(Math.min(1e4, matches.length));
|
|
973
|
+
}
|
|
974
|
+
if (signal.aborted) {
|
|
975
|
+
await disposeActionHandles(matches);
|
|
976
|
+
throw new BrowserRunActionUndispatched(1);
|
|
977
|
+
}
|
|
978
|
+
let tracker;
|
|
979
|
+
let disposing;
|
|
980
|
+
const dispose = () => disposing ??= disposeActionHandles(matches);
|
|
981
|
+
const onAbort = () => {
|
|
982
|
+
dispose();
|
|
983
|
+
};
|
|
984
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
985
|
+
try {
|
|
986
|
+
if (validate !== void 0 && !await validate(matches[0])) throw new BrowserRunActionUndispatched(1);
|
|
987
|
+
tracker = makeActionRequestTracker(page, signal);
|
|
988
|
+
if (signal.aborted) throw new BrowserRunActionUndispatched(1);
|
|
989
|
+
onDispatch();
|
|
990
|
+
const inputStarted = performance.now();
|
|
991
|
+
await action(matches[0]);
|
|
992
|
+
const inputFinished = performance.now();
|
|
993
|
+
onComplete?.();
|
|
994
|
+
tracker.markActionSettled();
|
|
995
|
+
const network = await tracker.wait();
|
|
996
|
+
const after = signal.aborted ? void 0 : await readActionTargetAfter(page, selector);
|
|
997
|
+
return {
|
|
998
|
+
before,
|
|
999
|
+
inputMillis: Math.max(0, inputFinished - inputStarted),
|
|
1000
|
+
observationMillis: Math.max(0, performance.now() - inputFinished),
|
|
1001
|
+
...after === void 0 ? {} : { after },
|
|
1002
|
+
afterUnavailable: after === void 0,
|
|
1003
|
+
network
|
|
1004
|
+
};
|
|
1005
|
+
} finally {
|
|
1006
|
+
tracker?.close();
|
|
1007
|
+
signal.removeEventListener("abort", onAbort);
|
|
1008
|
+
await dispose();
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
const guardedPageInput = async (element, expectedTarget, mode, value) => {
|
|
1012
|
+
if (Schema.decodeUnknownSync(TextObservation)(await element.evaluate(observePage, void 0, 262144, 1, {
|
|
1013
|
+
mode,
|
|
1014
|
+
expectedTarget,
|
|
1015
|
+
...value === void 0 ? {} : { value }
|
|
1016
|
+
}))._tag !== "Text") throw new BrowserRunActionUndispatched(1);
|
|
1017
|
+
};
|
|
1018
|
+
const makeProductionPage = (page) => {
|
|
1019
|
+
const prepareFileSelection = makeFileSelection(page);
|
|
1020
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
1021
|
+
return {
|
|
1022
|
+
identity: async () => {
|
|
1023
|
+
const cdp = await page.createCDPSession();
|
|
1024
|
+
try {
|
|
1025
|
+
const { targetInfo } = await cdp.send("Target.getTargetInfo");
|
|
1026
|
+
return Schema.decodeUnknownSync(BrowserRunPageIdentity)({
|
|
1027
|
+
contextId: targetInfo.browserContextId,
|
|
1028
|
+
targetId: targetInfo.targetId
|
|
1029
|
+
});
|
|
1030
|
+
} finally {
|
|
1031
|
+
await cdp.detach();
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
1034
|
+
close: async () => {
|
|
1035
|
+
if (!page.browser().isConnected()) return;
|
|
1036
|
+
try {
|
|
1037
|
+
await page.close();
|
|
1038
|
+
} catch (cause) {
|
|
1039
|
+
if (!isRemoteClosure(cause)) throw cause;
|
|
1040
|
+
}
|
|
1041
|
+
},
|
|
1042
|
+
setBypassServiceWorker: (enabled) => page.setBypassServiceWorker(enabled),
|
|
1043
|
+
setRequestInterception: (enabled) => page.setRequestInterception(enabled),
|
|
1044
|
+
onRequest: (listener) => {
|
|
1045
|
+
const sdkListener = (request) => listener(makeProductionRequest(request));
|
|
1046
|
+
listeners.set(listener, sdkListener);
|
|
1047
|
+
page.on("request", sdkListener);
|
|
1048
|
+
},
|
|
1049
|
+
offRequest: (listener) => {
|
|
1050
|
+
const sdkListener = listeners.get(listener);
|
|
1051
|
+
if (sdkListener !== void 0) {
|
|
1052
|
+
page.off("request", sdkListener);
|
|
1053
|
+
listeners.delete(listener);
|
|
1054
|
+
}
|
|
1055
|
+
},
|
|
1056
|
+
goto: async (url) => {
|
|
1057
|
+
await page.goto(url, {
|
|
1058
|
+
waitUntil: "domcontentloaded",
|
|
1059
|
+
timeout: 3e4
|
|
1060
|
+
});
|
|
1061
|
+
},
|
|
1062
|
+
url: () => page.url(),
|
|
1063
|
+
readText: async (selector, maximumBytes) => {
|
|
1064
|
+
const observation = Schema.decodeUnknownSync(TextObservation)(await page.evaluate(observePage, void 0, selector, maximumBytes, MAX_OBSERVED_CONTROLS));
|
|
1065
|
+
if (observation._tag === "Text") Schema.decodeSync(BrowserRunPageObservation)(observation.text);
|
|
1066
|
+
return observation;
|
|
1067
|
+
},
|
|
1068
|
+
fill: (selector, value, signal, onDispatch, onComplete, expectedTarget) => runObservedPageAction(page, selector, signal, onDispatch, (element) => expectedTarget !== void 0 ? guardedPageInput(element, expectedTarget, "fill", value) : element.evaluate((element, nextValue) => {
|
|
1069
|
+
let prototype = Reflect.getPrototypeOf(element);
|
|
1070
|
+
let setValue;
|
|
1071
|
+
while (prototype !== null) {
|
|
1072
|
+
const setter = Reflect.getOwnPropertyDescriptor(prototype, "value")?.set;
|
|
1073
|
+
if (typeof setter === "function") {
|
|
1074
|
+
setValue = setter;
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
prototype = Reflect.getPrototypeOf(prototype);
|
|
1078
|
+
}
|
|
1079
|
+
if (setValue === void 0) throw new Error("The selector did not resolve to a fillable field");
|
|
1080
|
+
const focus = Reflect.get(element, "focus");
|
|
1081
|
+
if (typeof focus === "function") Reflect.apply(focus, element, []);
|
|
1082
|
+
Reflect.apply(setValue, element, [nextValue]);
|
|
1083
|
+
const dispatchEvent = Reflect.get(element, "dispatchEvent");
|
|
1084
|
+
if (typeof dispatchEvent === "function") {
|
|
1085
|
+
Reflect.apply(dispatchEvent, element, [new Event("input", { bubbles: true })]);
|
|
1086
|
+
Reflect.apply(dispatchEvent, element, [new Event("change", { bubbles: true })]);
|
|
1087
|
+
}
|
|
1088
|
+
}, value), void 0, onComplete),
|
|
1089
|
+
click: (selector, signal, onDispatch, onComplete, expectedTarget) => runObservedPageAction(page, selector, signal, onDispatch, (element) => expectedTarget === void 0 ? element.click() : guardedPageInput(element, expectedTarget, "click"), void 0, onComplete),
|
|
1090
|
+
selectFile: async (request, signal, onDispatch, onComplete) => {
|
|
1091
|
+
const selection = await prepareFileSelection(request, signal);
|
|
1092
|
+
try {
|
|
1093
|
+
return await runObservedPageAction(page, request.selector, signal, onDispatch, selection.select, selection.validate, onComplete);
|
|
1094
|
+
} finally {
|
|
1095
|
+
await selection.close();
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
screenshot: (fullPage) => page.screenshot({
|
|
1099
|
+
type: "png",
|
|
1100
|
+
fullPage
|
|
1101
|
+
}),
|
|
1102
|
+
scroll: (deltaX, deltaY) => page.evaluate((x, y) => {
|
|
1103
|
+
const scrollBy = Reflect.get(globalThis, "scrollBy");
|
|
1104
|
+
Reflect.apply(scrollBy, globalThis, [{
|
|
1105
|
+
left: x,
|
|
1106
|
+
top: y,
|
|
1107
|
+
behavior: "instant"
|
|
1108
|
+
}]);
|
|
1109
|
+
}, deltaX, deltaY),
|
|
1110
|
+
createCdpSession: async () => makeProductionCdpSession(await page.createCDPSession()),
|
|
1111
|
+
setViewport: (viewport) => page.setViewport(viewport)
|
|
1112
|
+
};
|
|
1113
|
+
};
|
|
1114
|
+
const makeProductionContext = (context, viewport) => ({
|
|
1115
|
+
newPage: async () => {
|
|
1116
|
+
const page = await context.newPage();
|
|
1117
|
+
if (viewport !== void 0) await page.setViewport(viewport);
|
|
1118
|
+
return makeProductionPage(page);
|
|
1119
|
+
},
|
|
1120
|
+
close: async () => {
|
|
1121
|
+
if (!context.browser().isConnected()) return;
|
|
1122
|
+
try {
|
|
1123
|
+
await context.close();
|
|
1124
|
+
} catch (cause) {
|
|
1125
|
+
if (!isRemoteClosure(cause)) throw cause;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
const makeProductionBrowser = (browser, sessionId, viewport) => ({
|
|
1130
|
+
detach: () => browser.disconnect(),
|
|
1131
|
+
reattach: async (identity) => {
|
|
1132
|
+
const context = browser.browserContexts().find((candidate) => candidate.id === identity.contextId);
|
|
1133
|
+
if (context === void 0) return void 0;
|
|
1134
|
+
const pages = await context.pages();
|
|
1135
|
+
if (pages.length > 64) throw new Error("The browser target inventory exceeds its bound");
|
|
1136
|
+
for (const candidate of pages) {
|
|
1137
|
+
const page = makeProductionPage(candidate);
|
|
1138
|
+
if ((await page.identity?.())?.targetId === identity.targetId) return {
|
|
1139
|
+
context: makeProductionContext(context),
|
|
1140
|
+
page
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
createContext: async () => makeProductionContext(await browser.createBrowserContext(), viewport),
|
|
1145
|
+
close: async () => {
|
|
1146
|
+
if (!browser.isConnected()) return;
|
|
1147
|
+
try {
|
|
1148
|
+
await browser.close();
|
|
1149
|
+
} catch (cause) {
|
|
1150
|
+
if (!isRemoteClosure(cause)) throw cause;
|
|
1151
|
+
}
|
|
1152
|
+
},
|
|
1153
|
+
sessionId: () => sessionId,
|
|
1154
|
+
isConnected: () => browser.isConnected(),
|
|
1155
|
+
onDisconnected: (listener) => {
|
|
1156
|
+
browser.on("disconnected", listener);
|
|
1157
|
+
},
|
|
1158
|
+
offDisconnected: (listener) => {
|
|
1159
|
+
browser.off("disconnected", listener);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
const protocolError = (message, cause) => InteractiveBrowserProtocolError.make({
|
|
1163
|
+
implementation: browserRunInteractiveImplementation,
|
|
1164
|
+
message,
|
|
1165
|
+
...cause === void 0 ? {} : { cause }
|
|
1166
|
+
});
|
|
1167
|
+
const failReportedProtocol = (operation, error, evidence = error.evidence) => reportBrowserCause(operation, Cause.fail(error)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(InteractiveBrowserProtocolError.make({
|
|
1168
|
+
implementation: browserRunInteractiveImplementation,
|
|
1169
|
+
message: error.message,
|
|
1170
|
+
...evidence === void 0 ? {} : { evidence }
|
|
1171
|
+
})))));
|
|
1172
|
+
const actionError = (operation, cause, evidence) => InteractiveBrowserActionError.make({
|
|
1173
|
+
implementation: browserRunInteractiveImplementation,
|
|
1174
|
+
operation,
|
|
1175
|
+
...evidence === void 0 ? {} : { evidence },
|
|
1176
|
+
message: `The interactive browser ${operation} operation failed`,
|
|
1177
|
+
...cause === void 0 ? {} : { cause }
|
|
1178
|
+
});
|
|
1179
|
+
const undispatchedActionError = (operation) => InteractiveBrowserActionError.make({
|
|
1180
|
+
implementation: browserRunInteractiveImplementation,
|
|
1181
|
+
operation,
|
|
1182
|
+
message: `The interactive browser ${operation} operation was not dispatched`,
|
|
1183
|
+
evidence: {
|
|
1184
|
+
stage: "preparation",
|
|
1185
|
+
dispatch: "not-dispatched",
|
|
1186
|
+
session: "attached"
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
/** Recognizes a local pre-dispatch refusal without exposing selector or page content. */
|
|
1190
|
+
const isBrowserRunUndispatchedActionError = (error) => Schema.is(InteractiveBrowserActionError)(error) && error.implementation.identity === browserRunInteractiveImplementation.identity && error.evidence?.dispatch === "not-dispatched";
|
|
1191
|
+
const policyError = (message) => InteractiveBrowserPolicyDeniedError.make({
|
|
1192
|
+
implementation: browserRunInteractiveImplementation,
|
|
1193
|
+
message
|
|
1194
|
+
});
|
|
1195
|
+
const expiredError = (session = "lost") => InteractiveBrowserExpiredError.make({
|
|
1196
|
+
implementation: browserRunInteractiveImplementation,
|
|
1197
|
+
message: "The remote browser is no longer usable",
|
|
1198
|
+
evidence: {
|
|
1199
|
+
stage: "preparation",
|
|
1200
|
+
dispatch: "not-dispatched",
|
|
1201
|
+
session
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
const causeText = (cause) => {
|
|
1205
|
+
if (cause instanceof Error) return cause.message.slice(0, 8e3);
|
|
1206
|
+
return String(cause).slice(0, 8e3);
|
|
1207
|
+
};
|
|
1208
|
+
const isCapacityRefusal = (cause) => cause instanceof BrowserRunFailure && cause.status === 429;
|
|
1209
|
+
const isRemoteClosure = (cause) => /target closed|browser.*closed|session.*closed|connection.*closed|not connected|websocket.*closed/i.test(causeText(cause));
|
|
1210
|
+
const inputMayStillRun = (cause) => isRemoteClosure(cause) || cause instanceof Error && cause.name === "TimeoutError" || cause instanceof BrowserRunFailure && cause.reason === "timeout";
|
|
1211
|
+
const snapshotPolicy = Effect.fn("BrowserRunInteractive.snapshotPolicy")(function* (input) {
|
|
1212
|
+
const decoded = yield* Schema.decodeEffect(InteractiveBrowserPolicy, { onExcessProperty: "error" })(input).pipe(Effect.mapError(() => policyError("The interactive browser policy is malformed")));
|
|
1213
|
+
if (decoded.network._tag === "PublicWeb") return yield* InteractiveBrowserUnsupportedError.make({
|
|
1214
|
+
implementation: browserRunInteractiveImplementation,
|
|
1215
|
+
feature: "policy",
|
|
1216
|
+
message: "Cloudflare Browser Run cannot enforce the PublicWeb network policy for all session traffic"
|
|
1217
|
+
});
|
|
1218
|
+
return Object.freeze({
|
|
1219
|
+
network: decoded.network._tag === "ExactHosts" ? Object.freeze({
|
|
1220
|
+
_tag: decoded.network._tag,
|
|
1221
|
+
allowedHosts: Object.freeze([...decoded.network.allowedHosts])
|
|
1222
|
+
}) : Object.freeze({ _tag: decoded.network._tag }),
|
|
1223
|
+
maxActions: decoded.maxActions,
|
|
1224
|
+
maxElapsedMillis: decoded.maxElapsedMillis,
|
|
1225
|
+
maxReturnedBytes: decoded.maxReturnedBytes
|
|
1226
|
+
});
|
|
1227
|
+
});
|
|
1228
|
+
const hostAllowed = (policy, value) => {
|
|
1229
|
+
if (policy.network._tag === "Unrestricted") return true;
|
|
1230
|
+
try {
|
|
1231
|
+
const url = new URL(value);
|
|
1232
|
+
return url.protocol === "https:" && url.username === "" && url.password === "" && policy.network.allowedHosts.some((host) => host === url.host);
|
|
1233
|
+
} catch {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
const keepAliveMillis = (policy) => Math.max(MIN_KEEP_ALIVE_MILLIS, Math.min(MAX_KEEP_ALIVE_MILLIS, policy.maxElapsedMillis));
|
|
1238
|
+
var LateBrowserAcquisition = class extends Error {};
|
|
1239
|
+
const closeLateAcquisition = async (signal, acquire, close) => {
|
|
1240
|
+
const acquired = await acquire();
|
|
1241
|
+
if (!signal.aborted) return acquired;
|
|
1242
|
+
try {
|
|
1243
|
+
await close(acquired);
|
|
1244
|
+
} catch {}
|
|
1245
|
+
throw new LateBrowserAcquisition("The interrupted browser acquisition completed late");
|
|
1246
|
+
};
|
|
1247
|
+
const closeWithWarning = (close, warning) => Effect.tryPromise({
|
|
1248
|
+
try: close,
|
|
1249
|
+
catch: (cause) => protocolError(warning, cause)
|
|
1250
|
+
}).pipe(Effect.timeoutOrElse({
|
|
1251
|
+
duration: Duration.millis(CLEANUP_STEP_TIMEOUT_MILLIS),
|
|
1252
|
+
orElse: () => Effect.fail(protocolError(warning))
|
|
1253
|
+
}), Effect.catchCause((cause) => reportBrowserCause("interactive.cleanup", cause).pipe(Effect.andThen(Effect.logWarning(warning)))));
|
|
1254
|
+
const deadlineError = Effect.fn("BrowserRunInteractive.deadlineError")(function* (policy, startedAt) {
|
|
1255
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
1256
|
+
return yield* InteractiveBrowserLimitError.make({
|
|
1257
|
+
implementation: browserRunInteractiveImplementation,
|
|
1258
|
+
limit: "elapsed",
|
|
1259
|
+
maximum: policy.maxElapsedMillis,
|
|
1260
|
+
observed: Math.max(policy.maxElapsedMillis, now - startedAt),
|
|
1261
|
+
message: "The browser elapsed-time limit was reached"
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
const withinDeadline = Effect.fn("BrowserRunInteractive.withinDeadline")(function* (effect, policy, startedAt, onTimeout) {
|
|
1265
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
1266
|
+
const elapsed = Math.max(0, now - startedAt);
|
|
1267
|
+
const remaining = policy.maxElapsedMillis - elapsed;
|
|
1268
|
+
if (remaining <= 0) return yield* deadlineError(policy, startedAt);
|
|
1269
|
+
return yield* effect.pipe(Effect.timeoutOrElse({
|
|
1270
|
+
duration: Duration.millis(remaining),
|
|
1271
|
+
orElse: () => Effect.sync(() => onTimeout?.()).pipe(Effect.andThen(deadlineError(policy, startedAt)))
|
|
1272
|
+
}));
|
|
1273
|
+
});
|
|
1274
|
+
const trackPendingInput = (state, pending) => {
|
|
1275
|
+
state.pendingInputs.add(pending);
|
|
1276
|
+
pending.then(() => state.pendingInputs.delete(pending), (cause) => {
|
|
1277
|
+
state.pendingInputs.delete(pending);
|
|
1278
|
+
if (state.disconnected.value || inputMayStillRun(cause)) state.uncertain.value = true;
|
|
1279
|
+
if (isRemoteClosure(cause)) state.disconnected.value = true;
|
|
1280
|
+
});
|
|
1281
|
+
return pending;
|
|
1282
|
+
};
|
|
1283
|
+
const makeHostInputObserver = Effect.fnUntraced(function* (state, operation) {
|
|
1284
|
+
const reporterContext = yield* Effect.context();
|
|
1285
|
+
return (pending, signal) => {
|
|
1286
|
+
pending.then(void 0, (cause) => {
|
|
1287
|
+
if (signal.aborted) return Effect.runPromiseWith(reporterContext)(reportBrowserCause(operation, Cause.fail(cause)));
|
|
1288
|
+
});
|
|
1289
|
+
return trackPendingInput(state, pending);
|
|
1290
|
+
};
|
|
1291
|
+
});
|
|
1292
|
+
const stateFailure = (state) => {
|
|
1293
|
+
if (state.violation.value !== void 0) return state.violation.value;
|
|
1294
|
+
if (state.closed.value) return expiredError();
|
|
1295
|
+
if (state.disconnected.value) return expiredError("disconnected");
|
|
1296
|
+
};
|
|
1297
|
+
var BrowserRunRemoteFailure = class {
|
|
1298
|
+
cause;
|
|
1299
|
+
constructor(cause) {
|
|
1300
|
+
this.cause = cause;
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
const awaitPendingRequests = (state) => Effect.suspend(() => {
|
|
1304
|
+
const pending = [...state.pendingRequests];
|
|
1305
|
+
return pending.length === 0 ? Effect.void : Effect.promise(() => Promise.all(pending)).pipe(Effect.asVoid, Effect.andThen(awaitPendingRequests(state)));
|
|
1306
|
+
});
|
|
1307
|
+
const decodeNavigationResult = Effect.fn("BrowserRunInteractive.decodeNavigationResult")(function* (page, policy) {
|
|
1308
|
+
const url = yield* Effect.try({
|
|
1309
|
+
try: page.url,
|
|
1310
|
+
catch: (cause) => protocolError("Reading the browser navigation URL failed", cause)
|
|
1311
|
+
});
|
|
1312
|
+
const result = yield* Schema.decodeUnknownEffect(BrowserNavigationResult)({ url }).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed navigation URL", cause)));
|
|
1313
|
+
if (!hostAllowed(policy, result.url)) return yield* policyError("The browser returned an off-policy page URL");
|
|
1314
|
+
return result;
|
|
1315
|
+
});
|
|
1316
|
+
const decodeActionResult = Effect.fn("BrowserRunInteractive.decodeActionResult")(function* (page, policy) {
|
|
1317
|
+
const url = yield* Effect.try({
|
|
1318
|
+
try: page.url,
|
|
1319
|
+
catch: (cause) => protocolError("Reading the browser page URL failed", cause)
|
|
1320
|
+
});
|
|
1321
|
+
const result = yield* Schema.decodeUnknownEffect(BrowserActionResult)({ url }).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed page URL", cause)));
|
|
1322
|
+
if (!hostAllowed(policy, result.url)) return yield* policyError("The browser returned an off-policy page URL");
|
|
1323
|
+
return result;
|
|
1324
|
+
});
|
|
1325
|
+
const makeRequestListener = (policy, state) => (request) => {
|
|
1326
|
+
let allowed = false;
|
|
1327
|
+
try {
|
|
1328
|
+
allowed = hostAllowed(policy, request.url());
|
|
1329
|
+
} catch {
|
|
1330
|
+
allowed = false;
|
|
1331
|
+
}
|
|
1332
|
+
if (!allowed) state.violation.value = policyError("The browser requested an off-policy URL");
|
|
1333
|
+
let settlement;
|
|
1334
|
+
try {
|
|
1335
|
+
settlement = allowed ? request.continue() : request.abort();
|
|
1336
|
+
} catch (cause) {
|
|
1337
|
+
state.violation.value = protocolError("Resolving an intercepted browser request failed", cause);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const observed = settlement.catch((cause) => {
|
|
1341
|
+
state.violation.value = protocolError("Resolving an intercepted browser request failed", cause);
|
|
1342
|
+
}).finally(() => {
|
|
1343
|
+
state.pendingRequests.delete(observed);
|
|
1344
|
+
});
|
|
1345
|
+
state.pendingRequests.add(observed);
|
|
1346
|
+
};
|
|
1347
|
+
const makeHandle = Effect.fn("BrowserRunInteractive.makeHandle")(function* (page, policy, startedAt, state, close) {
|
|
1348
|
+
const permits = yield* Semaphore.make(1);
|
|
1349
|
+
let currentOperation;
|
|
1350
|
+
let generation = 0;
|
|
1351
|
+
let evidence = {
|
|
1352
|
+
stage: "preparation",
|
|
1353
|
+
dispatch: "not-dispatched",
|
|
1354
|
+
session: "attached"
|
|
1355
|
+
};
|
|
1356
|
+
const reporterContext = yield* Effect.context();
|
|
1357
|
+
const remote = (operation, evaluate, observed = false) => Effect.suspend(() => {
|
|
1358
|
+
currentOperation = operation;
|
|
1359
|
+
const mutating = operation !== "read-text" && operation !== "screenshot";
|
|
1360
|
+
const currentGeneration = ++generation;
|
|
1361
|
+
evidence = {
|
|
1362
|
+
stage: observed ? "preparation" : mutating ? "input" : "observation",
|
|
1363
|
+
dispatch: observed || !mutating ? "not-dispatched" : "running",
|
|
1364
|
+
session: "attached"
|
|
1365
|
+
};
|
|
1366
|
+
return Effect.tryPromise({
|
|
1367
|
+
try: (signal) => {
|
|
1368
|
+
const pending = evaluate(signal);
|
|
1369
|
+
if (mutating) trackPendingInput(state, pending);
|
|
1370
|
+
pending.then(() => {
|
|
1371
|
+
if (mutating && generation === currentGeneration) evidence = {
|
|
1372
|
+
...evidence,
|
|
1373
|
+
stage: "observation",
|
|
1374
|
+
dispatch: "completed"
|
|
1375
|
+
};
|
|
1376
|
+
}, (cause) => {
|
|
1377
|
+
if (state.disconnected.value || isRemoteClosure(cause)) {
|
|
1378
|
+
state.disconnected.value = true;
|
|
1379
|
+
if (mutating) state.uncertain.value = true;
|
|
1380
|
+
}
|
|
1381
|
+
if (generation === currentGeneration && evidence.dispatch === "running") evidence = {
|
|
1382
|
+
...evidence,
|
|
1383
|
+
dispatch: "unknown"
|
|
1384
|
+
};
|
|
1385
|
+
if (signal.aborted && !(cause instanceof BrowserRunActionUndispatched)) Effect.runPromiseWith(reporterContext)(reportBrowserCause("interactive.input.late", Cause.fail(cause)));
|
|
1386
|
+
});
|
|
1387
|
+
return pending;
|
|
1388
|
+
},
|
|
1389
|
+
catch: (cause) => new BrowserRunRemoteFailure(cause)
|
|
1390
|
+
}).pipe(Effect.catch((failure) => {
|
|
1391
|
+
if (failure.cause instanceof BrowserRunActionUndispatched) return Effect.logInfo("Browser interactive action was not dispatched").pipe(Effect.annotateLogs({
|
|
1392
|
+
"browser.action": operation,
|
|
1393
|
+
"browser.selector_match_count": failure.cause.matchCount
|
|
1394
|
+
}), Effect.andThen(Effect.fail(undispatchedActionError(operation))));
|
|
1395
|
+
if (mutating && inputMayStillRun(failure.cause)) state.uncertain.value = true;
|
|
1396
|
+
if (isRemoteClosure(failure.cause)) {
|
|
1397
|
+
state.disconnected.value = true;
|
|
1398
|
+
if (mutating) state.uncertain.value = true;
|
|
1399
|
+
}
|
|
1400
|
+
evidence = {
|
|
1401
|
+
...evidence,
|
|
1402
|
+
dispatch: evidence.dispatch === "running" ? "unknown" : evidence.dispatch,
|
|
1403
|
+
session: state.disconnected.value ? "disconnected" : "attached"
|
|
1404
|
+
};
|
|
1405
|
+
return reportBrowserCause(`interactive.${operation}.${evidence.stage}.${evidence.dispatch}`, Cause.fail(failure.cause)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(actionError(operation, void 0, evidence)))));
|
|
1406
|
+
}));
|
|
1407
|
+
});
|
|
1408
|
+
const observedAction = (operation, evaluate) => remote(operation, (signal) => {
|
|
1409
|
+
const observedGeneration = generation;
|
|
1410
|
+
return evaluate(signal, () => {
|
|
1411
|
+
if (generation === observedGeneration) evidence = {
|
|
1412
|
+
...evidence,
|
|
1413
|
+
stage: "input",
|
|
1414
|
+
dispatch: "running"
|
|
1415
|
+
};
|
|
1416
|
+
}, () => {
|
|
1417
|
+
if (generation === observedGeneration) evidence = {
|
|
1418
|
+
...evidence,
|
|
1419
|
+
stage: "observation",
|
|
1420
|
+
dispatch: "completed"
|
|
1421
|
+
};
|
|
1422
|
+
});
|
|
1423
|
+
}, true).pipe(Effect.onInterrupt(() => Effect.logWarning("Browser interactive action interrupted").pipe(Effect.annotateLogs({
|
|
1424
|
+
"browser.action": operation,
|
|
1425
|
+
"browser.action_dispatched": evidence.dispatch !== "not-dispatched",
|
|
1426
|
+
"browser.action_outcome_unknown": evidence.dispatch === "running" || evidence.dispatch === "unknown"
|
|
1427
|
+
}))));
|
|
1428
|
+
const decodeActionObservation = Effect.fn("BrowserRunInteractive.decodeActionObservation")(function* (raw) {
|
|
1429
|
+
return yield* Schema.decodeUnknownEffect(ActionObservation)(raw).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed action observation", cause)));
|
|
1430
|
+
});
|
|
1431
|
+
const logActionObservation = (operation, observation) => Effect.logInfo("Browser interactive action observed").pipe(Effect.annotateLogs({
|
|
1432
|
+
"browser.action": operation,
|
|
1433
|
+
"browser.input_millis": observation.inputMillis,
|
|
1434
|
+
"browser.observation_millis": observation.observationMillis,
|
|
1435
|
+
"browser.selector_match_count": observation.before.matchCount,
|
|
1436
|
+
...observation.before.kind === void 0 ? {} : { "browser.target_kind": observation.before.kind },
|
|
1437
|
+
...observation.before.checked === void 0 ? {} : { "browser.target_checked_before": observation.before.checked },
|
|
1438
|
+
...observation.after?.checked === void 0 ? {} : { "browser.target_checked_after": observation.after.checked },
|
|
1439
|
+
...observation.before.selected === void 0 ? {} : { "browser.target_selected_before": observation.before.selected },
|
|
1440
|
+
...observation.after?.selected === void 0 ? {} : { "browser.target_selected_after": observation.after.selected },
|
|
1441
|
+
...observation.before.disabled === void 0 ? {} : { "browser.target_disabled_before": observation.before.disabled },
|
|
1442
|
+
...observation.after?.disabled === void 0 ? {} : { "browser.target_disabled_after": observation.after.disabled },
|
|
1443
|
+
...observation.before.required === void 0 ? {} : { "browser.target_required_before": observation.before.required },
|
|
1444
|
+
...observation.after?.required === void 0 ? {} : { "browser.target_required_after": observation.after.required },
|
|
1445
|
+
...observation.before.valid === void 0 ? {} : { "browser.target_valid_before": observation.before.valid },
|
|
1446
|
+
...observation.after?.valid === void 0 ? {} : { "browser.target_valid_after": observation.after.valid },
|
|
1447
|
+
...observation.before.formValid === void 0 ? {} : { "browser.form_valid_before": observation.before.formValid },
|
|
1448
|
+
...observation.after?.formValid === void 0 ? {} : { "browser.form_valid_after": observation.after.formValid },
|
|
1449
|
+
"browser.target_after_unavailable": observation.afterUnavailable,
|
|
1450
|
+
"browser.fetch_xhr_total": observation.network.total,
|
|
1451
|
+
"browser.fetch_xhr_2xx": observation.network.status2xx,
|
|
1452
|
+
"browser.fetch_xhr_3xx": observation.network.status3xx,
|
|
1453
|
+
"browser.fetch_xhr_4xx": observation.network.status4xx,
|
|
1454
|
+
"browser.fetch_xhr_5xx": observation.network.status5xx,
|
|
1455
|
+
"browser.fetch_xhr_failed": observation.network.failed,
|
|
1456
|
+
"browser.fetch_xhr_pending": observation.network.pending,
|
|
1457
|
+
"browser.network_settle_timed_out": observation.network.settleTimedOut
|
|
1458
|
+
}));
|
|
1459
|
+
const run = (effect, preflight = Effect.void, consumeAction = true, mutating = true) => permits.withPermitsIfAvailable(1)(Effect.gen(function* () {
|
|
1460
|
+
const unavailable = stateFailure(state);
|
|
1461
|
+
if (unavailable !== void 0) {
|
|
1462
|
+
if (unavailable._tag === "InteractiveBrowserProtocolError") {
|
|
1463
|
+
yield* reportBrowserCause("interactive.policy", Cause.fail(unavailable));
|
|
1464
|
+
const safe = reportedBrowserError(InteractiveBrowserProtocolError.make({
|
|
1465
|
+
implementation: browserRunInteractiveImplementation,
|
|
1466
|
+
message: unavailable.message,
|
|
1467
|
+
evidence: unavailable.evidence ?? {
|
|
1468
|
+
stage: "preparation",
|
|
1469
|
+
dispatch: "not-dispatched",
|
|
1470
|
+
session: "attached"
|
|
1471
|
+
}
|
|
1472
|
+
}));
|
|
1473
|
+
if (state.violation.value === unavailable) state.violation.value = safe;
|
|
1474
|
+
return yield* safe;
|
|
1475
|
+
}
|
|
1476
|
+
return yield* unavailable;
|
|
1477
|
+
}
|
|
1478
|
+
if (mutating && (state.uncertain.value || state.pendingInputs.size > 0)) return yield* InteractiveBrowserBusyError.make({
|
|
1479
|
+
implementation: browserRunInteractiveImplementation,
|
|
1480
|
+
message: "Earlier browser input has not been proven stopped"
|
|
1481
|
+
});
|
|
1482
|
+
currentOperation = void 0;
|
|
1483
|
+
generation++;
|
|
1484
|
+
yield* preflight;
|
|
1485
|
+
if (consumeAction) {
|
|
1486
|
+
const admitted = {
|
|
1487
|
+
allowed: state.actions.value < policy.maxActions,
|
|
1488
|
+
observed: state.actions.value + 1
|
|
1489
|
+
};
|
|
1490
|
+
if (admitted.allowed) state.actions.value++;
|
|
1491
|
+
if (!admitted.allowed) return yield* InteractiveBrowserLimitError.make({
|
|
1492
|
+
implementation: browserRunInteractiveImplementation,
|
|
1493
|
+
limit: "actions",
|
|
1494
|
+
maximum: policy.maxActions,
|
|
1495
|
+
observed: admitted.observed,
|
|
1496
|
+
message: "The browser action limit was reached"
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
const completed = effect.pipe(Effect.flatMap((result) => awaitPendingRequests(state).pipe(Effect.andThen(Effect.suspend(() => {
|
|
1500
|
+
const failure = stateFailure(state);
|
|
1501
|
+
return failure === void 0 ? Effect.succeed(result) : Effect.fail(failure);
|
|
1502
|
+
})))));
|
|
1503
|
+
return yield* withinDeadline(completed, policy, startedAt).pipe(Effect.mapError((error) => currentOperation === void 0 ? error : error._tag === "InteractiveBrowserLimitError" ? InteractiveBrowserLimitError.make({
|
|
1504
|
+
implementation: error.implementation,
|
|
1505
|
+
limit: error.limit,
|
|
1506
|
+
maximum: error.maximum,
|
|
1507
|
+
observed: error.observed,
|
|
1508
|
+
message: error.message,
|
|
1509
|
+
evidence
|
|
1510
|
+
}) : error._tag === "InteractiveBrowserPolicyDeniedError" ? InteractiveBrowserPolicyDeniedError.make({
|
|
1511
|
+
implementation: error.implementation,
|
|
1512
|
+
message: error.message,
|
|
1513
|
+
evidence
|
|
1514
|
+
}) : error), Effect.catchIf((error) => currentOperation !== void 0 && error._tag !== "InteractiveBrowserActionError" && error._tag !== "InteractiveBrowserLimitError" && error._tag !== "InteractiveBrowserPolicyDeniedError" && error._tag !== "InteractiveBrowserExpiredError", (error) => {
|
|
1515
|
+
const safe = reportedBrowserError(error._tag === "InteractiveBrowserProtocolError" ? InteractiveBrowserProtocolError.make({
|
|
1516
|
+
implementation: browserRunInteractiveImplementation,
|
|
1517
|
+
message: error.message,
|
|
1518
|
+
evidence
|
|
1519
|
+
}) : actionError(currentOperation ?? "read-text", void 0, evidence));
|
|
1520
|
+
if (state.violation.value === error) state.violation.value = safe;
|
|
1521
|
+
return reportBrowserCause(`interactive.${currentOperation}.observation`, Cause.fail(error)).pipe(Effect.andThen(Effect.fail(safe)));
|
|
1522
|
+
}));
|
|
1523
|
+
})).pipe(Effect.flatMap(Effect.fromOption(() => InteractiveBrowserBusyError.make({
|
|
1524
|
+
implementation: browserRunInteractiveImplementation,
|
|
1525
|
+
message: "The browser handle already has an operation in flight"
|
|
1526
|
+
}))));
|
|
1527
|
+
return {
|
|
1528
|
+
handle: {
|
|
1529
|
+
navigate: (request) => run(Effect.gen(function* () {
|
|
1530
|
+
yield* remote("navigate", () => page.goto(request.url));
|
|
1531
|
+
return yield* decodeNavigationResult(page, policy);
|
|
1532
|
+
}), Effect.suspend(() => Schema.is(InteractiveBrowserTargetUrl)(request.url) && hostAllowed(policy, request.url) ? Effect.void : Effect.fail(policyError("The navigation URL is outside the browser policy")))),
|
|
1533
|
+
readText: (request) => run(Effect.gen(function* () {
|
|
1534
|
+
const raw = yield* remote("read-text", () => page.readText(request.selector, policy.maxReturnedBytes));
|
|
1535
|
+
const observation = yield* Schema.decodeUnknownEffect(TextObservation)(raw).pipe(Effect.mapError((cause) => protocolError("The browser returned a malformed text observation", cause)));
|
|
1536
|
+
if (observation._tag === "MissingElement") return yield* actionError("read-text", void 0, evidence);
|
|
1537
|
+
if (observation._tag === "OverLimit") return yield* InteractiveBrowserLimitError.make({
|
|
1538
|
+
implementation: browserRunInteractiveImplementation,
|
|
1539
|
+
limit: "returned-bytes",
|
|
1540
|
+
maximum: policy.maxReturnedBytes,
|
|
1541
|
+
observed: observation.observed,
|
|
1542
|
+
message: "The browser returned-text limit was reached"
|
|
1543
|
+
});
|
|
1544
|
+
const observed = new TextEncoder().encode(observation.text).byteLength;
|
|
1545
|
+
if (observed > policy.maxReturnedBytes) return yield* InteractiveBrowserLimitError.make({
|
|
1546
|
+
implementation: browserRunInteractiveImplementation,
|
|
1547
|
+
limit: "returned-bytes",
|
|
1548
|
+
maximum: policy.maxReturnedBytes,
|
|
1549
|
+
observed,
|
|
1550
|
+
message: "The browser returned-text limit was reached"
|
|
1551
|
+
});
|
|
1552
|
+
return yield* Schema.decodeEffect(BrowserTextResult)({ text: observation.text }).pipe(Effect.mapError((cause) => protocolError("The browser returned malformed page text", cause)));
|
|
1553
|
+
}), Effect.void, true, false),
|
|
1554
|
+
fill: (request) => run(Effect.gen(function* () {
|
|
1555
|
+
const observation = yield* observedAction("fill", (signal, onDispatch, onComplete) => page.fill(request.selector, request.value, signal, onDispatch, onComplete, request.expectedTarget)).pipe(Effect.flatMap(decodeActionObservation));
|
|
1556
|
+
yield* logActionObservation("fill", observation);
|
|
1557
|
+
return yield* decodeActionResult(page, policy);
|
|
1558
|
+
})),
|
|
1559
|
+
click: (request) => run(Effect.gen(function* () {
|
|
1560
|
+
const observation = yield* observedAction("click", (signal, onDispatch, onComplete) => page.click(request.selector, signal, onDispatch, onComplete, request.expectedTarget)).pipe(Effect.flatMap(decodeActionObservation));
|
|
1561
|
+
yield* logActionObservation("click", observation);
|
|
1562
|
+
return yield* decodeActionResult(page, policy);
|
|
1563
|
+
})),
|
|
1564
|
+
selectFile: (request) => Schema.decodeEffect(BrowserSelectFileRequest)(request).pipe(Effect.mapError(() => policyError("The browser file selection request is malformed")), Effect.flatMap((decoded) => run(Effect.gen(function* () {
|
|
1565
|
+
const observation = yield* observedAction("select-file", (signal, onDispatch, onComplete) => page.selectFile(decoded, signal, onDispatch, onComplete)).pipe(Effect.flatMap(decodeActionObservation));
|
|
1566
|
+
yield* logActionObservation("select-file", observation);
|
|
1567
|
+
const result = yield* decodeActionResult(page, policy);
|
|
1568
|
+
return BrowserFileSelectionResult.make({
|
|
1569
|
+
url: result.url,
|
|
1570
|
+
fileName: decoded.fileName,
|
|
1571
|
+
mediaType: decoded.mediaType,
|
|
1572
|
+
size: decoded.bytes.length
|
|
1573
|
+
});
|
|
1574
|
+
})))),
|
|
1575
|
+
screenshot: (request) => Schema.decodeEffect(BrowserScreenshotRequest)(request).pipe(Effect.mapError(() => policyError("The browser screenshot request is malformed")), Effect.flatMap((decoded) => run(Effect.gen(function* () {
|
|
1576
|
+
const raw = yield* remote("screenshot", () => page.screenshot(decoded.fullPage));
|
|
1577
|
+
const bytes = yield* Schema.decodeUnknownEffect(PngBytes)(raw).pipe(Effect.mapError(() => protocolError("The browser returned a malformed PNG screenshot")));
|
|
1578
|
+
if (bytes.length > policy.maxReturnedBytes) return yield* InteractiveBrowserLimitError.make({
|
|
1579
|
+
implementation: browserRunInteractiveImplementation,
|
|
1580
|
+
limit: "returned-bytes",
|
|
1581
|
+
maximum: policy.maxReturnedBytes,
|
|
1582
|
+
observed: bytes.length,
|
|
1583
|
+
message: "The browser screenshot byte limit was reached"
|
|
1584
|
+
});
|
|
1585
|
+
return yield* Schema.decodeEffect(PageScreenshotResult)({
|
|
1586
|
+
implementation: browserRunInteractiveImplementation,
|
|
1587
|
+
mediaType: "image/png",
|
|
1588
|
+
bytes: new Uint8Array(bytes)
|
|
1589
|
+
}).pipe(Effect.mapError(() => protocolError("The browser returned a malformed PNG screenshot")));
|
|
1590
|
+
}), decodeActionResult(page, policy).pipe(Effect.asVoid), true, false))),
|
|
1591
|
+
scroll: (request) => Schema.decodeEffect(BrowserScrollRequest)(request).pipe(Effect.mapError(() => policyError("The browser scroll request is malformed")), Effect.flatMap((decoded) => run(remote("scroll", () => page.scroll(decoded.deltaX, decoded.deltaY)).pipe(Effect.andThen(decodeActionResult(page, policy))), decodeActionResult(page, policy).pipe(Effect.asVoid)))),
|
|
1592
|
+
close
|
|
1593
|
+
},
|
|
1594
|
+
run
|
|
1595
|
+
};
|
|
1596
|
+
});
|
|
1597
|
+
const remainingMillis = Effect.fn("BrowserRunInteractive.remainingMillis")(function* (policy, startedAt) {
|
|
1598
|
+
const now = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
1599
|
+
return Math.max(0, policy.maxElapsedMillis - Math.max(0, now - startedAt));
|
|
1600
|
+
});
|
|
1601
|
+
const closeEntry = (close, warning) => ({
|
|
1602
|
+
close: Effect.tryPromise({
|
|
1603
|
+
try: close,
|
|
1604
|
+
catch: (cause) => actionError("close", cause)
|
|
1605
|
+
}).pipe(Effect.timeoutOrElse({
|
|
1606
|
+
duration: Duration.millis(CLEANUP_STEP_TIMEOUT_MILLIS),
|
|
1607
|
+
orElse: () => Effect.fail(actionError("close"))
|
|
1608
|
+
})),
|
|
1609
|
+
warning
|
|
1610
|
+
});
|
|
1611
|
+
const syncCloseEntry = (close, warning) => ({
|
|
1612
|
+
close: Effect.try({
|
|
1613
|
+
try: close,
|
|
1614
|
+
catch: (cause) => actionError("close", cause)
|
|
1615
|
+
}),
|
|
1616
|
+
warning
|
|
1617
|
+
});
|
|
1618
|
+
const runTeardown = (entries) => Effect.forEach([...entries].reverse(), (entry) => entry.close.pipe(Effect.match({
|
|
1619
|
+
onFailure: (error) => ({
|
|
1620
|
+
error,
|
|
1621
|
+
warning: entry.warning
|
|
1622
|
+
}),
|
|
1623
|
+
onSuccess: () => void 0
|
|
1624
|
+
}))).pipe(Effect.map((failures) => failures.filter((failure) => failure !== void 0)));
|
|
1625
|
+
const cdpCommand = (page, state, command, parameters, output, malformedMessage) => Effect.scoped(Effect.gen(function* () {
|
|
1626
|
+
const cleanupContext = yield* Effect.context();
|
|
1627
|
+
const observeInput = yield* makeHostInputObserver(state, "interactive.control.late");
|
|
1628
|
+
const cdp = yield* Effect.acquireRelease(Effect.tryPromise({
|
|
1629
|
+
try: (signal) => {
|
|
1630
|
+
const pending = closeLateAcquisition(signal, page.createCdpSession, (acquired) => Effect.runPromiseWith(cleanupContext)(Effect.tryPromise({
|
|
1631
|
+
try: () => acquired.detach(),
|
|
1632
|
+
catch: (cause) => actionError("close", cause)
|
|
1633
|
+
}).pipe(Effect.tapCause((cause) => reportBrowserCause("interactive.lateDetach", cause)))));
|
|
1634
|
+
pending.then(void 0, (cause) => {
|
|
1635
|
+
if (signal.aborted && !(cause instanceof LateBrowserAcquisition)) return Effect.runPromiseWith(cleanupContext)(reportBrowserCause("interactive.connect", Cause.fail(cause)));
|
|
1636
|
+
});
|
|
1637
|
+
return pending;
|
|
1638
|
+
},
|
|
1639
|
+
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the Cloudflare browser control session failed", cause)
|
|
1640
|
+
}), (acquired) => closeWithWarning(acquired.detach, "Detaching the Cloudflare browser control session failed"), { interruptible: true });
|
|
1641
|
+
const raw = yield* Effect.tryPromise({
|
|
1642
|
+
try: (signal) => {
|
|
1643
|
+
const pending = cdp.send(command, parameters);
|
|
1644
|
+
return command === "Cloudflare.getHandoffState" ? pending : observeInput(pending, signal);
|
|
1645
|
+
},
|
|
1646
|
+
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("The Cloudflare browser control command failed", cause)
|
|
1647
|
+
});
|
|
1648
|
+
return yield* Schema.decodeUnknownEffect(output)(raw).pipe(Effect.catch((error) => reportBrowserCause("interactive.command.decode", Cause.fail(error)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(protocolError(malformedMessage)))))));
|
|
1649
|
+
})).pipe(Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.control", error)));
|
|
1650
|
+
const makeHostService = (binding) => {
|
|
1651
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1652
|
+
const closeSession = (sessionId) => binding.closeSession(sessionId).pipe(Effect.tap(() => Effect.sync(() => {
|
|
1653
|
+
owners.delete(Redacted.value(sessionId));
|
|
1654
|
+
})));
|
|
1655
|
+
const terminate = Effect.fn("BrowserRunInteractiveHost.terminate")(function* (sessionId, entries) {
|
|
1656
|
+
const deadline = (yield* Clock.currentTimeMillis) + 1e4;
|
|
1657
|
+
yield* closeSession(sessionId).pipe(Effect.interruptible, Effect.timeoutOrElse({
|
|
1658
|
+
duration: "10 seconds",
|
|
1659
|
+
orElse: () => Effect.fail(actionError("close"))
|
|
1660
|
+
}));
|
|
1661
|
+
const remaining = deadline - (yield* Clock.currentTimeMillis);
|
|
1662
|
+
if (remaining <= 0) return yield* Effect.logWarning("Local browser teardown skipped after confirmed termination deadline");
|
|
1663
|
+
yield* runTeardown(entries).pipe(Effect.flatMap((failures) => Effect.forEach(failures, (failure) => reportBrowserCause("interactive.cleanup", Cause.fail(failure.error)).pipe(Effect.andThen(Effect.logWarning(failure.warning))), { discard: true })), Effect.interruptible, Effect.timeout(`${remaining} millis`), Effect.catchCause((cause) => reportBrowserCause("interactive.cleanup", cause).pipe(Effect.andThen(Effect.logWarning("Local browser teardown incomplete after confirmed termination")))));
|
|
1664
|
+
});
|
|
1665
|
+
const closeAcquired = Effect.fn("BrowserRunInteractiveHost.closeAcquired")(function* (browser) {
|
|
1666
|
+
const sessionId = yield* Effect.try({
|
|
1667
|
+
try: browser.sessionId,
|
|
1668
|
+
catch: (cause) => actionError("close", cause)
|
|
1669
|
+
}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.mapError((cause) => actionError("close", cause)), Effect.onError(() => closeWithWarning(browser.close, "Closing an unidentified browser failed")));
|
|
1670
|
+
yield* terminate(Redacted.make(sessionId), [closeEntry(browser.close, "Closing the local browser connection failed")]);
|
|
1671
|
+
});
|
|
1672
|
+
const acquire = Effect.fn("BrowserRunInteractiveHost.acquire")(function* (policy, resume, pendingInput = false) {
|
|
1673
|
+
const scope = yield* Scope.Scope;
|
|
1674
|
+
const fixedPolicy = yield* snapshotPolicy(policy);
|
|
1675
|
+
const startedAt = resume?.startedAt ?? (yield* Clock.currentTimeMillis);
|
|
1676
|
+
const runCleanup = (effect) => Effect.runPromiseWith(cleanupContext)(effect.pipe(Effect.tapCause((cause) => reportBrowserCause("interactive.lateCleanup", cause))));
|
|
1677
|
+
const cleanupContext = yield* Effect.context();
|
|
1678
|
+
const observeLate = (signal, pending) => {
|
|
1679
|
+
pending.then(void 0, (cause) => {
|
|
1680
|
+
if (signal.aborted && !(cause instanceof LateBrowserAcquisition)) return runCleanup(reportBrowserCause("interactive.acquire", Cause.fail(cause)));
|
|
1681
|
+
});
|
|
1682
|
+
return pending;
|
|
1683
|
+
};
|
|
1684
|
+
const state = {
|
|
1685
|
+
closed: { value: false },
|
|
1686
|
+
disconnected: { value: false },
|
|
1687
|
+
uncertain: { value: pendingInput || resume !== void 0 && resume.inputState !== "idle" },
|
|
1688
|
+
pendingInputs: /* @__PURE__ */ new Set(),
|
|
1689
|
+
actions: { value: resume?.consumedActions ?? 0 },
|
|
1690
|
+
violation: { value: void 0 },
|
|
1691
|
+
pendingRequests: /* @__PURE__ */ new Set()
|
|
1692
|
+
};
|
|
1693
|
+
const lifecycle = {
|
|
1694
|
+
managedTeardownInstalled: false,
|
|
1695
|
+
retained: resume !== void 0
|
|
1696
|
+
};
|
|
1697
|
+
const closers = [];
|
|
1698
|
+
const releaseBeforeManaged = (entry) => Effect.suspend(() => lifecycle.managedTeardownInstalled || lifecycle.retained ? Effect.void : entry.close.pipe(Effect.catchCause((cause) => reportBrowserCause("interactive.cleanup", cause).pipe(Effect.andThen(Effect.logWarning(entry.warning))))));
|
|
1699
|
+
const sessionIdValue = yield* Effect.acquireRelease(withinDeadline(resume === void 0 ? Effect.tryPromise({
|
|
1700
|
+
try: (signal) => observeLate(signal, closeLateAcquisition(signal, () => binding.acquire(keepAliveMillis(fixedPolicy)), (id) => runCleanup(Schema.decodeUnknownEffect(BrowserRunSessionId)(id).pipe(Effect.flatMap((value) => terminate(Redacted.make(value), [])))))),
|
|
1701
|
+
catch: (cause) => isCapacityRefusal(cause) ? InteractiveBrowserCapacityError.make({
|
|
1702
|
+
implementation: browserRunInteractiveImplementation,
|
|
1703
|
+
message: "Browser Run has no capacity for a new browser session"
|
|
1704
|
+
}) : protocolError("Acquiring the Browser Run session failed", cause)
|
|
1705
|
+
}).pipe(Effect.flatMap(Schema.decodeUnknownEffect(BrowserRunSessionId)), Effect.catchTag("SchemaError", () => protocolError("The Browser Run session identity was malformed"))) : Effect.succeed(Redacted.value(resume.sessionId)), fixedPolicy, startedAt), (id) => Effect.suspend(() => {
|
|
1706
|
+
state.closed.value = true;
|
|
1707
|
+
state.disconnected.value = true;
|
|
1708
|
+
return lifecycle.managedTeardownInstalled || lifecycle.retained ? Effect.void : terminate(Redacted.make(id), []).pipe(Effect.catchCause((cause) => reportBrowserCause("interactive.close", cause).pipe(Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")))));
|
|
1709
|
+
}), { interruptible: true });
|
|
1710
|
+
const owner = Symbol();
|
|
1711
|
+
const releaseOwner = () => {
|
|
1712
|
+
if (owners.get(sessionIdValue) === owner) owners.delete(sessionIdValue);
|
|
1713
|
+
};
|
|
1714
|
+
const connect = yield* Effect.cached(Effect.gen(function* () {
|
|
1715
|
+
if (owners.has(sessionIdValue)) return yield* InteractiveBrowserBusyError.make({
|
|
1716
|
+
implementation: browserRunInteractiveImplementation,
|
|
1717
|
+
message: "The previous local browser owner must detach before resuming"
|
|
1718
|
+
});
|
|
1719
|
+
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
1720
|
+
owners.set(sessionIdValue, owner);
|
|
1721
|
+
}), () => Effect.sync(() => {
|
|
1722
|
+
if (!lifecycle.managedTeardownInstalled) releaseOwner();
|
|
1723
|
+
}));
|
|
1724
|
+
const browser = yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
1725
|
+
try: (signal) => observeLate(signal, closeLateAcquisition(signal, () => binding.connect(sessionIdValue, signal), (acquired) => runCleanup(resume === void 0 ? closeAcquired(acquired) : closeEntry(acquired.detach ?? (async () => {}), "Detaching the browser failed").close))),
|
|
1726
|
+
catch: (cause) => resume !== void 0 && cause instanceof BrowserRunFailure && (cause.status === 404 || cause.status === 410) ? expiredError() : protocolError("Connecting to the Browser Run session failed", cause)
|
|
1727
|
+
}).pipe(Effect.timeoutOrElse({
|
|
1728
|
+
duration: "10 seconds",
|
|
1729
|
+
orElse: () => Effect.fail(protocolError("Connecting to the Browser Run session timed out"))
|
|
1730
|
+
})), fixedPolicy, startedAt), (acquired) => lifecycle.managedTeardownInstalled ? Effect.void : lifecycle.retained ? closeEntry(acquired.detach ?? (async () => {}), "Detaching the browser failed").close.pipe(Effect.tap(() => Effect.sync(releaseOwner)), Effect.catchCause((cause) => reportBrowserCause("interactive.detach", cause))) : releaseBeforeManaged(closeEntry(acquired.close, "Closing the local browser connection failed")), { interruptible: true });
|
|
1731
|
+
if (resume !== void 0) {
|
|
1732
|
+
if ((yield* Effect.try({
|
|
1733
|
+
try: browser.sessionId,
|
|
1734
|
+
catch: (cause) => protocolError("Reading the reattached browser identity failed", cause)
|
|
1735
|
+
})) !== sessionIdValue) return yield* protocolError("The reattached browser session identity did not match");
|
|
1736
|
+
}
|
|
1737
|
+
closers.push(closeEntry(browser.close, "Closing the interactive browser failed"));
|
|
1738
|
+
const disconnected = () => {
|
|
1739
|
+
state.disconnected.value = true;
|
|
1740
|
+
};
|
|
1741
|
+
yield* Effect.acquireRelease(Effect.try({
|
|
1742
|
+
try: () => browser.onDisconnected(disconnected),
|
|
1743
|
+
catch: (cause) => protocolError("Installing the browser disconnect listener failed", cause)
|
|
1744
|
+
}), () => releaseBeforeManaged(syncCloseEntry(() => browser.offDisconnected(disconnected), "Removing the browser disconnect listener failed")));
|
|
1745
|
+
closers.push(syncCloseEntry(() => browser.offDisconnected(disconnected), "Removing the browser disconnect listener failed"));
|
|
1746
|
+
if (!(yield* Effect.try({
|
|
1747
|
+
try: browser.isConnected,
|
|
1748
|
+
catch: (cause) => protocolError("Reading the Browser Run connection state failed", cause)
|
|
1749
|
+
}))) {
|
|
1750
|
+
state.disconnected.value = true;
|
|
1751
|
+
return yield* expiredError();
|
|
1752
|
+
}
|
|
1753
|
+
const reattached = resume === void 0 ? void 0 : yield* withinDeadline(Effect.tryPromise({
|
|
1754
|
+
try: async () => {
|
|
1755
|
+
if (browser.reattach === void 0 || browser.detach === void 0) throw new Error("The browser binding does not support exact reattachment");
|
|
1756
|
+
return await browser.reattach(resume.page);
|
|
1757
|
+
},
|
|
1758
|
+
catch: (cause) => protocolError("Reattaching the exact browser page failed", cause)
|
|
1759
|
+
}).pipe(Effect.timeoutOrElse({
|
|
1760
|
+
duration: "10 seconds",
|
|
1761
|
+
orElse: () => Effect.fail(protocolError("Reattaching the exact browser page timed out"))
|
|
1762
|
+
})), fixedPolicy, startedAt);
|
|
1763
|
+
if (resume !== void 0 && reattached === void 0) return yield* expiredError();
|
|
1764
|
+
const context = reattached?.context ?? (yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
1765
|
+
try: (signal) => observeLate(signal, closeLateAcquisition(signal, browser.createContext, (acquired) => runCleanup(Effect.tryPromise({
|
|
1766
|
+
try: () => acquired.close(),
|
|
1767
|
+
catch: (cause) => actionError("close", cause)
|
|
1768
|
+
})))),
|
|
1769
|
+
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser context failed", cause)
|
|
1770
|
+
}), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser context failed")), { interruptible: true }));
|
|
1771
|
+
closers.push(closeEntry(context.close, "Closing the interactive browser context failed"));
|
|
1772
|
+
const page = reattached?.page ?? (yield* Effect.acquireRelease(withinDeadline(Effect.tryPromise({
|
|
1773
|
+
try: (signal) => observeLate(signal, closeLateAcquisition(signal, context.newPage, (acquired) => runCleanup(Effect.tryPromise({
|
|
1774
|
+
try: () => acquired.close(),
|
|
1775
|
+
catch: (cause) => actionError("close", cause)
|
|
1776
|
+
})))),
|
|
1777
|
+
catch: (cause) => state.disconnected.value || isRemoteClosure(cause) ? expiredError() : protocolError("Creating the browser page failed", cause)
|
|
1778
|
+
}), fixedPolicy, startedAt), (acquired) => releaseBeforeManaged(closeEntry(acquired.close, "Closing the interactive browser page failed")), { interruptible: true }));
|
|
1779
|
+
closers.push(closeEntry(page.close, "Closing the interactive browser page failed"));
|
|
1780
|
+
yield* withinDeadline(Effect.tryPromise({
|
|
1781
|
+
try: () => page.setBypassServiceWorker(true),
|
|
1782
|
+
catch: (cause) => protocolError("Bypassing browser service workers failed", cause)
|
|
1783
|
+
}), fixedPolicy, startedAt);
|
|
1784
|
+
const requestListener = makeRequestListener(fixedPolicy, state);
|
|
1785
|
+
yield* Effect.acquireRelease(Effect.try({
|
|
1786
|
+
try: () => page.onRequest(requestListener),
|
|
1787
|
+
catch: (cause) => protocolError("Installing the browser request listener failed", cause)
|
|
1788
|
+
}), () => releaseBeforeManaged(syncCloseEntry(() => page.offRequest(requestListener), "Removing the browser request policy failed")));
|
|
1789
|
+
closers.push(syncCloseEntry(() => page.offRequest(requestListener), "Removing the browser request policy failed"));
|
|
1790
|
+
yield* withinDeadline(Effect.tryPromise({
|
|
1791
|
+
try: () => page.setRequestInterception(true),
|
|
1792
|
+
catch: (cause) => protocolError("Installing the browser request policy failed", cause)
|
|
1793
|
+
}), fixedPolicy, startedAt);
|
|
1794
|
+
yield* withinDeadline(awaitPendingRequests(state), fixedPolicy, startedAt);
|
|
1795
|
+
const setupFailure = stateFailure(state);
|
|
1796
|
+
if (setupFailure !== void 0) return yield* setupFailure;
|
|
1797
|
+
const detach = yield* Effect.cached(Effect.uninterruptible(Effect.gen(function* () {
|
|
1798
|
+
if (browser.detach === void 0) return yield* protocolError("The browser binding does not support detachment");
|
|
1799
|
+
lifecycle.retained = true;
|
|
1800
|
+
state.closed.value = true;
|
|
1801
|
+
state.disconnected.value = true;
|
|
1802
|
+
if (state.pendingInputs.size > 0) state.uncertain.value = true;
|
|
1803
|
+
yield* closeEntry(async () => {
|
|
1804
|
+
page.offRequest(requestListener);
|
|
1805
|
+
browser.offDisconnected(disconnected);
|
|
1806
|
+
await browser.detach?.();
|
|
1807
|
+
}, "Detaching the browser transport failed").close;
|
|
1808
|
+
releaseOwner();
|
|
1809
|
+
}).pipe(Effect.catchTag("InteractiveBrowserActionError", (error) => reportBrowserCause("interactive.detach", Cause.fail(error)).pipe(Effect.andThen(Effect.fail(reportedBrowserError(actionError("close")))))), Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.detach", error)))));
|
|
1810
|
+
const teardown = yield* Effect.uninterruptible(Effect.gen(function* () {
|
|
1811
|
+
const cached = yield* Effect.cached(terminate(Redacted.make(sessionIdValue), closers).pipe(Effect.tapCause((cause) => reportBrowserCause("interactive.close", cause)), Effect.mapError(reportedBrowserError)));
|
|
1812
|
+
lifecycle.managedTeardownInstalled = true;
|
|
1813
|
+
lifecycle.retained = false;
|
|
1814
|
+
yield* Effect.addFinalizer(() => Effect.uninterruptible(Effect.sync(() => {
|
|
1815
|
+
state.closed.value = true;
|
|
1816
|
+
state.disconnected.value = true;
|
|
1817
|
+
}).pipe(Effect.andThen(Effect.suspend(() => lifecycle.retained ? detach : cached)), Effect.catchCause((cause) => reportBrowserCause("interactive.close", cause).pipe(Effect.andThen(Effect.logWarning("Whole-browser cleanup remains unconfirmed")))))));
|
|
1818
|
+
return cached;
|
|
1819
|
+
}));
|
|
1820
|
+
const close = Effect.uninterruptible(Effect.suspend(() => {
|
|
1821
|
+
if (lifecycle.retained || owners.get(sessionIdValue) !== owner) return Effect.void;
|
|
1822
|
+
state.closed.value = true;
|
|
1823
|
+
state.disconnected.value = true;
|
|
1824
|
+
return teardown;
|
|
1825
|
+
}));
|
|
1826
|
+
const runtime = yield* makeHandle(page, fixedPolicy, startedAt, state, close);
|
|
1827
|
+
const currentPagePreflight = decodeActionResult(page, fixedPolicy).pipe(Effect.asVoid);
|
|
1828
|
+
const requestFitsSession = (requestedMillis) => remainingMillis(fixedPolicy, startedAt).pipe(Effect.flatMap((remaining) => remaining > 0 && requestedMillis <= remaining ? Effect.void : Effect.fail(policyError("The host browser request exceeds the remaining session time"))));
|
|
1829
|
+
const inputState = Effect.sync(() => state.uncertain.value ? "unknown" : state.pendingInputs.size > 0 ? "running" : "idle");
|
|
1830
|
+
const pageIdentity = yield* Effect.cached(Effect.tryPromise({
|
|
1831
|
+
try: async () => {
|
|
1832
|
+
if (page.identity === void 0) throw new Error("The browser binding does not expose page identity");
|
|
1833
|
+
return await page.identity();
|
|
1834
|
+
},
|
|
1835
|
+
catch: (cause) => protocolError("Reading the exact browser page identity failed", cause)
|
|
1836
|
+
}).pipe(Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.checkpoint", error))));
|
|
1837
|
+
const observeResize = yield* makeHostInputObserver(state, "interactive.resize.late");
|
|
1838
|
+
return {
|
|
1839
|
+
handle: runtime.handle,
|
|
1840
|
+
checkpoint: withinDeadline(Effect.gen(function* () {
|
|
1841
|
+
const identity = resume?.page ?? (yield* pageIdentity);
|
|
1842
|
+
return BrowserRunInteractiveCheckpoint.make({
|
|
1843
|
+
sessionId: Redacted.make(sessionIdValue),
|
|
1844
|
+
page: identity,
|
|
1845
|
+
policy: InteractiveBrowserPolicy.make(fixedPolicy),
|
|
1846
|
+
startedAt,
|
|
1847
|
+
consumedActions: state.actions.value,
|
|
1848
|
+
inputState: yield* inputState
|
|
1849
|
+
});
|
|
1850
|
+
}), fixedPolicy, startedAt),
|
|
1851
|
+
detach,
|
|
1852
|
+
inputState,
|
|
1853
|
+
drainInput: Effect.suspend(() => Effect.promise(() => boundedBestEffort(Promise.allSettled(state.pendingInputs), 500)).pipe(Effect.andThen(inputState))),
|
|
1854
|
+
sessionId: Redacted.make(sessionIdValue),
|
|
1855
|
+
resizeViewport: (viewport) => decodeViewport(viewport).pipe(Effect.flatMap((decoded) => runtime.run(Effect.tryPromise({
|
|
1856
|
+
try: (signal) => observeResize(page.setViewport(decoded), signal),
|
|
1857
|
+
catch: (cause) => {
|
|
1858
|
+
if (state.disconnected.value || isRemoteClosure(cause)) {
|
|
1859
|
+
state.disconnected.value = true;
|
|
1860
|
+
return expiredError();
|
|
1861
|
+
}
|
|
1862
|
+
return protocolError("Resizing the browser viewport failed", cause);
|
|
1863
|
+
}
|
|
1864
|
+
}).pipe(Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.resize", error))), currentPagePreflight, false))),
|
|
1865
|
+
getLiveView: (request) => Schema.decodeEffect(BrowserRunLiveViewRequest)(request).pipe(Effect.mapError(() => policyError("The Live View request is malformed")), Effect.flatMap((decoded) => runtime.run(cdpCommand(page, state, "Cloudflare.getLiveView", {
|
|
1866
|
+
mode: decoded.mode,
|
|
1867
|
+
expiresInMs: decoded.expiresInMs
|
|
1868
|
+
}, LiveViewObservation, "Cloudflare returned a malformed Live View response").pipe(Effect.flatMap((observation) => Schema.decodeEffect(BrowserRunLiveViewResult)({ devtoolsFrontendUrl: Redacted.make(observation.devtoolsFrontendUrl) }).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed Live View response"))))), currentPagePreflight.pipe(Effect.andThen(requestFitsSession(decoded.expiresInMs))), true, true))),
|
|
1869
|
+
handoff: (request) => Schema.decodeEffect(BrowserRunHandoffRequest)(request).pipe(Effect.mapError(() => policyError("The browser handoff request is malformed")), Effect.flatMap((decoded) => runtime.run(cdpCommand(page, state, "Cloudflare.handoff", {
|
|
1870
|
+
instructions: decoded.instructions,
|
|
1871
|
+
timeout: decoded.timeout
|
|
1872
|
+
}, HandoffObservation, "Cloudflare returned a malformed browser handoff response").pipe(Effect.flatMap((observation) => Schema.decodeEffect(BrowserRunHandoffResult)({ handoffId: Redacted.make(observation.handoffId) }).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed browser handoff response"))))), currentPagePreflight.pipe(Effect.andThen(requestFitsSession(decoded.timeout)))))),
|
|
1873
|
+
getHandoffState: runtime.run(cdpCommand(page, state, "Cloudflare.getHandoffState", {}, HandoffStateObservation, "Cloudflare returned a malformed browser handoff state").pipe(Effect.flatMap((observation) => Schema.decodeEffect(BrowserRunHandoffState)({
|
|
1874
|
+
active: observation.active,
|
|
1875
|
+
...observation.handoffId === void 0 ? {} : { handoffId: Redacted.make(observation.handoffId) },
|
|
1876
|
+
...observation.durationMs === void 0 ? {} : { durationMs: observation.durationMs }
|
|
1877
|
+
}).pipe(Effect.mapError(() => protocolError("Cloudflare returned a malformed browser handoff state"))))), currentPagePreflight, true, false),
|
|
1878
|
+
close
|
|
1879
|
+
};
|
|
1880
|
+
}).pipe(Effect.provideService(Scope.Scope, scope)));
|
|
1881
|
+
return {
|
|
1882
|
+
sessionId: Redacted.make(sessionIdValue),
|
|
1883
|
+
connect: Effect.suspend(() => state.closed.value ? expiredError() : connect)
|
|
1884
|
+
};
|
|
1885
|
+
});
|
|
1886
|
+
return BrowserRunInteractiveHost.of({
|
|
1887
|
+
acquire,
|
|
1888
|
+
open: (policy) => acquire(policy).pipe(Effect.flatMap((acquired) => acquired.connect)),
|
|
1889
|
+
resume: (checkpoint, options) => Schema.decodeEffect(Schema.toType(BrowserRunInteractiveCheckpoint))(checkpoint).pipe(Effect.mapError(() => policyError("The browser checkpoint is malformed")), Effect.flatMap((decoded) => acquire(decoded.policy, decoded, options.pendingInput)), Effect.flatMap((acquired) => acquired.connect), Effect.catchTag("InteractiveBrowserProtocolError", (error) => failReportedProtocol("interactive.resume", error, {
|
|
1890
|
+
stage: "preparation",
|
|
1891
|
+
dispatch: "not-dispatched",
|
|
1892
|
+
session: "disconnected"
|
|
1893
|
+
}))),
|
|
1894
|
+
closeSession,
|
|
1895
|
+
cleanupSemantics: "confirmed-terminal"
|
|
1896
|
+
});
|
|
1897
|
+
};
|
|
1898
|
+
/** Cloudflare host controls and private session identity for one scoped Browser Run pass. */
|
|
1899
|
+
const browserRunInteractiveHostLayer = () => Layer.effect(BrowserRunInteractiveHost, Effect.gen(function* () {
|
|
1900
|
+
return makeHostService(yield* BrowserRunInteractiveBinding);
|
|
1901
|
+
}));
|
|
1902
|
+
/** Worker-only generic adapter; Cloudflare identity and controls remain host-only. */
|
|
1903
|
+
const browserRunInteractiveLayer = () => Layer.effect(InteractiveBrowser, Effect.gen(function* () {
|
|
1904
|
+
const binding = yield* BrowserRunInteractiveBinding;
|
|
1905
|
+
const host = makeHostService(binding);
|
|
1906
|
+
return InteractiveBrowser.of({ open: (policy) => host.open(policy).pipe(Effect.map((session) => session.handle)) });
|
|
1907
|
+
}));
|
|
1908
|
+
const interactiveBindingLayer = (options) => BrowserRunInteractiveBinding.layer(options).pipe(Layer.provide(BrowserRunSessionLifecycle.layer(options)));
|
|
1909
|
+
/** Worker-only assembly; importing ordinary capture never loads Puppeteer. */
|
|
1910
|
+
const CloudflareInteractiveBrowser = {
|
|
1911
|
+
/**
|
|
1912
|
+
* Assemble binding, confirmed-session cleanup, and the generic browser service.
|
|
1913
|
+
* HttpClient and construction errors remain visible. Opening a pass still requires
|
|
1914
|
+
* an explicit policy and Scope; no browser is launched during Layer construction.
|
|
1915
|
+
*/
|
|
1916
|
+
layer: (options) => browserRunInteractiveLayer().pipe(Layer.provide(interactiveBindingLayer(options))),
|
|
1917
|
+
/** Opt into private host controls instead of exposing them through the generic service. */
|
|
1918
|
+
hostLayer: (options) => browserRunInteractiveHostLayer().pipe(Layer.provide(interactiveBindingLayer(options)))
|
|
1919
|
+
};
|
|
1920
|
+
//#endregion
|
|
1921
|
+
export { reportedBrowserError as C, reportBrowserCause as S, BrowserRunCleanupError as _, BrowserRunInteractiveCheckpoint as a, BrowserRunFailure as b, BrowserRunLiveViewResult as c, BrowserRunViewport as d, CloudflareInteractiveBrowser as f, isBrowserRunUndispatchedActionError as g, browserRunInteractiveLayer as h, BrowserRunInteractiveBinding as i, BrowserRunPageIdentity as l, browserRunInteractiveImplementation as m, BrowserRunHandoffResult as n, BrowserRunInteractiveHost as o, browserRunInteractiveHostLayer as p, BrowserRunHandoffState as r, BrowserRunLiveViewRequest as s, BrowserRunHandoffRequest as t, BrowserRunPageObservation as u, BrowserRunSessionLifecycle as v, browserFailure as x, BrowserRunBinding as y };
|
|
1922
|
+
|
|
1923
|
+
//# sourceMappingURL=InteractiveBrowser-CxFT-Nwm.mjs.map
|