@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.
Files changed (106) hide show
  1. package/dist/Alarm.d.mts +244 -0
  2. package/dist/Alarm.mjs +947 -0
  3. package/dist/Alarm.mjs.map +1 -0
  4. package/dist/BrowserCredentials.d.mts +91 -0
  5. package/dist/BrowserCredentials.mjs +120 -0
  6. package/dist/BrowserCredentials.mjs.map +1 -0
  7. package/dist/BrowserRestCapture.d.mts +34 -0
  8. package/dist/BrowserRestCapture.mjs +238 -0
  9. package/dist/BrowserRestCapture.mjs.map +1 -0
  10. package/dist/BrowserRestCrawl.d.mts +16 -0
  11. package/dist/BrowserRestCrawl.mjs +244 -0
  12. package/dist/BrowserRestCrawl.mjs.map +1 -0
  13. package/dist/BrowserSession-DP6b-cpL.d.mts +125 -0
  14. package/dist/BrowserSession-DWoRC_iM.mjs +505 -0
  15. package/dist/BrowserSession-DWoRC_iM.mjs.map +1 -0
  16. package/dist/BrowserSession.d.mts +3 -0
  17. package/dist/BrowserSession.mjs +4 -0
  18. package/dist/CloudflareAiGateway.d.mts +64 -0
  19. package/dist/CloudflareAiGateway.mjs +70 -0
  20. package/dist/CloudflareAiGateway.mjs.map +1 -0
  21. package/dist/CloudflareBindings.d.mts +115 -0
  22. package/dist/CloudflareBindings.mjs +113 -0
  23. package/dist/CloudflareBindings.mjs.map +1 -0
  24. package/dist/CloudflareBrowser-DV6kya1H.mjs +495 -0
  25. package/dist/CloudflareBrowser-DV6kya1H.mjs.map +1 -0
  26. package/dist/CloudflareBrowser-Doe1M7R5.d.mts +80 -0
  27. package/dist/CloudflareBrowser.d.mts +2 -0
  28. package/dist/CloudflareBrowser.mjs +2 -0
  29. package/dist/CloudflareCodeMode.d.mts +44 -0
  30. package/dist/CloudflareCodeMode.mjs +616 -0
  31. package/dist/CloudflareCodeMode.mjs.map +1 -0
  32. package/dist/CloudflareConfig-DKXGo8L3.d.mts +106 -0
  33. package/dist/CloudflareConfig.d.mts +2 -0
  34. package/dist/CloudflareConfig.mjs +128 -0
  35. package/dist/CloudflareConfig.mjs.map +1 -0
  36. package/dist/CloudflareMemory.d.mts +142 -0
  37. package/dist/CloudflareMemory.mjs +202 -0
  38. package/dist/CloudflareMemory.mjs.map +1 -0
  39. package/dist/CloudflareScheduling.d.mts +52 -0
  40. package/dist/CloudflareScheduling.mjs +396 -0
  41. package/dist/CloudflareScheduling.mjs.map +1 -0
  42. package/dist/CloudflareSubscriptions.d.mts +87 -0
  43. package/dist/CloudflareSubscriptions.mjs +530 -0
  44. package/dist/CloudflareSubscriptions.mjs.map +1 -0
  45. package/dist/CloudflareThreadClient.d.mts +1752 -0
  46. package/dist/CloudflareThreadClient.mjs +382 -0
  47. package/dist/CloudflareThreadClient.mjs.map +1 -0
  48. package/dist/InteractiveBrowser-BnqY_Y-F.d.mts +228 -0
  49. package/dist/InteractiveBrowser-CxFT-Nwm.mjs +1923 -0
  50. package/dist/InteractiveBrowser-CxFT-Nwm.mjs.map +1 -0
  51. package/dist/InteractiveBrowser.d.mts +2 -0
  52. package/dist/InteractiveBrowser.mjs +2 -0
  53. package/dist/ThreadObject-CVhZC-7l.mjs +849 -0
  54. package/dist/ThreadObject-CVhZC-7l.mjs.map +1 -0
  55. package/dist/ThreadObject-D8GzJxJB.d.mts +839 -0
  56. package/dist/ThreadObject.d.mts +2 -0
  57. package/dist/ThreadObject.mjs +3 -0
  58. package/dist/WakeScheduler.d.mts +23 -0
  59. package/dist/WakeScheduler.mjs +57 -0
  60. package/dist/WakeScheduler.mjs.map +1 -0
  61. package/dist/boundary-BguazVkh.mjs +42 -0
  62. package/dist/boundary-BguazVkh.mjs.map +1 -0
  63. package/dist/index.d.mts +15 -1550
  64. package/dist/index.mjs +15 -1716
  65. package/dist/prepared-admission-DU0_T-ev.mjs +73 -0
  66. package/dist/prepared-admission-DU0_T-ev.mjs.map +1 -0
  67. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  68. package/package.json +1 -56
  69. package/src/Alarm.ts +1979 -0
  70. package/src/BrowserCredentials.ts +146 -0
  71. package/src/BrowserRestCapture.ts +477 -0
  72. package/src/BrowserRestCrawl.ts +540 -0
  73. package/src/BrowserSession.ts +624 -0
  74. package/src/CloudflareAiGateway.ts +170 -0
  75. package/src/CloudflareBindings.ts +199 -0
  76. package/src/CloudflareBrowser.ts +15 -0
  77. package/src/{code-mode-executor.ts → CloudflareCodeMode.ts} +308 -283
  78. package/src/{config.ts → CloudflareConfig.ts} +19 -12
  79. package/src/CloudflareMemory.ts +420 -0
  80. package/src/CloudflareScheduling.ts +747 -0
  81. package/src/CloudflareSubscriptions.ts +1041 -0
  82. package/src/CloudflareThreadClient.ts +809 -0
  83. package/src/InteractiveBrowser.ts +3246 -0
  84. package/src/ThreadObject.ts +1190 -0
  85. package/src/{wake-scheduler.ts → WakeScheduler.ts} +32 -26
  86. package/src/index.ts +14 -23
  87. package/src/internal/boundary.ts +55 -0
  88. package/src/internal/browser-binding.ts +322 -0
  89. package/src/internal/browser-credentials.ts +362 -0
  90. package/src/internal/browser-failure.ts +92 -0
  91. package/src/internal/browser-file-selection.ts +126 -0
  92. package/src/internal/browser-quick-action.ts +857 -0
  93. package/src/internal/browser-readonly-live-view.ts +111 -0
  94. package/src/internal/browser-session-lifecycle.ts +193 -0
  95. package/src/internal/layers.ts +773 -0
  96. package/src/internal/message-delivery.ts +131 -0
  97. package/src/internal/prepared-admission.ts +116 -0
  98. package/src/internal/progress-wait.ts +121 -0
  99. package/src/internal/transport.ts +44 -0
  100. package/dist/index.mjs.map +0 -1
  101. package/src/alarm.ts +0 -334
  102. package/src/bindings.ts +0 -145
  103. package/src/client.ts +0 -646
  104. package/src/conversation-object.ts +0 -768
  105. package/src/layers.ts +0 -376
  106. package/src/transport.ts +0 -38
