@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.
Files changed (212) hide show
  1. package/AUTHORING.md +208 -2
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +114 -10
  4. package/SUBMISSION.md +87 -0
  5. package/bin/apifuse-check.ts +86 -4
  6. package/bin/apifuse-dev.ts +87 -13
  7. package/bin/apifuse-pack-check.ts +80 -0
  8. package/bin/apifuse-pack-smoke.ts +303 -2
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +2538 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/ceremonies/index.d.ts +41 -0
  14. package/dist/ceremonies/index.js +490 -0
  15. package/dist/choice-token.d.ts +24 -0
  16. package/dist/choice-token.js +74 -0
  17. package/dist/cli/commands.d.ts +10 -0
  18. package/dist/cli/commands.js +80 -0
  19. package/dist/cli/create.d.ts +47 -0
  20. package/dist/cli/create.js +762 -0
  21. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  22. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  23. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  24. package/dist/cli/templates/provider/README.md.tpl +160 -0
  25. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  26. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  27. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  28. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  29. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  30. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  31. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  32. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  33. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  34. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  35. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  36. package/dist/config/loader.d.ts +107 -0
  37. package/dist/config/loader.js +935 -0
  38. package/dist/contract-json.d.ts +9 -0
  39. package/dist/contract-json.js +51 -0
  40. package/dist/contract-serialization.d.ts +4 -0
  41. package/dist/contract-serialization.js +78 -0
  42. package/dist/contract-types.d.ts +49 -0
  43. package/dist/contract-types.js +1 -0
  44. package/dist/contract.d.ts +6 -0
  45. package/dist/contract.js +155 -0
  46. package/dist/define.d.ts +97 -0
  47. package/dist/define.js +1320 -0
  48. package/dist/dev.d.ts +9 -0
  49. package/dist/dev.js +15 -0
  50. package/dist/errors.d.ts +59 -0
  51. package/dist/errors.js +97 -0
  52. package/dist/i18n/catalog.d.ts +29 -0
  53. package/dist/i18n/catalog.js +159 -0
  54. package/dist/i18n/index.d.ts +2 -0
  55. package/dist/i18n/index.js +2 -0
  56. package/dist/i18n/keys.d.ts +10 -0
  57. package/dist/i18n/keys.js +34 -0
  58. package/dist/index.d.ts +41 -0
  59. package/dist/index.js +37 -0
  60. package/dist/lint.d.ts +73 -0
  61. package/dist/lint.js +702 -0
  62. package/dist/observability.d.ts +5 -0
  63. package/dist/observability.js +39 -0
  64. package/dist/provider.d.ts +9 -0
  65. package/dist/provider.js +8 -0
  66. package/dist/public-schema-field-lint.d.ts +2 -0
  67. package/dist/public-schema-field-lint.js +158 -0
  68. package/dist/recipes/gov-api.d.ts +19 -0
  69. package/dist/recipes/gov-api.js +72 -0
  70. package/dist/recipes/rest-api.d.ts +21 -0
  71. package/dist/recipes/rest-api.js +115 -0
  72. package/dist/runtime/auth-flow.d.ts +14 -0
  73. package/dist/runtime/auth-flow.js +44 -0
  74. package/dist/runtime/browser.d.ts +25 -0
  75. package/dist/runtime/browser.js +1034 -0
  76. package/dist/runtime/cache.d.ts +10 -0
  77. package/dist/runtime/cache.js +372 -0
  78. package/dist/runtime/choice.d.ts +15 -0
  79. package/dist/runtime/choice.js +435 -0
  80. package/dist/runtime/credential.d.ts +8 -0
  81. package/dist/runtime/credential.js +61 -0
  82. package/dist/runtime/env.d.ts +2 -0
  83. package/dist/runtime/env.js +10 -0
  84. package/dist/runtime/executor.d.ts +16 -0
  85. package/dist/runtime/executor.js +51 -0
  86. package/dist/runtime/http.d.ts +8 -0
  87. package/dist/runtime/http.js +706 -0
  88. package/dist/runtime/insights.d.ts +9 -0
  89. package/dist/runtime/insights.js +324 -0
  90. package/dist/runtime/instrumentation.d.ts +8 -0
  91. package/dist/runtime/instrumentation.js +269 -0
  92. package/dist/runtime/key-derivation.d.ts +24 -0
  93. package/dist/runtime/key-derivation.js +73 -0
  94. package/dist/runtime/keyring.d.ts +25 -0
  95. package/dist/runtime/keyring.js +93 -0
  96. package/dist/runtime/namespace.d.ts +9 -0
  97. package/dist/runtime/namespace.js +19 -0
  98. package/dist/runtime/otlp.d.ts +39 -0
  99. package/dist/runtime/otlp.js +103 -0
  100. package/dist/runtime/perf.d.ts +12 -0
  101. package/dist/runtime/perf.js +52 -0
  102. package/dist/runtime/prevalidate.d.ts +12 -0
  103. package/dist/runtime/prevalidate.js +173 -0
  104. package/dist/runtime/provider.d.ts +2 -0
  105. package/dist/runtime/provider.js +11 -0
  106. package/dist/runtime/proxy-errors.d.ts +21 -0
  107. package/dist/runtime/proxy-errors.js +83 -0
  108. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  109. package/dist/runtime/proxy-telemetry.js +174 -0
  110. package/dist/runtime/redis.d.ts +17 -0
  111. package/dist/runtime/redis.js +82 -0
  112. package/dist/runtime/request-options.d.ts +3 -0
  113. package/dist/runtime/request-options.js +42 -0
  114. package/dist/runtime/state.d.ts +17 -0
  115. package/dist/runtime/state.js +344 -0
  116. package/dist/runtime/stealth.d.ts +18 -0
  117. package/dist/runtime/stealth.js +834 -0
  118. package/dist/runtime/stt.d.ts +22 -0
  119. package/dist/runtime/stt.js +480 -0
  120. package/dist/runtime/trace.d.ts +26 -0
  121. package/dist/runtime/trace.js +142 -0
  122. package/dist/runtime/waterfall.d.ts +12 -0
  123. package/dist/runtime/waterfall.js +147 -0
  124. package/dist/schema.d.ts +74 -0
  125. package/dist/schema.js +243 -0
  126. package/dist/serve.d.ts +1 -0
  127. package/dist/serve.js +1 -0
  128. package/dist/server/index.d.ts +3 -0
  129. package/dist/server/index.js +2 -0
  130. package/dist/server/serve.d.ts +64 -0
  131. package/dist/server/serve.js +1110 -0
  132. package/dist/server/types.d.ts +136 -0
  133. package/dist/server/types.js +86 -0
  134. package/dist/stealth/profiles.d.ts +4 -0
  135. package/dist/stealth/profiles.js +259 -0
  136. package/dist/stream.d.ts +44 -0
  137. package/dist/stream.js +151 -0
  138. package/dist/testing/helpers.d.ts +23 -0
  139. package/dist/testing/helpers.js +95 -0
  140. package/dist/testing/index.d.ts +2 -0
  141. package/dist/testing/index.js +2 -0
  142. package/dist/testing/run.d.ts +34 -0
  143. package/dist/testing/run.js +303 -0
  144. package/dist/types.d.ts +1326 -0
  145. package/dist/types.js +61 -0
  146. package/dist/utils/date.d.ts +6 -0
  147. package/dist/utils/date.js +101 -0
  148. package/dist/utils/parse.d.ts +16 -0
  149. package/dist/utils/parse.js +51 -0
  150. package/dist/utils/text.d.ts +4 -0
  151. package/dist/utils/text.js +14 -0
  152. package/dist/utils/transform.d.ts +8 -0
  153. package/dist/utils/transform.js +48 -0
  154. package/package.json +57 -30
  155. package/src/ceremonies/index.ts +8 -2
  156. package/src/choice-token.ts +165 -0
  157. package/src/cli/commands.ts +34 -11
  158. package/src/cli/create.ts +214 -52
  159. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  160. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  161. package/src/cli/templates/provider/README.md.tpl +120 -1
  162. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  163. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  164. package/src/cli/templates/provider/index.ts.tpl +5 -48
  165. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  166. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  167. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  168. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  169. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  170. package/src/cli/templates/provider/start.ts.tpl +1 -1
  171. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  172. package/src/config/loader.ts +1224 -9
  173. package/src/contract-json.ts +75 -0
  174. package/src/contract-serialization.ts +89 -0
  175. package/src/contract-types.ts +52 -0
  176. package/src/contract.ts +215 -0
  177. package/src/define.ts +1688 -48
  178. package/src/errors.ts +27 -0
  179. package/src/i18n/catalog.ts +277 -0
  180. package/src/i18n/index.ts +2 -0
  181. package/src/i18n/keys.ts +64 -0
  182. package/src/index.ts +174 -9
  183. package/src/lint.ts +547 -73
  184. package/src/observability.ts +41 -0
  185. package/src/provider.ts +104 -4
  186. package/src/public-schema-field-lint.ts +237 -0
  187. package/src/runtime/auth-flow.ts +7 -0
  188. package/src/runtime/browser.ts +762 -51
  189. package/src/runtime/cache.ts +528 -0
  190. package/src/runtime/choice.ts +760 -0
  191. package/src/runtime/executor.ts +32 -3
  192. package/src/runtime/http.ts +939 -195
  193. package/src/runtime/insights.ts +11 -11
  194. package/src/runtime/instrumentation.ts +12 -4
  195. package/src/runtime/key-derivation.ts +1 -1
  196. package/src/runtime/keyring.ts +4 -3
  197. package/src/runtime/proxy-errors.ts +132 -0
  198. package/src/runtime/proxy-telemetry.ts +253 -0
  199. package/src/runtime/redis.ts +116 -0
  200. package/src/runtime/request-options.ts +66 -0
  201. package/src/runtime/state.ts +563 -0
  202. package/src/runtime/stealth.ts +1159 -0
  203. package/src/runtime/stt.ts +629 -0
  204. package/src/runtime/trace.ts +1 -1
  205. package/src/schema.ts +363 -1
  206. package/src/server/serve.ts +1157 -75
  207. package/src/server/types.ts +37 -0
  208. package/src/stream.ts +210 -0
  209. package/src/testing/run.ts +31 -5
  210. package/src/types.ts +1107 -59
  211. package/src/runtime/tls.ts +0 -434
  212. package/src/types/playwright-stealth.d.ts +0 -9
