@effect-agent/platform-cloudflare 0.1.0-beta.42 → 0.1.0-beta.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-quick-action.mjs.map +1 -1
- package/dist/browser-rest-capture.mjs.map +1 -1
- package/dist/browser-rest-crawl.mjs.map +1 -1
- package/dist/browser-session-lifecycle-DqntvG-Y.d.mts +21 -0
- package/dist/browser-session-lifecycle-ZGgb3pnK.mjs +84 -0
- package/dist/browser-session-lifecycle-ZGgb3pnK.mjs.map +1 -0
- package/dist/index.d.mts +47 -34
- package/dist/index.mjs +3 -3
- package/dist/index.mjs.map +1 -1
- package/dist/interactive-browser.d.mts +1 -18
- package/dist/interactive-browser.mjs +2 -81
- package/dist/interactive-browser.mjs.map +1 -1
- package/dist/{prepared-admission-BKp_Upw2.mjs → prepared-admission-BhW2eT_a.mjs} +3 -3
- package/dist/prepared-admission-BhW2eT_a.mjs.map +1 -0
- package/dist/protected-browser.d.mts +62 -0
- package/dist/protected-browser.mjs +714 -0
- package/dist/protected-browser.mjs.map +1 -0
- package/dist/scheduling.mjs +1 -1
- package/dist/scheduling.mjs.map +1 -1
- package/dist/subscriptions.mjs +1 -1
- package/dist/subscriptions.mjs.map +1 -1
- package/package.json +1 -81
- package/src/alarm.ts +277 -242
- package/src/bindings.ts +5 -0
- package/src/boundary.ts +4 -0
- package/src/browser-quick-action.ts +52 -0
- package/src/browser-rest-capture.ts +30 -0
- package/src/browser-rest-crawl.ts +43 -0
- package/src/browser-session-lifecycle.ts +18 -0
- package/src/client.ts +40 -0
- package/src/code-mode-executor.ts +50 -0
- package/src/interactive-browser.ts +176 -0
- package/src/layers.ts +13 -3
- package/src/memory.ts +42 -3
- package/src/prepared-admission.ts +1 -0
- package/src/progress-wait.ts +14 -0
- package/src/protected-browser/binding.ts +185 -0
- package/src/protected-browser/inspect-frame.ts +82 -0
- package/src/protected-browser/native.ts +384 -0
- package/src/protected-browser/policy.ts +594 -0
- package/src/protected-browser.ts +2 -0
- package/src/scheduling.ts +34 -0
- package/src/subscriptions.ts +50 -0
- package/src/thread-object.ts +55 -1
- package/src/transport.ts +1 -0
- package/src/wake-scheduler.ts +1 -0
- package/dist/prepared-admission-BKp_Upw2.mjs.map +0 -1
package/src/boundary.ts
CHANGED
|
@@ -9,6 +9,7 @@ const boundForeignDiagnostic = (message: string): string =>
|
|
|
9
9
|
export const safeCauseMessage = (cause: unknown, fallback: string): string => {
|
|
10
10
|
try {
|
|
11
11
|
const message = cause instanceof Error ? cause.message : cause;
|
|
12
|
+
|
|
12
13
|
return boundForeignDiagnostic(typeof message === "string" ? message : String(message));
|
|
13
14
|
} catch {
|
|
14
15
|
return boundForeignDiagnostic(fallback);
|
|
@@ -38,9 +39,12 @@ export const cloudflareFailureSignals = (cause: unknown): CloudflareFailureSigna
|
|
|
38
39
|
const retryableValue = Reflect.get(cause, "retryable");
|
|
39
40
|
const overloadedValue = Reflect.get(cause, "overloaded");
|
|
40
41
|
const resetValue = Reflect.get(cause, "durableObjectReset");
|
|
42
|
+
|
|
41
43
|
const retryable =
|
|
42
44
|
typeof retryableValue === "boolean" ? retryableValue : resetValue === true ? true : undefined;
|
|
45
|
+
|
|
43
46
|
const overloaded = typeof overloadedValue === "boolean" ? overloadedValue : undefined;
|
|
47
|
+
|
|
44
48
|
return {
|
|
45
49
|
...(retryable === undefined ? {} : { retryable }),
|
|
46
50
|
...(overloaded === undefined ? {} : { overloaded }),
|
|
@@ -97,6 +97,7 @@ export class BrowserQuickActionBrowserBinding extends Context.Service<
|
|
|
97
97
|
options: BrowserQuickActionCaptureOptions,
|
|
98
98
|
): Layer.Layer<BrowserQuickActionBrowserBinding> {
|
|
99
99
|
const browser = options.browser;
|
|
100
|
+
|
|
100
101
|
const invoke = Effect.fn("BrowserQuickActionBrowserBinding.invoke")(function* (
|
|
101
102
|
action: "screenshot" | "content" | "markdown" | "links" | "scrape" | "json",
|
|
102
103
|
evaluate: () => Promise<Response>,
|
|
@@ -106,6 +107,7 @@ export class BrowserQuickActionBrowserBinding extends Context.Service<
|
|
|
106
107
|
catch: (cause) => BrowserQuickActionRpcError.make({ action, cause }),
|
|
107
108
|
});
|
|
108
109
|
});
|
|
110
|
+
|
|
109
111
|
return Layer.succeed(BrowserQuickActionBrowserBinding)({
|
|
110
112
|
screenshot: (request) =>
|
|
111
113
|
invoke("screenshot", () => browser.quickAction("screenshot", request)),
|
|
@@ -175,8 +177,10 @@ const decodeEnvelope = Schema.decodeUnknownOption(Schema.fromJsonString(QuickAct
|
|
|
175
177
|
const quickActionCommonOptions = (request: PageCaptureRequest): BrowserRunCommonOptions => {
|
|
176
178
|
const options: BrowserRunBaseOptions = {};
|
|
177
179
|
const navigation = request.navigation;
|
|
180
|
+
|
|
178
181
|
if (navigation !== undefined) {
|
|
179
182
|
const goto: NonNullable<BrowserRunBaseOptions["gotoOptions"]> = {};
|
|
183
|
+
|
|
180
184
|
if (navigation.waitUntil !== undefined) goto.waitUntil = navigation.waitUntil;
|
|
181
185
|
if (navigation.timeoutMillis !== undefined) goto.timeout = navigation.timeoutMillis;
|
|
182
186
|
if (Object.keys(goto).length > 0) options.gotoOptions = goto;
|
|
@@ -200,6 +204,7 @@ const quickActionCommonOptions = (request: PageCaptureRequest): BrowserRunCommon
|
|
|
200
204
|
options.allowRequestPattern = [...request.resourcePolicy.allowRequestPatterns];
|
|
201
205
|
}
|
|
202
206
|
}
|
|
207
|
+
|
|
203
208
|
return request.target._tag === "PageUrlTarget"
|
|
204
209
|
? { ...options, url: request.target.url }
|
|
205
210
|
: { ...options, html: request.target.html };
|
|
@@ -211,6 +216,7 @@ const executeQuickAction = (
|
|
|
211
216
|
request: PageCaptureRequest,
|
|
212
217
|
): Effect.Effect<Response, BrowserQuickActionRpcError> => {
|
|
213
218
|
const options = quickActionCommonOptions(request);
|
|
219
|
+
|
|
214
220
|
switch (request.action._tag) {
|
|
215
221
|
case "CapturePageContent": {
|
|
216
222
|
return browser.content(options);
|
|
@@ -284,6 +290,7 @@ const readBoundedResponse = Effect.fn("BrowserQuickActionCapture.readResponse")(
|
|
|
284
290
|
request: PageCaptureRequest,
|
|
285
291
|
) {
|
|
286
292
|
const body = response.body;
|
|
293
|
+
|
|
287
294
|
if (body === null) return "";
|
|
288
295
|
|
|
289
296
|
const reader = yield* Effect.acquireRelease(
|
|
@@ -293,6 +300,7 @@ const readBoundedResponse = Effect.fn("BrowserQuickActionCapture.readResponse")(
|
|
|
293
300
|
}),
|
|
294
301
|
releaseResponseReader,
|
|
295
302
|
);
|
|
303
|
+
|
|
296
304
|
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
|
|
297
305
|
let observedBytes = 0;
|
|
298
306
|
let bodyText = "";
|
|
@@ -302,6 +310,7 @@ const readBoundedResponse = Effect.fn("BrowserQuickActionCapture.readResponse")(
|
|
|
302
310
|
try: () => reader.read(),
|
|
303
311
|
catch: (cause) => protocolError("Reading the Quick Action response failed", cause),
|
|
304
312
|
});
|
|
313
|
+
|
|
305
314
|
if (chunk.done) break;
|
|
306
315
|
|
|
307
316
|
observedBytes += chunk.value.byteLength;
|
|
@@ -334,25 +343,32 @@ const readBoundedResponse = Effect.fn("BrowserQuickActionCapture.readResponse")(
|
|
|
334
343
|
*/
|
|
335
344
|
const retryAfterMillis = (response: Response): number | undefined => {
|
|
336
345
|
const header = response.headers.get("Retry-After");
|
|
346
|
+
|
|
337
347
|
if (header === null) return undefined;
|
|
338
348
|
const seconds = Number(header);
|
|
349
|
+
|
|
339
350
|
if (!Number.isSafeInteger(seconds) || seconds < 0) return undefined;
|
|
340
351
|
const millis = seconds * 1_000;
|
|
352
|
+
|
|
341
353
|
return Number.isSafeInteger(millis) ? millis : undefined;
|
|
342
354
|
};
|
|
343
355
|
|
|
344
356
|
const browserMillis = (response: Response): number | undefined => {
|
|
345
357
|
const header = response.headers.get("X-Browser-Ms-Used");
|
|
358
|
+
|
|
346
359
|
if (header === null) return undefined;
|
|
347
360
|
const millis = Number(header);
|
|
361
|
+
|
|
348
362
|
return Number.isSafeInteger(millis) && millis >= 0 ? millis : undefined;
|
|
349
363
|
};
|
|
350
364
|
|
|
351
365
|
/** Only trusted response metadata chooses transport framing; page text never does. */
|
|
352
366
|
const isJsonResponse = (response: Response): boolean => {
|
|
353
367
|
const contentType = response.headers.get("Content-Type");
|
|
368
|
+
|
|
354
369
|
if (contentType === null) return false;
|
|
355
370
|
const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase();
|
|
371
|
+
|
|
356
372
|
return mediaType === "application/json" || mediaType?.endsWith("+json") === true;
|
|
357
373
|
};
|
|
358
374
|
|
|
@@ -368,6 +384,7 @@ const parseOutput = (
|
|
|
368
384
|
);
|
|
369
385
|
}
|
|
370
386
|
const envelope = decodeEnvelope(bodyText);
|
|
387
|
+
|
|
371
388
|
if (Option.isNone(envelope)) {
|
|
372
389
|
return protocolError(
|
|
373
390
|
"The JSON Quick Action response did not carry a valid response envelope",
|
|
@@ -386,6 +403,7 @@ const parseOutput = (
|
|
|
386
403
|
if (typeof envelope.value.result !== "string") {
|
|
387
404
|
return protocolError("The Quick Action envelope carried a non-text result");
|
|
388
405
|
}
|
|
406
|
+
|
|
389
407
|
return action._tag === "CapturePageContent"
|
|
390
408
|
? PageContentCaptured.make({ html: envelope.value.result })
|
|
391
409
|
: PageMarkdownCaptured.make({ markdown: envelope.value.result });
|
|
@@ -395,9 +413,11 @@ const parseOutput = (
|
|
|
395
413
|
_tag: "PageLinksCaptured",
|
|
396
414
|
links: envelope.value.result,
|
|
397
415
|
});
|
|
416
|
+
|
|
398
417
|
if (Option.isNone(decoded)) {
|
|
399
418
|
return protocolError("The links Quick Action did not return a bounded array of valid URLs");
|
|
400
419
|
}
|
|
420
|
+
|
|
401
421
|
return decoded.value;
|
|
402
422
|
}
|
|
403
423
|
case "CapturePageScrape": {
|
|
@@ -405,11 +425,13 @@ const parseOutput = (
|
|
|
405
425
|
_tag: "PageScrapeCaptured",
|
|
406
426
|
groups: envelope.value.result,
|
|
407
427
|
});
|
|
428
|
+
|
|
408
429
|
if (Option.isNone(decoded)) {
|
|
409
430
|
return protocolError(
|
|
410
431
|
"The scrape Quick Action did not return bounded grouped element records",
|
|
411
432
|
);
|
|
412
433
|
}
|
|
434
|
+
|
|
413
435
|
return decoded.value;
|
|
414
436
|
}
|
|
415
437
|
case "CapturePageStructured": {
|
|
@@ -437,6 +459,7 @@ const makeCapture = (
|
|
|
437
459
|
}
|
|
438
460
|
|
|
439
461
|
const usesWorkersAi = request.action._tag === "CapturePageStructured";
|
|
462
|
+
|
|
440
463
|
if (usesWorkersAi) {
|
|
441
464
|
if (workersAi === undefined) {
|
|
442
465
|
return yield* PageCaptureUnsupportedError.make({
|
|
@@ -467,11 +490,14 @@ const makeCapture = (
|
|
|
467
490
|
protocolError("The browser binding rejected the Quick Action", error.cause),
|
|
468
491
|
),
|
|
469
492
|
);
|
|
493
|
+
|
|
470
494
|
const bodyText = yield* readBoundedResponse(response, request);
|
|
495
|
+
|
|
471
496
|
if (response.status === 429) {
|
|
472
497
|
const retryAfter = retryAfterMillis(response);
|
|
473
498
|
const reason = isQuotaMessage(bodyText) ? "quota" : "rate";
|
|
474
499
|
const cause = privateResponseCause(bodyText);
|
|
500
|
+
|
|
475
501
|
return yield* PageCaptureRateLimitedError.make({
|
|
476
502
|
implementation: browserQuickActionImplementation,
|
|
477
503
|
reason,
|
|
@@ -486,12 +512,15 @@ const makeCapture = (
|
|
|
486
512
|
if (!response.ok) {
|
|
487
513
|
const message = `The Quick Action answered HTTP ${response.status}`;
|
|
488
514
|
const cause = privateResponseCause(bodyText);
|
|
515
|
+
|
|
489
516
|
if (response.status >= 500) {
|
|
490
517
|
return yield* protocolError(message, cause);
|
|
491
518
|
}
|
|
519
|
+
|
|
492
520
|
return yield* navigationError(message, cause);
|
|
493
521
|
}
|
|
494
522
|
const output = parseOutput(request.action, bodyText, response);
|
|
523
|
+
|
|
495
524
|
if (
|
|
496
525
|
output._tag === "PageCaptureNavigationError" ||
|
|
497
526
|
output._tag === "PageCaptureProtocolError"
|
|
@@ -499,6 +528,7 @@ const makeCapture = (
|
|
|
499
528
|
return yield* output;
|
|
500
529
|
}
|
|
501
530
|
const millis = browserMillis(response);
|
|
531
|
+
|
|
502
532
|
return PageCaptureResult.make({
|
|
503
533
|
implementation: browserQuickActionImplementation,
|
|
504
534
|
output,
|
|
@@ -543,6 +573,7 @@ export const browserQuickActionWorkersAiCaptureLayer = (): Layer.Layer<
|
|
|
543
573
|
Effect.gen(function* () {
|
|
544
574
|
const browser = yield* BrowserQuickActionBrowserBinding;
|
|
545
575
|
const workersAi = yield* BrowserQuickActionWorkersAi;
|
|
576
|
+
|
|
546
577
|
return PageCapture.of({ capture: makeCapture(browser, workersAi) });
|
|
547
578
|
}),
|
|
548
579
|
);
|
|
@@ -580,6 +611,7 @@ export const CloudflareBrowser = {
|
|
|
580
611
|
|
|
581
612
|
const screenshotOptions = (request: PageScreenshotRequest): BrowserRunScreenshotOptions => {
|
|
582
613
|
const options: BrowserRunBaseOptions = {};
|
|
614
|
+
|
|
583
615
|
if (
|
|
584
616
|
request.navigation?.waitUntil !== undefined ||
|
|
585
617
|
request.navigation?.timeoutMillis !== undefined
|
|
@@ -610,6 +642,7 @@ const screenshotOptions = (request: PageScreenshotRequest): BrowserRunScreenshot
|
|
|
610
642
|
if (request.resourcePolicy?.allowRequestPatterns !== undefined) {
|
|
611
643
|
options.allowRequestPattern = [...request.resourcePolicy.allowRequestPatterns];
|
|
612
644
|
}
|
|
645
|
+
|
|
613
646
|
return {
|
|
614
647
|
...options,
|
|
615
648
|
...(request.target._tag === "PageUrlTarget"
|
|
@@ -643,8 +676,10 @@ const pngResponse = (response: Response): boolean =>
|
|
|
643
676
|
|
|
644
677
|
const declaredLength = (response: Response): number | undefined => {
|
|
645
678
|
const raw = response.headers.get("Content-Length");
|
|
679
|
+
|
|
646
680
|
if (raw === null || !/^(0|[1-9][0-9]*)$/.test(raw)) return undefined;
|
|
647
681
|
const length = Number(raw);
|
|
682
|
+
|
|
648
683
|
return Number.isSafeInteger(length) ? length : undefined;
|
|
649
684
|
};
|
|
650
685
|
|
|
@@ -653,22 +688,27 @@ const readScreenshot = Effect.fn("BrowserQuickActionScreenshot.read")(function*
|
|
|
653
688
|
request: PageScreenshotRequest,
|
|
654
689
|
) {
|
|
655
690
|
const body = response.body;
|
|
691
|
+
|
|
656
692
|
if (body === null) {
|
|
657
693
|
return yield* protocolError("The screenshot response had no body");
|
|
658
694
|
}
|
|
659
695
|
if (!pngResponse(response)) {
|
|
660
696
|
yield* cancelBody(body);
|
|
697
|
+
|
|
661
698
|
return yield* protocolError("The screenshot response was not image/png");
|
|
662
699
|
}
|
|
663
700
|
const length = declaredLength(response);
|
|
701
|
+
|
|
664
702
|
if (length !== undefined && length > request.limits.maxOutputBytes) {
|
|
665
703
|
yield* cancelBody(body);
|
|
704
|
+
|
|
666
705
|
return yield* PageScreenshotOutputLimitError.make({
|
|
667
706
|
implementation: browserQuickActionImplementation,
|
|
668
707
|
limit: request.limits.maxOutputBytes,
|
|
669
708
|
observed: length,
|
|
670
709
|
});
|
|
671
710
|
}
|
|
711
|
+
|
|
672
712
|
const reader = yield* Effect.acquireRelease(
|
|
673
713
|
Effect.try({
|
|
674
714
|
try: () => body.getReader(),
|
|
@@ -676,13 +716,16 @@ const readScreenshot = Effect.fn("BrowserQuickActionScreenshot.read")(function*
|
|
|
676
716
|
}),
|
|
677
717
|
releaseScreenshotReader,
|
|
678
718
|
);
|
|
719
|
+
|
|
679
720
|
const chunks: Array<Uint8Array> = [];
|
|
680
721
|
let observed = 0;
|
|
722
|
+
|
|
681
723
|
while (true) {
|
|
682
724
|
const next = yield* Effect.tryPromise({
|
|
683
725
|
try: () => reader.read(),
|
|
684
726
|
catch: (cause) => protocolError("Reading the screenshot response failed", cause),
|
|
685
727
|
});
|
|
728
|
+
|
|
686
729
|
if (next.done) break;
|
|
687
730
|
observed += next.value.byteLength;
|
|
688
731
|
if (observed > request.limits.maxOutputBytes) {
|
|
@@ -696,10 +739,12 @@ const readScreenshot = Effect.fn("BrowserQuickActionScreenshot.read")(function*
|
|
|
696
739
|
}
|
|
697
740
|
const bytes = new Uint8Array(observed);
|
|
698
741
|
let offset = 0;
|
|
742
|
+
|
|
699
743
|
for (const chunk of chunks) {
|
|
700
744
|
bytes.set(chunk, offset);
|
|
701
745
|
offset += chunk.byteLength;
|
|
702
746
|
}
|
|
747
|
+
|
|
703
748
|
return bytes;
|
|
704
749
|
}, Effect.scoped);
|
|
705
750
|
|
|
@@ -714,6 +759,7 @@ const makeScreenshot = (browser: BrowserQuickActionClient): PageScreenshotCaptur
|
|
|
714
759
|
message: "The browser binding's screenshot action exposes no engine selector",
|
|
715
760
|
});
|
|
716
761
|
}
|
|
762
|
+
|
|
717
763
|
const response = yield* browser
|
|
718
764
|
.screenshot(screenshotOptions(request))
|
|
719
765
|
.pipe(
|
|
@@ -721,9 +767,12 @@ const makeScreenshot = (browser: BrowserQuickActionClient): PageScreenshotCaptur
|
|
|
721
767
|
protocolError("The browser binding rejected the screenshot", error.cause),
|
|
722
768
|
),
|
|
723
769
|
);
|
|
770
|
+
|
|
724
771
|
if (response.status === 429) {
|
|
725
772
|
const body = response.body;
|
|
773
|
+
|
|
726
774
|
if (body !== null) yield* cancelBody(body);
|
|
775
|
+
|
|
727
776
|
return yield* PageCaptureRateLimitedError.make({
|
|
728
777
|
implementation: browserQuickActionImplementation,
|
|
729
778
|
reason: "rate",
|
|
@@ -735,11 +784,14 @@ const makeScreenshot = (browser: BrowserQuickActionClient): PageScreenshotCaptur
|
|
|
735
784
|
}
|
|
736
785
|
if (!response.ok) {
|
|
737
786
|
const body = response.body;
|
|
787
|
+
|
|
738
788
|
if (body !== null) yield* cancelBody(body);
|
|
739
789
|
const message = `The screenshot Quick Action answered HTTP ${response.status}`;
|
|
790
|
+
|
|
740
791
|
return yield* response.status >= 500 ? protocolError(message) : navigationError(message);
|
|
741
792
|
}
|
|
742
793
|
const bytes = yield* readScreenshot(response, request);
|
|
794
|
+
|
|
743
795
|
return PageScreenshotResult.make({
|
|
744
796
|
implementation: browserQuickActionImplementation,
|
|
745
797
|
mediaType: "image/png",
|
|
@@ -51,6 +51,7 @@ const RestSuccessEnvelope = Schema.Struct({
|
|
|
51
51
|
errors: Schema.optionalKey(Schema.Array(Schema.Unknown)),
|
|
52
52
|
meta: Schema.optionalKey(Schema.Unknown),
|
|
53
53
|
});
|
|
54
|
+
|
|
54
55
|
const RestErrorEnvelope = Schema.Struct({
|
|
55
56
|
success: Schema.Literal(false),
|
|
56
57
|
errors: Schema.Array(
|
|
@@ -62,6 +63,7 @@ const RestErrorEnvelope = Schema.Struct({
|
|
|
62
63
|
result: Schema.optionalKey(Schema.Json),
|
|
63
64
|
meta: Schema.optionalKey(Schema.Unknown),
|
|
64
65
|
});
|
|
66
|
+
|
|
65
67
|
const RestEnvelope = Schema.Union([RestSuccessEnvelope, RestErrorEnvelope]);
|
|
66
68
|
const decodeEnvelope = Schema.decodeUnknownOption(Schema.fromJsonString(RestEnvelope));
|
|
67
69
|
|
|
@@ -86,6 +88,7 @@ const privateResponseCause = (
|
|
|
86
88
|
if (bodyText.length === 0) return undefined;
|
|
87
89
|
const token = Redacted.value(apiToken);
|
|
88
90
|
const diagnostic = boundedDiagnostic(bodyText);
|
|
91
|
+
|
|
89
92
|
return new Error(token.length === 0 ? diagnostic : diagnostic.replaceAll(token, "[REDACTED]"));
|
|
90
93
|
};
|
|
91
94
|
|
|
@@ -94,8 +97,10 @@ const requestBody = (request: PageCaptureRequest): Record<string, unknown> => {
|
|
|
94
97
|
request.target._tag === "PageUrlTarget"
|
|
95
98
|
? { url: request.target.url }
|
|
96
99
|
: { html: request.target.html };
|
|
100
|
+
|
|
97
101
|
if (request.navigation !== undefined) {
|
|
98
102
|
const goto: Record<string, unknown> = {};
|
|
103
|
+
|
|
99
104
|
if (request.navigation.waitUntil !== undefined) goto.waitUntil = request.navigation.waitUntil;
|
|
100
105
|
if (request.navigation.timeoutMillis !== undefined)
|
|
101
106
|
goto.timeout = request.navigation.timeoutMillis;
|
|
@@ -116,6 +121,7 @@ const requestBody = (request: PageCaptureRequest): Record<string, unknown> => {
|
|
|
116
121
|
if (request.resourcePolicy?.allowRequestPatterns !== undefined) {
|
|
117
122
|
body.allowRequestPattern = [...request.resourcePolicy.allowRequestPatterns];
|
|
118
123
|
}
|
|
124
|
+
|
|
119
125
|
return body;
|
|
120
126
|
};
|
|
121
127
|
|
|
@@ -138,6 +144,7 @@ const actionName = (
|
|
|
138
144
|
|
|
139
145
|
const actionBody = (request: PageCaptureRequest): Record<string, unknown> => {
|
|
140
146
|
const body = requestBody(request);
|
|
147
|
+
|
|
141
148
|
switch (request.action._tag) {
|
|
142
149
|
case "CapturePageContent":
|
|
143
150
|
case "CapturePageMarkdown":
|
|
@@ -167,8 +174,10 @@ const retryAfterMillis = (
|
|
|
167
174
|
headers: Readonly<Record<string, string | undefined>>,
|
|
168
175
|
): number | undefined => {
|
|
169
176
|
const seconds = Number(headers["retry-after"]);
|
|
177
|
+
|
|
170
178
|
if (!Number.isSafeInteger(seconds) || seconds < 0) return undefined;
|
|
171
179
|
const millis = seconds * 1_000;
|
|
180
|
+
|
|
172
181
|
return Number.isSafeInteger(millis) ? millis : undefined;
|
|
173
182
|
};
|
|
174
183
|
|
|
@@ -176,14 +185,17 @@ const browserMillis = (
|
|
|
176
185
|
headers: Readonly<Record<string, string | undefined>>,
|
|
177
186
|
): number | undefined => {
|
|
178
187
|
const millis = Number(headers["x-browser-ms-used"]);
|
|
188
|
+
|
|
179
189
|
return Number.isSafeInteger(millis) && millis >= 0 ? millis : undefined;
|
|
180
190
|
};
|
|
181
191
|
|
|
182
192
|
/** Response framing is provider metadata, never content controlled by the rendered page. */
|
|
183
193
|
const isJsonResponse = (headers: Readonly<Record<string, string | undefined>>): boolean => {
|
|
184
194
|
const contentType = headers["content-type"];
|
|
195
|
+
|
|
185
196
|
if (contentType === undefined) return false;
|
|
186
197
|
const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase();
|
|
198
|
+
|
|
187
199
|
return mediaType === "application/json" || mediaType?.endsWith("+json") === true;
|
|
188
200
|
};
|
|
189
201
|
|
|
@@ -192,6 +204,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCapture.readResponse")(functio
|
|
|
192
204
|
request: PageCaptureRequest,
|
|
193
205
|
): Effect.fn.Return<string, PageCaptureError> {
|
|
194
206
|
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
|
|
207
|
+
|
|
195
208
|
const chunks = yield* Stream.runFoldEffect<
|
|
196
209
|
Uint8Array,
|
|
197
210
|
HttpClientError.HttpClientError,
|
|
@@ -204,6 +217,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCapture.readResponse")(functio
|
|
|
204
217
|
() => ({ observed: 0, text: "" }),
|
|
205
218
|
(state, chunk) => {
|
|
206
219
|
const observed = state.observed + chunk.byteLength;
|
|
220
|
+
|
|
207
221
|
if (observed > request.limits.maxOutputBytes) {
|
|
208
222
|
return Effect.fail(
|
|
209
223
|
PageCaptureOutputLimitError.make({
|
|
@@ -213,6 +227,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCapture.readResponse")(functio
|
|
|
213
227
|
}),
|
|
214
228
|
);
|
|
215
229
|
}
|
|
230
|
+
|
|
216
231
|
return Effect.try({
|
|
217
232
|
try: () => ({ observed, text: state.text + decoder.decode(chunk, { stream: true }) }),
|
|
218
233
|
catch: (cause) => protocolError("Decoding the Browser Run response failed", cause),
|
|
@@ -225,6 +240,7 @@ const readBoundedResponse = Effect.fn("BrowserRestCapture.readResponse")(functio
|
|
|
225
240
|
: protocolError("Reading the Browser Run response failed", error),
|
|
226
241
|
),
|
|
227
242
|
);
|
|
243
|
+
|
|
228
244
|
return yield* Effect.try({
|
|
229
245
|
try: () => chunks.text + decoder.decode(),
|
|
230
246
|
catch: (cause) => protocolError("Decoding the Browser Run response failed", cause),
|
|
@@ -237,6 +253,7 @@ const parseOutput = (
|
|
|
237
253
|
apiToken: Redacted.Redacted<string>,
|
|
238
254
|
): PageCaptureOutput | PageCaptureNavigationError | PageCaptureProtocolError => {
|
|
239
255
|
const envelope = decodeEnvelope(bodyText);
|
|
256
|
+
|
|
240
257
|
if (Option.isNone(envelope)) {
|
|
241
258
|
return protocolError(
|
|
242
259
|
"The Browser Run response did not carry a valid response envelope",
|
|
@@ -255,6 +272,7 @@ const parseOutput = (
|
|
|
255
272
|
if (typeof envelope.value.result !== "string") {
|
|
256
273
|
return protocolError("The Browser Run response envelope carried a non-text result");
|
|
257
274
|
}
|
|
275
|
+
|
|
258
276
|
return action._tag === "CapturePageContent"
|
|
259
277
|
? PageContentCaptured.make({ html: envelope.value.result })
|
|
260
278
|
: PageMarkdownCaptured.make({ markdown: envelope.value.result });
|
|
@@ -263,6 +281,7 @@ const parseOutput = (
|
|
|
263
281
|
_tag: "PageLinksCaptured",
|
|
264
282
|
links: envelope.value.result,
|
|
265
283
|
});
|
|
284
|
+
|
|
266
285
|
return Option.isSome(decoded)
|
|
267
286
|
? decoded.value
|
|
268
287
|
: protocolError("Browser Run links did not return a bounded array of valid URLs");
|
|
@@ -272,6 +291,7 @@ const parseOutput = (
|
|
|
272
291
|
_tag: "PageScrapeCaptured",
|
|
273
292
|
groups: envelope.value.result,
|
|
274
293
|
});
|
|
294
|
+
|
|
275
295
|
return Option.isSome(decoded)
|
|
276
296
|
? decoded.value
|
|
277
297
|
: protocolError("Browser Run scrape did not return bounded grouped element records");
|
|
@@ -292,6 +312,7 @@ const makeCapture = (
|
|
|
292
312
|
request: PageCaptureRequest,
|
|
293
313
|
): Effect.fn.Return<PageCaptureResult, PageCaptureError> {
|
|
294
314
|
const usesWorkersAi = request.action._tag === "CapturePageStructured";
|
|
315
|
+
|
|
295
316
|
if (usesWorkersAi) {
|
|
296
317
|
if (workersAi === undefined) {
|
|
297
318
|
return yield* PageCaptureUnsupportedError.make({
|
|
@@ -318,6 +339,7 @@ const makeCapture = (
|
|
|
318
339
|
}
|
|
319
340
|
const action = actionName(request.action);
|
|
320
341
|
const path = `${API_ORIGIN}/client/v4/accounts/${encodeURIComponent(options.accountId)}/browser-rendering/${action}`;
|
|
342
|
+
|
|
321
343
|
const requestWithBody = yield* HttpClientRequest.post(path).pipe(
|
|
322
344
|
request.engine === "kitesurf"
|
|
323
345
|
? HttpClientRequest.setUrlParam("browser", "kitesurf")
|
|
@@ -327,14 +349,18 @@ const makeCapture = (
|
|
|
327
349
|
HttpClientRequest.bodyJson(actionBody(request)),
|
|
328
350
|
Effect.mapError((cause) => protocolError("Encoding the Browser Run request failed", cause)),
|
|
329
351
|
);
|
|
352
|
+
|
|
330
353
|
const response = yield* client
|
|
331
354
|
.execute(requestWithBody)
|
|
332
355
|
.pipe(
|
|
333
356
|
Effect.mapError((cause) => protocolError("Calling the Browser Run REST API failed", cause)),
|
|
334
357
|
);
|
|
358
|
+
|
|
335
359
|
const bodyText = yield* readBoundedResponse(response, request);
|
|
360
|
+
|
|
336
361
|
if (response.status === 429) {
|
|
337
362
|
const reason = isQuotaMessage(bodyText) ? "quota" : "rate";
|
|
363
|
+
|
|
338
364
|
return yield* PageCaptureRateLimitedError.make({
|
|
339
365
|
implementation: browserRestCaptureImplementation,
|
|
340
366
|
reason,
|
|
@@ -361,6 +387,7 @@ const makeCapture = (
|
|
|
361
387
|
`Browser Run answered HTTP ${String(response.status)}`,
|
|
362
388
|
privateResponseCause(bodyText, options.apiToken),
|
|
363
389
|
);
|
|
390
|
+
|
|
364
391
|
return yield* error;
|
|
365
392
|
}
|
|
366
393
|
if (!isJsonResponse(response.headers)) {
|
|
@@ -370,12 +397,14 @@ const makeCapture = (
|
|
|
370
397
|
);
|
|
371
398
|
}
|
|
372
399
|
const output = parseOutput(request.action, bodyText, options.apiToken);
|
|
400
|
+
|
|
373
401
|
if (
|
|
374
402
|
output._tag === "PageCaptureNavigationError" ||
|
|
375
403
|
output._tag === "PageCaptureProtocolError"
|
|
376
404
|
) {
|
|
377
405
|
return yield* output;
|
|
378
406
|
}
|
|
407
|
+
|
|
379
408
|
return PageCaptureResult.make({
|
|
380
409
|
implementation: browserRestCaptureImplementation,
|
|
381
410
|
output,
|
|
@@ -415,6 +444,7 @@ export const browserRestWorkersAiCaptureLayer = (
|
|
|
415
444
|
Effect.gen(function* () {
|
|
416
445
|
const client = yield* HttpClient.HttpClient;
|
|
417
446
|
const workersAi = yield* BrowserQuickActionWorkersAi;
|
|
447
|
+
|
|
418
448
|
return PageCapture.of({ capture: makeCapture(client, options, workersAi) });
|
|
419
449
|
}),
|
|
420
450
|
);
|