@apifuse/provider-sdk 2.1.0-beta.6 → 2.1.0-beta.9
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/CHANGELOG.md +15 -0
- package/bin/apifuse-perf.ts +18 -9
- 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/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 +109 -107
- package/src/runtime/stealth.ts +8 -1
- package/src/types.ts +2 -0
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { ProviderError } from "../errors";
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
5
|
+
const SELECTOR_POLL_INTERVAL_MS = 100;
|
|
6
|
+
function getDefaultCdpPoolUrl(env = process.env) {
|
|
7
|
+
return env.APIFUSE__CDP_POOL__URL;
|
|
8
|
+
}
|
|
9
|
+
async function importOptionalModule(moduleName) {
|
|
10
|
+
return (await import(moduleName));
|
|
11
|
+
}
|
|
12
|
+
function unwrapModuleDefault(module) {
|
|
13
|
+
if ("default" in module) {
|
|
14
|
+
const defaultExport = module.default;
|
|
15
|
+
if ((typeof defaultExport === "object" && defaultExport !== null) ||
|
|
16
|
+
typeof defaultExport === "function") {
|
|
17
|
+
return defaultExport;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return module;
|
|
21
|
+
}
|
|
22
|
+
function isModuleNotFoundError(error) {
|
|
23
|
+
if (!(error instanceof Error)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const code = "code" in error ? error.code : undefined;
|
|
27
|
+
return (code === "MODULE_NOT_FOUND" ||
|
|
28
|
+
code === "ERR_MODULE_NOT_FOUND" ||
|
|
29
|
+
error.message.includes("Cannot find module") ||
|
|
30
|
+
error.message.includes("Cannot find package"));
|
|
31
|
+
}
|
|
32
|
+
function delay(ms) {
|
|
33
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
}
|
|
35
|
+
function formatExpression(fn) {
|
|
36
|
+
if (typeof fn === "string") {
|
|
37
|
+
return fn;
|
|
38
|
+
}
|
|
39
|
+
return `(${fn.toString()})()`;
|
|
40
|
+
}
|
|
41
|
+
function toLaunchOptions(options) {
|
|
42
|
+
return {
|
|
43
|
+
args: options.extraArgs,
|
|
44
|
+
executablePath: options.executablePath,
|
|
45
|
+
headless: options.headless ?? true,
|
|
46
|
+
proxy: options.proxy ? { server: options.proxy } : undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
class PlaywrightBrowserLocator {
|
|
50
|
+
locator;
|
|
51
|
+
constructor(locator) {
|
|
52
|
+
this.locator = locator;
|
|
53
|
+
}
|
|
54
|
+
async click() {
|
|
55
|
+
await this.locator.click();
|
|
56
|
+
}
|
|
57
|
+
async fill(text) {
|
|
58
|
+
await this.locator.fill(text);
|
|
59
|
+
}
|
|
60
|
+
async textContent() {
|
|
61
|
+
return await this.locator.textContent();
|
|
62
|
+
}
|
|
63
|
+
async waitFor(options) {
|
|
64
|
+
await this.locator.waitFor(options);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
class PlaywrightBrowserFrame {
|
|
68
|
+
frame;
|
|
69
|
+
constructor(frame) {
|
|
70
|
+
this.frame = frame;
|
|
71
|
+
}
|
|
72
|
+
get id() {
|
|
73
|
+
return this.frame.name() || this.frame.url();
|
|
74
|
+
}
|
|
75
|
+
get name() {
|
|
76
|
+
const name = this.frame.name();
|
|
77
|
+
return name.length > 0 ? name : undefined;
|
|
78
|
+
}
|
|
79
|
+
get parentId() {
|
|
80
|
+
const parent = this.frame.parentFrame();
|
|
81
|
+
return parent ? parent.name() || parent.url() : undefined;
|
|
82
|
+
}
|
|
83
|
+
async url() {
|
|
84
|
+
return this.frame.url();
|
|
85
|
+
}
|
|
86
|
+
async title() {
|
|
87
|
+
return await this.frame.evaluate("document.title");
|
|
88
|
+
}
|
|
89
|
+
async content() {
|
|
90
|
+
return await this.frame.content();
|
|
91
|
+
}
|
|
92
|
+
async evaluate(fn) {
|
|
93
|
+
if (typeof fn === "string") {
|
|
94
|
+
return await this.frame.evaluate(fn);
|
|
95
|
+
}
|
|
96
|
+
return await this.frame.evaluate(fn);
|
|
97
|
+
}
|
|
98
|
+
locator(selector) {
|
|
99
|
+
return new PlaywrightBrowserLocator(this.frame.locator(selector));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function loadPlaywright() {
|
|
103
|
+
try {
|
|
104
|
+
await importOptionalModule("playwright");
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (isModuleNotFoundError(error)) {
|
|
108
|
+
throw new ProviderError("Playwright is not installed", {
|
|
109
|
+
cause: error instanceof Error ? error : undefined,
|
|
110
|
+
fix: "Run: bun add playwright",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
return unwrapModuleDefault(await importOptionalModule("playwright"));
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (isModuleNotFoundError(error)) {
|
|
120
|
+
throw new ProviderError("Playwright is not installed", {
|
|
121
|
+
cause: error instanceof Error ? error : undefined,
|
|
122
|
+
fix: "Run: bun add playwright",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const playwrightExtraStealthLaunchers = new WeakSet();
|
|
129
|
+
async function loadPlaywrightExtra() {
|
|
130
|
+
try {
|
|
131
|
+
require("playwright");
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (isModuleNotFoundError(error)) {
|
|
135
|
+
throw new ProviderError("Playwright is not installed", {
|
|
136
|
+
cause: error instanceof Error ? error : undefined,
|
|
137
|
+
fix: "Run: bun add playwright",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
return unwrapModuleDefault(await importOptionalModule("playwright-extra"));
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (isModuleNotFoundError(error)) {
|
|
147
|
+
throw new ProviderError("playwright-extra is not installed", {
|
|
148
|
+
cause: error instanceof Error ? error : undefined,
|
|
149
|
+
fix: "Run: bun add playwright-extra puppeteer-extra-plugin-stealth",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function loadStealthPluginFactory() {
|
|
156
|
+
try {
|
|
157
|
+
return unwrapModuleDefault(await importOptionalModule("puppeteer-extra-plugin-stealth"));
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (isModuleNotFoundError(error)) {
|
|
161
|
+
throw new ProviderError("puppeteer-extra-plugin-stealth is not installed", {
|
|
162
|
+
cause: error instanceof Error ? error : undefined,
|
|
163
|
+
fix: "Run: bun add playwright-extra puppeteer-extra-plugin-stealth",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function loadChromiumLauncher(options) {
|
|
170
|
+
if (!(options.stealth ?? true)) {
|
|
171
|
+
return (await loadPlaywright()).chromium;
|
|
172
|
+
}
|
|
173
|
+
const playwrightExtra = await loadPlaywrightExtra();
|
|
174
|
+
if (!playwrightExtraStealthLaunchers.has(playwrightExtra.chromium)) {
|
|
175
|
+
const createStealthPlugin = await loadStealthPluginFactory();
|
|
176
|
+
playwrightExtra.chromium.use(createStealthPlugin());
|
|
177
|
+
playwrightExtraStealthLaunchers.add(playwrightExtra.chromium);
|
|
178
|
+
}
|
|
179
|
+
return playwrightExtra.chromium;
|
|
180
|
+
}
|
|
181
|
+
async function loadNodriver() {
|
|
182
|
+
try {
|
|
183
|
+
await importOptionalModule("nodriver");
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (isModuleNotFoundError(error)) {
|
|
187
|
+
throw new ProviderError("nodriver is not installed", {
|
|
188
|
+
cause: error instanceof Error ? error : undefined,
|
|
189
|
+
fix: "Run: pip install nodriver (Python) or bun add nodriver",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function loadSeleniumBase() {
|
|
196
|
+
try {
|
|
197
|
+
await importOptionalModule("seleniumbase");
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (isModuleNotFoundError(error)) {
|
|
201
|
+
throw new ProviderError("seleniumbase is not installed", {
|
|
202
|
+
cause: error instanceof Error ? error : undefined,
|
|
203
|
+
fix: "Run: pip install seleniumbase",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
class PlaywrightBrowserPage {
|
|
210
|
+
page;
|
|
211
|
+
id = "main";
|
|
212
|
+
pageId;
|
|
213
|
+
constructor(page) {
|
|
214
|
+
this.page = page;
|
|
215
|
+
}
|
|
216
|
+
async goto(url) {
|
|
217
|
+
await this.page.goto(url);
|
|
218
|
+
}
|
|
219
|
+
async evaluate(fn) {
|
|
220
|
+
if (typeof fn === "string") {
|
|
221
|
+
return await this.page.evaluate(fn);
|
|
222
|
+
}
|
|
223
|
+
return await this.page.evaluate(fn);
|
|
224
|
+
}
|
|
225
|
+
async waitForSelector(selector, options) {
|
|
226
|
+
await this.page.waitForSelector(selector, options);
|
|
227
|
+
}
|
|
228
|
+
async click(selector) {
|
|
229
|
+
await this.page.click(selector);
|
|
230
|
+
}
|
|
231
|
+
async type(selector, text) {
|
|
232
|
+
await this.page.locator(selector).fill("");
|
|
233
|
+
await this.page.type(selector, text);
|
|
234
|
+
}
|
|
235
|
+
async fill(selector, text) {
|
|
236
|
+
await this.page.fill(selector, text);
|
|
237
|
+
}
|
|
238
|
+
async frames() {
|
|
239
|
+
return this.page.frames().map((frame) => new PlaywrightBrowserFrame(frame));
|
|
240
|
+
}
|
|
241
|
+
locator(selector) {
|
|
242
|
+
return new PlaywrightBrowserLocator(this.page.locator(selector));
|
|
243
|
+
}
|
|
244
|
+
async title() {
|
|
245
|
+
return await this.page.title();
|
|
246
|
+
}
|
|
247
|
+
async url() {
|
|
248
|
+
return this.page.url();
|
|
249
|
+
}
|
|
250
|
+
async content() {
|
|
251
|
+
return await this.page.content();
|
|
252
|
+
}
|
|
253
|
+
async screenshot(options) {
|
|
254
|
+
return await this.page.screenshot(options);
|
|
255
|
+
}
|
|
256
|
+
async close() {
|
|
257
|
+
await this.page.close();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
class PlaywrightBrowserClient {
|
|
261
|
+
options;
|
|
262
|
+
browser = null;
|
|
263
|
+
engine = "playwright-stealth";
|
|
264
|
+
constructor(options = {}) {
|
|
265
|
+
this.options = options;
|
|
266
|
+
}
|
|
267
|
+
async ensureBrowser() {
|
|
268
|
+
if (this.browser?.isConnected()) {
|
|
269
|
+
return this.browser;
|
|
270
|
+
}
|
|
271
|
+
const chromium = await loadChromiumLauncher(this.options);
|
|
272
|
+
this.browser = await chromium.launch(toLaunchOptions(this.options));
|
|
273
|
+
return this.browser;
|
|
274
|
+
}
|
|
275
|
+
async newPage() {
|
|
276
|
+
const browser = await this.ensureBrowser();
|
|
277
|
+
const page = await browser.newPage();
|
|
278
|
+
return new PlaywrightBrowserPage(page);
|
|
279
|
+
}
|
|
280
|
+
async rawPage() {
|
|
281
|
+
throw new ProviderError("ctx.browser.rawPage() requires a CDP pool", {
|
|
282
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
283
|
+
fix: "Set APIFUSE__CDP_POOL__URL and use the SDK CDP pool-backed browser runtime. Local Chromium launch is not allowed for rawPage().",
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async withIsolatedContext(handler) {
|
|
287
|
+
const browser = await this.ensureBrowser();
|
|
288
|
+
const context = await browser.newContext();
|
|
289
|
+
const page = await context.newPage();
|
|
290
|
+
const browserPage = new PlaywrightBrowserPage(page);
|
|
291
|
+
try {
|
|
292
|
+
return await handler(browserPage);
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
try {
|
|
296
|
+
await browserPage.close();
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
await context.close();
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async close() {
|
|
304
|
+
const browser = this.browser;
|
|
305
|
+
this.browser = null;
|
|
306
|
+
if (!browser) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
await browser.close();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function normalizeWebSocketEndpoint(endpoint) {
|
|
313
|
+
const url = new URL(endpoint);
|
|
314
|
+
if (url.protocol === "http:") {
|
|
315
|
+
url.protocol = "ws:";
|
|
316
|
+
return url.toString();
|
|
317
|
+
}
|
|
318
|
+
if (url.protocol === "https:") {
|
|
319
|
+
url.protocol = "wss:";
|
|
320
|
+
return url.toString();
|
|
321
|
+
}
|
|
322
|
+
if (url.protocol === "ws:" || url.protocol === "wss:") {
|
|
323
|
+
return endpoint;
|
|
324
|
+
}
|
|
325
|
+
throw new Error(`Unsupported WebSocket endpoint protocol: ${url.protocol}`);
|
|
326
|
+
}
|
|
327
|
+
class JsonRpcWebSocketClient {
|
|
328
|
+
nextId = 1;
|
|
329
|
+
endpoint;
|
|
330
|
+
listeners = new Map();
|
|
331
|
+
pending = new Map();
|
|
332
|
+
socket;
|
|
333
|
+
socketPromise;
|
|
334
|
+
constructor(endpoint) {
|
|
335
|
+
this.endpoint = normalizeWebSocketEndpoint(endpoint);
|
|
336
|
+
}
|
|
337
|
+
on(method, listener) {
|
|
338
|
+
const listeners = this.listeners.get(method) ?? new Set();
|
|
339
|
+
listeners.add(listener);
|
|
340
|
+
this.listeners.set(method, listeners);
|
|
341
|
+
return () => {
|
|
342
|
+
listeners.delete(listener);
|
|
343
|
+
if (listeners.size === 0) {
|
|
344
|
+
this.listeners.delete(method);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
async send(method, params = {}) {
|
|
349
|
+
const socket = await this.getSocket();
|
|
350
|
+
const id = this.nextId++;
|
|
351
|
+
return await new Promise((resolve, reject) => {
|
|
352
|
+
this.pending.set(id, { resolve, reject });
|
|
353
|
+
socket.send(JSON.stringify({
|
|
354
|
+
id,
|
|
355
|
+
jsonrpc: "2.0",
|
|
356
|
+
method,
|
|
357
|
+
params,
|
|
358
|
+
}));
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
async close() {
|
|
362
|
+
for (const pending of this.pending.values()) {
|
|
363
|
+
pending.reject(new Error(`WebSocket closed: ${this.endpoint}`));
|
|
364
|
+
}
|
|
365
|
+
this.pending.clear();
|
|
366
|
+
this.listeners.clear();
|
|
367
|
+
this.socket?.close();
|
|
368
|
+
this.socket = undefined;
|
|
369
|
+
this.socketPromise = undefined;
|
|
370
|
+
}
|
|
371
|
+
async getSocket() {
|
|
372
|
+
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
373
|
+
return this.socket;
|
|
374
|
+
}
|
|
375
|
+
if (this.socketPromise) {
|
|
376
|
+
return this.socketPromise;
|
|
377
|
+
}
|
|
378
|
+
this.socketPromise = new Promise((resolve, reject) => {
|
|
379
|
+
const socket = new WebSocket(this.endpoint);
|
|
380
|
+
socket.addEventListener("open", () => {
|
|
381
|
+
this.socket = socket;
|
|
382
|
+
resolve(socket);
|
|
383
|
+
});
|
|
384
|
+
socket.addEventListener("message", (event) => {
|
|
385
|
+
const rawData = typeof event.data === "string"
|
|
386
|
+
? event.data
|
|
387
|
+
: Buffer.from(event.data).toString("utf8");
|
|
388
|
+
const payload = JSON.parse(rawData);
|
|
389
|
+
if (typeof payload.id === "number") {
|
|
390
|
+
const pending = this.pending.get(payload.id);
|
|
391
|
+
if (!pending) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
this.pending.delete(payload.id);
|
|
395
|
+
if (payload.error) {
|
|
396
|
+
pending.reject(new Error(payload.error.message ?? "JSON-RPC command failed"));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
pending.resolve(payload.result ?? {});
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (!payload.method) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
for (const listener of this.listeners.get(payload.method) ?? []) {
|
|
406
|
+
listener(payload.params);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
socket.addEventListener("close", () => {
|
|
410
|
+
for (const pending of this.pending.values()) {
|
|
411
|
+
pending.reject(new Error(`WebSocket closed: ${this.endpoint}`));
|
|
412
|
+
}
|
|
413
|
+
this.pending.clear();
|
|
414
|
+
this.socket = undefined;
|
|
415
|
+
this.socketPromise = undefined;
|
|
416
|
+
});
|
|
417
|
+
socket.addEventListener("error", () => {
|
|
418
|
+
reject(new Error(`Unable to connect to WebSocket endpoint: ${this.endpoint}`));
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
return this.socketPromise;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function flattenCdpFrameTree(node, out = []) {
|
|
425
|
+
if (!node) {
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
out.push(node.frame);
|
|
429
|
+
for (const child of node.childFrames ?? []) {
|
|
430
|
+
flattenCdpFrameTree(child, out);
|
|
431
|
+
}
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
function isRecord(value) {
|
|
435
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
436
|
+
}
|
|
437
|
+
function parsePoolAcquireResponse(value) {
|
|
438
|
+
if (!isRecord(value) ||
|
|
439
|
+
typeof value.pageId !== "string" ||
|
|
440
|
+
typeof value.wsEndpoint !== "string") {
|
|
441
|
+
throw new ProviderError("CDP Pool returned an invalid acquire response", {
|
|
442
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
if (value.browserContextId !== undefined &&
|
|
446
|
+
typeof value.browserContextId !== "string") {
|
|
447
|
+
throw new ProviderError("CDP Pool returned an invalid acquire response", {
|
|
448
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
...(value.browserContextId
|
|
453
|
+
? { browserContextId: value.browserContextId }
|
|
454
|
+
: {}),
|
|
455
|
+
pageId: value.pageId,
|
|
456
|
+
wsEndpoint: value.wsEndpoint,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function parseCdpFrameTreeNode(value) {
|
|
460
|
+
if (!isRecord(value) || !isRecord(value.frame)) {
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
const frameId = value.frame.id;
|
|
464
|
+
if (typeof frameId !== "string") {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
const childFrames = Array.isArray(value.childFrames)
|
|
468
|
+
? value.childFrames
|
|
469
|
+
.map(parseCdpFrameTreeNode)
|
|
470
|
+
.filter((child) => child !== undefined)
|
|
471
|
+
: undefined;
|
|
472
|
+
return {
|
|
473
|
+
frame: {
|
|
474
|
+
id: frameId,
|
|
475
|
+
name: typeof value.frame.name === "string" ? value.frame.name : undefined,
|
|
476
|
+
parentId: typeof value.frame.parentId === "string"
|
|
477
|
+
? value.frame.parentId
|
|
478
|
+
: undefined,
|
|
479
|
+
url: typeof value.frame.url === "string" ? value.frame.url : undefined,
|
|
480
|
+
},
|
|
481
|
+
...(childFrames ? { childFrames } : {}),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function getCdpExecutionContext(params) {
|
|
485
|
+
if (!isRecord(params) || !isRecord(params.context)) {
|
|
486
|
+
return {};
|
|
487
|
+
}
|
|
488
|
+
const contextId = params.context.id;
|
|
489
|
+
const auxData = params.context.auxData;
|
|
490
|
+
return {
|
|
491
|
+
frameId: isRecord(auxData) && typeof auxData.frameId === "string"
|
|
492
|
+
? auxData.frameId
|
|
493
|
+
: undefined,
|
|
494
|
+
id: typeof contextId === "number" ? contextId : undefined,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
class CdpBrowserLocator {
|
|
498
|
+
frame;
|
|
499
|
+
selector;
|
|
500
|
+
constructor(frame, selector) {
|
|
501
|
+
this.frame = frame;
|
|
502
|
+
this.selector = selector;
|
|
503
|
+
}
|
|
504
|
+
async click() {
|
|
505
|
+
await this.waitFor();
|
|
506
|
+
await this.frame.evaluate(`(() => {
|
|
507
|
+
const element = document.querySelector(${JSON.stringify(this.selector)});
|
|
508
|
+
if (!(element instanceof HTMLElement)) {
|
|
509
|
+
throw new Error(${JSON.stringify(`Selector not found: ${this.selector}`)});
|
|
510
|
+
}
|
|
511
|
+
element.click();
|
|
512
|
+
})()`);
|
|
513
|
+
}
|
|
514
|
+
async fill(text) {
|
|
515
|
+
await this.waitFor();
|
|
516
|
+
await this.frame.evaluate(`(() => {
|
|
517
|
+
const element = document.querySelector(${JSON.stringify(this.selector)});
|
|
518
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
519
|
+
throw new Error(${JSON.stringify(`Unsupported input target: ${this.selector}`)});
|
|
520
|
+
}
|
|
521
|
+
element.focus();
|
|
522
|
+
element.value = ${JSON.stringify(text)};
|
|
523
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
524
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
525
|
+
})()`);
|
|
526
|
+
}
|
|
527
|
+
async textContent() {
|
|
528
|
+
return await this.frame.evaluate(`document.querySelector(${JSON.stringify(this.selector)})?.textContent ?? null`);
|
|
529
|
+
}
|
|
530
|
+
async waitFor(options) {
|
|
531
|
+
if (this.frame.waitForSelector) {
|
|
532
|
+
await this.frame.waitForSelector(this.selector, options);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
536
|
+
const deadline = Date.now() + timeout;
|
|
537
|
+
while (Date.now() < deadline) {
|
|
538
|
+
const exists = await this.frame.evaluate(`Boolean(document.querySelector(${JSON.stringify(this.selector)}))`);
|
|
539
|
+
if (exists) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
await delay(SELECTOR_POLL_INTERVAL_MS);
|
|
543
|
+
}
|
|
544
|
+
throw new Error(`Timed out waiting for selector: ${this.selector}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
class CdpBrowserFrame {
|
|
548
|
+
id;
|
|
549
|
+
page;
|
|
550
|
+
initialUrl;
|
|
551
|
+
name;
|
|
552
|
+
parentId;
|
|
553
|
+
constructor(id, page, initialUrl = "", name, parentId) {
|
|
554
|
+
this.id = id;
|
|
555
|
+
this.page = page;
|
|
556
|
+
this.initialUrl = initialUrl;
|
|
557
|
+
this.name = name;
|
|
558
|
+
this.parentId = parentId;
|
|
559
|
+
}
|
|
560
|
+
async url() {
|
|
561
|
+
const evaluatedUrl = await this.evaluate("window.location.href");
|
|
562
|
+
return evaluatedUrl || this.initialUrl;
|
|
563
|
+
}
|
|
564
|
+
async title() {
|
|
565
|
+
return await this.evaluate("document.title");
|
|
566
|
+
}
|
|
567
|
+
async content() {
|
|
568
|
+
return await this.evaluate("document.documentElement.outerHTML");
|
|
569
|
+
}
|
|
570
|
+
async evaluate(fn) {
|
|
571
|
+
return await this.page.evaluateInFrame(this.id, fn);
|
|
572
|
+
}
|
|
573
|
+
locator(selector) {
|
|
574
|
+
return new CdpBrowserLocator(this, selector);
|
|
575
|
+
}
|
|
576
|
+
async waitForSelector(selector, options) {
|
|
577
|
+
await this.page.waitForSelectorInFrame(this.id, selector, options);
|
|
578
|
+
}
|
|
579
|
+
fallbackUrl() {
|
|
580
|
+
return this.initialUrl;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
class CdpPoolBrowserPage {
|
|
584
|
+
pageId;
|
|
585
|
+
browserContextId;
|
|
586
|
+
pageClient;
|
|
587
|
+
release;
|
|
588
|
+
closed = false;
|
|
589
|
+
initialized = false;
|
|
590
|
+
frameExecutionContexts = new Map();
|
|
591
|
+
constructor(pageId, browserContextId, pageClient, release) {
|
|
592
|
+
this.pageId = pageId;
|
|
593
|
+
this.browserContextId = browserContextId;
|
|
594
|
+
this.pageClient = pageClient;
|
|
595
|
+
this.release = release;
|
|
596
|
+
}
|
|
597
|
+
get id() {
|
|
598
|
+
return this.pageId;
|
|
599
|
+
}
|
|
600
|
+
async goto(url) {
|
|
601
|
+
await this.initialize();
|
|
602
|
+
const startedAt = Date.now();
|
|
603
|
+
let loadEventSeen = false;
|
|
604
|
+
const unsubscribe = this.pageClient.on("Page.loadEventFired", () => {
|
|
605
|
+
loadEventSeen = true;
|
|
606
|
+
});
|
|
607
|
+
try {
|
|
608
|
+
await this.pageClient.send("Page.navigate", { url });
|
|
609
|
+
await this.waitForDocumentReady(startedAt + DEFAULT_WAIT_TIMEOUT_MS, () => loadEventSeen);
|
|
610
|
+
}
|
|
611
|
+
finally {
|
|
612
|
+
unsubscribe();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async evaluate(fn) {
|
|
616
|
+
await this.initialize();
|
|
617
|
+
return await this.evaluateWithContext(fn);
|
|
618
|
+
}
|
|
619
|
+
async evaluateInFrame(frameId, fn) {
|
|
620
|
+
await this.initialize();
|
|
621
|
+
const contextId = await this.getFrameExecutionContextId(frameId);
|
|
622
|
+
return await this.evaluateWithContext(fn, contextId);
|
|
623
|
+
}
|
|
624
|
+
async waitForSelectorInFrame(frameId, selector, options) {
|
|
625
|
+
const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
626
|
+
const deadline = Date.now() + timeout;
|
|
627
|
+
while (Date.now() < deadline) {
|
|
628
|
+
const exists = await this.evaluateInFrame(frameId, `Boolean(document.querySelector(${JSON.stringify(selector)}))`);
|
|
629
|
+
if (exists) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
await delay(SELECTOR_POLL_INTERVAL_MS);
|
|
633
|
+
}
|
|
634
|
+
throw new Error(`Timed out waiting for selector: ${selector}`);
|
|
635
|
+
}
|
|
636
|
+
async evaluateWithContext(fn, contextId) {
|
|
637
|
+
const result = await this.pageClient.send("Runtime.evaluate", {
|
|
638
|
+
awaitPromise: true,
|
|
639
|
+
...(contextId === undefined ? {} : { contextId }),
|
|
640
|
+
expression: formatExpression(fn),
|
|
641
|
+
returnByValue: true,
|
|
642
|
+
});
|
|
643
|
+
if (result.exceptionDetails) {
|
|
644
|
+
throw new Error(String(result.exceptionDetails.text ??
|
|
645
|
+
"Browser evaluation failed"));
|
|
646
|
+
}
|
|
647
|
+
return result.result?.value;
|
|
648
|
+
}
|
|
649
|
+
async waitForSelector(selector, options) {
|
|
650
|
+
const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
651
|
+
const deadline = Date.now() + timeout;
|
|
652
|
+
while (Date.now() < deadline) {
|
|
653
|
+
const exists = await this.evaluate(`Boolean(document.querySelector(${JSON.stringify(selector)}))`);
|
|
654
|
+
if (exists) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
await delay(SELECTOR_POLL_INTERVAL_MS);
|
|
658
|
+
}
|
|
659
|
+
throw new Error(`Timed out waiting for selector: ${selector}`);
|
|
660
|
+
}
|
|
661
|
+
async click(selector) {
|
|
662
|
+
await this.waitForSelector(selector);
|
|
663
|
+
await this.evaluate(`(() => {
|
|
664
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
665
|
+
if (!(element instanceof HTMLElement)) {
|
|
666
|
+
throw new Error(${JSON.stringify(`Selector not found: ${selector}`)});
|
|
667
|
+
}
|
|
668
|
+
element.click();
|
|
669
|
+
})()`);
|
|
670
|
+
}
|
|
671
|
+
async type(selector, text) {
|
|
672
|
+
await this.fill(selector, text);
|
|
673
|
+
}
|
|
674
|
+
async fill(selector, text) {
|
|
675
|
+
await this.waitForSelector(selector);
|
|
676
|
+
await this.evaluate(`(() => {
|
|
677
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
678
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
679
|
+
throw new Error(${JSON.stringify(`Unsupported input target: ${selector}`)});
|
|
680
|
+
}
|
|
681
|
+
element.focus();
|
|
682
|
+
element.value = "";
|
|
683
|
+
})()`);
|
|
684
|
+
await this.pageClient.send("Input.insertText", { text });
|
|
685
|
+
await this.evaluate(`(() => {
|
|
686
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
687
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
688
|
+
throw new Error(${JSON.stringify(`Unsupported input target: ${selector}`)});
|
|
689
|
+
}
|
|
690
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
691
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
692
|
+
})()`);
|
|
693
|
+
}
|
|
694
|
+
async frames() {
|
|
695
|
+
await this.initialize();
|
|
696
|
+
const result = await this.pageClient.send("Page.getFrameTree");
|
|
697
|
+
const frames = flattenCdpFrameTree(parseCdpFrameTreeNode(result.frameTree));
|
|
698
|
+
return frames.map((frame) => new CdpBrowserFrame(frame.id, this, frame.url ?? "", frame.name, frame.parentId));
|
|
699
|
+
}
|
|
700
|
+
locator(selector) {
|
|
701
|
+
return new CdpBrowserLocator(this, selector);
|
|
702
|
+
}
|
|
703
|
+
async title() {
|
|
704
|
+
return await this.evaluate("document.title");
|
|
705
|
+
}
|
|
706
|
+
async url() {
|
|
707
|
+
const [mainFrame] = await this.frames();
|
|
708
|
+
const frameUrl = mainFrame instanceof CdpBrowserFrame ? mainFrame.fallbackUrl() : "";
|
|
709
|
+
return frameUrl || (await this.evaluate("window.location.href"));
|
|
710
|
+
}
|
|
711
|
+
async content() {
|
|
712
|
+
return await this.evaluate("document.documentElement.outerHTML");
|
|
713
|
+
}
|
|
714
|
+
async screenshot(options) {
|
|
715
|
+
await this.initialize();
|
|
716
|
+
const result = await this.pageClient.send("Page.captureScreenshot", {
|
|
717
|
+
captureBeyondViewport: options?.fullPage ?? false,
|
|
718
|
+
format: "png",
|
|
719
|
+
fromSurface: true,
|
|
720
|
+
});
|
|
721
|
+
return Buffer.from(String(result.data ?? ""), "base64");
|
|
722
|
+
}
|
|
723
|
+
async close() {
|
|
724
|
+
if (this.closed) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
this.closed = true;
|
|
728
|
+
try {
|
|
729
|
+
await this.release({
|
|
730
|
+
...(this.browserContextId
|
|
731
|
+
? { browserContextId: this.browserContextId }
|
|
732
|
+
: {}),
|
|
733
|
+
pageId: this.pageId,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
await this.pageClient.close();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async initialize() {
|
|
741
|
+
if (this.initialized) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
this.pageClient.on("Runtime.executionContextCreated", (params) => {
|
|
745
|
+
const context = getCdpExecutionContext(params);
|
|
746
|
+
if (context.frameId && context.id !== undefined) {
|
|
747
|
+
this.frameExecutionContexts.set(context.frameId, context.id);
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
await this.pageClient.send("Page.enable");
|
|
751
|
+
await this.pageClient.send("Runtime.enable");
|
|
752
|
+
this.initialized = true;
|
|
753
|
+
}
|
|
754
|
+
async getFrameExecutionContextId(frameId) {
|
|
755
|
+
const existing = this.frameExecutionContexts.get(frameId);
|
|
756
|
+
if (existing !== undefined) {
|
|
757
|
+
return existing;
|
|
758
|
+
}
|
|
759
|
+
const result = await this.pageClient.send("Page.createIsolatedWorld", {
|
|
760
|
+
frameId,
|
|
761
|
+
grantUniveralAccess: true,
|
|
762
|
+
worldName: "apifuse-provider-sdk",
|
|
763
|
+
});
|
|
764
|
+
const contextId = result.executionContextId;
|
|
765
|
+
if (typeof contextId !== "number") {
|
|
766
|
+
throw new Error(`Unable to resolve execution context for frame: ${frameId}`);
|
|
767
|
+
}
|
|
768
|
+
this.frameExecutionContexts.set(frameId, contextId);
|
|
769
|
+
return contextId;
|
|
770
|
+
}
|
|
771
|
+
async waitForDocumentReady(deadline, isLoadEventSeen) {
|
|
772
|
+
while (Date.now() < deadline) {
|
|
773
|
+
const readyState = await this.evaluate("document.readyState");
|
|
774
|
+
if (readyState === "complete" || readyState === "interactive") {
|
|
775
|
+
if (isLoadEventSeen() || readyState === "complete") {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
await delay(SELECTOR_POLL_INTERVAL_MS);
|
|
780
|
+
}
|
|
781
|
+
throw new Error("Timed out waiting for page load");
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
class CdpPoolBrowserClient {
|
|
785
|
+
allowedHosts;
|
|
786
|
+
poolClient;
|
|
787
|
+
engine = "playwright-stealth";
|
|
788
|
+
constructor(options) {
|
|
789
|
+
if (!options.cdpUrl) {
|
|
790
|
+
throw new ProviderError("CDP Pool URL is required", {
|
|
791
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
this.allowedHosts = [...new Set(options.allowedHosts ?? [])];
|
|
795
|
+
this.poolClient = new JsonRpcWebSocketClient(options.cdpUrl);
|
|
796
|
+
}
|
|
797
|
+
async newPage() {
|
|
798
|
+
return await this.acquirePage({ isolatedContext: true });
|
|
799
|
+
}
|
|
800
|
+
async acquirePage(options) {
|
|
801
|
+
const acquireResult = parsePoolAcquireResponse(await this.poolClient.send("acquire", {
|
|
802
|
+
...(this.allowedHosts.length > 0
|
|
803
|
+
? { allowedHosts: this.allowedHosts }
|
|
804
|
+
: {}),
|
|
805
|
+
...(options?.isolatedContext
|
|
806
|
+
? { isolationMode: "browserContext" }
|
|
807
|
+
: {}),
|
|
808
|
+
}));
|
|
809
|
+
const pageClient = new JsonRpcWebSocketClient(acquireResult.wsEndpoint);
|
|
810
|
+
const page = new CdpPoolBrowserPage(acquireResult.pageId, acquireResult.browserContextId, pageClient, async (request) => {
|
|
811
|
+
await this.poolClient.send("release", request);
|
|
812
|
+
});
|
|
813
|
+
try {
|
|
814
|
+
await page.evaluate(`window.navigator.webdriver === true ? Object.defineProperty(window.navigator, "webdriver", { configurable: true, get: () => undefined }) : undefined`);
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
await page.close().catch(() => undefined);
|
|
818
|
+
throw error;
|
|
819
|
+
}
|
|
820
|
+
return page;
|
|
821
|
+
}
|
|
822
|
+
async rawPage() {
|
|
823
|
+
return await this.newPage();
|
|
824
|
+
}
|
|
825
|
+
async withIsolatedContext(handler) {
|
|
826
|
+
const page = await this.acquirePage({ isolatedContext: true });
|
|
827
|
+
try {
|
|
828
|
+
return await handler(page);
|
|
829
|
+
}
|
|
830
|
+
finally {
|
|
831
|
+
await page.close();
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async close() {
|
|
835
|
+
await this.poolClient.close();
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
class UnsupportedBrowserEngineClient {
|
|
839
|
+
engine;
|
|
840
|
+
constructor(engine) {
|
|
841
|
+
this.engine = engine;
|
|
842
|
+
}
|
|
843
|
+
async newPage() {
|
|
844
|
+
if (this.engine === "nodriver") {
|
|
845
|
+
await loadNodriver();
|
|
846
|
+
throw new ProviderError("nodriver engine requires Python runtime", {
|
|
847
|
+
fix: "Use provider language: python and ctx.browser in Python",
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
await loadSeleniumBase();
|
|
851
|
+
throw new ProviderError("selenium-uc engine requires Python runtime", {
|
|
852
|
+
fix: "Use provider language: python",
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
async rawPage() {
|
|
856
|
+
return await this.newPage();
|
|
857
|
+
}
|
|
858
|
+
async withIsolatedContext() {
|
|
859
|
+
return await this.newPage();
|
|
860
|
+
}
|
|
861
|
+
async close() { }
|
|
862
|
+
}
|
|
863
|
+
function createPlaywrightStealthClient(options = {}) {
|
|
864
|
+
return new PlaywrightBrowserClient(options);
|
|
865
|
+
}
|
|
866
|
+
function createCdpPoolBrowserClient(options) {
|
|
867
|
+
return new CdpPoolBrowserClient(options);
|
|
868
|
+
}
|
|
869
|
+
function createNodriverClient() {
|
|
870
|
+
return new UnsupportedBrowserEngineClient("nodriver");
|
|
871
|
+
}
|
|
872
|
+
function createSeleniumUCClient() {
|
|
873
|
+
return new UnsupportedBrowserEngineClient("selenium-uc");
|
|
874
|
+
}
|
|
875
|
+
export class BrowserClient {
|
|
876
|
+
client;
|
|
877
|
+
cdpUrl;
|
|
878
|
+
activePage;
|
|
879
|
+
activePages = new Set();
|
|
880
|
+
_engine;
|
|
881
|
+
constructor(options = {}) {
|
|
882
|
+
const resolvedOptions = {
|
|
883
|
+
...options,
|
|
884
|
+
cdpUrl: options.cdpUrl ?? getDefaultCdpPoolUrl(),
|
|
885
|
+
};
|
|
886
|
+
const engine = resolvedOptions.engine ?? "playwright-stealth";
|
|
887
|
+
this._engine = engine;
|
|
888
|
+
this.cdpUrl = resolvedOptions.cdpUrl;
|
|
889
|
+
if (resolvedOptions.requireCdpPool && !resolvedOptions.cdpUrl) {
|
|
890
|
+
throw new ProviderError("Managed CDP Pool is required for browser providers in production", {
|
|
891
|
+
code: "BROWSER_CDP_POOL_REQUIRED",
|
|
892
|
+
fix: "Set APIFUSE__CDP_POOL__URL for deployed browser providers. Local standalone development may omit it.",
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
switch (engine) {
|
|
896
|
+
case "nodriver":
|
|
897
|
+
this.client = createNodriverClient();
|
|
898
|
+
break;
|
|
899
|
+
case "selenium-uc":
|
|
900
|
+
this.client = createSeleniumUCClient();
|
|
901
|
+
break;
|
|
902
|
+
default:
|
|
903
|
+
this.client = resolvedOptions.cdpUrl
|
|
904
|
+
? createCdpPoolBrowserClient(resolvedOptions)
|
|
905
|
+
: createPlaywrightStealthClient(resolvedOptions);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
get engine() {
|
|
909
|
+
return this._engine;
|
|
910
|
+
}
|
|
911
|
+
async newPage() {
|
|
912
|
+
const page = await this.client.newPage();
|
|
913
|
+
return this.activatePage(page);
|
|
914
|
+
}
|
|
915
|
+
async rawPage() {
|
|
916
|
+
if (!this.cdpUrl) {
|
|
917
|
+
throw new ProviderError("ctx.browser.rawPage() requires a CDP pool", {
|
|
918
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
919
|
+
fix: "Set APIFUSE__CDP_POOL__URL. The SDK escape hatch is CDP pool-backed only and never launches local Chromium.",
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
const page = await this.client.rawPage();
|
|
923
|
+
return this.activatePage(page);
|
|
924
|
+
}
|
|
925
|
+
async withIsolatedContext(handler) {
|
|
926
|
+
const previousActivePage = this.activePage;
|
|
927
|
+
let trackedPage;
|
|
928
|
+
return await this.client.withIsolatedContext(async (page) => {
|
|
929
|
+
trackedPage = this.activatePage(page);
|
|
930
|
+
try {
|
|
931
|
+
return await handler(trackedPage);
|
|
932
|
+
}
|
|
933
|
+
finally {
|
|
934
|
+
this.activePages.delete(trackedPage);
|
|
935
|
+
if (this.activePage === trackedPage) {
|
|
936
|
+
this.activePage = previousActivePage;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
activatePage(page) {
|
|
942
|
+
let closed = false;
|
|
943
|
+
const originalClose = page.close.bind(page);
|
|
944
|
+
const trackedPage = new Proxy(page, {
|
|
945
|
+
get: (target, property, receiver) => {
|
|
946
|
+
if (property === "close") {
|
|
947
|
+
return async () => {
|
|
948
|
+
if (closed)
|
|
949
|
+
return;
|
|
950
|
+
closed = true;
|
|
951
|
+
try {
|
|
952
|
+
await originalClose();
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
this.activePages.delete(trackedPage);
|
|
956
|
+
if (this.activePage === trackedPage) {
|
|
957
|
+
this.activePage = undefined;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
const value = Reflect.get(target, property, receiver);
|
|
963
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
this.activePages.add(trackedPage);
|
|
967
|
+
this.activePage = trackedPage;
|
|
968
|
+
return trackedPage;
|
|
969
|
+
}
|
|
970
|
+
async solveChallenge(request) {
|
|
971
|
+
if (request.type !== "recaptcha") {
|
|
972
|
+
throw new ProviderError(`Unsupported browser challenge: ${request.type}`, {
|
|
973
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
if (this.activePage) {
|
|
977
|
+
return await solveRecaptchaChallenge(this.activePage, request);
|
|
978
|
+
}
|
|
979
|
+
const page = await this.client.newPage();
|
|
980
|
+
try {
|
|
981
|
+
return await solveRecaptchaChallenge(page, request);
|
|
982
|
+
}
|
|
983
|
+
finally {
|
|
984
|
+
if (this.activePage === page) {
|
|
985
|
+
this.activePage = undefined;
|
|
986
|
+
}
|
|
987
|
+
await page.close();
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async close() {
|
|
991
|
+
const pages = Array.from(this.activePages);
|
|
992
|
+
this.activePages.clear();
|
|
993
|
+
this.activePage = undefined;
|
|
994
|
+
try {
|
|
995
|
+
await Promise.all(pages.map((page) => page.close()));
|
|
996
|
+
}
|
|
997
|
+
finally {
|
|
998
|
+
await this.client.close();
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
async function solveRecaptchaChallenge(page, request) {
|
|
1003
|
+
const timeout = request.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
1004
|
+
const deadline = Date.now() + timeout;
|
|
1005
|
+
while (Date.now() < deadline) {
|
|
1006
|
+
const frames = await page.frames();
|
|
1007
|
+
const recaptchaFrame = await findRecaptchaFrame(frames, request.siteKey);
|
|
1008
|
+
if (recaptchaFrame) {
|
|
1009
|
+
await recaptchaFrame.locator("#recaptcha-anchor").click();
|
|
1010
|
+
return {
|
|
1011
|
+
type: "recaptcha",
|
|
1012
|
+
solved: true,
|
|
1013
|
+
frameUrl: await recaptchaFrame.url(),
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
await delay(SELECTOR_POLL_INTERVAL_MS);
|
|
1017
|
+
}
|
|
1018
|
+
throw new Error("Timed out waiting for reCAPTCHA iframe");
|
|
1019
|
+
}
|
|
1020
|
+
async function findRecaptchaFrame(frames, siteKey) {
|
|
1021
|
+
for (const frame of frames) {
|
|
1022
|
+
const url = await frame.url();
|
|
1023
|
+
const matchesRecaptcha = url.includes("google.com/recaptcha") ||
|
|
1024
|
+
url.includes("recaptcha.net/recaptcha");
|
|
1025
|
+
const matchesSiteKey = !siteKey || url.includes(siteKey);
|
|
1026
|
+
if (matchesRecaptcha && matchesSiteKey) {
|
|
1027
|
+
return frame;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return undefined;
|
|
1031
|
+
}
|
|
1032
|
+
export function createBrowserClient(options = {}) {
|
|
1033
|
+
return new BrowserClient(options);
|
|
1034
|
+
}
|