@@ -0,0 +1,146 @@
1
+ import { Context, type Effect, Schema } from "effect";
2
+
3
+ const Selector = Schema.NonEmptyString.check(Schema.isMaxLength(2048));
4
+
5
+ export const CredentialKind = Schema.Literals(["login", "card"]);
6
+
7
+ export const CredentialFieldRole = Schema.Literals([
8
+ "username",
9
+ "password",
10
+ "card-name",
11
+ "card-number",
12
+ "card-expiry",
13
+ "card-expiry-month",
14
+ "card-expiry-year",
15
+ "card-security-code",
16
+ ]);
17
+
18
+ /** Canonical HTTPS origins, including non-default ports. Grants must match exactly. */
19
+ export const CredentialOrigin = Schema.String.check(
20
+ Schema.isMaxLength(2048),
21
+ Schema.makeFilter((value) => {
22
+ try {
23
+ const url = new URL(value);
24
+
25
+ return url.protocol === "https:" && url.origin === value && !url.username && !url.password;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }),
30
+ );
31
+
32
+ export class CredentialTarget extends Schema.Class<CredentialTarget>("CredentialTarget")({
33
+ topOrigin: CredentialOrigin,
34
+ frameOrigin: CredentialOrigin,
35
+ recipientOrigin: CredentialOrigin,
36
+ }) {}
37
+
38
+ /** Selectors identify one native form. Roles explicitly select material; no field inference occurs. */
39
+ export class FillCredentialRequest extends Schema.Class<FillCredentialRequest>(
40
+ "FillCredentialRequest",
41
+ )({
42
+ credential: Schema.NonEmptyString.check(Schema.isMaxLength(256)),
43
+ kind: CredentialKind,
44
+ frame: Schema.optionalKey(Schema.Array(Selector).check(Schema.isMaxLength(8))),
45
+ fields: Schema.Array(Schema.Struct({ selector: Selector, role: CredentialFieldRole })).check(
46
+ Schema.isMinLength(1),
47
+ Schema.isMaxLength(8),
48
+ ),
49
+ }) {}
50
+
51
+ const SecretText = Schema.Redacted(Schema.NonEmptyString.check(Schema.isMaxLength(1024)));
52
+
53
+ /** Host-only material. Never return or encode it through a Tool, log, trace, or checkpoint. */
54
+ export const LoginCredential = Schema.TaggedStruct("LoginCredential", {
55
+ username: SecretText,
56
+ password: SecretText,
57
+ });
58
+
59
+ /** Security codes are transient; the host must not persist them. */
60
+ export const CardCredential = Schema.TaggedStruct("CardCredential", {
61
+ name: SecretText,
62
+ number: Schema.Redacted(Schema.String.check(Schema.isPattern(/^\d{12,19}$/))),
63
+ expiry: SecretText,
64
+ expiryMonth: Schema.Redacted(Schema.String.check(Schema.isPattern(/^(0[1-9]|1[0-2])$/))),
65
+ expiryYear: Schema.Redacted(Schema.String.check(Schema.isPattern(/^\d{4}$/))),
66
+ securityCode: Schema.optionalKey(
67
+ Schema.Redacted(Schema.String.check(Schema.isPattern(/^\d{3,4}$/))),
68
+ ),
69
+ });
70
+
71
+ export const BrowserCredentialMaterial = Schema.Union([LoginCredential, CardCredential]);
72
+ export type BrowserCredentialMaterial = typeof BrowserCredentialMaterial.Type;
73
+
74
+ export class CredentialAccessError extends Schema.TaggedError<CredentialAccessError>()(
75
+ "CredentialAccessError",
76
+ {
77
+ reason: Schema.Literals([
78
+ "denied",
79
+ "missing-credential",
80
+ "needs-attention",
81
+ "busy",
82
+ "resolver",
83
+ ]),
84
+ },
85
+ ) {}
86
+
87
+ export interface CredentialAccessRequest {
88
+ readonly credential: string;
89
+ readonly kind: typeof CredentialKind.Type;
90
+ readonly target: CredentialTarget;
91
+ readonly roles: ReadonlyArray<typeof CredentialFieldRole.Type>;
92
+ }
93
+
94
+ /**
95
+ * Invocation-owned authority. Derive caller/ownership from the host, not the public credential
96
+ * identifier. Authorize the actual merchant, frame, recipient, roles, and side effects of filling.
97
+ * Repeated checks must consult current grants. Never log or trace resolved material.
98
+ * Bind caller identity and identifier-to-vault-item selection for the whole invocation; an alias
99
+ * changing between resolve and authorize must never authorize different material.
100
+ */
101
+ export class BrowserCredentialAccess extends Context.Service<
102
+ BrowserCredentialAccess,
103
+ {
104
+ readonly authorize: (
105
+ request: CredentialAccessRequest,
106
+ ) => Effect.Effect<void, CredentialAccessError>;
107
+ readonly resolve: (
108
+ request: CredentialAccessRequest,
109
+ ) => Effect.Effect<BrowserCredentialMaterial, CredentialAccessError>;
110
+ }
111
+ >()("@effect-agent/platform-cloudflare/BrowserCredentialAccess") {}
112
+
113
+ export const CredentialDispatch = Schema.Literals([
114
+ "not-dispatched",
115
+ "possibly-dispatched",
116
+ "dispatched",
117
+ ]);
118
+
119
+ /** Filled counts acknowledged assignments, not authentication, submission, or website acceptance. */
120
+ export class CredentialFillResult extends Schema.Class<CredentialFillResult>(
121
+ "CredentialFillResult",
122
+ )({
123
+ dispatch: Schema.Literal("dispatched"),
124
+ filled: Schema.Natural.check(Schema.isLessThanOrEqualTo(8)),
125
+ }) {}
126
+
127
+ /** Content-free evidence. A lost reply never authorizes a retry or proves that no write occurred. */
128
+ export class CredentialFillError extends Schema.TaggedError<CredentialFillError>()(
129
+ "CredentialFillError",
130
+ {
131
+ reason: Schema.Literals([
132
+ "denied",
133
+ "missing-credential",
134
+ "stale-target",
135
+ "unsupported",
136
+ "needs-attention",
137
+ "busy",
138
+ "provider",
139
+ "resolver",
140
+ "timeout",
141
+ ]),
142
+ dispatch: CredentialDispatch,
143
+ filled: Schema.Natural.check(Schema.isLessThanOrEqualTo(8)),
144
+ cleanup: Schema.Literals(["not-requested", "confirmed", "unconfirmed"]),
145
+ },
146
+ ) {}
@@ -0,0 +1,477 @@
1
+ import { Effect, Layer, Option, Redacted, Schema, Stream } from "effect";
2
+ import {
3
+ PageCapture,
4
+ PageCaptureInferencePolicyError,
5
+ PageCaptureInferenceUse,
6
+ PageCaptureNavigationError,
7
+ PageCaptureOutputLimitError,
8
+ PageCaptureProtocolError,
9
+ PageCaptureRateLimitedError,
10
+ PageCaptureResourceUse,
11
+ PageCaptureResult,
12
+ PageContentCaptured,
13
+ PageLinksCaptured,
14
+ PageMarkdownCaptured,
15
+ PageScrapeCaptured,
16
+ PageStructuredCaptured,
17
+ PageCaptureUnsupportedError,
18
+ type PageCaptureAction,
19
+ type PageCaptureCapture,
20
+ type PageCaptureError,
21
+ type PageCaptureOutput,
22
+ type PageCaptureRequest,
23
+ } from "effect-agent/page-capture";
24
+ import { SandboxImplementation } from "effect-agent/sandbox";
25
+ import { HttpClient, HttpClientRequest, type HttpClientError } from "effect/unstable/http";
26
+
27
+ import {
28
+ BrowserQuickActionWorkersAi,
29
+ type BrowserQuickActionWorkersAiPolicy,
30
+ } from "./CloudflareBrowser.ts";
31
+
32
+ /** Node-safe REST PageCapture implementation; it never imports Worker runtime modules. */
33
+ export const browserRestCaptureImplementation = SandboxImplementation.make({
34
+ isolation: "isolated",
35
+ identity: "cloudflare-browser-rest",
36
+ });
37
+
38
+ const API_ORIGIN = "https://api.cloudflare.com";
39
+ const MAX_DIAGNOSTIC_LENGTH = 8_000;
40
+ const boundedDiagnostic = (message: string): string => message.slice(0, MAX_DIAGNOSTIC_LENGTH);
41
+
42
+ /** Explicit construction inputs; credentials are only projected into the fixed-origin Authorization header. */
43
+ export interface BrowserRestCaptureOptions {
44
+ readonly accountId: string;
45
+ readonly apiToken: Redacted.Redacted<string>;
46
+ }
47
+
48
+ const RestSuccessEnvelope = Schema.Struct({
49
+ success: Schema.Literal(true),
50
+ result: Schema.Json,
51
+ errors: Schema.optionalKey(Schema.Array(Schema.Unknown)),
52
+ meta: Schema.optionalKey(Schema.Unknown),
53
+ });
54
+
55
+ const RestErrorEnvelope = Schema.Struct({
56
+ success: Schema.Literal(false),
57
+ errors: Schema.Array(
58
+ Schema.Struct({
59
+ message: Schema.String,
60
+ code: Schema.optionalKey(Schema.Number),
61
+ }),
62
+ ),
63
+ result: Schema.optionalKey(Schema.Json),
64
+ meta: Schema.optionalKey(Schema.Unknown),
65
+ });
66
+
67
+ const RestEnvelope = Schema.Union([RestSuccessEnvelope, RestErrorEnvelope]);
68
+ const decodeEnvelope = Schema.decodeUnknownOption(Schema.fromJsonString(RestEnvelope));
69
+
70
+ const protocolError = (message: string, cause?: unknown): PageCaptureProtocolError =>
71
+ PageCaptureProtocolError.make({
72
+ implementation: browserRestCaptureImplementation,
73
+ message: boundedDiagnostic(message),
74
+ ...(cause === undefined ? {} : { cause }),
75
+ });
76
+
77
+ const navigationError = (message: string, cause?: unknown): PageCaptureNavigationError =>
78
+ PageCaptureNavigationError.make({
79
+ implementation: browserRestCaptureImplementation,
80
+ message: boundedDiagnostic(message),
81
+ ...(cause === undefined ? {} : { cause }),
82
+ });
83
+
84
+ const privateResponseCause = (
85
+ bodyText: string,
86
+ apiToken: Redacted.Redacted<string>,
87
+ ): Error | undefined => {
88
+ if (bodyText.length === 0) return undefined;
89
+ const token = Redacted.value(apiToken);
90
+ const diagnostic = boundedDiagnostic(bodyText);
91
+
92
+ return new Error(token.length === 0 ? diagnostic : diagnostic.replaceAll(token, "[REDACTED]"));
93
+ };
94
+
95
+ const requestBody = (request: PageCaptureRequest): Record<string, unknown> => {
96
+ const body: Record<string, unknown> =
97
+ request.target._tag === "PageUrlTarget"
98
+ ? { url: request.target.url }
99
+ : { html: request.target.html };
100
+
101
+ if (request.navigation !== undefined) {
102
+ const goto: Record<string, unknown> = {};
103
+
104
+ if (request.navigation.waitUntil !== undefined) goto.waitUntil = request.navigation.waitUntil;
105
+ if (request.navigation.timeoutMillis !== undefined)
106
+ goto.timeout = request.navigation.timeoutMillis;
107
+ if (Object.keys(goto).length > 0) body.gotoOptions = goto;
108
+ if (request.navigation.waitForSelector !== undefined) {
109
+ body.waitForSelector = {
110
+ selector: request.navigation.waitForSelector.selector,
111
+ ...(request.navigation.waitForSelector.timeoutMillis === undefined
112
+ ? {}
113
+ : { timeout: request.navigation.waitForSelector.timeoutMillis }),
114
+ };
115
+ }
116
+ }
117
+ if (request.viewport !== undefined) body.viewport = request.viewport;
118
+ if (request.resourcePolicy?.rejectResourceTypes !== undefined) {
119
+ body.rejectResourceTypes = [...request.resourcePolicy.rejectResourceTypes];
120
+ }
121
+ if (request.resourcePolicy?.allowRequestPatterns !== undefined) {
122
+ body.allowRequestPattern = [...request.resourcePolicy.allowRequestPatterns];
123
+ }
124
+
125
+ return body;
126
+ };
127
+
128
+ const actionName = (
129
+ action: PageCaptureAction,
130
+ ): "content" | "markdown" | "links" | "scrape" | "json" => {
131
+ switch (action._tag) {
132
+ case "CapturePageContent":
133
+ return "content";
134
+ case "CapturePageMarkdown":
135
+ return "markdown";
136
+ case "CapturePageLinks":
137
+ return "links";
138
+ case "CapturePageScrape":
139
+ return "scrape";
140
+ case "CapturePageStructured":
141
+ return "json";
142
+ }
143
+ };
144
+
145
+ const actionBody = (request: PageCaptureRequest): Record<string, unknown> => {
146
+ const body = requestBody(request);
147
+
148
+ switch (request.action._tag) {
149
+ case "CapturePageContent":
150
+ case "CapturePageMarkdown":
151
+ return body;
152
+ case "CapturePageLinks":
153
+ return {
154
+ ...body,
155
+ ...(request.action.visibleLinksOnly === undefined
156
+ ? {}
157
+ : { visibleLinksOnly: request.action.visibleLinksOnly }),
158
+ };
159
+ case "CapturePageScrape":
160
+ return {
161
+ ...body,
162
+ elements: request.action.selectors.map((selector) => ({ selector })),
163
+ };
164
+ case "CapturePageStructured":
165
+ return {
166
+ ...body,
167
+ response_format: { type: "json_schema", json_schema: request.action.responseFormat },
168
+ ...(request.action.prompt === undefined ? {} : { prompt: request.action.prompt }),
169
+ };
170
+ }
171
+ };
172
+
173
+ const retryAfterMillis = (
174
+ headers: Readonly<Record<string, string | undefined>>,
175
+ ): number | undefined => {
176
+ const seconds = Number(headers["retry-after"]);
177
+
178
+ if (!Number.isSafeInteger(seconds) || seconds < 0) return undefined;
179
+ const millis = seconds * 1_000;
180
+
181
+ return Number.isSafeInteger(millis) ? millis : undefined;
182
+ };
183
+
184
+ const browserMillis = (
185
+ headers: Readonly<Record<string, string | undefined>>,
186
+ ): number | undefined => {
187
+ const millis = Number(headers["x-browser-ms-used"]);
188
+
189
+ return Number.isSafeInteger(millis) && millis >= 0 ? millis : undefined;
190
+ };
191
+
192
+ /** Response framing is provider metadata, never content controlled by the rendered page. */
193
+ const isJsonResponse = (headers: Readonly<Record<string, string | undefined>>): boolean => {
194
+ const contentType = headers["content-type"];
195
+
196
+ if (contentType === undefined) return false;
197
+ const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase();
198
+
199
+ return mediaType === "application/json" || mediaType?.endsWith("+json") === true;
200
+ };
201
+
202
+ const readBoundedResponse = Effect.fn("BrowserRestCapture.readResponse")(function* (
203
+ response: { readonly stream: Stream.Stream<Uint8Array, HttpClientError.HttpClientError> },
204
+ request: PageCaptureRequest,
205
+ ): Effect.fn.Return<string, PageCaptureError> {
206
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
207
+
208
+ const chunks = yield* Stream.runFoldEffect<
209
+ Uint8Array,
210
+ HttpClientError.HttpClientError,
211
+ never,
212
+ { readonly observed: number; readonly text: string },
213
+ PageCaptureOutputLimitError | PageCaptureProtocolError,
214
+ never
215
+ >(
216
+ response.stream,
217
+ () => ({ observed: 0, text: "" }),
218
+ (state, chunk) => {
219
+ const observed = state.observed + chunk.byteLength;
220
+
221
+ if (observed > request.limits.maxOutputBytes) {
222
+ return Effect.fail(
223
+ PageCaptureOutputLimitError.make({
224
+ implementation: browserRestCaptureImplementation,
225
+ limit: request.limits.maxOutputBytes,
226
+ observed,
227
+ }),
228
+ );
229
+ }
230
+
231
+ return Effect.try({
232
+ try: () => ({ observed, text: state.text + decoder.decode(chunk, { stream: true }) }),
233
+ catch: (cause) => protocolError("Decoding the Browser Run response failed", cause),
234
+ });
235
+ },
236
+ ).pipe(
237
+ Effect.mapError((error) =>
238
+ Schema.is(PageCaptureOutputLimitError)(error) || Schema.is(PageCaptureProtocolError)(error)
239
+ ? error
240
+ : protocolError("Reading the Browser Run response failed", error),
241
+ ),
242
+ );
243
+
244
+ return yield* Effect.try({
245
+ try: () => chunks.text + decoder.decode(),
246
+ catch: (cause) => protocolError("Decoding the Browser Run response failed", cause),
247
+ });
248
+ });
249
+
250
+ const parseOutput = (
251
+ action: PageCaptureAction,
252
+ bodyText: string,
253
+ apiToken: Redacted.Redacted<string>,
254
+ ): PageCaptureOutput | PageCaptureNavigationError | PageCaptureProtocolError => {
255
+ const envelope = decodeEnvelope(bodyText);
256
+
257
+ if (Option.isNone(envelope)) {
258
+ return protocolError(
259
+ "The Browser Run response did not carry a valid response envelope",
260
+ privateResponseCause(bodyText, apiToken),
261
+ );
262
+ }
263
+ if (!envelope.value.success) {
264
+ return navigationError(
265
+ "Browser Run reported a navigation failure",
266
+ privateResponseCause(bodyText, apiToken),
267
+ );
268
+ }
269
+ switch (action._tag) {
270
+ case "CapturePageContent":
271
+ case "CapturePageMarkdown":
272
+ if (typeof envelope.value.result !== "string") {
273
+ return protocolError("The Browser Run response envelope carried a non-text result");
274
+ }
275
+
276
+ return action._tag === "CapturePageContent"
277
+ ? PageContentCaptured.make({ html: envelope.value.result })
278
+ : PageMarkdownCaptured.make({ markdown: envelope.value.result });
279
+ case "CapturePageLinks": {
280
+ const decoded = Schema.decodeUnknownOption(PageLinksCaptured)({
281
+ _tag: "PageLinksCaptured",
282
+ links: envelope.value.result,
283
+ });
284
+
285
+ return Option.isSome(decoded)
286
+ ? decoded.value
287
+ : protocolError("Browser Run links did not return a bounded array of valid URLs");
288
+ }
289
+ case "CapturePageScrape": {
290
+ const decoded = Schema.decodeUnknownOption(PageScrapeCaptured)({
291
+ _tag: "PageScrapeCaptured",
292
+ groups: envelope.value.result,
293
+ });
294
+
295
+ return Option.isSome(decoded)
296
+ ? decoded.value
297
+ : protocolError("Browser Run scrape did not return bounded grouped element records");
298
+ }
299
+ case "CapturePageStructured":
300
+ return PageStructuredCaptured.make({ value: envelope.value.result });
301
+ }
302
+ };
303
+
304
+ const isQuotaMessage = (text: string): boolean => /time limit|daily|quota/i.test(text);
305
+
306
+ const makeCapture = (
307
+ client: HttpClient.HttpClient,
308
+ options: BrowserRestCaptureOptions,
309
+ workersAi?: BrowserQuickActionWorkersAiPolicy,
310
+ ): PageCaptureCapture =>
311
+ Effect.fn("BrowserRestCapture.capture")(function* (
312
+ request: PageCaptureRequest,
313
+ ): Effect.fn.Return<PageCaptureResult, PageCaptureError> {
314
+ const usesWorkersAi = request.action._tag === "CapturePageStructured";
315
+
316
+ if (usesWorkersAi) {
317
+ if (workersAi === undefined) {
318
+ return yield* PageCaptureUnsupportedError.make({
319
+ implementation: browserRestCaptureImplementation,
320
+ feature: "action",
321
+ message:
322
+ "Structured capture invokes separately billed Workers AI and requires an explicit authorization and accounting policy",
323
+ });
324
+ }
325
+ yield* workersAi.authorizeAndAccount(request).pipe(
326
+ Effect.mapError((cause) =>
327
+ PageCaptureInferencePolicyError.make({
328
+ implementation: browserRestCaptureImplementation,
329
+ provider: "cloudflare-workers-ai",
330
+ reason: cause.reason,
331
+ message:
332
+ cause.reason === "authorization"
333
+ ? "Workers AI extraction was not authorized"
334
+ : "Workers AI extraction could not be accounted for",
335
+ cause,
336
+ }),
337
+ ),
338
+ );
339
+ }
340
+ const action = actionName(request.action);
341
+ const path = `${API_ORIGIN}/client/v4/accounts/${encodeURIComponent(options.accountId)}/browser-rendering/${action}`;
342
+
343
+ const requestWithBody = yield* HttpClientRequest.post(path).pipe(
344
+ request.engine === "kitesurf"
345
+ ? HttpClientRequest.setUrlParam("browser", "kitesurf")
346
+ : (value) => value,
347
+ HttpClientRequest.bearerToken(options.apiToken),
348
+ HttpClientRequest.acceptJson,
349
+ HttpClientRequest.bodyJson(actionBody(request)),
350
+ Effect.mapError((cause) => protocolError("Encoding the Browser Run request failed", cause)),
351
+ );
352
+
353
+ const response = yield* client
354
+ .execute(requestWithBody)
355
+ .pipe(
356
+ Effect.mapError((cause) => protocolError("Calling the Browser Run REST API failed", cause)),
357
+ );
358
+
359
+ const bodyText = yield* readBoundedResponse(response, request);
360
+
361
+ if (response.status === 429) {
362
+ const reason = isQuotaMessage(bodyText) ? "quota" : "rate";
363
+
364
+ return yield* PageCaptureRateLimitedError.make({
365
+ implementation: browserRestCaptureImplementation,
366
+ reason,
367
+ ...(retryAfterMillis(response.headers) === undefined
368
+ ? {}
369
+ : { retryAfterMillis: retryAfterMillis(response.headers) }),
370
+ ...(privateResponseCause(bodyText, options.apiToken) === undefined
371
+ ? {}
372
+ : { cause: privateResponseCause(bodyText, options.apiToken) }),
373
+ message:
374
+ reason === "quota"
375
+ ? "Browser Run exceeded its browser quota"
376
+ : "Browser Run was rate limited",
377
+ });
378
+ }
379
+ if (response.status < 200 || response.status >= 300) {
380
+ const error =
381
+ response.status >= 500
382
+ ? protocolError(
383
+ `Browser Run answered HTTP ${String(response.status)}`,
384
+ privateResponseCause(bodyText, options.apiToken),
385
+ )
386
+ : navigationError(
387
+ `Browser Run answered HTTP ${String(response.status)}`,
388
+ privateResponseCause(bodyText, options.apiToken),
389
+ );
390
+
391
+ return yield* error;
392
+ }
393
+ if (!isJsonResponse(response.headers)) {
394
+ return yield* protocolError(
395
+ "The Browser Run success response was not a JSON response envelope",
396
+ privateResponseCause(bodyText, options.apiToken),
397
+ );
398
+ }
399
+ const output = parseOutput(request.action, bodyText, options.apiToken);
400
+
401
+ if (
402
+ output._tag === "PageCaptureNavigationError" ||
403
+ output._tag === "PageCaptureProtocolError"
404
+ ) {
405
+ return yield* output;
406
+ }
407
+
408
+ return PageCaptureResult.make({
409
+ implementation: browserRestCaptureImplementation,
410
+ output,
411
+ resourceUse: PageCaptureResourceUse.make({
412
+ ...(browserMillis(response.headers) === undefined
413
+ ? {}
414
+ : { browserMillis: browserMillis(response.headers) }),
415
+ ...(usesWorkersAi
416
+ ? {
417
+ inference: PageCaptureInferenceUse.make({
418
+ provider: "cloudflare-workers-ai",
419
+ modelCalls: 1,
420
+ }),
421
+ }
422
+ : {}),
423
+ }),
424
+ });
425
+ });
426
+
427
+ /** REST PageCapture layer for Node and other non-Worker composition roots. */
428
+ export const browserRestCaptureLayer = (
429
+ options: BrowserRestCaptureOptions,
430
+ ): Layer.Layer<PageCapture, never, HttpClient.HttpClient> =>
431
+ Layer.effect(
432
+ PageCapture,
433
+ Effect.map(HttpClient.HttpClient, (client) =>
434
+ PageCapture.of({ capture: makeCapture(client, options) }),
435
+ ),
436
+ );
437
+
438
+ /** REST structured extraction additionally requires explicit Workers AI authorization/accounting. */
439
+ export const browserRestWorkersAiCaptureLayer = (
440
+ options: BrowserRestCaptureOptions,
441
+ ): Layer.Layer<PageCapture, never, HttpClient.HttpClient | BrowserQuickActionWorkersAi> =>
442
+ Layer.effect(
443
+ PageCapture,
444
+ Effect.gen(function* () {
445
+ const client = yield* HttpClient.HttpClient;
446
+ const workersAi = yield* BrowserQuickActionWorkersAi;
447
+
448
+ return PageCapture.of({ capture: makeCapture(client, options, workersAi) });
449
+ }),
450
+ );
451
+
452
+ /** Explicit REST credentials and optional authorization for separately billed extraction. */
453
+ export interface CloudflareBrowserRestOptions extends BrowserRestCaptureOptions {
454
+ readonly workersAi?: BrowserQuickActionWorkersAiPolicy;
455
+ }
456
+
457
+ /** Node-safe WebCapture handler assembly; the application supplies its HttpClient. */
458
+ export const CloudflareBrowserRest = {
459
+ /**
460
+ * Supports capture, scrape, and extraction definitions. Only PageCapture is supplied;
461
+ * handler errors and other services remain visible. Extraction fails closed without
462
+ * workersAi. Existing host policies, response limits, and scoped cleanup are unchanged.
463
+ */
464
+ layer: <A, E, R>(
465
+ definition: { readonly handlers: Layer.Layer<A, E, R> },
466
+ options: CloudflareBrowserRestOptions,
467
+ ): Layer.Layer<A, E, Exclude<R, PageCapture> | HttpClient.HttpClient> => {
468
+ const capture =
469
+ options.workersAi === undefined
470
+ ? browserRestCaptureLayer(options)
471
+ : browserRestWorkersAiCaptureLayer(options).pipe(
472
+ Layer.provide(BrowserQuickActionWorkersAi.layer(options.workersAi)),
473
+ );
474
+
475
+ return definition.handlers.pipe(Layer.provide(capture));
476
+ },
477
+ };