@@ -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 PlaywrightStealthModule = {
17
- stealth(page: unknown): Promise<void>;
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 BrowserPageContract = {
41
- close(): Promise<void>;
42
- content(): Promise<string>;
43
- evaluate<T>(fn: string | (() => T)): Promise<T>;
44
- fill?(selector: string, text: string): Promise<void>;
45
- goto(url: string): Promise<void>;
46
- pageId?: string;
47
- screenshot(options?: { fullPage?: boolean }): Promise<Buffer>;
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 = BrowserClientContract & {
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.CDP_POOL_URL ?? env.APIFUSE_CDP_POOL_URL;
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
- error instanceof Error &&
94
- ("code" in error
95
- ? (error as Error & { code?: string }).code === "MODULE_NOT_FOUND"
96
- : error.message.includes("Cannot find module"))
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
- require("playwright");
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
- async function loadPlaywrightStealth(): Promise<PlaywrightStealthModule> {
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<PlaywrightStealthModule>("playwright-stealth"),
253
+ await importOptionalModule<PlaywrightExtraModule>("playwright-extra"),
155
254
  );
156
255
  } catch (error) {
157
256
  if (isModuleNotFoundError(error)) {
158
- throw new ProviderError("playwright-stealth is not installed", {
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 playwright = await loadPlaywright();
260
- this.browser = await playwright.chromium.launch(
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(private readonly endpoint: string) {}
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: (pageId: string) => Promise<void>,
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(this.pageId);
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
- const acquireResult = (await this.poolClient.send(
616
- "acquire",
617
- )) as PoolAcquireResponse;
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 (pageId) => {
623
- await this.poolClient.send("release", { pageId });
1153
+ async (request) => {
1154
+ await this.poolClient.send("release", request);
624
1155
  },
625
1156
  );
626
1157
 
627
- await page.evaluate(
628
- `window.navigator.webdriver === true ? Object.defineProperty(window.navigator, "webdriver", { configurable: true, get: () => undefined }) : undefined`,
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<unknown> {
712
- return await this.client.newPage();
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
- await this.client.close();
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 {