@effect-agent/platform-cloudflare 0.1.0-beta.107 → 0.1.0-beta.108
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/CloudflareThreadClient.d.mts +33 -33
- package/dist/{InteractiveBrowser-B3S8if51.d.mts → InteractiveBrowser-CKp6pqmw.d.mts} +2 -1
- package/dist/{InteractiveBrowser-xVNX7ARl.mjs → InteractiveBrowser-l6J4eP6T.mjs} +192 -37
- package/dist/InteractiveBrowser-l6J4eP6T.mjs.map +1 -0
- package/dist/InteractiveBrowser.d.mts +1 -1
- package/dist/InteractiveBrowser.mjs +1 -1
- package/dist/ProtectedBrowser.d.mts +3 -3
- package/dist/ProtectedBrowser.mjs +160 -135
- package/dist/ProtectedBrowser.mjs.map +1 -1
- package/dist/{ThreadObject-CnqCuWlA.d.mts → ThreadObject-D_vvj7Qx.d.mts} +33 -33
- package/dist/ThreadObject.d.mts +1 -1
- package/dist/index.d.mts +1 -1
- package/package.json +1 -1
- package/src/InteractiveBrowser.ts +189 -46
- package/src/internal/browser-binding.ts +82 -0
- package/src/internal/browser-failure.ts +92 -0
- package/src/internal/browser-session-lifecycle.ts +38 -5
- package/src/protected-browser/binding.ts +170 -129
- package/src/protected-browser/host.ts +52 -19
- package/src/protected-browser/native.ts +45 -17
- package/src/protected-browser/policy.ts +75 -39
- package/dist/InteractiveBrowser-xVNX7ARl.mjs.map +0 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
+
import puppeteer, { type Browser } from "@cloudflare/puppeteer";
|
|
3
|
+
import { Context, Layer, Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
import { browserFailure, BrowserRunFailure } from "./browser-failure.ts";
|
|
6
|
+
|
|
7
|
+
const Acquired = Schema.Struct({ sessionId: Schema.String.check(Schema.isUUID()) });
|
|
8
|
+
|
|
9
|
+
/** Native transport port shared by both scoped adapters; the Layer owns the foreign binding. */
|
|
10
|
+
export class BrowserRunBinding extends Context.Service<
|
|
11
|
+
BrowserRunBinding,
|
|
12
|
+
{
|
|
13
|
+
readonly acquire: (
|
|
14
|
+
keepAliveMillis: number,
|
|
15
|
+
operation: "protected.acquire" | "interactive.acquire",
|
|
16
|
+
) => Promise<string>;
|
|
17
|
+
readonly connect: (sessionId: string, operation: string) => Promise<Browser>;
|
|
18
|
+
}
|
|
19
|
+
>()("@effect-agent/platform-cloudflare/internal/BrowserRunBinding") {
|
|
20
|
+
static layer(browser: Pick<BrowserRun, "fetch">) {
|
|
21
|
+
return Layer.succeed(this)({
|
|
22
|
+
// One allocation path, with recording explicitly disabled for private sessions.
|
|
23
|
+
acquire: async (keepAliveMillis, operation) => {
|
|
24
|
+
const response = await browser.fetch(
|
|
25
|
+
`https://browser-rendering.cloudflare.com/v1/devtools/browser?keep_alive=${keepAliveMillis}&recording=false`,
|
|
26
|
+
{ method: "POST" },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (response.status !== 200) {
|
|
30
|
+
const failure = new BrowserRunFailure({
|
|
31
|
+
operation,
|
|
32
|
+
reason: "provider",
|
|
33
|
+
status: response.status,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await response.body?.cancel();
|
|
38
|
+
} catch {
|
|
39
|
+
// Body cancellation cannot replace the acquisition refusal.
|
|
40
|
+
}
|
|
41
|
+
throw failure;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Schema.decodeUnknownSync(Acquired)(await response.json()).sessionId;
|
|
45
|
+
},
|
|
46
|
+
// The SDK embeds bodies and session IDs in errors; retain status before that loss.
|
|
47
|
+
connect: async (sessionId, operation) => {
|
|
48
|
+
let failure: BrowserRunFailure | undefined;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
return await puppeteer.connect(
|
|
52
|
+
{
|
|
53
|
+
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
54
|
+
try {
|
|
55
|
+
const response = await browser.fetch(input, init);
|
|
56
|
+
|
|
57
|
+
if (response.status !== 101) {
|
|
58
|
+
failure = new BrowserRunFailure({
|
|
59
|
+
operation,
|
|
60
|
+
reason: "provider",
|
|
61
|
+
status: response.status,
|
|
62
|
+
});
|
|
63
|
+
await response.body?.cancel();
|
|
64
|
+
throw failure;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return response;
|
|
68
|
+
} catch (cause) {
|
|
69
|
+
failure ??= browserFailure(operation, cause);
|
|
70
|
+
throw failure;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
sessionId,
|
|
75
|
+
);
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
throw failure ?? browserFailure(operation, cause);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Effect } from "effect";
|
|
2
|
+
import { Cause, Data, ErrorReporter, Predicate, Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
/** Source-authored stages and classifications only; never retain SDK text or session capabilities. */
|
|
5
|
+
export class BrowserRunFailure extends Data.TaggedError("BrowserRunFailure")<{
|
|
6
|
+
readonly operation: string;
|
|
7
|
+
readonly reason:
|
|
8
|
+
| "provider"
|
|
9
|
+
| "timeout"
|
|
10
|
+
| "malformed"
|
|
11
|
+
| "configuration"
|
|
12
|
+
| "authorization"
|
|
13
|
+
| "rate-limited"
|
|
14
|
+
| "pending";
|
|
15
|
+
readonly status?: number;
|
|
16
|
+
readonly cause?: BrowserRunFailure | { readonly name: string };
|
|
17
|
+
}> {
|
|
18
|
+
readonly [ErrorReporter.severity] = "Error" as const;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A public projection of an already reported private failure must not produce another issue. */
|
|
22
|
+
export const reportedBrowserError = <A extends object>(error: A): A =>
|
|
23
|
+
Object.assign(error, { [ErrorReporter.ignore]: true });
|
|
24
|
+
|
|
25
|
+
export const inheritBrowserReport = <A extends object>(original: unknown, error: A): A =>
|
|
26
|
+
ErrorReporter.isIgnored(original) ? reportedBrowserError(error) : error;
|
|
27
|
+
|
|
28
|
+
const reasonSchema = Schema.Literals([
|
|
29
|
+
"provider",
|
|
30
|
+
"timeout",
|
|
31
|
+
"malformed",
|
|
32
|
+
"configuration",
|
|
33
|
+
"authorization",
|
|
34
|
+
"rate-limited",
|
|
35
|
+
"pending",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const statusSchema = Schema.Int.check(Schema.isBetween({ minimum: 100, maximum: 599 }));
|
|
39
|
+
|
|
40
|
+
const failureNames = new Set([
|
|
41
|
+
"TypeError",
|
|
42
|
+
"RangeError",
|
|
43
|
+
"SyntaxError",
|
|
44
|
+
"TimeoutError",
|
|
45
|
+
"SchemaError",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export const browserFailure = (operation: string, cause?: unknown): BrowserRunFailure =>
|
|
49
|
+
cause instanceof BrowserRunFailure
|
|
50
|
+
? cause
|
|
51
|
+
: new BrowserRunFailure({
|
|
52
|
+
operation,
|
|
53
|
+
reason:
|
|
54
|
+
Predicate.isObject(cause) && "reason" in cause && Schema.is(reasonSchema)(cause.reason)
|
|
55
|
+
? cause.reason
|
|
56
|
+
: "provider",
|
|
57
|
+
...(Predicate.isObject(cause) && "status" in cause && Schema.is(statusSchema)(cause.status)
|
|
58
|
+
? { status: cause.status }
|
|
59
|
+
: {}),
|
|
60
|
+
...(Predicate.isObject(cause) &&
|
|
61
|
+
"cause" in cause &&
|
|
62
|
+
cause.cause instanceof BrowserRunFailure
|
|
63
|
+
? { cause: cause.cause }
|
|
64
|
+
: Predicate.isObject(cause) &&
|
|
65
|
+
"name" in cause &&
|
|
66
|
+
typeof cause.name === "string" &&
|
|
67
|
+
failureNames.has(cause.name)
|
|
68
|
+
? { cause: { name: cause.name } }
|
|
69
|
+
: {}),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** Keep mixed defects and interruptions distinct while discarding all foreign diagnostic content. */
|
|
73
|
+
export const reportBrowserCause = <E>(
|
|
74
|
+
operation: string,
|
|
75
|
+
cause: Cause.Cause<E>,
|
|
76
|
+
): Effect.Effect<void> =>
|
|
77
|
+
ErrorReporter.report(
|
|
78
|
+
Cause.fromReasons(
|
|
79
|
+
cause.reasons
|
|
80
|
+
.filter((reason) => !Cause.isFailReason(reason) || !ErrorReporter.isIgnored(reason.error))
|
|
81
|
+
.map((reason) => {
|
|
82
|
+
switch (reason._tag) {
|
|
83
|
+
case "Fail":
|
|
84
|
+
return Cause.makeFailReason(browserFailure(operation, reason.error));
|
|
85
|
+
case "Die":
|
|
86
|
+
return Cause.makeDieReason(browserFailure(operation, reason.defect));
|
|
87
|
+
case "Interrupt":
|
|
88
|
+
return reason;
|
|
89
|
+
}
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Option, Redacted, Schema, Stream } from "effect";
|
|
2
2
|
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
3
3
|
|
|
4
|
+
import { browserFailure } from "./browser-failure.ts";
|
|
5
|
+
|
|
4
6
|
export class BrowserRunCleanupError extends Schema.TaggedError<BrowserRunCleanupError>()(
|
|
5
7
|
"BrowserRunCleanupError",
|
|
6
8
|
{
|
|
@@ -14,6 +16,7 @@ export class BrowserRunCleanupError extends Schema.TaggedError<BrowserRunCleanup
|
|
|
14
16
|
"pending",
|
|
15
17
|
]),
|
|
16
18
|
status: Schema.optionalKey(Schema.Int),
|
|
19
|
+
cause: Schema.optionalKey(Schema.Defect()),
|
|
17
20
|
},
|
|
18
21
|
) {}
|
|
19
22
|
|
|
@@ -65,7 +68,13 @@ export class BrowserRunSessionLifecycle extends Context.Service<
|
|
|
65
68
|
.pipe(
|
|
66
69
|
// workerd supports manual/follow, not error. Reject every redirect below.
|
|
67
70
|
Effect.provideService(FetchHttpClient.RequestInit, { redirect: "manual" }),
|
|
68
|
-
Effect.mapError(
|
|
71
|
+
Effect.mapError(
|
|
72
|
+
(cause) =>
|
|
73
|
+
new BrowserRunCleanupError({
|
|
74
|
+
reason: "provider",
|
|
75
|
+
cause: browserFailure("browser.close.request", cause),
|
|
76
|
+
}),
|
|
77
|
+
),
|
|
69
78
|
);
|
|
70
79
|
|
|
71
80
|
if (response.status === 401 || response.status === 403)
|
|
@@ -99,11 +108,23 @@ export class BrowserRunSessionLifecycle extends Context.Service<
|
|
|
99
108
|
|
|
100
109
|
return Effect.succeed(combined);
|
|
101
110
|
},
|
|
102
|
-
).pipe(
|
|
111
|
+
).pipe(
|
|
112
|
+
Effect.mapError(
|
|
113
|
+
(cause) =>
|
|
114
|
+
new BrowserRunCleanupError({
|
|
115
|
+
reason: "malformed",
|
|
116
|
+
cause: browserFailure("browser.close.response", cause),
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
103
120
|
|
|
104
121
|
const body = yield* Effect.try({
|
|
105
122
|
try: () => new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes),
|
|
106
|
-
catch: () =>
|
|
123
|
+
catch: (cause) =>
|
|
124
|
+
new BrowserRunCleanupError({
|
|
125
|
+
reason: "malformed",
|
|
126
|
+
cause: browserFailure("browser.close.decode", cause),
|
|
127
|
+
}),
|
|
107
128
|
});
|
|
108
129
|
|
|
109
130
|
if (response.status === 404) {
|
|
@@ -117,14 +138,26 @@ export class BrowserRunSessionLifecycle extends Context.Service<
|
|
|
117
138
|
}
|
|
118
139
|
if (method === "DELETE") {
|
|
119
140
|
const result = yield* Schema.decodeEffect(Schema.fromJsonString(Closed))(body).pipe(
|
|
120
|
-
Effect.mapError(
|
|
141
|
+
Effect.mapError(
|
|
142
|
+
(cause) =>
|
|
143
|
+
new BrowserRunCleanupError({
|
|
144
|
+
reason: "malformed",
|
|
145
|
+
cause: browserFailure("browser.close.response", cause),
|
|
146
|
+
}),
|
|
147
|
+
),
|
|
121
148
|
);
|
|
122
149
|
|
|
123
150
|
return result.status === "closed";
|
|
124
151
|
}
|
|
125
152
|
|
|
126
153
|
const result = yield* Schema.decodeEffect(Schema.fromJsonString(Metadata))(body).pipe(
|
|
127
|
-
Effect.mapError(
|
|
154
|
+
Effect.mapError(
|
|
155
|
+
(cause) =>
|
|
156
|
+
new BrowserRunCleanupError({
|
|
157
|
+
reason: "malformed",
|
|
158
|
+
cause: browserFailure("browser.close.response", cause),
|
|
159
|
+
}),
|
|
160
|
+
),
|
|
128
161
|
);
|
|
129
162
|
|
|
130
163
|
if (result.sessionId !== sessionId)
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/// <reference types="@cloudflare/workers-types" />
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import { type Browser, type Page, type CDPSession } from "@cloudflare/puppeteer";
|
|
3
|
+
import { Cause, Context, Crypto, Effect, Layer, Redacted, Schema, type Scope } from "effect";
|
|
4
4
|
import { type InteractiveBrowserPolicy } from "effect-agent/interactive-browser";
|
|
5
5
|
import { ProtectedBrowserError } from "effect-agent/protected-browser";
|
|
6
6
|
|
|
7
|
+
import { BrowserRunBinding } from "../internal/browser-binding.ts";
|
|
8
|
+
import {
|
|
9
|
+
browserFailure,
|
|
10
|
+
BrowserRunFailure,
|
|
11
|
+
reportBrowserCause,
|
|
12
|
+
reportedBrowserError,
|
|
13
|
+
} from "../internal/browser-failure.ts";
|
|
7
14
|
import { BrowserRunSessionLifecycle } from "../internal/browser-session-lifecycle.ts";
|
|
8
15
|
import { makeProtectedNativeTransport, ProtectedNativeSession } from "./native.ts";
|
|
9
16
|
import {
|
|
@@ -50,6 +57,7 @@ const protectedBindingLayer = (options: { readonly browser: Pick<BrowserRun, "fe
|
|
|
50
57
|
Layer.effect(BrowserRunProtectedBinding)(
|
|
51
58
|
Effect.gen(function* () {
|
|
52
59
|
const lifecycle = yield* BrowserRunSessionLifecycle;
|
|
60
|
+
const binding = yield* BrowserRunBinding;
|
|
53
61
|
const crypto = yield* Crypto.Crypto;
|
|
54
62
|
|
|
55
63
|
const open = Effect.fn("BrowserRunProtectedTransport.open")(function* (
|
|
@@ -73,8 +81,8 @@ const protectedBindingLayer = (options: { readonly browser: Pick<BrowserRun, "fe
|
|
|
73
81
|
let driver: ProtectedBrowserTransport | undefined;
|
|
74
82
|
let invalid = false;
|
|
75
83
|
// SDK acquisition may finish after interruption. Its late-reply callback must await cleanup,
|
|
76
|
-
//
|
|
77
|
-
const runCleanup = Effect.runPromiseWith(
|
|
84
|
+
// retaining this pass's clock and private reporting scope.
|
|
85
|
+
const runCleanup = Effect.runPromiseWith(yield* Effect.context<never>());
|
|
78
86
|
|
|
79
87
|
const terminate = Effect.gen(function* () {
|
|
80
88
|
invalid = true;
|
|
@@ -83,11 +91,19 @@ const protectedBindingLayer = (options: { readonly browser: Pick<BrowserRun, "fe
|
|
|
83
91
|
|
|
84
92
|
const cleanup = yield* lifecycle.close(sessionId).pipe(
|
|
85
93
|
Effect.as("confirmed" as const),
|
|
86
|
-
Effect.catchCause(() =>
|
|
94
|
+
Effect.catchCause((cause) =>
|
|
95
|
+
reportBrowserCause("protected.close", cause).pipe(Effect.as("unconfirmed" as const)),
|
|
96
|
+
),
|
|
87
97
|
Effect.interruptible,
|
|
88
98
|
Effect.timeoutOrElse({
|
|
89
99
|
duration: "10 seconds",
|
|
90
|
-
orElse: () =>
|
|
100
|
+
orElse: () =>
|
|
101
|
+
reportBrowserCause(
|
|
102
|
+
"protected.close",
|
|
103
|
+
Cause.fail(
|
|
104
|
+
new BrowserRunFailure({ operation: "protected.close", reason: "timeout" }),
|
|
105
|
+
),
|
|
106
|
+
).pipe(Effect.as("unconfirmed" as const)),
|
|
91
107
|
}),
|
|
92
108
|
);
|
|
93
109
|
|
|
@@ -96,9 +112,21 @@ const protectedBindingLayer = (options: { readonly browser: Pick<BrowserRun, "fe
|
|
|
96
112
|
|
|
97
113
|
if (connected !== undefined)
|
|
98
114
|
yield* Effect.promise(() => connected.disconnect()).pipe(
|
|
99
|
-
Effect.catchCause(() =>
|
|
115
|
+
Effect.catchCause((cause) => reportBrowserCause("protected.disconnect", cause)),
|
|
100
116
|
Effect.interruptible,
|
|
101
|
-
Effect.timeoutOrElse({
|
|
117
|
+
Effect.timeoutOrElse({
|
|
118
|
+
duration: "1 second",
|
|
119
|
+
orElse: () =>
|
|
120
|
+
reportBrowserCause(
|
|
121
|
+
"protected.disconnect",
|
|
122
|
+
Cause.fail(
|
|
123
|
+
new BrowserRunFailure({
|
|
124
|
+
operation: "protected.disconnect",
|
|
125
|
+
reason: "timeout",
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
}),
|
|
102
130
|
);
|
|
103
131
|
|
|
104
132
|
return cleanup;
|
|
@@ -106,144 +134,138 @@ const protectedBindingLayer = (options: { readonly browser: Pick<BrowserRun, "fe
|
|
|
106
134
|
|
|
107
135
|
const close = yield* Effect.cached(terminate);
|
|
108
136
|
|
|
109
|
-
yield* Effect.addFinalizer(() =>
|
|
110
|
-
|
|
111
|
-
? Effect.void
|
|
112
|
-
: close.pipe(
|
|
113
|
-
Effect.flatMap((state) =>
|
|
114
|
-
state === "confirmed"
|
|
115
|
-
? Effect.void
|
|
116
|
-
: Effect.logWarning("Protected browser exact-session closure unconfirmed"),
|
|
117
|
-
),
|
|
118
|
-
),
|
|
119
|
-
);
|
|
137
|
+
yield* Effect.addFinalizer(() => (detached ? Effect.void : close));
|
|
138
|
+
let stage = identity === undefined ? "protected.acquire" : "protected.connect";
|
|
120
139
|
|
|
121
140
|
const acquired = yield* Effect.tryPromise({
|
|
122
141
|
try: async (signal) => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
try {
|
|
143
|
+
sessionId =
|
|
144
|
+
identity === undefined
|
|
145
|
+
? Redacted.make(
|
|
146
|
+
await binding.acquire(
|
|
147
|
+
Math.min(600_000, Math.max(10_000, policy.maxElapsedMillis)),
|
|
148
|
+
"protected.acquire",
|
|
149
|
+
),
|
|
150
|
+
)
|
|
151
|
+
: identity.sessionId;
|
|
152
|
+
if (signal.aborted || invalid) {
|
|
153
|
+
await runCleanup(terminate);
|
|
154
|
+
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
155
|
+
}
|
|
156
|
+
// Resume only a host-persisted exact page. Never open a replacement page or context.
|
|
157
|
+
stage = "protected.connect";
|
|
158
|
+
browser = await binding.connect(Redacted.value(sessionId), stage);
|
|
159
|
+
if (signal.aborted || invalid) {
|
|
160
|
+
await runCleanup(terminate);
|
|
161
|
+
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
162
|
+
}
|
|
163
|
+
let page: Page;
|
|
133
164
|
|
|
134
|
-
|
|
135
|
-
|
|
165
|
+
if (identity === undefined) {
|
|
166
|
+
stage = "protected.context";
|
|
167
|
+
const context = await browser.createBrowserContext();
|
|
136
168
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const acquired =
|
|
142
|
-
identity === undefined
|
|
143
|
-
? await puppeteer.acquire(binding, {
|
|
144
|
-
recording: false,
|
|
145
|
-
// Provider inactivity timeout, independent of the finite total pass deadline.
|
|
146
|
-
keep_alive: Math.min(600_000, Math.max(10_000, policy.maxElapsedMillis)),
|
|
147
|
-
})
|
|
148
|
-
: { sessionId: Redacted.value(identity.sessionId) };
|
|
149
|
-
|
|
150
|
-
sessionId = Redacted.make(
|
|
151
|
-
Schema.decodeSync(Schema.String.check(Schema.isUUID()))(acquired.sessionId),
|
|
152
|
-
);
|
|
153
|
-
if ((identity === undefined && !recordingDisabled) || signal.aborted || invalid) {
|
|
154
|
-
await runCleanup(terminate);
|
|
155
|
-
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
156
|
-
}
|
|
157
|
-
// Resume only a host-persisted exact page. Never open a replacement page or context.
|
|
158
|
-
browser = await puppeteer.connect(options.browser, acquired.sessionId);
|
|
159
|
-
if (signal.aborted || invalid) {
|
|
160
|
-
await runCleanup(terminate);
|
|
161
|
-
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
162
|
-
}
|
|
163
|
-
let page: Page;
|
|
169
|
+
stage = "protected.page";
|
|
170
|
+
page = await context.newPage();
|
|
171
|
+
} else {
|
|
172
|
+
stage = "protected.resume";
|
|
164
173
|
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
const context = browser
|
|
175
|
+
.browserContexts()
|
|
176
|
+
.find((candidate) => candidate.id === Redacted.value(identity.contextId));
|
|
167
177
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
.find((candidate) => candidate.id === Redacted.value(identity.contextId));
|
|
178
|
+
if (context === undefined)
|
|
179
|
+
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
180
|
+
const pages = await context.pages();
|
|
181
|
+
let found: Page | undefined;
|
|
173
182
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
let found: Page | undefined;
|
|
183
|
+
for (const candidate of pages) {
|
|
184
|
+
const client = await candidate.createCDPSession();
|
|
185
|
+
const info = await client.send("Target.getTargetInfo");
|
|
178
186
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
187
|
+
await client.detach();
|
|
188
|
+
if (info.targetInfo.targetId === Redacted.value(identity.targetId))
|
|
189
|
+
found = candidate;
|
|
190
|
+
}
|
|
191
|
+
if (found === undefined || pages.length !== 1)
|
|
192
|
+
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
193
|
+
page = found;
|
|
186
194
|
}
|
|
187
|
-
|
|
195
|
+
stage = "protected.control";
|
|
196
|
+
control = await page.createCDPSession();
|
|
197
|
+
const info = await control.send("Target.getTargetInfo");
|
|
198
|
+
|
|
199
|
+
stage = "protected.identity";
|
|
200
|
+
providerIdentity = Schema.decodeSync(ProtectedProviderIdentity)({
|
|
201
|
+
sessionId,
|
|
202
|
+
contextId: Redacted.make(page.browserContext().id ?? ""),
|
|
203
|
+
targetId: Redacted.make(info.targetInfo.targetId),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
stage = "protected.interception";
|
|
207
|
+
await page.setBypassServiceWorker(true);
|
|
208
|
+
await page.setRequestInterception(true);
|
|
209
|
+
page.on("request", (request) => {
|
|
210
|
+
let allowed = policy.network._tag === "Unrestricted";
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const url = new URL(request.url());
|
|
214
|
+
|
|
215
|
+
allowed ||=
|
|
216
|
+
policy.network._tag === "ExactHosts" &&
|
|
217
|
+
url.protocol === "https:" &&
|
|
218
|
+
!url.username &&
|
|
219
|
+
!url.password &&
|
|
220
|
+
policy.network.allowedHosts.includes(url.host);
|
|
221
|
+
} catch {
|
|
222
|
+
/* Refuse malformed destinations. */
|
|
223
|
+
}
|
|
224
|
+
void (
|
|
225
|
+
allowed && !invalid ? request.continue() : request.abort("blockedbyclient")
|
|
226
|
+
).catch(async (cause) => {
|
|
227
|
+
invalid = true;
|
|
228
|
+
driver?.invalidate();
|
|
229
|
+
await runCleanup(reportBrowserCause("protected.interception", Cause.fail(cause)));
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
if (signal.aborted || invalid) {
|
|
233
|
+
await runCleanup(terminate);
|
|
188
234
|
throw new ProtectedTransportError({ reason: "stale-reference" });
|
|
189
|
-
page = found;
|
|
190
|
-
}
|
|
191
|
-
control = await page.createCDPSession();
|
|
192
|
-
const info = await control.send("Target.getTargetInfo");
|
|
193
|
-
|
|
194
|
-
providerIdentity = Schema.decodeSync(ProtectedProviderIdentity)({
|
|
195
|
-
sessionId,
|
|
196
|
-
contextId: Redacted.make(page.browserContext().id ?? ""),
|
|
197
|
-
targetId: Redacted.make(info.targetInfo.targetId),
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
await page.setBypassServiceWorker(true);
|
|
201
|
-
await page.setRequestInterception(true);
|
|
202
|
-
page.on("request", (request) => {
|
|
203
|
-
let allowed = policy.network._tag === "Unrestricted";
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
const url = new URL(request.url());
|
|
207
|
-
|
|
208
|
-
allowed ||=
|
|
209
|
-
policy.network._tag === "ExactHosts" &&
|
|
210
|
-
url.protocol === "https:" &&
|
|
211
|
-
!url.username &&
|
|
212
|
-
!url.password &&
|
|
213
|
-
policy.network.allowedHosts.includes(url.host);
|
|
214
|
-
} catch {
|
|
215
|
-
/* Refuse malformed destinations. */
|
|
216
235
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
236
|
+
|
|
237
|
+
return ProtectedNativeSession.of({
|
|
238
|
+
browser,
|
|
239
|
+
page,
|
|
240
|
+
close,
|
|
241
|
+
release: Effect.suspend(() => (detached ? Effect.void : close.pipe(Effect.asVoid))),
|
|
222
242
|
});
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
throw
|
|
243
|
+
} catch (cause) {
|
|
244
|
+
if (signal.aborted && !(cause instanceof ProtectedTransportError))
|
|
245
|
+
await runCleanup(reportBrowserCause(stage, Cause.fail(cause)));
|
|
246
|
+
throw cause;
|
|
227
247
|
}
|
|
228
|
-
|
|
229
|
-
return ProtectedNativeSession.of({
|
|
230
|
-
browser,
|
|
231
|
-
page,
|
|
232
|
-
close,
|
|
233
|
-
release: Effect.suspend(() => (detached ? Effect.void : close.pipe(Effect.asVoid))),
|
|
234
|
-
});
|
|
235
248
|
},
|
|
236
|
-
catch:
|
|
249
|
+
catch: (cause) => browserFailure(stage, cause),
|
|
237
250
|
}).pipe(
|
|
238
251
|
Effect.timeoutOrElse({
|
|
239
252
|
duration: Math.min(policy.maxElapsedMillis, 30_000),
|
|
240
253
|
orElse: () =>
|
|
241
|
-
Effect.fail(new
|
|
254
|
+
Effect.fail(new BrowserRunFailure({ operation: stage, reason: "timeout" })),
|
|
242
255
|
}),
|
|
243
256
|
Effect.catch((error) =>
|
|
244
|
-
|
|
257
|
+
reportBrowserCause(stage, Cause.fail(error)).pipe(
|
|
258
|
+
Effect.andThen(close),
|
|
245
259
|
Effect.flatMap((cleanup) =>
|
|
246
|
-
Effect.fail(
|
|
260
|
+
Effect.fail(
|
|
261
|
+
reportedBrowserError(
|
|
262
|
+
new ProtectedBrowserError({
|
|
263
|
+
...failure(),
|
|
264
|
+
reason: error.reason === "timeout" ? "timeout" : "provider",
|
|
265
|
+
cleanup,
|
|
266
|
+
}),
|
|
267
|
+
),
|
|
268
|
+
),
|
|
247
269
|
),
|
|
248
270
|
),
|
|
249
271
|
),
|
|
@@ -271,12 +293,20 @@ const protectedBindingLayer = (options: { readonly browser: Pick<BrowserRun, "fe
|
|
|
271
293
|
|
|
272
294
|
return await Reflect.apply(send, control, [method, parameters]);
|
|
273
295
|
},
|
|
274
|
-
catch:
|
|
296
|
+
catch: (cause) => browserFailure("protected.command", cause),
|
|
275
297
|
}).pipe(
|
|
276
298
|
Effect.timeoutOrElse({
|
|
277
299
|
duration: "10 seconds",
|
|
278
|
-
orElse: () =>
|
|
300
|
+
orElse: () =>
|
|
301
|
+
Effect.fail(
|
|
302
|
+
new BrowserRunFailure({ operation: "protected.command", reason: "timeout" }),
|
|
303
|
+
),
|
|
279
304
|
}),
|
|
305
|
+
Effect.catch((error) =>
|
|
306
|
+
reportBrowserCause("protected.command", Cause.fail(error)).pipe(
|
|
307
|
+
Effect.andThen(Effect.fail(reportedBrowserError(failure()))),
|
|
308
|
+
),
|
|
309
|
+
),
|
|
280
310
|
),
|
|
281
311
|
detach: Effect.tryPromise({
|
|
282
312
|
try: async () => {
|
|
@@ -286,16 +316,27 @@ const protectedBindingLayer = (options: { readonly browser: Pick<BrowserRun, "fe
|
|
|
286
316
|
detached = true;
|
|
287
317
|
driver?.invalidate();
|
|
288
318
|
},
|
|
289
|
-
catch:
|
|
319
|
+
catch: (cause) => browserFailure("protected.detach", cause),
|
|
290
320
|
}).pipe(
|
|
291
|
-
Effect.timeoutOrElse({
|
|
321
|
+
Effect.timeoutOrElse({
|
|
322
|
+
duration: "10 seconds",
|
|
323
|
+
orElse: () =>
|
|
324
|
+
Effect.fail(
|
|
325
|
+
new BrowserRunFailure({ operation: "protected.detach", reason: "timeout" }),
|
|
326
|
+
),
|
|
327
|
+
}),
|
|
328
|
+
Effect.catch((error) =>
|
|
329
|
+
reportBrowserCause("protected.detach", Cause.fail(error)).pipe(
|
|
330
|
+
Effect.andThen(Effect.fail(reportedBrowserError(failure()))),
|
|
331
|
+
),
|
|
332
|
+
),
|
|
292
333
|
),
|
|
293
334
|
};
|
|
294
335
|
}, Effect.withTracerEnabled(false));
|
|
295
336
|
|
|
296
337
|
return { open };
|
|
297
338
|
}),
|
|
298
|
-
);
|
|
339
|
+
).pipe(Layer.provide(BrowserRunBinding.layer(options.browser)));
|
|
299
340
|
|
|
300
341
|
/** One binding implementation serves scoped tools and host-managed protected handoffs. */
|
|
301
342
|
export const browserRunProtectedBindingLayer = (options: {
|