@apifuse/provider-sdk 2.1.0-beta.2 → 2.1.0-beta.21
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/AUTHORING.md +330 -8
- package/CHANGELOG.md +89 -1
- package/README.md +64 -17
- package/SUBMISSION.md +86 -0
- package/bin/apifuse-check.ts +60 -6
- package/bin/apifuse-dev.ts +58 -8
- package/bin/apifuse-pack-check.ts +32 -2
- package/bin/apifuse-pack-smoke.ts +133 -6
- package/bin/apifuse-perf.ts +142 -49
- package/bin/apifuse-record.ts +182 -104
- package/bin/apifuse-submit-check.ts +3243 -0
- package/bin/apifuse.ts +1 -1
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +436 -0
- package/dist/ceremonies/index.d.ts +41 -0
- package/dist/ceremonies/index.js +490 -0
- package/dist/choice-token.d.ts +24 -0
- package/dist/choice-token.js +74 -0
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +80 -0
- package/dist/cli/create.d.ts +47 -0
- package/dist/cli/create.js +777 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +163 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/config/loader.d.ts +107 -0
- package/dist/config/loader.js +935 -0
- package/dist/contract-json.d.ts +9 -0
- package/dist/contract-json.js +51 -0
- package/dist/contract-serialization.d.ts +4 -0
- package/dist/contract-serialization.js +78 -0
- package/dist/contract-types.d.ts +49 -0
- package/dist/contract-types.js +1 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +156 -0
- package/dist/define.d.ts +100 -0
- package/dist/define.js +1383 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.js +15 -0
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +97 -0
- package/dist/i18n/catalog.d.ts +29 -0
- package/dist/i18n/catalog.js +159 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/keys.d.ts +10 -0
- package/dist/i18n/keys.js +34 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +38 -0
- package/dist/lint.d.ts +74 -0
- package/dist/lint.js +729 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +11 -0
- package/dist/provider.js +9 -0
- package/dist/public-schema-field-lint.d.ts +2 -0
- package/dist/public-schema-field-lint.js +158 -0
- package/dist/recipes/gov-api.d.ts +19 -0
- package/dist/recipes/gov-api.js +72 -0
- package/dist/recipes/rest-api.d.ts +21 -0
- package/dist/recipes/rest-api.js +115 -0
- package/dist/runtime/auth-flow.d.ts +14 -0
- package/dist/runtime/auth-flow.js +46 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1237 -0
- package/dist/runtime/cache.d.ts +10 -0
- package/dist/runtime/cache.js +372 -0
- package/dist/runtime/choice.d.ts +15 -0
- package/dist/runtime/choice.js +435 -0
- package/dist/runtime/credential.d.ts +8 -0
- package/dist/runtime/credential.js +61 -0
- package/dist/runtime/env.d.ts +2 -0
- package/dist/runtime/env.js +10 -0
- package/dist/runtime/executor.d.ts +16 -0
- package/dist/runtime/executor.js +51 -0
- package/dist/runtime/http.d.ts +8 -0
- package/dist/runtime/http.js +726 -0
- package/dist/runtime/insights.d.ts +9 -0
- package/dist/runtime/insights.js +324 -0
- package/dist/runtime/instrumentation.d.ts +8 -0
- package/dist/runtime/instrumentation.js +269 -0
- package/dist/runtime/key-derivation.d.ts +24 -0
- package/dist/runtime/key-derivation.js +73 -0
- package/dist/runtime/keyring.d.ts +25 -0
- package/dist/runtime/keyring.js +93 -0
- package/dist/runtime/namespace.d.ts +9 -0
- package/dist/runtime/namespace.js +19 -0
- package/dist/runtime/otlp.d.ts +39 -0
- package/dist/runtime/otlp.js +103 -0
- package/dist/runtime/perf.d.ts +12 -0
- package/dist/runtime/perf.js +52 -0
- package/dist/runtime/prevalidate.d.ts +12 -0
- package/dist/runtime/prevalidate.js +173 -0
- package/dist/runtime/provider.d.ts +2 -0
- package/dist/runtime/provider.js +11 -0
- package/dist/runtime/proxy-errors.d.ts +21 -0
- package/dist/runtime/proxy-errors.js +83 -0
- package/dist/runtime/proxy-telemetry.d.ts +8 -0
- package/dist/runtime/proxy-telemetry.js +174 -0
- package/dist/runtime/redis.d.ts +17 -0
- package/dist/runtime/redis.js +82 -0
- package/dist/runtime/request-options.d.ts +3 -0
- package/dist/runtime/request-options.js +42 -0
- package/dist/runtime/state.d.ts +17 -0
- package/dist/runtime/state.js +344 -0
- package/dist/runtime/stealth.d.ts +21 -0
- package/dist/runtime/stealth.js +980 -0
- package/dist/runtime/stt.d.ts +22 -0
- package/dist/runtime/stt.js +480 -0
- package/dist/runtime/trace.d.ts +26 -0
- package/dist/runtime/trace.js +142 -0
- package/dist/runtime/waterfall.d.ts +12 -0
- package/dist/runtime/waterfall.js +147 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.js +243 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/serve.d.ts +64 -0
- package/dist/server/serve.js +1118 -0
- package/dist/server/types.d.ts +136 -0
- package/dist/server/types.js +86 -0
- package/dist/stealth/profiles.d.ts +4 -0
- package/dist/stealth/profiles.js +259 -0
- package/dist/stream.d.ts +44 -0
- package/dist/stream.js +151 -0
- package/dist/testing/helpers.d.ts +23 -0
- package/dist/testing/helpers.js +95 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/run.d.ts +34 -0
- package/dist/testing/run.js +307 -0
- package/dist/types.d.ts +1467 -0
- package/dist/types.js +61 -0
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/date.js +101 -0
- package/dist/utils/parse.d.ts +16 -0
- package/dist/utils/parse.js +51 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +14 -0
- package/dist/utils/transform.d.ts +8 -0
- package/dist/utils/transform.js +48 -0
- package/package.json +57 -29
- package/src/auth.ts +786 -0
- package/src/ceremonies/index.ts +8 -2
- package/src/choice-token.ts +165 -0
- package/src/cli/commands.ts +34 -11
- package/src/cli/create.ts +254 -128
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/src/cli/templates/provider/README.md.tpl +87 -7
- package/src/cli/templates/provider/dev.ts.tpl +1 -1
- package/src/cli/templates/provider/domain/README.md.tpl +3 -0
- package/src/cli/templates/provider/index.ts.tpl +5 -47
- package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/src/cli/templates/provider/meta.ts.tpl +7 -0
- package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/src/cli/templates/provider/start.ts.tpl +1 -1
- package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/src/config/loader.ts +1224 -9
- package/src/contract-json.ts +75 -0
- package/src/contract-serialization.ts +89 -0
- package/src/contract-types.ts +52 -0
- package/src/contract.ts +216 -0
- package/src/define.ts +1820 -70
- package/src/errors.ts +27 -0
- package/src/i18n/catalog.ts +277 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/keys.ts +64 -0
- package/src/index.ts +189 -9
- package/src/lint.ts +580 -73
- package/src/observability.ts +41 -0
- package/src/provider.ts +131 -4
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +9 -0
- package/src/runtime/browser.ts +1054 -51
- package/src/runtime/cache.ts +528 -0
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +32 -3
- package/src/runtime/http.ts +980 -195
- package/src/runtime/insights.ts +11 -11
- package/src/runtime/instrumentation.ts +12 -4
- package/src/runtime/key-derivation.ts +1 -1
- package/src/runtime/keyring.ts +4 -3
- package/src/runtime/proxy-errors.ts +132 -0
- package/src/runtime/proxy-telemetry.ts +253 -0
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/request-options.ts +66 -0
- package/src/runtime/state.ts +563 -0
- package/src/runtime/stealth.ts +1336 -0
- package/src/runtime/stt.ts +629 -0
- package/src/runtime/trace.ts +1 -1
- package/src/schema.ts +363 -1
- package/src/server/serve.ts +1192 -75
- package/src/server/types.ts +37 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +40 -6
- package/src/types.ts +1283 -59
- package/src/runtime/tls.ts +0 -434
- package/src/types/playwright-stealth.d.ts +0 -9
package/src/runtime/browser.ts
CHANGED
|
@@ -1,27 +1,49 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import type { LaunchOptions, Page } from "playwright";
|
|
2
|
+
import type { Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
|
|
3
3
|
|
|
4
4
|
import { ProviderError } from "../errors";
|
|
5
5
|
import type {
|
|
6
|
+
BrowserChallengeRequest,
|
|
7
|
+
BrowserChallengeResult,
|
|
6
8
|
BrowserClient as BrowserClientContract,
|
|
7
9
|
BrowserEngine,
|
|
10
|
+
BrowserFrame,
|
|
11
|
+
BrowserLocator,
|
|
8
12
|
BrowserOptions,
|
|
13
|
+
BrowserPage,
|
|
14
|
+
BrowserResourceBody,
|
|
15
|
+
BrowserResourceDecision,
|
|
16
|
+
BrowserResourceMethod,
|
|
17
|
+
BrowserResourcePolicy,
|
|
18
|
+
BrowserResourceRequest,
|
|
9
19
|
} from "../types";
|
|
10
20
|
|
|
11
21
|
const require = createRequire(import.meta.url);
|
|
12
22
|
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
13
23
|
const SELECTOR_POLL_INTERVAL_MS = 100;
|
|
24
|
+
const RESOURCE_POLICY_ROUTE_PATTERN = "**/*";
|
|
25
|
+
const DEFAULT_RESOURCE_METHODS = ["GET", "HEAD"] as const;
|
|
14
26
|
|
|
15
27
|
type PlaywrightModule = typeof import("playwright");
|
|
16
|
-
type
|
|
17
|
-
|
|
28
|
+
type PlaywrightExtraModule = {
|
|
29
|
+
chromium: PlaywrightModule["chromium"] & { use(plugin: unknown): unknown };
|
|
18
30
|
};
|
|
19
31
|
|
|
32
|
+
type StealthPluginFactory = (options?: {
|
|
33
|
+
enabledEvasions?: Set<string>;
|
|
34
|
+
}) => unknown;
|
|
35
|
+
|
|
20
36
|
type PoolAcquireResponse = {
|
|
37
|
+
browserContextId?: string;
|
|
21
38
|
pageId: string;
|
|
22
39
|
wsEndpoint: string;
|
|
23
40
|
};
|
|
24
41
|
|
|
42
|
+
type PoolReleaseRequest = {
|
|
43
|
+
browserContextId?: string;
|
|
44
|
+
pageId: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
25
47
|
type JsonRpcId = number;
|
|
26
48
|
|
|
27
49
|
type JsonRpcError = {
|
|
@@ -37,35 +59,182 @@ type JsonRpcMessage = {
|
|
|
37
59
|
result?: Record<string, unknown>;
|
|
38
60
|
};
|
|
39
61
|
|
|
40
|
-
type
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
62
|
+
type CdpFrameTreeNode = {
|
|
63
|
+
childFrames?: CdpFrameTreeNode[];
|
|
64
|
+
frame: {
|
|
65
|
+
id: string;
|
|
66
|
+
name?: string;
|
|
67
|
+
parentId?: string;
|
|
68
|
+
url?: string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type CdpFetchFulfillParams = {
|
|
73
|
+
readonly requestId: string;
|
|
74
|
+
readonly responseCode: number;
|
|
75
|
+
readonly responseHeaders?: readonly {
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly value: string;
|
|
78
|
+
}[];
|
|
79
|
+
readonly body?: string;
|
|
54
80
|
};
|
|
55
81
|
|
|
82
|
+
type BrowserPageContract = BrowserPage;
|
|
83
|
+
|
|
84
|
+
function toResourceBody(
|
|
85
|
+
body: BrowserResourceBody | undefined,
|
|
86
|
+
): Buffer | string | undefined {
|
|
87
|
+
if (body === undefined || typeof body === "string" || Buffer.isBuffer(body)) {
|
|
88
|
+
return body;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (body instanceof ArrayBuffer) {
|
|
92
|
+
return Buffer.from(new Uint8Array(body));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return Buffer.from(body);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isResourceMethod(method: string): method is BrowserResourceMethod {
|
|
99
|
+
return method === "GET" || method === "HEAD";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function toResourceRequest(
|
|
103
|
+
request: Request,
|
|
104
|
+
): Promise<BrowserResourceRequest | null> {
|
|
105
|
+
const method = request.method().toUpperCase();
|
|
106
|
+
if (!isResourceMethod(method)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
headers: await request.allHeaders(),
|
|
112
|
+
method,
|
|
113
|
+
resourceType: request.resourceType(),
|
|
114
|
+
url: request.url(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toCdpResourceRequest(
|
|
119
|
+
params: unknown,
|
|
120
|
+
): { requestId: string; request: BrowserResourceRequest } | null {
|
|
121
|
+
if (!isRecord(params)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const requestId = params.requestId;
|
|
126
|
+
const rawRequest = params.request;
|
|
127
|
+
if (typeof requestId !== "string" || !isRecord(rawRequest)) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const url = rawRequest.url;
|
|
132
|
+
const method = String(rawRequest.method ?? "").toUpperCase();
|
|
133
|
+
if (typeof url !== "string" || !isResourceMethod(method)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
requestId,
|
|
139
|
+
request: {
|
|
140
|
+
headers: toCdpResourceHeaders(rawRequest.headers),
|
|
141
|
+
method,
|
|
142
|
+
resourceType:
|
|
143
|
+
typeof params.resourceType === "string" ? params.resourceType : undefined,
|
|
144
|
+
url,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getCdpPausedRequestId(params: unknown): string | null {
|
|
150
|
+
if (!isRecord(params) || typeof params.requestId !== "string") {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return params.requestId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function toCdpResourceHeaders(value: unknown): Record<string, string> {
|
|
158
|
+
if (!isRecord(value)) {
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const headers: Record<string, string> = {};
|
|
163
|
+
for (const [name, headerValue] of Object.entries(value)) {
|
|
164
|
+
if (typeof headerValue === "string") {
|
|
165
|
+
headers[name] = headerValue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return headers;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function matchesResourceRoute(
|
|
173
|
+
match: BrowserResourcePolicy["routes"][number]["match"],
|
|
174
|
+
request: BrowserResourceRequest,
|
|
175
|
+
): boolean {
|
|
176
|
+
if (typeof match === "string") {
|
|
177
|
+
return request.url === match;
|
|
178
|
+
}
|
|
179
|
+
if (match instanceof RegExp) {
|
|
180
|
+
return match.test(request.url);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return match(request);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function toCdpFulfillParams(
|
|
187
|
+
requestId: string,
|
|
188
|
+
decision: Extract<BrowserResourceDecision, { readonly action: "fulfill" }>,
|
|
189
|
+
): CdpFetchFulfillParams {
|
|
190
|
+
const body = toResourceBody(decision.body);
|
|
191
|
+
return {
|
|
192
|
+
...(body === undefined
|
|
193
|
+
? {}
|
|
194
|
+
: { body: Buffer.from(body).toString("base64") }),
|
|
195
|
+
...(decision.headers === undefined
|
|
196
|
+
? {}
|
|
197
|
+
: {
|
|
198
|
+
responseHeaders: Object.entries(decision.headers).map(
|
|
199
|
+
([name, value]) => ({ name, value }),
|
|
200
|
+
),
|
|
201
|
+
}),
|
|
202
|
+
requestId,
|
|
203
|
+
responseCode: decision.status ?? 200,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function fulfillResourceRoute(
|
|
208
|
+
route: Route,
|
|
209
|
+
decision: Extract<BrowserResourceDecision, { readonly action: "fulfill" }>,
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
const body = toResourceBody(decision.body);
|
|
212
|
+
await route.fulfill({
|
|
213
|
+
...(body === undefined ? {} : { body }),
|
|
214
|
+
...(decision.headers === undefined ? {} : { headers: decision.headers }),
|
|
215
|
+
status: decision.status ?? 200,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
56
219
|
export type BrowserClientOptions = BrowserOptions & {
|
|
220
|
+
allowedHosts?: string[];
|
|
57
221
|
cdpUrl?: string;
|
|
58
222
|
executablePath?: string;
|
|
59
223
|
extraArgs?: string[];
|
|
60
224
|
};
|
|
61
225
|
|
|
62
|
-
type SupportedBrowserClient =
|
|
226
|
+
type SupportedBrowserClient = {
|
|
227
|
+
readonly engine: BrowserEngine;
|
|
63
228
|
close(): Promise<void>;
|
|
64
229
|
newPage(): Promise<BrowserPageContract>;
|
|
230
|
+
rawPage(): Promise<BrowserPageContract>;
|
|
231
|
+
withIsolatedContext<T>(
|
|
232
|
+
handler: (page: BrowserPageContract) => Promise<T>,
|
|
233
|
+
): Promise<T>;
|
|
65
234
|
};
|
|
66
235
|
|
|
67
236
|
function getDefaultCdpPoolUrl(env = process.env): string | undefined {
|
|
68
|
-
return env.
|
|
237
|
+
return env.APIFUSE__CDP_POOL__URL;
|
|
69
238
|
}
|
|
70
239
|
|
|
71
240
|
async function importOptionalModule<T extends object>(
|
|
@@ -89,11 +258,16 @@ function unwrapModuleDefault<T extends object>(module: T): T {
|
|
|
89
258
|
}
|
|
90
259
|
|
|
91
260
|
function isModuleNotFoundError(error: unknown): boolean {
|
|
261
|
+
if (!(error instanceof Error)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const code = "code" in error ? error.code : undefined;
|
|
92
266
|
return (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
267
|
+
code === "MODULE_NOT_FOUND" ||
|
|
268
|
+
code === "ERR_MODULE_NOT_FOUND" ||
|
|
269
|
+
error.message.includes("Cannot find module") ||
|
|
270
|
+
error.message.includes("Cannot find package")
|
|
97
271
|
);
|
|
98
272
|
}
|
|
99
273
|
|
|
@@ -118,9 +292,71 @@ function toLaunchOptions(options: BrowserClientOptions): LaunchOptions {
|
|
|
118
292
|
};
|
|
119
293
|
}
|
|
120
294
|
|
|
295
|
+
class PlaywrightBrowserLocator implements BrowserLocator {
|
|
296
|
+
constructor(private readonly locator: Locator) {}
|
|
297
|
+
|
|
298
|
+
async click(): Promise<void> {
|
|
299
|
+
await this.locator.click();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async fill(text: string): Promise<void> {
|
|
303
|
+
await this.locator.fill(text);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async textContent(): Promise<string | null> {
|
|
307
|
+
return await this.locator.textContent();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async waitFor(options?: { timeout?: number }): Promise<void> {
|
|
311
|
+
await this.locator.waitFor(options);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
class PlaywrightBrowserFrame implements BrowserFrame {
|
|
316
|
+
constructor(private readonly frame: Frame) {}
|
|
317
|
+
|
|
318
|
+
get id(): string {
|
|
319
|
+
return this.frame.name() || this.frame.url();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
get name(): string | undefined {
|
|
323
|
+
const name = this.frame.name();
|
|
324
|
+
return name.length > 0 ? name : undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
get parentId(): string | undefined {
|
|
328
|
+
const parent = this.frame.parentFrame();
|
|
329
|
+
return parent ? parent.name() || parent.url() : undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async url(): Promise<string> {
|
|
333
|
+
return this.frame.url();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async title(): Promise<string> {
|
|
337
|
+
return await this.frame.evaluate("document.title");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async content(): Promise<string> {
|
|
341
|
+
return await this.frame.content();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async evaluate<T>(fn: string | (() => T)): Promise<T> {
|
|
345
|
+
if (typeof fn === "string") {
|
|
346
|
+
return await this.frame.evaluate(fn);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return await this.frame.evaluate(fn);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
locator(selector: string): BrowserLocator {
|
|
353
|
+
return new PlaywrightBrowserLocator(this.frame.locator(selector));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
121
357
|
async function loadPlaywright(): Promise<PlaywrightModule> {
|
|
122
358
|
try {
|
|
123
|
-
|
|
359
|
+
await importOptionalModule<PlaywrightModule>("playwright");
|
|
124
360
|
} catch (error) {
|
|
125
361
|
if (isModuleNotFoundError(error)) {
|
|
126
362
|
throw new ProviderError("Playwright is not installed", {
|
|
@@ -148,16 +384,31 @@ async function loadPlaywright(): Promise<PlaywrightModule> {
|
|
|
148
384
|
}
|
|
149
385
|
}
|
|
150
386
|
|
|
151
|
-
|
|
387
|
+
const playwrightExtraStealthLaunchers = new WeakSet<object>();
|
|
388
|
+
|
|
389
|
+
async function loadPlaywrightExtra(): Promise<PlaywrightExtraModule> {
|
|
390
|
+
try {
|
|
391
|
+
require("playwright");
|
|
392
|
+
} catch (error) {
|
|
393
|
+
if (isModuleNotFoundError(error)) {
|
|
394
|
+
throw new ProviderError("Playwright is not installed", {
|
|
395
|
+
cause: error instanceof Error ? error : undefined,
|
|
396
|
+
fix: "Run: bun add playwright",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
|
|
152
403
|
try {
|
|
153
404
|
return unwrapModuleDefault(
|
|
154
|
-
await importOptionalModule<
|
|
405
|
+
await importOptionalModule<PlaywrightExtraModule>("playwright-extra"),
|
|
155
406
|
);
|
|
156
407
|
} catch (error) {
|
|
157
408
|
if (isModuleNotFoundError(error)) {
|
|
158
|
-
throw new ProviderError("playwright-
|
|
409
|
+
throw new ProviderError("playwright-extra is not installed", {
|
|
159
410
|
cause: error instanceof Error ? error : undefined,
|
|
160
|
-
fix: "Run: bun add playwright-stealth",
|
|
411
|
+
fix: "Run: bun add playwright-extra puppeteer-extra-plugin-stealth",
|
|
161
412
|
});
|
|
162
413
|
}
|
|
163
414
|
|
|
@@ -165,6 +416,45 @@ async function loadPlaywrightStealth(): Promise<PlaywrightStealthModule> {
|
|
|
165
416
|
}
|
|
166
417
|
}
|
|
167
418
|
|
|
419
|
+
async function loadStealthPluginFactory(): Promise<StealthPluginFactory> {
|
|
420
|
+
try {
|
|
421
|
+
return unwrapModuleDefault(
|
|
422
|
+
await importOptionalModule<StealthPluginFactory>(
|
|
423
|
+
"puppeteer-extra-plugin-stealth",
|
|
424
|
+
),
|
|
425
|
+
);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (isModuleNotFoundError(error)) {
|
|
428
|
+
throw new ProviderError(
|
|
429
|
+
"puppeteer-extra-plugin-stealth is not installed",
|
|
430
|
+
{
|
|
431
|
+
cause: error instanceof Error ? error : undefined,
|
|
432
|
+
fix: "Run: bun add playwright-extra puppeteer-extra-plugin-stealth",
|
|
433
|
+
},
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function loadChromiumLauncher(
|
|
442
|
+
options: BrowserClientOptions,
|
|
443
|
+
): Promise<PlaywrightModule["chromium"]> {
|
|
444
|
+
if (!(options.stealth ?? true)) {
|
|
445
|
+
return (await loadPlaywright()).chromium;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const playwrightExtra = await loadPlaywrightExtra();
|
|
449
|
+
if (!playwrightExtraStealthLaunchers.has(playwrightExtra.chromium)) {
|
|
450
|
+
const createStealthPlugin = await loadStealthPluginFactory();
|
|
451
|
+
playwrightExtra.chromium.use(createStealthPlugin());
|
|
452
|
+
playwrightExtraStealthLaunchers.add(playwrightExtra.chromium);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return playwrightExtra.chromium;
|
|
456
|
+
}
|
|
457
|
+
|
|
168
458
|
async function loadNodriver(): Promise<void> {
|
|
169
459
|
try {
|
|
170
460
|
await importOptionalModule("nodriver");
|
|
@@ -196,6 +486,7 @@ async function loadSeleniumBase(): Promise<void> {
|
|
|
196
486
|
}
|
|
197
487
|
|
|
198
488
|
class PlaywrightBrowserPage implements BrowserPageContract {
|
|
489
|
+
readonly id = "main";
|
|
199
490
|
readonly pageId?: string;
|
|
200
491
|
|
|
201
492
|
constructor(private readonly page: Page) {}
|
|
@@ -232,6 +523,22 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
232
523
|
await this.page.fill(selector, text);
|
|
233
524
|
}
|
|
234
525
|
|
|
526
|
+
async frames(): Promise<BrowserFrame[]> {
|
|
527
|
+
return this.page.frames().map((frame) => new PlaywrightBrowserFrame(frame));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
locator(selector: string): BrowserLocator {
|
|
531
|
+
return new PlaywrightBrowserLocator(this.page.locator(selector));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async title(): Promise<string> {
|
|
535
|
+
return await this.page.title();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async url(): Promise<string> {
|
|
539
|
+
return this.page.url();
|
|
540
|
+
}
|
|
541
|
+
|
|
235
542
|
async content(): Promise<string> {
|
|
236
543
|
return await this.page.content();
|
|
237
544
|
}
|
|
@@ -243,6 +550,47 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
243
550
|
async close(): Promise<void> {
|
|
244
551
|
await this.page.close();
|
|
245
552
|
}
|
|
553
|
+
|
|
554
|
+
async withResourcePolicy<T>(
|
|
555
|
+
policy: BrowserResourcePolicy,
|
|
556
|
+
run: () => Promise<T>,
|
|
557
|
+
): Promise<T> {
|
|
558
|
+
const allowedMethods = new Set(
|
|
559
|
+
policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS,
|
|
560
|
+
);
|
|
561
|
+
const handler = async (route: Route): Promise<void> => {
|
|
562
|
+
const request = await toResourceRequest(route.request());
|
|
563
|
+
if (!request || !allowedMethods.has(request.method)) {
|
|
564
|
+
await route.abort("blockedbyclient");
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
for (const resourceRoute of policy.routes) {
|
|
569
|
+
if (!matchesResourceRoute(resourceRoute.match, request)) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const decision = await resourceRoute.handle(request);
|
|
574
|
+
switch (decision.action) {
|
|
575
|
+
case "fulfill":
|
|
576
|
+
await fulfillResourceRoute(route, decision);
|
|
577
|
+
return;
|
|
578
|
+
case "block":
|
|
579
|
+
await route.abort("blockedbyclient");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
await route.abort("blockedbyclient");
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
await this.page.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
|
|
588
|
+
try {
|
|
589
|
+
return await run();
|
|
590
|
+
} finally {
|
|
591
|
+
await this.page.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
246
594
|
}
|
|
247
595
|
|
|
248
596
|
class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
@@ -256,10 +604,8 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
256
604
|
return this.browser;
|
|
257
605
|
}
|
|
258
606
|
|
|
259
|
-
const
|
|
260
|
-
this.browser = await
|
|
261
|
-
toLaunchOptions(this.options),
|
|
262
|
-
);
|
|
607
|
+
const chromium = await loadChromiumLauncher(this.options);
|
|
608
|
+
this.browser = await chromium.launch(toLaunchOptions(this.options));
|
|
263
609
|
return this.browser;
|
|
264
610
|
}
|
|
265
611
|
|
|
@@ -267,14 +613,35 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
267
613
|
const browser = await this.ensureBrowser();
|
|
268
614
|
const page = await browser.newPage();
|
|
269
615
|
|
|
270
|
-
if (this.options.stealth ?? true) {
|
|
271
|
-
const { stealth } = await loadPlaywrightStealth();
|
|
272
|
-
await stealth(page);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
616
|
return new PlaywrightBrowserPage(page);
|
|
276
617
|
}
|
|
277
618
|
|
|
619
|
+
async rawPage(): Promise<BrowserPageContract> {
|
|
620
|
+
throw new ProviderError("ctx.browser.rawPage() requires a CDP pool", {
|
|
621
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
622
|
+
fix: "Set APIFUSE__CDP_POOL__URL and use the SDK CDP pool-backed browser runtime. Local Chromium launch is not allowed for rawPage().",
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async withIsolatedContext<T>(
|
|
627
|
+
handler: (page: BrowserPageContract) => Promise<T>,
|
|
628
|
+
): Promise<T> {
|
|
629
|
+
const browser = await this.ensureBrowser();
|
|
630
|
+
const context = await browser.newContext();
|
|
631
|
+
const page = await context.newPage();
|
|
632
|
+
const browserPage = new PlaywrightBrowserPage(page);
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
return await handler(browserPage);
|
|
636
|
+
} finally {
|
|
637
|
+
try {
|
|
638
|
+
await browserPage.close();
|
|
639
|
+
} finally {
|
|
640
|
+
await context.close();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
278
645
|
async close(): Promise<void> {
|
|
279
646
|
const browser = this.browser;
|
|
280
647
|
this.browser = null;
|
|
@@ -287,8 +654,25 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
287
654
|
}
|
|
288
655
|
}
|
|
289
656
|
|
|
657
|
+
function normalizeWebSocketEndpoint(endpoint: string): string {
|
|
658
|
+
const url = new URL(endpoint);
|
|
659
|
+
if (url.protocol === "http:") {
|
|
660
|
+
url.protocol = "ws:";
|
|
661
|
+
return url.toString();
|
|
662
|
+
}
|
|
663
|
+
if (url.protocol === "https:") {
|
|
664
|
+
url.protocol = "wss:";
|
|
665
|
+
return url.toString();
|
|
666
|
+
}
|
|
667
|
+
if (url.protocol === "ws:" || url.protocol === "wss:") {
|
|
668
|
+
return endpoint;
|
|
669
|
+
}
|
|
670
|
+
throw new Error(`Unsupported WebSocket endpoint protocol: ${url.protocol}`);
|
|
671
|
+
}
|
|
672
|
+
|
|
290
673
|
class JsonRpcWebSocketClient {
|
|
291
674
|
private nextId = 1;
|
|
675
|
+
private readonly endpoint: string;
|
|
292
676
|
private readonly listeners = new Map<
|
|
293
677
|
string,
|
|
294
678
|
Set<(params: unknown) => void>
|
|
@@ -303,7 +687,9 @@ class JsonRpcWebSocketClient {
|
|
|
303
687
|
private socket?: WebSocket;
|
|
304
688
|
private socketPromise?: Promise<WebSocket>;
|
|
305
689
|
|
|
306
|
-
constructor(
|
|
690
|
+
constructor(endpoint: string) {
|
|
691
|
+
this.endpoint = normalizeWebSocketEndpoint(endpoint);
|
|
692
|
+
}
|
|
307
693
|
|
|
308
694
|
on(method: string, listener: (params: unknown) => void): () => void {
|
|
309
695
|
const listeners = this.listeners.get(method) ?? new Set();
|
|
@@ -426,16 +812,233 @@ class JsonRpcWebSocketClient {
|
|
|
426
812
|
}
|
|
427
813
|
}
|
|
428
814
|
|
|
815
|
+
function flattenCdpFrameTree(
|
|
816
|
+
node: CdpFrameTreeNode | undefined,
|
|
817
|
+
out: CdpFrameTreeNode["frame"][] = [],
|
|
818
|
+
): CdpFrameTreeNode["frame"][] {
|
|
819
|
+
if (!node) {
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
out.push(node.frame);
|
|
824
|
+
for (const child of node.childFrames ?? []) {
|
|
825
|
+
flattenCdpFrameTree(child, out);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return out;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
832
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function parsePoolAcquireResponse(value: unknown): PoolAcquireResponse {
|
|
836
|
+
if (
|
|
837
|
+
!isRecord(value) ||
|
|
838
|
+
typeof value.pageId !== "string" ||
|
|
839
|
+
typeof value.wsEndpoint !== "string"
|
|
840
|
+
) {
|
|
841
|
+
throw new ProviderError("CDP Pool returned an invalid acquire response", {
|
|
842
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (
|
|
847
|
+
value.browserContextId !== undefined &&
|
|
848
|
+
typeof value.browserContextId !== "string"
|
|
849
|
+
) {
|
|
850
|
+
throw new ProviderError("CDP Pool returned an invalid acquire response", {
|
|
851
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
return {
|
|
856
|
+
...(value.browserContextId
|
|
857
|
+
? { browserContextId: value.browserContextId }
|
|
858
|
+
: {}),
|
|
859
|
+
pageId: value.pageId,
|
|
860
|
+
wsEndpoint: value.wsEndpoint,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function parseCdpFrameTreeNode(value: unknown): CdpFrameTreeNode | undefined {
|
|
865
|
+
if (!isRecord(value) || !isRecord(value.frame)) {
|
|
866
|
+
return undefined;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const frameId = value.frame.id;
|
|
870
|
+
if (typeof frameId !== "string") {
|
|
871
|
+
return undefined;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const childFrames = Array.isArray(value.childFrames)
|
|
875
|
+
? value.childFrames
|
|
876
|
+
.map(parseCdpFrameTreeNode)
|
|
877
|
+
.filter((child): child is CdpFrameTreeNode => child !== undefined)
|
|
878
|
+
: undefined;
|
|
879
|
+
|
|
880
|
+
return {
|
|
881
|
+
frame: {
|
|
882
|
+
id: frameId,
|
|
883
|
+
name: typeof value.frame.name === "string" ? value.frame.name : undefined,
|
|
884
|
+
parentId:
|
|
885
|
+
typeof value.frame.parentId === "string"
|
|
886
|
+
? value.frame.parentId
|
|
887
|
+
: undefined,
|
|
888
|
+
url: typeof value.frame.url === "string" ? value.frame.url : undefined,
|
|
889
|
+
},
|
|
890
|
+
...(childFrames ? { childFrames } : {}),
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function getCdpExecutionContext(params: unknown): {
|
|
895
|
+
frameId?: string;
|
|
896
|
+
id?: number;
|
|
897
|
+
} {
|
|
898
|
+
if (!isRecord(params) || !isRecord(params.context)) {
|
|
899
|
+
return {};
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const contextId = params.context.id;
|
|
903
|
+
const auxData = params.context.auxData;
|
|
904
|
+
return {
|
|
905
|
+
frameId:
|
|
906
|
+
isRecord(auxData) && typeof auxData.frameId === "string"
|
|
907
|
+
? auxData.frameId
|
|
908
|
+
: undefined,
|
|
909
|
+
id: typeof contextId === "number" ? contextId : undefined,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
class CdpBrowserLocator implements BrowserLocator {
|
|
914
|
+
constructor(
|
|
915
|
+
private readonly frame: {
|
|
916
|
+
evaluate<T>(fn: string | (() => T)): Promise<T>;
|
|
917
|
+
waitForSelector?(
|
|
918
|
+
selector: string,
|
|
919
|
+
options?: { timeout?: number },
|
|
920
|
+
): Promise<void>;
|
|
921
|
+
},
|
|
922
|
+
private readonly selector: string,
|
|
923
|
+
) {}
|
|
924
|
+
|
|
925
|
+
async click(): Promise<void> {
|
|
926
|
+
await this.waitFor();
|
|
927
|
+
await this.frame.evaluate(
|
|
928
|
+
`(() => {
|
|
929
|
+
const element = document.querySelector(${JSON.stringify(this.selector)});
|
|
930
|
+
if (!(element instanceof HTMLElement)) {
|
|
931
|
+
throw new Error(${JSON.stringify(`Selector not found: ${this.selector}`)});
|
|
932
|
+
}
|
|
933
|
+
element.click();
|
|
934
|
+
})()`,
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async fill(text: string): Promise<void> {
|
|
939
|
+
await this.waitFor();
|
|
940
|
+
await this.frame.evaluate(
|
|
941
|
+
`(() => {
|
|
942
|
+
const element = document.querySelector(${JSON.stringify(this.selector)});
|
|
943
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
944
|
+
throw new Error(${JSON.stringify(`Unsupported input target: ${this.selector}`)});
|
|
945
|
+
}
|
|
946
|
+
element.focus();
|
|
947
|
+
element.value = ${JSON.stringify(text)};
|
|
948
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
949
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
950
|
+
})()`,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async textContent(): Promise<string | null> {
|
|
955
|
+
return await this.frame.evaluate<string | null>(
|
|
956
|
+
`document.querySelector(${JSON.stringify(this.selector)})?.textContent ?? null`,
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
async waitFor(options?: { timeout?: number }): Promise<void> {
|
|
961
|
+
if (this.frame.waitForSelector) {
|
|
962
|
+
await this.frame.waitForSelector(this.selector, options);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
967
|
+
const deadline = Date.now() + timeout;
|
|
968
|
+
while (Date.now() < deadline) {
|
|
969
|
+
const exists = await this.frame.evaluate<boolean>(
|
|
970
|
+
`Boolean(document.querySelector(${JSON.stringify(this.selector)}))`,
|
|
971
|
+
);
|
|
972
|
+
if (exists) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
await delay(SELECTOR_POLL_INTERVAL_MS);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
throw new Error(`Timed out waiting for selector: ${this.selector}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
class CdpBrowserFrame implements BrowserFrame {
|
|
983
|
+
constructor(
|
|
984
|
+
readonly id: string,
|
|
985
|
+
private readonly page: CdpPoolBrowserPage,
|
|
986
|
+
private readonly initialUrl = "",
|
|
987
|
+
readonly name?: string,
|
|
988
|
+
readonly parentId?: string,
|
|
989
|
+
) {}
|
|
990
|
+
|
|
991
|
+
async url(): Promise<string> {
|
|
992
|
+
const evaluatedUrl = await this.evaluate<string | undefined>(
|
|
993
|
+
"window.location.href",
|
|
994
|
+
);
|
|
995
|
+
return evaluatedUrl || this.initialUrl;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async title(): Promise<string> {
|
|
999
|
+
return await this.evaluate<string>("document.title");
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async content(): Promise<string> {
|
|
1003
|
+
return await this.evaluate<string>("document.documentElement.outerHTML");
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
async evaluate<T>(fn: string | (() => T)): Promise<T> {
|
|
1007
|
+
return await this.page.evaluateInFrame<T>(this.id, fn);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
locator(selector: string): BrowserLocator {
|
|
1011
|
+
return new CdpBrowserLocator(this, selector);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
async waitForSelector(
|
|
1015
|
+
selector: string,
|
|
1016
|
+
options?: { timeout?: number },
|
|
1017
|
+
): Promise<void> {
|
|
1018
|
+
await this.page.waitForSelectorInFrame(this.id, selector, options);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
fallbackUrl(): string {
|
|
1022
|
+
return this.initialUrl;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
429
1026
|
class CdpPoolBrowserPage implements BrowserPageContract {
|
|
430
1027
|
private closed = false;
|
|
431
1028
|
private initialized = false;
|
|
1029
|
+
private readonly frameExecutionContexts = new Map<string, number>();
|
|
432
1030
|
|
|
433
1031
|
constructor(
|
|
434
1032
|
readonly pageId: string,
|
|
1033
|
+
private readonly browserContextId: string | undefined,
|
|
435
1034
|
private readonly pageClient: JsonRpcWebSocketClient,
|
|
436
|
-
private readonly release: (
|
|
1035
|
+
private readonly release: (request: PoolReleaseRequest) => Promise<void>,
|
|
437
1036
|
) {}
|
|
438
1037
|
|
|
1038
|
+
get id(): string {
|
|
1039
|
+
return this.pageId;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
439
1042
|
async goto(url: string): Promise<void> {
|
|
440
1043
|
await this.initialize();
|
|
441
1044
|
const startedAt = Date.now();
|
|
@@ -457,8 +1060,49 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
457
1060
|
|
|
458
1061
|
async evaluate<T>(fn: string | (() => T)): Promise<T> {
|
|
459
1062
|
await this.initialize();
|
|
1063
|
+
return await this.evaluateWithContext<T>(fn);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async evaluateInFrame<T>(
|
|
1067
|
+
frameId: string,
|
|
1068
|
+
fn: string | (() => T),
|
|
1069
|
+
): Promise<T> {
|
|
1070
|
+
await this.initialize();
|
|
1071
|
+
const contextId = await this.getFrameExecutionContextId(frameId);
|
|
1072
|
+
return await this.evaluateWithContext<T>(fn, contextId);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
async waitForSelectorInFrame(
|
|
1076
|
+
frameId: string,
|
|
1077
|
+
selector: string,
|
|
1078
|
+
options?: { timeout?: number },
|
|
1079
|
+
): Promise<void> {
|
|
1080
|
+
const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1081
|
+
const deadline = Date.now() + timeout;
|
|
1082
|
+
|
|
1083
|
+
while (Date.now() < deadline) {
|
|
1084
|
+
const exists = await this.evaluateInFrame<boolean>(
|
|
1085
|
+
frameId,
|
|
1086
|
+
`Boolean(document.querySelector(${JSON.stringify(selector)}))`,
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
if (exists) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
await delay(SELECTOR_POLL_INTERVAL_MS);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
throw new Error(`Timed out waiting for selector: ${selector}`);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
private async evaluateWithContext<T>(
|
|
1100
|
+
fn: string | (() => T),
|
|
1101
|
+
contextId?: number,
|
|
1102
|
+
): Promise<T> {
|
|
460
1103
|
const result = await this.pageClient.send("Runtime.evaluate", {
|
|
461
1104
|
awaitPromise: true,
|
|
1105
|
+
...(contextId === undefined ? {} : { contextId }),
|
|
462
1106
|
expression: formatExpression(fn),
|
|
463
1107
|
returnByValue: true,
|
|
464
1108
|
});
|
|
@@ -539,6 +1183,37 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
539
1183
|
);
|
|
540
1184
|
}
|
|
541
1185
|
|
|
1186
|
+
async frames(): Promise<BrowserFrame[]> {
|
|
1187
|
+
await this.initialize();
|
|
1188
|
+
const result = await this.pageClient.send("Page.getFrameTree");
|
|
1189
|
+
const frames = flattenCdpFrameTree(parseCdpFrameTreeNode(result.frameTree));
|
|
1190
|
+
return frames.map(
|
|
1191
|
+
(frame) =>
|
|
1192
|
+
new CdpBrowserFrame(
|
|
1193
|
+
frame.id,
|
|
1194
|
+
this,
|
|
1195
|
+
frame.url ?? "",
|
|
1196
|
+
frame.name,
|
|
1197
|
+
frame.parentId,
|
|
1198
|
+
),
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
locator(selector: string): BrowserLocator {
|
|
1203
|
+
return new CdpBrowserLocator(this, selector);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
async title(): Promise<string> {
|
|
1207
|
+
return await this.evaluate<string>("document.title");
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
async url(): Promise<string> {
|
|
1211
|
+
const [mainFrame] = await this.frames();
|
|
1212
|
+
const frameUrl =
|
|
1213
|
+
mainFrame instanceof CdpBrowserFrame ? mainFrame.fallbackUrl() : "";
|
|
1214
|
+
return frameUrl || (await this.evaluate<string>("window.location.href"));
|
|
1215
|
+
}
|
|
1216
|
+
|
|
542
1217
|
async content(): Promise<string> {
|
|
543
1218
|
return await this.evaluate<string>("document.documentElement.outerHTML");
|
|
544
1219
|
}
|
|
@@ -562,22 +1237,154 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
562
1237
|
this.closed = true;
|
|
563
1238
|
|
|
564
1239
|
try {
|
|
565
|
-
await this.release(
|
|
1240
|
+
await this.release({
|
|
1241
|
+
...(this.browserContextId
|
|
1242
|
+
? { browserContextId: this.browserContextId }
|
|
1243
|
+
: {}),
|
|
1244
|
+
pageId: this.pageId,
|
|
1245
|
+
});
|
|
566
1246
|
} finally {
|
|
567
1247
|
await this.pageClient.close();
|
|
568
1248
|
}
|
|
569
1249
|
}
|
|
570
1250
|
|
|
1251
|
+
async withResourcePolicy<T>(
|
|
1252
|
+
policy: BrowserResourcePolicy,
|
|
1253
|
+
run: () => Promise<T>,
|
|
1254
|
+
): Promise<T> {
|
|
1255
|
+
const allowedMethods = new Set(
|
|
1256
|
+
policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS,
|
|
1257
|
+
);
|
|
1258
|
+
const handlePausedRequest = (params: unknown): void => {
|
|
1259
|
+
void this.handleResourcePolicyPausedRequest(
|
|
1260
|
+
params,
|
|
1261
|
+
policy,
|
|
1262
|
+
allowedMethods,
|
|
1263
|
+
);
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
const unsubscribe = this.pageClient.on(
|
|
1267
|
+
"Fetch.requestPaused",
|
|
1268
|
+
handlePausedRequest,
|
|
1269
|
+
);
|
|
1270
|
+
|
|
1271
|
+
try {
|
|
1272
|
+
await this.pageClient.send("Fetch.enable", {
|
|
1273
|
+
patterns: [{ requestStage: "Request", urlPattern: "*" }],
|
|
1274
|
+
});
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
unsubscribe();
|
|
1277
|
+
throw new ProviderError(
|
|
1278
|
+
"CDP browser target does not support BrowserPage.withResourcePolicy()",
|
|
1279
|
+
{
|
|
1280
|
+
cause: error instanceof Error ? error : undefined,
|
|
1281
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
1282
|
+
fix: "Use a Chromium CDP target with the Fetch domain enabled, or use the local Playwright browser runtime.",
|
|
1283
|
+
},
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
try {
|
|
1288
|
+
return await run();
|
|
1289
|
+
} finally {
|
|
1290
|
+
unsubscribe();
|
|
1291
|
+
await this.pageClient.send("Fetch.disable");
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
private async handleResourcePolicyPausedRequest(
|
|
1296
|
+
params: unknown,
|
|
1297
|
+
policy: BrowserResourcePolicy,
|
|
1298
|
+
allowedMethods: ReadonlySet<BrowserResourceMethod>,
|
|
1299
|
+
): Promise<void> {
|
|
1300
|
+
const requestId = getCdpPausedRequestId(params);
|
|
1301
|
+
if (requestId === null) {
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
try {
|
|
1306
|
+
const parsed = toCdpResourceRequest(params);
|
|
1307
|
+
if (!parsed || !allowedMethods.has(parsed.request.method)) {
|
|
1308
|
+
await this.failCdpResourceRequest(requestId);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
for (const resourceRoute of policy.routes) {
|
|
1313
|
+
if (!matchesResourceRoute(resourceRoute.match, parsed.request)) {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const decision = await resourceRoute.handle(parsed.request);
|
|
1318
|
+
switch (decision.action) {
|
|
1319
|
+
case "fulfill":
|
|
1320
|
+
await this.pageClient.send(
|
|
1321
|
+
"Fetch.fulfillRequest",
|
|
1322
|
+
toCdpFulfillParams(parsed.requestId, decision),
|
|
1323
|
+
);
|
|
1324
|
+
return;
|
|
1325
|
+
case "block":
|
|
1326
|
+
await this.failCdpResourceRequest(parsed.requestId);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
await this.failCdpResourceRequest(parsed.requestId);
|
|
1332
|
+
} catch {
|
|
1333
|
+
await this.failCdpResourceRequest(requestId);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
private async failCdpResourceRequest(requestId: string): Promise<void> {
|
|
1338
|
+
try {
|
|
1339
|
+
await this.pageClient.send("Fetch.failRequest", {
|
|
1340
|
+
errorReason: "BlockedByClient",
|
|
1341
|
+
requestId,
|
|
1342
|
+
});
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (error instanceof Error) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
571
1350
|
private async initialize(): Promise<void> {
|
|
572
1351
|
if (this.initialized) {
|
|
573
1352
|
return;
|
|
574
1353
|
}
|
|
575
1354
|
|
|
1355
|
+
this.pageClient.on("Runtime.executionContextCreated", (params) => {
|
|
1356
|
+
const context = getCdpExecutionContext(params);
|
|
1357
|
+
if (context.frameId && context.id !== undefined) {
|
|
1358
|
+
this.frameExecutionContexts.set(context.frameId, context.id);
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
576
1361
|
await this.pageClient.send("Page.enable");
|
|
577
1362
|
await this.pageClient.send("Runtime.enable");
|
|
578
1363
|
this.initialized = true;
|
|
579
1364
|
}
|
|
580
1365
|
|
|
1366
|
+
private async getFrameExecutionContextId(frameId: string): Promise<number> {
|
|
1367
|
+
const existing = this.frameExecutionContexts.get(frameId);
|
|
1368
|
+
if (existing !== undefined) {
|
|
1369
|
+
return existing;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const result = await this.pageClient.send("Page.createIsolatedWorld", {
|
|
1373
|
+
frameId,
|
|
1374
|
+
grantUniveralAccess: true,
|
|
1375
|
+
worldName: "apifuse-provider-sdk",
|
|
1376
|
+
});
|
|
1377
|
+
const contextId = result.executionContextId;
|
|
1378
|
+
if (typeof contextId !== "number") {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`Unable to resolve execution context for frame: ${frameId}`,
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
this.frameExecutionContexts.set(frameId, contextId);
|
|
1385
|
+
return contextId;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
581
1388
|
private async waitForDocumentReady(
|
|
582
1389
|
deadline: number,
|
|
583
1390
|
isLoadEventSeen: () => boolean,
|
|
@@ -598,6 +1405,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
598
1405
|
}
|
|
599
1406
|
|
|
600
1407
|
class CdpPoolBrowserClient implements SupportedBrowserClient {
|
|
1408
|
+
private readonly allowedHosts: string[];
|
|
601
1409
|
private readonly poolClient: JsonRpcWebSocketClient;
|
|
602
1410
|
readonly engine = "playwright-stealth" satisfies BrowserEngine;
|
|
603
1411
|
|
|
@@ -608,29 +1416,65 @@ class CdpPoolBrowserClient implements SupportedBrowserClient {
|
|
|
608
1416
|
});
|
|
609
1417
|
}
|
|
610
1418
|
|
|
1419
|
+
this.allowedHosts = [...new Set(options.allowedHosts ?? [])];
|
|
611
1420
|
this.poolClient = new JsonRpcWebSocketClient(options.cdpUrl);
|
|
612
1421
|
}
|
|
613
1422
|
|
|
614
1423
|
async newPage(): Promise<BrowserPageContract> {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1424
|
+
return await this.acquirePage({ isolatedContext: true });
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
private async acquirePage(options?: {
|
|
1428
|
+
isolatedContext?: boolean;
|
|
1429
|
+
}): Promise<BrowserPageContract> {
|
|
1430
|
+
const acquireResult = parsePoolAcquireResponse(
|
|
1431
|
+
await this.poolClient.send("acquire", {
|
|
1432
|
+
...(this.allowedHosts.length > 0
|
|
1433
|
+
? { allowedHosts: this.allowedHosts }
|
|
1434
|
+
: {}),
|
|
1435
|
+
...(options?.isolatedContext
|
|
1436
|
+
? { isolationMode: "browserContext" }
|
|
1437
|
+
: {}),
|
|
1438
|
+
}),
|
|
1439
|
+
);
|
|
618
1440
|
const pageClient = new JsonRpcWebSocketClient(acquireResult.wsEndpoint);
|
|
619
1441
|
const page = new CdpPoolBrowserPage(
|
|
620
1442
|
acquireResult.pageId,
|
|
1443
|
+
acquireResult.browserContextId,
|
|
621
1444
|
pageClient,
|
|
622
|
-
async (
|
|
623
|
-
await this.poolClient.send("release",
|
|
1445
|
+
async (request) => {
|
|
1446
|
+
await this.poolClient.send("release", request);
|
|
624
1447
|
},
|
|
625
1448
|
);
|
|
626
1449
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1450
|
+
try {
|
|
1451
|
+
await page.evaluate(
|
|
1452
|
+
`window.navigator.webdriver === true ? Object.defineProperty(window.navigator, "webdriver", { configurable: true, get: () => undefined }) : undefined`,
|
|
1453
|
+
);
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
await page.close().catch(() => undefined);
|
|
1456
|
+
throw error;
|
|
1457
|
+
}
|
|
630
1458
|
|
|
631
1459
|
return page;
|
|
632
1460
|
}
|
|
633
1461
|
|
|
1462
|
+
async rawPage(): Promise<BrowserPageContract> {
|
|
1463
|
+
return await this.newPage();
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
async withIsolatedContext<T>(
|
|
1467
|
+
handler: (page: BrowserPageContract) => Promise<T>,
|
|
1468
|
+
): Promise<T> {
|
|
1469
|
+
const page = await this.acquirePage({ isolatedContext: true });
|
|
1470
|
+
|
|
1471
|
+
try {
|
|
1472
|
+
return await handler(page);
|
|
1473
|
+
} finally {
|
|
1474
|
+
await page.close();
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
634
1478
|
async close(): Promise<void> {
|
|
635
1479
|
await this.poolClient.close();
|
|
636
1480
|
}
|
|
@@ -655,6 +1499,14 @@ class UnsupportedBrowserEngineClient implements SupportedBrowserClient {
|
|
|
655
1499
|
});
|
|
656
1500
|
}
|
|
657
1501
|
|
|
1502
|
+
async rawPage(): Promise<never> {
|
|
1503
|
+
return await this.newPage();
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
async withIsolatedContext<T>(): Promise<T> {
|
|
1507
|
+
return await this.newPage();
|
|
1508
|
+
}
|
|
1509
|
+
|
|
658
1510
|
async close(): Promise<void> {}
|
|
659
1511
|
}
|
|
660
1512
|
|
|
@@ -680,6 +1532,9 @@ function createSeleniumUCClient(): SupportedBrowserClient {
|
|
|
680
1532
|
|
|
681
1533
|
export class BrowserClient implements BrowserClientContract {
|
|
682
1534
|
private readonly client: SupportedBrowserClient;
|
|
1535
|
+
private readonly cdpUrl?: string;
|
|
1536
|
+
private activePage?: BrowserPageContract;
|
|
1537
|
+
private readonly activePages = new Set<BrowserPageContract>();
|
|
683
1538
|
private readonly _engine: BrowserEngine;
|
|
684
1539
|
|
|
685
1540
|
constructor(options: BrowserClientOptions = {}) {
|
|
@@ -689,6 +1544,17 @@ export class BrowserClient implements BrowserClientContract {
|
|
|
689
1544
|
};
|
|
690
1545
|
const engine = resolvedOptions.engine ?? "playwright-stealth";
|
|
691
1546
|
this._engine = engine;
|
|
1547
|
+
this.cdpUrl = resolvedOptions.cdpUrl;
|
|
1548
|
+
|
|
1549
|
+
if (resolvedOptions.requireCdpPool && !resolvedOptions.cdpUrl) {
|
|
1550
|
+
throw new ProviderError(
|
|
1551
|
+
"Managed CDP Pool is required for browser providers in production",
|
|
1552
|
+
{
|
|
1553
|
+
code: "BROWSER_CDP_POOL_REQUIRED",
|
|
1554
|
+
fix: "Set APIFUSE__CDP_POOL__URL for deployed browser providers. Local standalone development may omit it.",
|
|
1555
|
+
},
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
692
1558
|
|
|
693
1559
|
switch (engine) {
|
|
694
1560
|
case "nodriver":
|
|
@@ -708,13 +1574,150 @@ export class BrowserClient implements BrowserClientContract {
|
|
|
708
1574
|
return this._engine;
|
|
709
1575
|
}
|
|
710
1576
|
|
|
711
|
-
async newPage(): Promise<
|
|
712
|
-
|
|
1577
|
+
async newPage(): Promise<BrowserPageContract> {
|
|
1578
|
+
const page = await this.client.newPage();
|
|
1579
|
+
return this.activatePage(page);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
async rawPage(): Promise<BrowserPageContract> {
|
|
1583
|
+
if (!this.cdpUrl) {
|
|
1584
|
+
throw new ProviderError("ctx.browser.rawPage() requires a CDP pool", {
|
|
1585
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
1586
|
+
fix: "Set APIFUSE__CDP_POOL__URL. The SDK escape hatch is CDP pool-backed only and never launches local Chromium.",
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
const page = await this.client.rawPage();
|
|
1591
|
+
return this.activatePage(page);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
async withIsolatedContext<T>(
|
|
1595
|
+
handler: (page: BrowserPageContract) => Promise<T>,
|
|
1596
|
+
): Promise<T> {
|
|
1597
|
+
const previousActivePage = this.activePage;
|
|
1598
|
+
let trackedPage: BrowserPageContract | undefined;
|
|
1599
|
+
|
|
1600
|
+
return await this.client.withIsolatedContext(async (page) => {
|
|
1601
|
+
trackedPage = this.activatePage(page);
|
|
1602
|
+
try {
|
|
1603
|
+
return await handler(trackedPage);
|
|
1604
|
+
} finally {
|
|
1605
|
+
this.activePages.delete(trackedPage);
|
|
1606
|
+
if (this.activePage === trackedPage) {
|
|
1607
|
+
this.activePage = previousActivePage;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
private activatePage(page: BrowserPageContract): BrowserPageContract {
|
|
1614
|
+
let closed = false;
|
|
1615
|
+
const originalClose = page.close.bind(page);
|
|
1616
|
+
const trackedPage = new Proxy(page, {
|
|
1617
|
+
get: (target, property, receiver) => {
|
|
1618
|
+
if (property === "close") {
|
|
1619
|
+
return async () => {
|
|
1620
|
+
if (closed) return;
|
|
1621
|
+
closed = true;
|
|
1622
|
+
try {
|
|
1623
|
+
await originalClose();
|
|
1624
|
+
} finally {
|
|
1625
|
+
this.activePages.delete(trackedPage);
|
|
1626
|
+
if (this.activePage === trackedPage) {
|
|
1627
|
+
this.activePage = undefined;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
const value = Reflect.get(target, property, receiver);
|
|
1633
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
1634
|
+
},
|
|
1635
|
+
});
|
|
1636
|
+
this.activePages.add(trackedPage);
|
|
1637
|
+
this.activePage = trackedPage;
|
|
1638
|
+
return trackedPage;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
async solveChallenge(
|
|
1642
|
+
request: BrowserChallengeRequest,
|
|
1643
|
+
): Promise<BrowserChallengeResult> {
|
|
1644
|
+
if (request.type !== "recaptcha") {
|
|
1645
|
+
throw new ProviderError(
|
|
1646
|
+
`Unsupported browser challenge: ${request.type}`,
|
|
1647
|
+
{
|
|
1648
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
1649
|
+
},
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
if (this.activePage) {
|
|
1654
|
+
return await solveRecaptchaChallenge(this.activePage, request);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
const page = await this.client.newPage();
|
|
1658
|
+
try {
|
|
1659
|
+
return await solveRecaptchaChallenge(page, request);
|
|
1660
|
+
} finally {
|
|
1661
|
+
if (this.activePage === page) {
|
|
1662
|
+
this.activePage = undefined;
|
|
1663
|
+
}
|
|
1664
|
+
await page.close();
|
|
1665
|
+
}
|
|
713
1666
|
}
|
|
714
1667
|
|
|
715
1668
|
async close(): Promise<void> {
|
|
716
|
-
|
|
1669
|
+
const pages = Array.from(this.activePages);
|
|
1670
|
+
this.activePages.clear();
|
|
1671
|
+
this.activePage = undefined;
|
|
1672
|
+
try {
|
|
1673
|
+
await Promise.all(pages.map((page) => page.close()));
|
|
1674
|
+
} finally {
|
|
1675
|
+
await this.client.close();
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
async function solveRecaptchaChallenge(
|
|
1681
|
+
page: BrowserPageContract,
|
|
1682
|
+
request: BrowserChallengeRequest,
|
|
1683
|
+
): Promise<BrowserChallengeResult> {
|
|
1684
|
+
const timeout = request.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1685
|
+
const deadline = Date.now() + timeout;
|
|
1686
|
+
|
|
1687
|
+
while (Date.now() < deadline) {
|
|
1688
|
+
const frames = await page.frames();
|
|
1689
|
+
const recaptchaFrame = await findRecaptchaFrame(frames, request.siteKey);
|
|
1690
|
+
if (recaptchaFrame) {
|
|
1691
|
+
await recaptchaFrame.locator("#recaptcha-anchor").click();
|
|
1692
|
+
return {
|
|
1693
|
+
type: "recaptcha",
|
|
1694
|
+
solved: true,
|
|
1695
|
+
frameUrl: await recaptchaFrame.url(),
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
await delay(SELECTOR_POLL_INTERVAL_MS);
|
|
717
1700
|
}
|
|
1701
|
+
|
|
1702
|
+
throw new Error("Timed out waiting for reCAPTCHA iframe");
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
async function findRecaptchaFrame(
|
|
1706
|
+
frames: BrowserFrame[],
|
|
1707
|
+
siteKey?: string,
|
|
1708
|
+
): Promise<BrowserFrame | undefined> {
|
|
1709
|
+
for (const frame of frames) {
|
|
1710
|
+
const url = await frame.url();
|
|
1711
|
+
const matchesRecaptcha =
|
|
1712
|
+
url.includes("google.com/recaptcha") ||
|
|
1713
|
+
url.includes("recaptcha.net/recaptcha");
|
|
1714
|
+
const matchesSiteKey = !siteKey || url.includes(siteKey);
|
|
1715
|
+
if (matchesRecaptcha && matchesSiteKey) {
|
|
1716
|
+
return frame;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
return undefined;
|
|
718
1721
|
}
|
|
719
1722
|
|
|
720
1723
|
export function createBrowserClient(
|