@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.
Files changed (163) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +2 -2
  3. package/SUBMISSION.md +2 -1
  4. package/bin/apifuse-check.ts +60 -6
  5. package/bin/apifuse-dev.ts +48 -5
  6. package/bin/apifuse-perf.ts +50 -11
  7. package/bin/apifuse-record.ts +35 -11
  8. package/bin/apifuse-submit-check.ts +1425 -3
  9. package/dist/ceremonies/index.d.ts +41 -0
  10. package/dist/ceremonies/index.js +490 -0
  11. package/dist/choice-token.d.ts +24 -0
  12. package/dist/choice-token.js +74 -0
  13. package/dist/cli/commands.d.ts +10 -0
  14. package/dist/cli/commands.js +80 -0
  15. package/dist/cli/create.d.ts +47 -0
  16. package/dist/cli/create.js +762 -0
  17. package/dist/config/loader.d.ts +107 -0
  18. package/dist/config/loader.js +935 -0
  19. package/dist/contract-json.d.ts +9 -0
  20. package/dist/contract-json.js +51 -0
  21. package/dist/contract-serialization.d.ts +4 -0
  22. package/dist/contract-serialization.js +78 -0
  23. package/dist/contract-types.d.ts +49 -0
  24. package/dist/contract-types.js +1 -0
  25. package/dist/contract.d.ts +6 -0
  26. package/dist/contract.js +155 -0
  27. package/dist/define.d.ts +97 -0
  28. package/dist/define.js +1320 -0
  29. package/dist/dev.d.ts +9 -0
  30. package/dist/dev.js +15 -0
  31. package/dist/errors.d.ts +59 -0
  32. package/dist/errors.js +97 -0
  33. package/dist/i18n/catalog.d.ts +29 -0
  34. package/dist/i18n/catalog.js +159 -0
  35. package/dist/i18n/index.d.ts +2 -0
  36. package/dist/i18n/index.js +2 -0
  37. package/dist/i18n/keys.d.ts +10 -0
  38. package/dist/i18n/keys.js +34 -0
  39. package/dist/index.d.ts +41 -0
  40. package/dist/index.js +37 -0
  41. package/dist/lint.d.ts +73 -0
  42. package/dist/lint.js +702 -0
  43. package/dist/observability.d.ts +5 -0
  44. package/dist/observability.js +39 -0
  45. package/dist/provider.d.ts +9 -0
  46. package/dist/provider.js +8 -0
  47. package/dist/public-schema-field-lint.d.ts +2 -0
  48. package/dist/public-schema-field-lint.js +158 -0
  49. package/dist/recipes/gov-api.d.ts +19 -0
  50. package/dist/recipes/gov-api.js +72 -0
  51. package/dist/recipes/rest-api.d.ts +21 -0
  52. package/dist/recipes/rest-api.js +115 -0
  53. package/dist/runtime/auth-flow.d.ts +14 -0
  54. package/dist/runtime/auth-flow.js +44 -0
  55. package/dist/runtime/browser.d.ts +25 -0
  56. package/dist/runtime/browser.js +1034 -0
  57. package/dist/runtime/cache.d.ts +10 -0
  58. package/dist/runtime/cache.js +372 -0
  59. package/dist/runtime/choice.d.ts +15 -0
  60. package/dist/runtime/choice.js +435 -0
  61. package/dist/runtime/credential.d.ts +8 -0
  62. package/dist/runtime/credential.js +61 -0
  63. package/dist/runtime/env.d.ts +2 -0
  64. package/dist/runtime/env.js +10 -0
  65. package/dist/runtime/executor.d.ts +16 -0
  66. package/dist/runtime/executor.js +51 -0
  67. package/dist/runtime/http.d.ts +8 -0
  68. package/dist/runtime/http.js +706 -0
  69. package/dist/runtime/insights.d.ts +9 -0
  70. package/dist/runtime/insights.js +324 -0
  71. package/dist/runtime/instrumentation.d.ts +8 -0
  72. package/dist/runtime/instrumentation.js +269 -0
  73. package/dist/runtime/key-derivation.d.ts +24 -0
  74. package/dist/runtime/key-derivation.js +73 -0
  75. package/dist/runtime/keyring.d.ts +25 -0
  76. package/dist/runtime/keyring.js +93 -0
  77. package/dist/runtime/namespace.d.ts +9 -0
  78. package/dist/runtime/namespace.js +19 -0
  79. package/dist/runtime/otlp.d.ts +39 -0
  80. package/dist/runtime/otlp.js +103 -0
  81. package/dist/runtime/perf.d.ts +12 -0
  82. package/dist/runtime/perf.js +52 -0
  83. package/dist/runtime/prevalidate.d.ts +12 -0
  84. package/dist/runtime/prevalidate.js +173 -0
  85. package/dist/runtime/provider.d.ts +2 -0
  86. package/dist/runtime/provider.js +11 -0
  87. package/dist/runtime/proxy-errors.d.ts +21 -0
  88. package/dist/runtime/proxy-errors.js +83 -0
  89. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  90. package/dist/runtime/proxy-telemetry.js +174 -0
  91. package/dist/runtime/redis.d.ts +17 -0
  92. package/dist/runtime/redis.js +82 -0
  93. package/dist/runtime/request-options.d.ts +3 -0
  94. package/dist/runtime/request-options.js +42 -0
  95. package/dist/runtime/state.d.ts +17 -0
  96. package/dist/runtime/state.js +344 -0
  97. package/dist/runtime/stealth.d.ts +18 -0
  98. package/dist/runtime/stealth.js +827 -0
  99. package/dist/runtime/stt.d.ts +22 -0
  100. package/dist/runtime/stt.js +480 -0
  101. package/dist/runtime/trace.d.ts +26 -0
  102. package/dist/runtime/trace.js +142 -0
  103. package/dist/runtime/waterfall.d.ts +12 -0
  104. package/dist/runtime/waterfall.js +147 -0
  105. package/dist/schema.d.ts +74 -0
  106. package/dist/schema.js +243 -0
  107. package/dist/serve.d.ts +1 -0
  108. package/dist/serve.js +1 -0
  109. package/dist/server/index.d.ts +3 -0
  110. package/dist/server/index.js +2 -0
  111. package/dist/server/serve.d.ts +64 -0
  112. package/dist/server/serve.js +1110 -0
  113. package/dist/server/types.d.ts +136 -0
  114. package/dist/server/types.js +86 -0
  115. package/dist/stealth/profiles.d.ts +4 -0
  116. package/dist/stealth/profiles.js +259 -0
  117. package/dist/stream.d.ts +44 -0
  118. package/dist/stream.js +151 -0
  119. package/dist/testing/helpers.d.ts +23 -0
  120. package/dist/testing/helpers.js +95 -0
  121. package/dist/testing/index.d.ts +2 -0
  122. package/dist/testing/index.js +2 -0
  123. package/dist/testing/run.d.ts +34 -0
  124. package/dist/testing/run.js +303 -0
  125. package/dist/types.d.ts +1324 -0
  126. package/dist/types.js +61 -0
  127. package/dist/utils/date.d.ts +6 -0
  128. package/dist/utils/date.js +101 -0
  129. package/dist/utils/parse.d.ts +16 -0
  130. package/dist/utils/parse.js +51 -0
  131. package/dist/utils/text.d.ts +4 -0
  132. package/dist/utils/text.js +14 -0
  133. package/dist/utils/transform.d.ts +8 -0
  134. package/dist/utils/transform.js +48 -0
  135. package/package.json +42 -25
  136. package/src/ceremonies/index.ts +8 -2
  137. package/src/choice-token.ts +1 -0
  138. package/src/cli/commands.ts +8 -5
  139. package/src/cli/create.ts +28 -0
  140. package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
  141. package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
  142. package/src/config/loader.ts +19 -1
  143. package/src/contract-json.ts +75 -0
  144. package/src/contract-serialization.ts +89 -0
  145. package/src/contract-types.ts +52 -0
  146. package/src/contract.ts +215 -0
  147. package/src/define.ts +37 -2
  148. package/src/errors.ts +15 -0
  149. package/src/i18n/catalog.ts +156 -0
  150. package/src/index.ts +22 -1
  151. package/src/lint.ts +256 -37
  152. package/src/provider.ts +45 -2
  153. package/src/runtime/browser.ts +685 -30
  154. package/src/runtime/cache.ts +35 -89
  155. package/src/runtime/choice.ts +760 -0
  156. package/src/runtime/executor.ts +19 -2
  157. package/src/runtime/redis.ts +116 -0
  158. package/src/runtime/state.ts +487 -0
  159. package/src/runtime/stealth.ts +8 -1
  160. package/src/server/serve.ts +361 -46
  161. package/src/server/types.ts +2 -0
  162. package/src/testing/run.ts +16 -3
  163. package/src/types.ts +209 -6
@@ -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 BrowserPageContract = {
45
- close(): Promise<void>;
46
- content(): Promise<string>;
47
- evaluate<T>(fn: string | (() => T)): Promise<T>;
48
- fill?(selector: string, text: string): Promise<void>;
49
- goto(url: string): Promise<void>;
50
- pageId?: string;
51
- screenshot(options?: { fullPage?: boolean }): Promise<Buffer>;
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 = BrowserClientContract & {
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(private readonly endpoint: string) {}
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: (pageId: string) => Promise<void>,
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(this.pageId);
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
- const acquireResult = (await this.poolClient.send(
672
- "acquire",
673
- )) 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
+ );
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 (pageId) => {
679
- await this.poolClient.send("release", { pageId });
1153
+ async (request) => {
1154
+ await this.poolClient.send("release", request);
680
1155
  },
681
1156
  );
682
1157
 
683
- await page.evaluate(
684
- `window.navigator.webdriver === true ? Object.defineProperty(window.navigator, "webdriver", { configurable: true, get: () => undefined }) : undefined`,
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<unknown> {
768
- 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
+ }
769
1374
  }
770
1375
 
771
1376
  async close(): Promise<void> {
772
- 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
+ }
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 {