@apifuse/provider-sdk 2.1.0-beta.1 → 2.1.0-beta.10
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 +208 -2
- package/CHANGELOG.md +47 -0
- package/README.md +114 -10
- package/SUBMISSION.md +87 -0
- package/bin/apifuse-check.ts +86 -4
- package/bin/apifuse-dev.ts +87 -13
- package/bin/apifuse-pack-check.ts +80 -0
- package/bin/apifuse-pack-smoke.ts +303 -2
- package/bin/apifuse-perf.ts +142 -49
- package/bin/apifuse-record.ts +182 -104
- package/bin/apifuse-submit-check.ts +2538 -0
- package/bin/apifuse.ts +1 -1
- 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 +762 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +160 -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/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 +155 -0
- package/dist/define.d.ts +97 -0
- package/dist/define.js +1320 -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 +41 -0
- package/dist/index.js +37 -0
- package/dist/lint.d.ts +73 -0
- package/dist/lint.js +702 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +9 -0
- package/dist/provider.js +8 -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 +44 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1034 -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 +706 -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 +18 -0
- package/dist/runtime/stealth.js +834 -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 +1110 -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 +303 -0
- package/dist/types.d.ts +1326 -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 -30
- 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 +214 -52
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- package/src/cli/templates/provider/README.md.tpl +120 -1
- 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 -48
- 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/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 +215 -0
- package/src/define.ts +1688 -48
- 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 +174 -9
- package/src/lint.ts +547 -73
- package/src/observability.ts +41 -0
- package/src/provider.ts +104 -4
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +7 -0
- package/src/runtime/browser.ts +762 -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 +939 -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 +1159 -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 +1157 -75
- package/src/server/types.ts +37 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +31 -5
- package/src/types.ts +1107 -59
- package/src/runtime/tls.ts +0 -434
- package/src/types/playwright-stealth.d.ts +0 -9
package/src/runtime/browser.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import type { LaunchOptions, Page } from "playwright";
|
|
2
|
+
import type { Frame, LaunchOptions, Locator, Page } 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,
|
|
9
14
|
} from "../types";
|
|
10
15
|
|
|
11
16
|
const require = createRequire(import.meta.url);
|
|
@@ -13,15 +18,25 @@ const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
|
13
18
|
const SELECTOR_POLL_INTERVAL_MS = 100;
|
|
14
19
|
|
|
15
20
|
type PlaywrightModule = typeof import("playwright");
|
|
16
|
-
type
|
|
17
|
-
|
|
21
|
+
type PlaywrightExtraModule = {
|
|
22
|
+
chromium: PlaywrightModule["chromium"] & { use(plugin: unknown): unknown };
|
|
18
23
|
};
|
|
19
24
|
|
|
25
|
+
type StealthPluginFactory = (options?: {
|
|
26
|
+
enabledEvasions?: Set<string>;
|
|
27
|
+
}) => unknown;
|
|
28
|
+
|
|
20
29
|
type PoolAcquireResponse = {
|
|
30
|
+
browserContextId?: string;
|
|
21
31
|
pageId: string;
|
|
22
32
|
wsEndpoint: string;
|
|
23
33
|
};
|
|
24
34
|
|
|
35
|
+
type PoolReleaseRequest = {
|
|
36
|
+
browserContextId?: string;
|
|
37
|
+
pageId: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
25
40
|
type JsonRpcId = number;
|
|
26
41
|
|
|
27
42
|
type JsonRpcError = {
|
|
@@ -37,35 +52,37 @@ type JsonRpcMessage = {
|
|
|
37
52
|
result?: Record<string, unknown>;
|
|
38
53
|
};
|
|
39
54
|
|
|
40
|
-
type
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
click(selector: string): Promise<void>;
|
|
49
|
-
type(selector: string, text: string): Promise<void>;
|
|
50
|
-
waitForSelector(
|
|
51
|
-
selector: string,
|
|
52
|
-
options?: { timeout?: number },
|
|
53
|
-
): Promise<void>;
|
|
55
|
+
type CdpFrameTreeNode = {
|
|
56
|
+
childFrames?: CdpFrameTreeNode[];
|
|
57
|
+
frame: {
|
|
58
|
+
id: string;
|
|
59
|
+
name?: string;
|
|
60
|
+
parentId?: string;
|
|
61
|
+
url?: string;
|
|
62
|
+
};
|
|
54
63
|
};
|
|
55
64
|
|
|
65
|
+
type BrowserPageContract = BrowserPage;
|
|
66
|
+
|
|
56
67
|
export type BrowserClientOptions = BrowserOptions & {
|
|
68
|
+
allowedHosts?: string[];
|
|
57
69
|
cdpUrl?: string;
|
|
58
70
|
executablePath?: string;
|
|
59
71
|
extraArgs?: string[];
|
|
60
72
|
};
|
|
61
73
|
|
|
62
|
-
type SupportedBrowserClient =
|
|
74
|
+
type SupportedBrowserClient = {
|
|
75
|
+
readonly engine: BrowserEngine;
|
|
63
76
|
close(): Promise<void>;
|
|
64
77
|
newPage(): Promise<BrowserPageContract>;
|
|
78
|
+
rawPage(): Promise<BrowserPageContract>;
|
|
79
|
+
withIsolatedContext<T>(
|
|
80
|
+
handler: (page: BrowserPageContract) => Promise<T>,
|
|
81
|
+
): Promise<T>;
|
|
65
82
|
};
|
|
66
83
|
|
|
67
84
|
function getDefaultCdpPoolUrl(env = process.env): string | undefined {
|
|
68
|
-
return env.
|
|
85
|
+
return env.APIFUSE__CDP_POOL__URL;
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
async function importOptionalModule<T extends object>(
|
|
@@ -89,11 +106,16 @@ function unwrapModuleDefault<T extends object>(module: T): T {
|
|
|
89
106
|
}
|
|
90
107
|
|
|
91
108
|
function isModuleNotFoundError(error: unknown): boolean {
|
|
109
|
+
if (!(error instanceof Error)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const code = "code" in error ? error.code : undefined;
|
|
92
114
|
return (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
115
|
+
code === "MODULE_NOT_FOUND" ||
|
|
116
|
+
code === "ERR_MODULE_NOT_FOUND" ||
|
|
117
|
+
error.message.includes("Cannot find module") ||
|
|
118
|
+
error.message.includes("Cannot find package")
|
|
97
119
|
);
|
|
98
120
|
}
|
|
99
121
|
|
|
@@ -118,9 +140,71 @@ function toLaunchOptions(options: BrowserClientOptions): LaunchOptions {
|
|
|
118
140
|
};
|
|
119
141
|
}
|
|
120
142
|
|
|
143
|
+
class PlaywrightBrowserLocator implements BrowserLocator {
|
|
144
|
+
constructor(private readonly locator: Locator) {}
|
|
145
|
+
|
|
146
|
+
async click(): Promise<void> {
|
|
147
|
+
await this.locator.click();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async fill(text: string): Promise<void> {
|
|
151
|
+
await this.locator.fill(text);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async textContent(): Promise<string | null> {
|
|
155
|
+
return await this.locator.textContent();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async waitFor(options?: { timeout?: number }): Promise<void> {
|
|
159
|
+
await this.locator.waitFor(options);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class PlaywrightBrowserFrame implements BrowserFrame {
|
|
164
|
+
constructor(private readonly frame: Frame) {}
|
|
165
|
+
|
|
166
|
+
get id(): string {
|
|
167
|
+
return this.frame.name() || this.frame.url();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
get name(): string | undefined {
|
|
171
|
+
const name = this.frame.name();
|
|
172
|
+
return name.length > 0 ? name : undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get parentId(): string | undefined {
|
|
176
|
+
const parent = this.frame.parentFrame();
|
|
177
|
+
return parent ? parent.name() || parent.url() : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async url(): Promise<string> {
|
|
181
|
+
return this.frame.url();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async title(): Promise<string> {
|
|
185
|
+
return await this.frame.evaluate("document.title");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async content(): Promise<string> {
|
|
189
|
+
return await this.frame.content();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async evaluate<T>(fn: string | (() => T)): Promise<T> {
|
|
193
|
+
if (typeof fn === "string") {
|
|
194
|
+
return await this.frame.evaluate(fn);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return await this.frame.evaluate(fn);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
locator(selector: string): BrowserLocator {
|
|
201
|
+
return new PlaywrightBrowserLocator(this.frame.locator(selector));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
121
205
|
async function loadPlaywright(): Promise<PlaywrightModule> {
|
|
122
206
|
try {
|
|
123
|
-
|
|
207
|
+
await importOptionalModule<PlaywrightModule>("playwright");
|
|
124
208
|
} catch (error) {
|
|
125
209
|
if (isModuleNotFoundError(error)) {
|
|
126
210
|
throw new ProviderError("Playwright is not installed", {
|
|
@@ -148,16 +232,31 @@ async function loadPlaywright(): Promise<PlaywrightModule> {
|
|
|
148
232
|
}
|
|
149
233
|
}
|
|
150
234
|
|
|
151
|
-
|
|
235
|
+
const playwrightExtraStealthLaunchers = new WeakSet<object>();
|
|
236
|
+
|
|
237
|
+
async function loadPlaywrightExtra(): Promise<PlaywrightExtraModule> {
|
|
238
|
+
try {
|
|
239
|
+
require("playwright");
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (isModuleNotFoundError(error)) {
|
|
242
|
+
throw new ProviderError("Playwright is not installed", {
|
|
243
|
+
cause: error instanceof Error ? error : undefined,
|
|
244
|
+
fix: "Run: bun add playwright",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
|
|
152
251
|
try {
|
|
153
252
|
return unwrapModuleDefault(
|
|
154
|
-
await importOptionalModule<
|
|
253
|
+
await importOptionalModule<PlaywrightExtraModule>("playwright-extra"),
|
|
155
254
|
);
|
|
156
255
|
} catch (error) {
|
|
157
256
|
if (isModuleNotFoundError(error)) {
|
|
158
|
-
throw new ProviderError("playwright-
|
|
257
|
+
throw new ProviderError("playwright-extra is not installed", {
|
|
159
258
|
cause: error instanceof Error ? error : undefined,
|
|
160
|
-
fix: "Run: bun add playwright-stealth",
|
|
259
|
+
fix: "Run: bun add playwright-extra puppeteer-extra-plugin-stealth",
|
|
161
260
|
});
|
|
162
261
|
}
|
|
163
262
|
|
|
@@ -165,6 +264,45 @@ async function loadPlaywrightStealth(): Promise<PlaywrightStealthModule> {
|
|
|
165
264
|
}
|
|
166
265
|
}
|
|
167
266
|
|
|
267
|
+
async function loadStealthPluginFactory(): Promise<StealthPluginFactory> {
|
|
268
|
+
try {
|
|
269
|
+
return unwrapModuleDefault(
|
|
270
|
+
await importOptionalModule<StealthPluginFactory>(
|
|
271
|
+
"puppeteer-extra-plugin-stealth",
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (isModuleNotFoundError(error)) {
|
|
276
|
+
throw new ProviderError(
|
|
277
|
+
"puppeteer-extra-plugin-stealth is not installed",
|
|
278
|
+
{
|
|
279
|
+
cause: error instanceof Error ? error : undefined,
|
|
280
|
+
fix: "Run: bun add playwright-extra puppeteer-extra-plugin-stealth",
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function loadChromiumLauncher(
|
|
290
|
+
options: BrowserClientOptions,
|
|
291
|
+
): Promise<PlaywrightModule["chromium"]> {
|
|
292
|
+
if (!(options.stealth ?? true)) {
|
|
293
|
+
return (await loadPlaywright()).chromium;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const playwrightExtra = await loadPlaywrightExtra();
|
|
297
|
+
if (!playwrightExtraStealthLaunchers.has(playwrightExtra.chromium)) {
|
|
298
|
+
const createStealthPlugin = await loadStealthPluginFactory();
|
|
299
|
+
playwrightExtra.chromium.use(createStealthPlugin());
|
|
300
|
+
playwrightExtraStealthLaunchers.add(playwrightExtra.chromium);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return playwrightExtra.chromium;
|
|
304
|
+
}
|
|
305
|
+
|
|
168
306
|
async function loadNodriver(): Promise<void> {
|
|
169
307
|
try {
|
|
170
308
|
await importOptionalModule("nodriver");
|
|
@@ -196,6 +334,7 @@ async function loadSeleniumBase(): Promise<void> {
|
|
|
196
334
|
}
|
|
197
335
|
|
|
198
336
|
class PlaywrightBrowserPage implements BrowserPageContract {
|
|
337
|
+
readonly id = "main";
|
|
199
338
|
readonly pageId?: string;
|
|
200
339
|
|
|
201
340
|
constructor(private readonly page: Page) {}
|
|
@@ -232,6 +371,22 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
232
371
|
await this.page.fill(selector, text);
|
|
233
372
|
}
|
|
234
373
|
|
|
374
|
+
async frames(): Promise<BrowserFrame[]> {
|
|
375
|
+
return this.page.frames().map((frame) => new PlaywrightBrowserFrame(frame));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
locator(selector: string): BrowserLocator {
|
|
379
|
+
return new PlaywrightBrowserLocator(this.page.locator(selector));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async title(): Promise<string> {
|
|
383
|
+
return await this.page.title();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async url(): Promise<string> {
|
|
387
|
+
return this.page.url();
|
|
388
|
+
}
|
|
389
|
+
|
|
235
390
|
async content(): Promise<string> {
|
|
236
391
|
return await this.page.content();
|
|
237
392
|
}
|
|
@@ -256,10 +411,8 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
256
411
|
return this.browser;
|
|
257
412
|
}
|
|
258
413
|
|
|
259
|
-
const
|
|
260
|
-
this.browser = await
|
|
261
|
-
toLaunchOptions(this.options),
|
|
262
|
-
);
|
|
414
|
+
const chromium = await loadChromiumLauncher(this.options);
|
|
415
|
+
this.browser = await chromium.launch(toLaunchOptions(this.options));
|
|
263
416
|
return this.browser;
|
|
264
417
|
}
|
|
265
418
|
|
|
@@ -267,14 +420,35 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
267
420
|
const browser = await this.ensureBrowser();
|
|
268
421
|
const page = await browser.newPage();
|
|
269
422
|
|
|
270
|
-
if (this.options.stealth ?? true) {
|
|
271
|
-
const { stealth } = await loadPlaywrightStealth();
|
|
272
|
-
await stealth(page);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
423
|
return new PlaywrightBrowserPage(page);
|
|
276
424
|
}
|
|
277
425
|
|
|
426
|
+
async rawPage(): Promise<BrowserPageContract> {
|
|
427
|
+
throw new ProviderError("ctx.browser.rawPage() requires a CDP pool", {
|
|
428
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
429
|
+
fix: "Set APIFUSE__CDP_POOL__URL and use the SDK CDP pool-backed browser runtime. Local Chromium launch is not allowed for rawPage().",
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async withIsolatedContext<T>(
|
|
434
|
+
handler: (page: BrowserPageContract) => Promise<T>,
|
|
435
|
+
): Promise<T> {
|
|
436
|
+
const browser = await this.ensureBrowser();
|
|
437
|
+
const context = await browser.newContext();
|
|
438
|
+
const page = await context.newPage();
|
|
439
|
+
const browserPage = new PlaywrightBrowserPage(page);
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
return await handler(browserPage);
|
|
443
|
+
} finally {
|
|
444
|
+
try {
|
|
445
|
+
await browserPage.close();
|
|
446
|
+
} finally {
|
|
447
|
+
await context.close();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
278
452
|
async close(): Promise<void> {
|
|
279
453
|
const browser = this.browser;
|
|
280
454
|
this.browser = null;
|
|
@@ -287,8 +461,25 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
287
461
|
}
|
|
288
462
|
}
|
|
289
463
|
|
|
464
|
+
function normalizeWebSocketEndpoint(endpoint: string): string {
|
|
465
|
+
const url = new URL(endpoint);
|
|
466
|
+
if (url.protocol === "http:") {
|
|
467
|
+
url.protocol = "ws:";
|
|
468
|
+
return url.toString();
|
|
469
|
+
}
|
|
470
|
+
if (url.protocol === "https:") {
|
|
471
|
+
url.protocol = "wss:";
|
|
472
|
+
return url.toString();
|
|
473
|
+
}
|
|
474
|
+
if (url.protocol === "ws:" || url.protocol === "wss:") {
|
|
475
|
+
return endpoint;
|
|
476
|
+
}
|
|
477
|
+
throw new Error(`Unsupported WebSocket endpoint protocol: ${url.protocol}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
290
480
|
class JsonRpcWebSocketClient {
|
|
291
481
|
private nextId = 1;
|
|
482
|
+
private readonly endpoint: string;
|
|
292
483
|
private readonly listeners = new Map<
|
|
293
484
|
string,
|
|
294
485
|
Set<(params: unknown) => void>
|
|
@@ -303,7 +494,9 @@ class JsonRpcWebSocketClient {
|
|
|
303
494
|
private socket?: WebSocket;
|
|
304
495
|
private socketPromise?: Promise<WebSocket>;
|
|
305
496
|
|
|
306
|
-
constructor(
|
|
497
|
+
constructor(endpoint: string) {
|
|
498
|
+
this.endpoint = normalizeWebSocketEndpoint(endpoint);
|
|
499
|
+
}
|
|
307
500
|
|
|
308
501
|
on(method: string, listener: (params: unknown) => void): () => void {
|
|
309
502
|
const listeners = this.listeners.get(method) ?? new Set();
|
|
@@ -426,16 +619,233 @@ class JsonRpcWebSocketClient {
|
|
|
426
619
|
}
|
|
427
620
|
}
|
|
428
621
|
|
|
622
|
+
function flattenCdpFrameTree(
|
|
623
|
+
node: CdpFrameTreeNode | undefined,
|
|
624
|
+
out: CdpFrameTreeNode["frame"][] = [],
|
|
625
|
+
): CdpFrameTreeNode["frame"][] {
|
|
626
|
+
if (!node) {
|
|
627
|
+
return out;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
out.push(node.frame);
|
|
631
|
+
for (const child of node.childFrames ?? []) {
|
|
632
|
+
flattenCdpFrameTree(child, out);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return out;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
639
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function parsePoolAcquireResponse(value: unknown): PoolAcquireResponse {
|
|
643
|
+
if (
|
|
644
|
+
!isRecord(value) ||
|
|
645
|
+
typeof value.pageId !== "string" ||
|
|
646
|
+
typeof value.wsEndpoint !== "string"
|
|
647
|
+
) {
|
|
648
|
+
throw new ProviderError("CDP Pool returned an invalid acquire response", {
|
|
649
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (
|
|
654
|
+
value.browserContextId !== undefined &&
|
|
655
|
+
typeof value.browserContextId !== "string"
|
|
656
|
+
) {
|
|
657
|
+
throw new ProviderError("CDP Pool returned an invalid acquire response", {
|
|
658
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
...(value.browserContextId
|
|
664
|
+
? { browserContextId: value.browserContextId }
|
|
665
|
+
: {}),
|
|
666
|
+
pageId: value.pageId,
|
|
667
|
+
wsEndpoint: value.wsEndpoint,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function parseCdpFrameTreeNode(value: unknown): CdpFrameTreeNode | undefined {
|
|
672
|
+
if (!isRecord(value) || !isRecord(value.frame)) {
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const frameId = value.frame.id;
|
|
677
|
+
if (typeof frameId !== "string") {
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const childFrames = Array.isArray(value.childFrames)
|
|
682
|
+
? value.childFrames
|
|
683
|
+
.map(parseCdpFrameTreeNode)
|
|
684
|
+
.filter((child): child is CdpFrameTreeNode => child !== undefined)
|
|
685
|
+
: undefined;
|
|
686
|
+
|
|
687
|
+
return {
|
|
688
|
+
frame: {
|
|
689
|
+
id: frameId,
|
|
690
|
+
name: typeof value.frame.name === "string" ? value.frame.name : undefined,
|
|
691
|
+
parentId:
|
|
692
|
+
typeof value.frame.parentId === "string"
|
|
693
|
+
? value.frame.parentId
|
|
694
|
+
: undefined,
|
|
695
|
+
url: typeof value.frame.url === "string" ? value.frame.url : undefined,
|
|
696
|
+
},
|
|
697
|
+
...(childFrames ? { childFrames } : {}),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function getCdpExecutionContext(params: unknown): {
|
|
702
|
+
frameId?: string;
|
|
703
|
+
id?: number;
|
|
704
|
+
} {
|
|
705
|
+
if (!isRecord(params) || !isRecord(params.context)) {
|
|
706
|
+
return {};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const contextId = params.context.id;
|
|
710
|
+
const auxData = params.context.auxData;
|
|
711
|
+
return {
|
|
712
|
+
frameId:
|
|
713
|
+
isRecord(auxData) && typeof auxData.frameId === "string"
|
|
714
|
+
? auxData.frameId
|
|
715
|
+
: undefined,
|
|
716
|
+
id: typeof contextId === "number" ? contextId : undefined,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
class CdpBrowserLocator implements BrowserLocator {
|
|
721
|
+
constructor(
|
|
722
|
+
private readonly frame: {
|
|
723
|
+
evaluate<T>(fn: string | (() => T)): Promise<T>;
|
|
724
|
+
waitForSelector?(
|
|
725
|
+
selector: string,
|
|
726
|
+
options?: { timeout?: number },
|
|
727
|
+
): Promise<void>;
|
|
728
|
+
},
|
|
729
|
+
private readonly selector: string,
|
|
730
|
+
) {}
|
|
731
|
+
|
|
732
|
+
async click(): Promise<void> {
|
|
733
|
+
await this.waitFor();
|
|
734
|
+
await this.frame.evaluate(
|
|
735
|
+
`(() => {
|
|
736
|
+
const element = document.querySelector(${JSON.stringify(this.selector)});
|
|
737
|
+
if (!(element instanceof HTMLElement)) {
|
|
738
|
+
throw new Error(${JSON.stringify(`Selector not found: ${this.selector}`)});
|
|
739
|
+
}
|
|
740
|
+
element.click();
|
|
741
|
+
})()`,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
async fill(text: string): Promise<void> {
|
|
746
|
+
await this.waitFor();
|
|
747
|
+
await this.frame.evaluate(
|
|
748
|
+
`(() => {
|
|
749
|
+
const element = document.querySelector(${JSON.stringify(this.selector)});
|
|
750
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
751
|
+
throw new Error(${JSON.stringify(`Unsupported input target: ${this.selector}`)});
|
|
752
|
+
}
|
|
753
|
+
element.focus();
|
|
754
|
+
element.value = ${JSON.stringify(text)};
|
|
755
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
756
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
757
|
+
})()`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async textContent(): Promise<string | null> {
|
|
762
|
+
return await this.frame.evaluate<string | null>(
|
|
763
|
+
`document.querySelector(${JSON.stringify(this.selector)})?.textContent ?? null`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async waitFor(options?: { timeout?: number }): Promise<void> {
|
|
768
|
+
if (this.frame.waitForSelector) {
|
|
769
|
+
await this.frame.waitForSelector(this.selector, options);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
774
|
+
const deadline = Date.now() + timeout;
|
|
775
|
+
while (Date.now() < deadline) {
|
|
776
|
+
const exists = await this.frame.evaluate<boolean>(
|
|
777
|
+
`Boolean(document.querySelector(${JSON.stringify(this.selector)}))`,
|
|
778
|
+
);
|
|
779
|
+
if (exists) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
await delay(SELECTOR_POLL_INTERVAL_MS);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
throw new Error(`Timed out waiting for selector: ${this.selector}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
class CdpBrowserFrame implements BrowserFrame {
|
|
790
|
+
constructor(
|
|
791
|
+
readonly id: string,
|
|
792
|
+
private readonly page: CdpPoolBrowserPage,
|
|
793
|
+
private readonly initialUrl = "",
|
|
794
|
+
readonly name?: string,
|
|
795
|
+
readonly parentId?: string,
|
|
796
|
+
) {}
|
|
797
|
+
|
|
798
|
+
async url(): Promise<string> {
|
|
799
|
+
const evaluatedUrl = await this.evaluate<string | undefined>(
|
|
800
|
+
"window.location.href",
|
|
801
|
+
);
|
|
802
|
+
return evaluatedUrl || this.initialUrl;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async title(): Promise<string> {
|
|
806
|
+
return await this.evaluate<string>("document.title");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async content(): Promise<string> {
|
|
810
|
+
return await this.evaluate<string>("document.documentElement.outerHTML");
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async evaluate<T>(fn: string | (() => T)): Promise<T> {
|
|
814
|
+
return await this.page.evaluateInFrame<T>(this.id, fn);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
locator(selector: string): BrowserLocator {
|
|
818
|
+
return new CdpBrowserLocator(this, selector);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
async waitForSelector(
|
|
822
|
+
selector: string,
|
|
823
|
+
options?: { timeout?: number },
|
|
824
|
+
): Promise<void> {
|
|
825
|
+
await this.page.waitForSelectorInFrame(this.id, selector, options);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
fallbackUrl(): string {
|
|
829
|
+
return this.initialUrl;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
429
833
|
class CdpPoolBrowserPage implements BrowserPageContract {
|
|
430
834
|
private closed = false;
|
|
431
835
|
private initialized = false;
|
|
836
|
+
private readonly frameExecutionContexts = new Map<string, number>();
|
|
432
837
|
|
|
433
838
|
constructor(
|
|
434
839
|
readonly pageId: string,
|
|
840
|
+
private readonly browserContextId: string | undefined,
|
|
435
841
|
private readonly pageClient: JsonRpcWebSocketClient,
|
|
436
|
-
private readonly release: (
|
|
842
|
+
private readonly release: (request: PoolReleaseRequest) => Promise<void>,
|
|
437
843
|
) {}
|
|
438
844
|
|
|
845
|
+
get id(): string {
|
|
846
|
+
return this.pageId;
|
|
847
|
+
}
|
|
848
|
+
|
|
439
849
|
async goto(url: string): Promise<void> {
|
|
440
850
|
await this.initialize();
|
|
441
851
|
const startedAt = Date.now();
|
|
@@ -457,8 +867,49 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
457
867
|
|
|
458
868
|
async evaluate<T>(fn: string | (() => T)): Promise<T> {
|
|
459
869
|
await this.initialize();
|
|
870
|
+
return await this.evaluateWithContext<T>(fn);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async evaluateInFrame<T>(
|
|
874
|
+
frameId: string,
|
|
875
|
+
fn: string | (() => T),
|
|
876
|
+
): Promise<T> {
|
|
877
|
+
await this.initialize();
|
|
878
|
+
const contextId = await this.getFrameExecutionContextId(frameId);
|
|
879
|
+
return await this.evaluateWithContext<T>(fn, contextId);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
async waitForSelectorInFrame(
|
|
883
|
+
frameId: string,
|
|
884
|
+
selector: string,
|
|
885
|
+
options?: { timeout?: number },
|
|
886
|
+
): Promise<void> {
|
|
887
|
+
const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
888
|
+
const deadline = Date.now() + timeout;
|
|
889
|
+
|
|
890
|
+
while (Date.now() < deadline) {
|
|
891
|
+
const exists = await this.evaluateInFrame<boolean>(
|
|
892
|
+
frameId,
|
|
893
|
+
`Boolean(document.querySelector(${JSON.stringify(selector)}))`,
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
if (exists) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
await delay(SELECTOR_POLL_INTERVAL_MS);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
throw new Error(`Timed out waiting for selector: ${selector}`);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private async evaluateWithContext<T>(
|
|
907
|
+
fn: string | (() => T),
|
|
908
|
+
contextId?: number,
|
|
909
|
+
): Promise<T> {
|
|
460
910
|
const result = await this.pageClient.send("Runtime.evaluate", {
|
|
461
911
|
awaitPromise: true,
|
|
912
|
+
...(contextId === undefined ? {} : { contextId }),
|
|
462
913
|
expression: formatExpression(fn),
|
|
463
914
|
returnByValue: true,
|
|
464
915
|
});
|
|
@@ -539,6 +990,37 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
539
990
|
);
|
|
540
991
|
}
|
|
541
992
|
|
|
993
|
+
async frames(): Promise<BrowserFrame[]> {
|
|
994
|
+
await this.initialize();
|
|
995
|
+
const result = await this.pageClient.send("Page.getFrameTree");
|
|
996
|
+
const frames = flattenCdpFrameTree(parseCdpFrameTreeNode(result.frameTree));
|
|
997
|
+
return frames.map(
|
|
998
|
+
(frame) =>
|
|
999
|
+
new CdpBrowserFrame(
|
|
1000
|
+
frame.id,
|
|
1001
|
+
this,
|
|
1002
|
+
frame.url ?? "",
|
|
1003
|
+
frame.name,
|
|
1004
|
+
frame.parentId,
|
|
1005
|
+
),
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
locator(selector: string): BrowserLocator {
|
|
1010
|
+
return new CdpBrowserLocator(this, selector);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async title(): Promise<string> {
|
|
1014
|
+
return await this.evaluate<string>("document.title");
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
async url(): Promise<string> {
|
|
1018
|
+
const [mainFrame] = await this.frames();
|
|
1019
|
+
const frameUrl =
|
|
1020
|
+
mainFrame instanceof CdpBrowserFrame ? mainFrame.fallbackUrl() : "";
|
|
1021
|
+
return frameUrl || (await this.evaluate<string>("window.location.href"));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
542
1024
|
async content(): Promise<string> {
|
|
543
1025
|
return await this.evaluate<string>("document.documentElement.outerHTML");
|
|
544
1026
|
}
|
|
@@ -562,7 +1044,12 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
562
1044
|
this.closed = true;
|
|
563
1045
|
|
|
564
1046
|
try {
|
|
565
|
-
await this.release(
|
|
1047
|
+
await this.release({
|
|
1048
|
+
...(this.browserContextId
|
|
1049
|
+
? { browserContextId: this.browserContextId }
|
|
1050
|
+
: {}),
|
|
1051
|
+
pageId: this.pageId,
|
|
1052
|
+
});
|
|
566
1053
|
} finally {
|
|
567
1054
|
await this.pageClient.close();
|
|
568
1055
|
}
|
|
@@ -573,11 +1060,39 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
573
1060
|
return;
|
|
574
1061
|
}
|
|
575
1062
|
|
|
1063
|
+
this.pageClient.on("Runtime.executionContextCreated", (params) => {
|
|
1064
|
+
const context = getCdpExecutionContext(params);
|
|
1065
|
+
if (context.frameId && context.id !== undefined) {
|
|
1066
|
+
this.frameExecutionContexts.set(context.frameId, context.id);
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
576
1069
|
await this.pageClient.send("Page.enable");
|
|
577
1070
|
await this.pageClient.send("Runtime.enable");
|
|
578
1071
|
this.initialized = true;
|
|
579
1072
|
}
|
|
580
1073
|
|
|
1074
|
+
private async getFrameExecutionContextId(frameId: string): Promise<number> {
|
|
1075
|
+
const existing = this.frameExecutionContexts.get(frameId);
|
|
1076
|
+
if (existing !== undefined) {
|
|
1077
|
+
return existing;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const result = await this.pageClient.send("Page.createIsolatedWorld", {
|
|
1081
|
+
frameId,
|
|
1082
|
+
grantUniveralAccess: true,
|
|
1083
|
+
worldName: "apifuse-provider-sdk",
|
|
1084
|
+
});
|
|
1085
|
+
const contextId = result.executionContextId;
|
|
1086
|
+
if (typeof contextId !== "number") {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
`Unable to resolve execution context for frame: ${frameId}`,
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
this.frameExecutionContexts.set(frameId, contextId);
|
|
1093
|
+
return contextId;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
581
1096
|
private async waitForDocumentReady(
|
|
582
1097
|
deadline: number,
|
|
583
1098
|
isLoadEventSeen: () => boolean,
|
|
@@ -598,6 +1113,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
598
1113
|
}
|
|
599
1114
|
|
|
600
1115
|
class CdpPoolBrowserClient implements SupportedBrowserClient {
|
|
1116
|
+
private readonly allowedHosts: string[];
|
|
601
1117
|
private readonly poolClient: JsonRpcWebSocketClient;
|
|
602
1118
|
readonly engine = "playwright-stealth" satisfies BrowserEngine;
|
|
603
1119
|
|
|
@@ -608,29 +1124,65 @@ class CdpPoolBrowserClient implements SupportedBrowserClient {
|
|
|
608
1124
|
});
|
|
609
1125
|
}
|
|
610
1126
|
|
|
1127
|
+
this.allowedHosts = [...new Set(options.allowedHosts ?? [])];
|
|
611
1128
|
this.poolClient = new JsonRpcWebSocketClient(options.cdpUrl);
|
|
612
1129
|
}
|
|
613
1130
|
|
|
614
1131
|
async newPage(): Promise<BrowserPageContract> {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1132
|
+
return await this.acquirePage({ isolatedContext: true });
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
private async acquirePage(options?: {
|
|
1136
|
+
isolatedContext?: boolean;
|
|
1137
|
+
}): Promise<BrowserPageContract> {
|
|
1138
|
+
const acquireResult = parsePoolAcquireResponse(
|
|
1139
|
+
await this.poolClient.send("acquire", {
|
|
1140
|
+
...(this.allowedHosts.length > 0
|
|
1141
|
+
? { allowedHosts: this.allowedHosts }
|
|
1142
|
+
: {}),
|
|
1143
|
+
...(options?.isolatedContext
|
|
1144
|
+
? { isolationMode: "browserContext" }
|
|
1145
|
+
: {}),
|
|
1146
|
+
}),
|
|
1147
|
+
);
|
|
618
1148
|
const pageClient = new JsonRpcWebSocketClient(acquireResult.wsEndpoint);
|
|
619
1149
|
const page = new CdpPoolBrowserPage(
|
|
620
1150
|
acquireResult.pageId,
|
|
1151
|
+
acquireResult.browserContextId,
|
|
621
1152
|
pageClient,
|
|
622
|
-
async (
|
|
623
|
-
await this.poolClient.send("release",
|
|
1153
|
+
async (request) => {
|
|
1154
|
+
await this.poolClient.send("release", request);
|
|
624
1155
|
},
|
|
625
1156
|
);
|
|
626
1157
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1158
|
+
try {
|
|
1159
|
+
await page.evaluate(
|
|
1160
|
+
`window.navigator.webdriver === true ? Object.defineProperty(window.navigator, "webdriver", { configurable: true, get: () => undefined }) : undefined`,
|
|
1161
|
+
);
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
await page.close().catch(() => undefined);
|
|
1164
|
+
throw error;
|
|
1165
|
+
}
|
|
630
1166
|
|
|
631
1167
|
return page;
|
|
632
1168
|
}
|
|
633
1169
|
|
|
1170
|
+
async rawPage(): Promise<BrowserPageContract> {
|
|
1171
|
+
return await this.newPage();
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
async withIsolatedContext<T>(
|
|
1175
|
+
handler: (page: BrowserPageContract) => Promise<T>,
|
|
1176
|
+
): Promise<T> {
|
|
1177
|
+
const page = await this.acquirePage({ isolatedContext: true });
|
|
1178
|
+
|
|
1179
|
+
try {
|
|
1180
|
+
return await handler(page);
|
|
1181
|
+
} finally {
|
|
1182
|
+
await page.close();
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
634
1186
|
async close(): Promise<void> {
|
|
635
1187
|
await this.poolClient.close();
|
|
636
1188
|
}
|
|
@@ -655,6 +1207,14 @@ class UnsupportedBrowserEngineClient implements SupportedBrowserClient {
|
|
|
655
1207
|
});
|
|
656
1208
|
}
|
|
657
1209
|
|
|
1210
|
+
async rawPage(): Promise<never> {
|
|
1211
|
+
return await this.newPage();
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async withIsolatedContext<T>(): Promise<T> {
|
|
1215
|
+
return await this.newPage();
|
|
1216
|
+
}
|
|
1217
|
+
|
|
658
1218
|
async close(): Promise<void> {}
|
|
659
1219
|
}
|
|
660
1220
|
|
|
@@ -680,6 +1240,9 @@ function createSeleniumUCClient(): SupportedBrowserClient {
|
|
|
680
1240
|
|
|
681
1241
|
export class BrowserClient implements BrowserClientContract {
|
|
682
1242
|
private readonly client: SupportedBrowserClient;
|
|
1243
|
+
private readonly cdpUrl?: string;
|
|
1244
|
+
private activePage?: BrowserPageContract;
|
|
1245
|
+
private readonly activePages = new Set<BrowserPageContract>();
|
|
683
1246
|
private readonly _engine: BrowserEngine;
|
|
684
1247
|
|
|
685
1248
|
constructor(options: BrowserClientOptions = {}) {
|
|
@@ -689,6 +1252,17 @@ export class BrowserClient implements BrowserClientContract {
|
|
|
689
1252
|
};
|
|
690
1253
|
const engine = resolvedOptions.engine ?? "playwright-stealth";
|
|
691
1254
|
this._engine = engine;
|
|
1255
|
+
this.cdpUrl = resolvedOptions.cdpUrl;
|
|
1256
|
+
|
|
1257
|
+
if (resolvedOptions.requireCdpPool && !resolvedOptions.cdpUrl) {
|
|
1258
|
+
throw new ProviderError(
|
|
1259
|
+
"Managed CDP Pool is required for browser providers in production",
|
|
1260
|
+
{
|
|
1261
|
+
code: "BROWSER_CDP_POOL_REQUIRED",
|
|
1262
|
+
fix: "Set APIFUSE__CDP_POOL__URL for deployed browser providers. Local standalone development may omit it.",
|
|
1263
|
+
},
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
692
1266
|
|
|
693
1267
|
switch (engine) {
|
|
694
1268
|
case "nodriver":
|
|
@@ -708,15 +1282,152 @@ export class BrowserClient implements BrowserClientContract {
|
|
|
708
1282
|
return this._engine;
|
|
709
1283
|
}
|
|
710
1284
|
|
|
711
|
-
async newPage(): Promise<
|
|
712
|
-
|
|
1285
|
+
async newPage(): Promise<BrowserPageContract> {
|
|
1286
|
+
const page = await this.client.newPage();
|
|
1287
|
+
return this.activatePage(page);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
async rawPage(): Promise<BrowserPageContract> {
|
|
1291
|
+
if (!this.cdpUrl) {
|
|
1292
|
+
throw new ProviderError("ctx.browser.rawPage() requires a CDP pool", {
|
|
1293
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
1294
|
+
fix: "Set APIFUSE__CDP_POOL__URL. The SDK escape hatch is CDP pool-backed only and never launches local Chromium.",
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const page = await this.client.rawPage();
|
|
1299
|
+
return this.activatePage(page);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
async withIsolatedContext<T>(
|
|
1303
|
+
handler: (page: BrowserPageContract) => Promise<T>,
|
|
1304
|
+
): Promise<T> {
|
|
1305
|
+
const previousActivePage = this.activePage;
|
|
1306
|
+
let trackedPage: BrowserPageContract | undefined;
|
|
1307
|
+
|
|
1308
|
+
return await this.client.withIsolatedContext(async (page) => {
|
|
1309
|
+
trackedPage = this.activatePage(page);
|
|
1310
|
+
try {
|
|
1311
|
+
return await handler(trackedPage);
|
|
1312
|
+
} finally {
|
|
1313
|
+
this.activePages.delete(trackedPage);
|
|
1314
|
+
if (this.activePage === trackedPage) {
|
|
1315
|
+
this.activePage = previousActivePage;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
private activatePage(page: BrowserPageContract): BrowserPageContract {
|
|
1322
|
+
let closed = false;
|
|
1323
|
+
const originalClose = page.close.bind(page);
|
|
1324
|
+
const trackedPage = new Proxy(page, {
|
|
1325
|
+
get: (target, property, receiver) => {
|
|
1326
|
+
if (property === "close") {
|
|
1327
|
+
return async () => {
|
|
1328
|
+
if (closed) return;
|
|
1329
|
+
closed = true;
|
|
1330
|
+
try {
|
|
1331
|
+
await originalClose();
|
|
1332
|
+
} finally {
|
|
1333
|
+
this.activePages.delete(trackedPage);
|
|
1334
|
+
if (this.activePage === trackedPage) {
|
|
1335
|
+
this.activePage = undefined;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const value = Reflect.get(target, property, receiver);
|
|
1341
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
1342
|
+
},
|
|
1343
|
+
});
|
|
1344
|
+
this.activePages.add(trackedPage);
|
|
1345
|
+
this.activePage = trackedPage;
|
|
1346
|
+
return trackedPage;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
async solveChallenge(
|
|
1350
|
+
request: BrowserChallengeRequest,
|
|
1351
|
+
): Promise<BrowserChallengeResult> {
|
|
1352
|
+
if (request.type !== "recaptcha") {
|
|
1353
|
+
throw new ProviderError(
|
|
1354
|
+
`Unsupported browser challenge: ${request.type}`,
|
|
1355
|
+
{
|
|
1356
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
1357
|
+
},
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (this.activePage) {
|
|
1362
|
+
return await solveRecaptchaChallenge(this.activePage, request);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const page = await this.client.newPage();
|
|
1366
|
+
try {
|
|
1367
|
+
return await solveRecaptchaChallenge(page, request);
|
|
1368
|
+
} finally {
|
|
1369
|
+
if (this.activePage === page) {
|
|
1370
|
+
this.activePage = undefined;
|
|
1371
|
+
}
|
|
1372
|
+
await page.close();
|
|
1373
|
+
}
|
|
713
1374
|
}
|
|
714
1375
|
|
|
715
1376
|
async close(): Promise<void> {
|
|
716
|
-
|
|
1377
|
+
const pages = Array.from(this.activePages);
|
|
1378
|
+
this.activePages.clear();
|
|
1379
|
+
this.activePage = undefined;
|
|
1380
|
+
try {
|
|
1381
|
+
await Promise.all(pages.map((page) => page.close()));
|
|
1382
|
+
} finally {
|
|
1383
|
+
await this.client.close();
|
|
1384
|
+
}
|
|
717
1385
|
}
|
|
718
1386
|
}
|
|
719
1387
|
|
|
1388
|
+
async function solveRecaptchaChallenge(
|
|
1389
|
+
page: BrowserPageContract,
|
|
1390
|
+
request: BrowserChallengeRequest,
|
|
1391
|
+
): Promise<BrowserChallengeResult> {
|
|
1392
|
+
const timeout = request.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1393
|
+
const deadline = Date.now() + timeout;
|
|
1394
|
+
|
|
1395
|
+
while (Date.now() < deadline) {
|
|
1396
|
+
const frames = await page.frames();
|
|
1397
|
+
const recaptchaFrame = await findRecaptchaFrame(frames, request.siteKey);
|
|
1398
|
+
if (recaptchaFrame) {
|
|
1399
|
+
await recaptchaFrame.locator("#recaptcha-anchor").click();
|
|
1400
|
+
return {
|
|
1401
|
+
type: "recaptcha",
|
|
1402
|
+
solved: true,
|
|
1403
|
+
frameUrl: await recaptchaFrame.url(),
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
await delay(SELECTOR_POLL_INTERVAL_MS);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
throw new Error("Timed out waiting for reCAPTCHA iframe");
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
async function findRecaptchaFrame(
|
|
1414
|
+
frames: BrowserFrame[],
|
|
1415
|
+
siteKey?: string,
|
|
1416
|
+
): Promise<BrowserFrame | undefined> {
|
|
1417
|
+
for (const frame of frames) {
|
|
1418
|
+
const url = await frame.url();
|
|
1419
|
+
const matchesRecaptcha =
|
|
1420
|
+
url.includes("google.com/recaptcha") ||
|
|
1421
|
+
url.includes("recaptcha.net/recaptcha");
|
|
1422
|
+
const matchesSiteKey = !siteKey || url.includes(siteKey);
|
|
1423
|
+
if (matchesRecaptcha && matchesSiteKey) {
|
|
1424
|
+
return frame;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
return undefined;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
720
1431
|
export function createBrowserClient(
|
|
721
1432
|
options: BrowserClientOptions = {},
|
|
722
1433
|
): BrowserClient {
|