@apifuse/provider-sdk 2.1.0-beta.5 → 2.1.0-beta.8
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 +16 -0
- package/README.md +2 -2
- package/SUBMISSION.md +2 -1
- package/bin/apifuse-check.ts +60 -6
- package/bin/apifuse-dev.ts +48 -5
- package/bin/apifuse-perf.ts +50 -11
- package/bin/apifuse-record.ts +35 -11
- package/bin/apifuse-submit-check.ts +1425 -3
- 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 +827 -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 +1324 -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 +42 -25
- package/src/ceremonies/index.ts +8 -2
- package/src/choice-token.ts +1 -0
- package/src/cli/commands.ts +8 -5
- package/src/cli/create.ts +28 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
- package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
- package/src/config/loader.ts +19 -1
- 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 +37 -2
- package/src/errors.ts +15 -0
- package/src/i18n/catalog.ts +156 -0
- package/src/index.ts +22 -1
- package/src/lint.ts +256 -37
- package/src/provider.ts +45 -2
- package/src/runtime/browser.ts +685 -30
- package/src/runtime/cache.ts +35 -89
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +19 -2
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/state.ts +487 -0
- package/src/runtime/stealth.ts +8 -1
- package/src/server/serve.ts +361 -46
- package/src/server/types.ts +2 -0
- package/src/testing/run.ts +16 -3
- package/src/types.ts +209 -6
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);
|
|
@@ -22,10 +27,16 @@ type StealthPluginFactory = (options?: {
|
|
|
22
27
|
}) => unknown;
|
|
23
28
|
|
|
24
29
|
type PoolAcquireResponse = {
|
|
30
|
+
browserContextId?: string;
|
|
25
31
|
pageId: string;
|
|
26
32
|
wsEndpoint: string;
|
|
27
33
|
};
|
|
28
34
|
|
|
35
|
+
type PoolReleaseRequest = {
|
|
36
|
+
browserContextId?: string;
|
|
37
|
+
pageId: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
29
40
|
type JsonRpcId = number;
|
|
30
41
|
|
|
31
42
|
type JsonRpcError = {
|
|
@@ -41,31 +52,33 @@ type JsonRpcMessage = {
|
|
|
41
52
|
result?: Record<string, unknown>;
|
|
42
53
|
};
|
|
43
54
|
|
|
44
|
-
type
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
click(selector: string): Promise<void>;
|
|
53
|
-
type(selector: string, text: string): Promise<void>;
|
|
54
|
-
waitForSelector(
|
|
55
|
-
selector: string,
|
|
56
|
-
options?: { timeout?: number },
|
|
57
|
-
): Promise<void>;
|
|
55
|
+
type CdpFrameTreeNode = {
|
|
56
|
+
childFrames?: CdpFrameTreeNode[];
|
|
57
|
+
frame: {
|
|
58
|
+
id: string;
|
|
59
|
+
name?: string;
|
|
60
|
+
parentId?: string;
|
|
61
|
+
url?: string;
|
|
62
|
+
};
|
|
58
63
|
};
|
|
59
64
|
|
|
65
|
+
type BrowserPageContract = BrowserPage;
|
|
66
|
+
|
|
60
67
|
export type BrowserClientOptions = BrowserOptions & {
|
|
68
|
+
allowedHosts?: string[];
|
|
61
69
|
cdpUrl?: string;
|
|
62
70
|
executablePath?: string;
|
|
63
71
|
extraArgs?: string[];
|
|
64
72
|
};
|
|
65
73
|
|
|
66
|
-
type SupportedBrowserClient =
|
|
74
|
+
type SupportedBrowserClient = {
|
|
75
|
+
readonly engine: BrowserEngine;
|
|
67
76
|
close(): Promise<void>;
|
|
68
77
|
newPage(): Promise<BrowserPageContract>;
|
|
78
|
+
rawPage(): Promise<BrowserPageContract>;
|
|
79
|
+
withIsolatedContext<T>(
|
|
80
|
+
handler: (page: BrowserPageContract) => Promise<T>,
|
|
81
|
+
): Promise<T>;
|
|
69
82
|
};
|
|
70
83
|
|
|
71
84
|
function getDefaultCdpPoolUrl(env = process.env): string | undefined {
|
|
@@ -127,6 +140,68 @@ function toLaunchOptions(options: BrowserClientOptions): LaunchOptions {
|
|
|
127
140
|
};
|
|
128
141
|
}
|
|
129
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
|
+
|
|
130
205
|
async function loadPlaywright(): Promise<PlaywrightModule> {
|
|
131
206
|
try {
|
|
132
207
|
await importOptionalModule<PlaywrightModule>("playwright");
|
|
@@ -259,6 +334,7 @@ async function loadSeleniumBase(): Promise<void> {
|
|
|
259
334
|
}
|
|
260
335
|
|
|
261
336
|
class PlaywrightBrowserPage implements BrowserPageContract {
|
|
337
|
+
readonly id = "main";
|
|
262
338
|
readonly pageId?: string;
|
|
263
339
|
|
|
264
340
|
constructor(private readonly page: Page) {}
|
|
@@ -295,6 +371,22 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
295
371
|
await this.page.fill(selector, text);
|
|
296
372
|
}
|
|
297
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
|
+
|
|
298
390
|
async content(): Promise<string> {
|
|
299
391
|
return await this.page.content();
|
|
300
392
|
}
|
|
@@ -331,6 +423,32 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
331
423
|
return new PlaywrightBrowserPage(page);
|
|
332
424
|
}
|
|
333
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
|
+
|
|
334
452
|
async close(): Promise<void> {
|
|
335
453
|
const browser = this.browser;
|
|
336
454
|
this.browser = null;
|
|
@@ -343,8 +461,25 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
|
343
461
|
}
|
|
344
462
|
}
|
|
345
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
|
+
|
|
346
480
|
class JsonRpcWebSocketClient {
|
|
347
481
|
private nextId = 1;
|
|
482
|
+
private readonly endpoint: string;
|
|
348
483
|
private readonly listeners = new Map<
|
|
349
484
|
string,
|
|
350
485
|
Set<(params: unknown) => void>
|
|
@@ -359,7 +494,9 @@ class JsonRpcWebSocketClient {
|
|
|
359
494
|
private socket?: WebSocket;
|
|
360
495
|
private socketPromise?: Promise<WebSocket>;
|
|
361
496
|
|
|
362
|
-
constructor(
|
|
497
|
+
constructor(endpoint: string) {
|
|
498
|
+
this.endpoint = normalizeWebSocketEndpoint(endpoint);
|
|
499
|
+
}
|
|
363
500
|
|
|
364
501
|
on(method: string, listener: (params: unknown) => void): () => void {
|
|
365
502
|
const listeners = this.listeners.get(method) ?? new Set();
|
|
@@ -482,16 +619,233 @@ class JsonRpcWebSocketClient {
|
|
|
482
619
|
}
|
|
483
620
|
}
|
|
484
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
|
+
|
|
485
833
|
class CdpPoolBrowserPage implements BrowserPageContract {
|
|
486
834
|
private closed = false;
|
|
487
835
|
private initialized = false;
|
|
836
|
+
private readonly frameExecutionContexts = new Map<string, number>();
|
|
488
837
|
|
|
489
838
|
constructor(
|
|
490
839
|
readonly pageId: string,
|
|
840
|
+
private readonly browserContextId: string | undefined,
|
|
491
841
|
private readonly pageClient: JsonRpcWebSocketClient,
|
|
492
|
-
private readonly release: (
|
|
842
|
+
private readonly release: (request: PoolReleaseRequest) => Promise<void>,
|
|
493
843
|
) {}
|
|
494
844
|
|
|
845
|
+
get id(): string {
|
|
846
|
+
return this.pageId;
|
|
847
|
+
}
|
|
848
|
+
|
|
495
849
|
async goto(url: string): Promise<void> {
|
|
496
850
|
await this.initialize();
|
|
497
851
|
const startedAt = Date.now();
|
|
@@ -513,8 +867,49 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
513
867
|
|
|
514
868
|
async evaluate<T>(fn: string | (() => T)): Promise<T> {
|
|
515
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> {
|
|
516
910
|
const result = await this.pageClient.send("Runtime.evaluate", {
|
|
517
911
|
awaitPromise: true,
|
|
912
|
+
...(contextId === undefined ? {} : { contextId }),
|
|
518
913
|
expression: formatExpression(fn),
|
|
519
914
|
returnByValue: true,
|
|
520
915
|
});
|
|
@@ -595,6 +990,37 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
595
990
|
);
|
|
596
991
|
}
|
|
597
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
|
+
|
|
598
1024
|
async content(): Promise<string> {
|
|
599
1025
|
return await this.evaluate<string>("document.documentElement.outerHTML");
|
|
600
1026
|
}
|
|
@@ -618,7 +1044,12 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
618
1044
|
this.closed = true;
|
|
619
1045
|
|
|
620
1046
|
try {
|
|
621
|
-
await this.release(
|
|
1047
|
+
await this.release({
|
|
1048
|
+
...(this.browserContextId
|
|
1049
|
+
? { browserContextId: this.browserContextId }
|
|
1050
|
+
: {}),
|
|
1051
|
+
pageId: this.pageId,
|
|
1052
|
+
});
|
|
622
1053
|
} finally {
|
|
623
1054
|
await this.pageClient.close();
|
|
624
1055
|
}
|
|
@@ -629,11 +1060,39 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
629
1060
|
return;
|
|
630
1061
|
}
|
|
631
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
|
+
});
|
|
632
1069
|
await this.pageClient.send("Page.enable");
|
|
633
1070
|
await this.pageClient.send("Runtime.enable");
|
|
634
1071
|
this.initialized = true;
|
|
635
1072
|
}
|
|
636
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
|
+
|
|
637
1096
|
private async waitForDocumentReady(
|
|
638
1097
|
deadline: number,
|
|
639
1098
|
isLoadEventSeen: () => boolean,
|
|
@@ -654,6 +1113,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
654
1113
|
}
|
|
655
1114
|
|
|
656
1115
|
class CdpPoolBrowserClient implements SupportedBrowserClient {
|
|
1116
|
+
private readonly allowedHosts: string[];
|
|
657
1117
|
private readonly poolClient: JsonRpcWebSocketClient;
|
|
658
1118
|
readonly engine = "playwright-stealth" satisfies BrowserEngine;
|
|
659
1119
|
|
|
@@ -664,29 +1124,65 @@ class CdpPoolBrowserClient implements SupportedBrowserClient {
|
|
|
664
1124
|
});
|
|
665
1125
|
}
|
|
666
1126
|
|
|
1127
|
+
this.allowedHosts = [...new Set(options.allowedHosts ?? [])];
|
|
667
1128
|
this.poolClient = new JsonRpcWebSocketClient(options.cdpUrl);
|
|
668
1129
|
}
|
|
669
1130
|
|
|
670
1131
|
async newPage(): Promise<BrowserPageContract> {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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
|
+
);
|
|
674
1148
|
const pageClient = new JsonRpcWebSocketClient(acquireResult.wsEndpoint);
|
|
675
1149
|
const page = new CdpPoolBrowserPage(
|
|
676
1150
|
acquireResult.pageId,
|
|
1151
|
+
acquireResult.browserContextId,
|
|
677
1152
|
pageClient,
|
|
678
|
-
async (
|
|
679
|
-
await this.poolClient.send("release",
|
|
1153
|
+
async (request) => {
|
|
1154
|
+
await this.poolClient.send("release", request);
|
|
680
1155
|
},
|
|
681
1156
|
);
|
|
682
1157
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
+
}
|
|
686
1166
|
|
|
687
1167
|
return page;
|
|
688
1168
|
}
|
|
689
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
|
+
|
|
690
1186
|
async close(): Promise<void> {
|
|
691
1187
|
await this.poolClient.close();
|
|
692
1188
|
}
|
|
@@ -711,6 +1207,14 @@ class UnsupportedBrowserEngineClient implements SupportedBrowserClient {
|
|
|
711
1207
|
});
|
|
712
1208
|
}
|
|
713
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
|
+
|
|
714
1218
|
async close(): Promise<void> {}
|
|
715
1219
|
}
|
|
716
1220
|
|
|
@@ -736,6 +1240,9 @@ function createSeleniumUCClient(): SupportedBrowserClient {
|
|
|
736
1240
|
|
|
737
1241
|
export class BrowserClient implements BrowserClientContract {
|
|
738
1242
|
private readonly client: SupportedBrowserClient;
|
|
1243
|
+
private readonly cdpUrl?: string;
|
|
1244
|
+
private activePage?: BrowserPageContract;
|
|
1245
|
+
private readonly activePages = new Set<BrowserPageContract>();
|
|
739
1246
|
private readonly _engine: BrowserEngine;
|
|
740
1247
|
|
|
741
1248
|
constructor(options: BrowserClientOptions = {}) {
|
|
@@ -745,6 +1252,17 @@ export class BrowserClient implements BrowserClientContract {
|
|
|
745
1252
|
};
|
|
746
1253
|
const engine = resolvedOptions.engine ?? "playwright-stealth";
|
|
747
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
|
+
}
|
|
748
1266
|
|
|
749
1267
|
switch (engine) {
|
|
750
1268
|
case "nodriver":
|
|
@@ -764,15 +1282,152 @@ export class BrowserClient implements BrowserClientContract {
|
|
|
764
1282
|
return this._engine;
|
|
765
1283
|
}
|
|
766
1284
|
|
|
767
|
-
async newPage(): Promise<
|
|
768
|
-
|
|
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
|
+
}
|
|
769
1374
|
}
|
|
770
1375
|
|
|
771
1376
|
async close(): Promise<void> {
|
|
772
|
-
|
|
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
|
+
}
|
|
773
1385
|
}
|
|
774
1386
|
}
|
|
775
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
|
+
|
|
776
1431
|
export function createBrowserClient(
|
|
777
1432
|
options: BrowserClientOptions = {},
|
|
778
1433
|
): BrowserClient {
|