@apifuse/provider-sdk 2.1.0-beta.2 → 2.1.0-beta.21

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 (231) hide show
  1. package/AUTHORING.md +330 -8
  2. package/CHANGELOG.md +89 -1
  3. package/README.md +64 -17
  4. package/SUBMISSION.md +86 -0
  5. package/bin/apifuse-check.ts +60 -6
  6. package/bin/apifuse-dev.ts +58 -8
  7. package/bin/apifuse-pack-check.ts +32 -2
  8. package/bin/apifuse-pack-smoke.ts +133 -6
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +3243 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/auth.d.ts +76 -0
  14. package/dist/auth.js +436 -0
  15. package/dist/ceremonies/index.d.ts +41 -0
  16. package/dist/ceremonies/index.js +490 -0
  17. package/dist/choice-token.d.ts +24 -0
  18. package/dist/choice-token.js +74 -0
  19. package/dist/cli/commands.d.ts +10 -0
  20. package/dist/cli/commands.js +80 -0
  21. package/dist/cli/create.d.ts +47 -0
  22. package/dist/cli/create.js +777 -0
  23. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  24. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  25. package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
  26. package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
  27. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  28. package/dist/cli/templates/provider/README.md.tpl +163 -0
  29. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  30. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  31. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  32. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  33. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  34. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  35. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  36. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  37. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  38. package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  39. package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  40. package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  41. package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  42. package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  43. package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  44. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  45. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  46. package/dist/config/loader.d.ts +107 -0
  47. package/dist/config/loader.js +935 -0
  48. package/dist/contract-json.d.ts +9 -0
  49. package/dist/contract-json.js +51 -0
  50. package/dist/contract-serialization.d.ts +4 -0
  51. package/dist/contract-serialization.js +78 -0
  52. package/dist/contract-types.d.ts +49 -0
  53. package/dist/contract-types.js +1 -0
  54. package/dist/contract.d.ts +6 -0
  55. package/dist/contract.js +156 -0
  56. package/dist/define.d.ts +100 -0
  57. package/dist/define.js +1383 -0
  58. package/dist/dev.d.ts +9 -0
  59. package/dist/dev.js +15 -0
  60. package/dist/errors.d.ts +59 -0
  61. package/dist/errors.js +97 -0
  62. package/dist/i18n/catalog.d.ts +29 -0
  63. package/dist/i18n/catalog.js +159 -0
  64. package/dist/i18n/index.d.ts +2 -0
  65. package/dist/i18n/index.js +2 -0
  66. package/dist/i18n/keys.d.ts +10 -0
  67. package/dist/i18n/keys.js +34 -0
  68. package/dist/index.d.ts +42 -0
  69. package/dist/index.js +38 -0
  70. package/dist/lint.d.ts +74 -0
  71. package/dist/lint.js +729 -0
  72. package/dist/observability.d.ts +5 -0
  73. package/dist/observability.js +39 -0
  74. package/dist/provider.d.ts +11 -0
  75. package/dist/provider.js +9 -0
  76. package/dist/public-schema-field-lint.d.ts +2 -0
  77. package/dist/public-schema-field-lint.js +158 -0
  78. package/dist/recipes/gov-api.d.ts +19 -0
  79. package/dist/recipes/gov-api.js +72 -0
  80. package/dist/recipes/rest-api.d.ts +21 -0
  81. package/dist/recipes/rest-api.js +115 -0
  82. package/dist/runtime/auth-flow.d.ts +14 -0
  83. package/dist/runtime/auth-flow.js +46 -0
  84. package/dist/runtime/browser.d.ts +25 -0
  85. package/dist/runtime/browser.js +1237 -0
  86. package/dist/runtime/cache.d.ts +10 -0
  87. package/dist/runtime/cache.js +372 -0
  88. package/dist/runtime/choice.d.ts +15 -0
  89. package/dist/runtime/choice.js +435 -0
  90. package/dist/runtime/credential.d.ts +8 -0
  91. package/dist/runtime/credential.js +61 -0
  92. package/dist/runtime/env.d.ts +2 -0
  93. package/dist/runtime/env.js +10 -0
  94. package/dist/runtime/executor.d.ts +16 -0
  95. package/dist/runtime/executor.js +51 -0
  96. package/dist/runtime/http.d.ts +8 -0
  97. package/dist/runtime/http.js +726 -0
  98. package/dist/runtime/insights.d.ts +9 -0
  99. package/dist/runtime/insights.js +324 -0
  100. package/dist/runtime/instrumentation.d.ts +8 -0
  101. package/dist/runtime/instrumentation.js +269 -0
  102. package/dist/runtime/key-derivation.d.ts +24 -0
  103. package/dist/runtime/key-derivation.js +73 -0
  104. package/dist/runtime/keyring.d.ts +25 -0
  105. package/dist/runtime/keyring.js +93 -0
  106. package/dist/runtime/namespace.d.ts +9 -0
  107. package/dist/runtime/namespace.js +19 -0
  108. package/dist/runtime/otlp.d.ts +39 -0
  109. package/dist/runtime/otlp.js +103 -0
  110. package/dist/runtime/perf.d.ts +12 -0
  111. package/dist/runtime/perf.js +52 -0
  112. package/dist/runtime/prevalidate.d.ts +12 -0
  113. package/dist/runtime/prevalidate.js +173 -0
  114. package/dist/runtime/provider.d.ts +2 -0
  115. package/dist/runtime/provider.js +11 -0
  116. package/dist/runtime/proxy-errors.d.ts +21 -0
  117. package/dist/runtime/proxy-errors.js +83 -0
  118. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  119. package/dist/runtime/proxy-telemetry.js +174 -0
  120. package/dist/runtime/redis.d.ts +17 -0
  121. package/dist/runtime/redis.js +82 -0
  122. package/dist/runtime/request-options.d.ts +3 -0
  123. package/dist/runtime/request-options.js +42 -0
  124. package/dist/runtime/state.d.ts +17 -0
  125. package/dist/runtime/state.js +344 -0
  126. package/dist/runtime/stealth.d.ts +21 -0
  127. package/dist/runtime/stealth.js +980 -0
  128. package/dist/runtime/stt.d.ts +22 -0
  129. package/dist/runtime/stt.js +480 -0
  130. package/dist/runtime/trace.d.ts +26 -0
  131. package/dist/runtime/trace.js +142 -0
  132. package/dist/runtime/waterfall.d.ts +12 -0
  133. package/dist/runtime/waterfall.js +147 -0
  134. package/dist/schema.d.ts +74 -0
  135. package/dist/schema.js +243 -0
  136. package/dist/serve.d.ts +1 -0
  137. package/dist/serve.js +1 -0
  138. package/dist/server/index.d.ts +3 -0
  139. package/dist/server/index.js +2 -0
  140. package/dist/server/serve.d.ts +64 -0
  141. package/dist/server/serve.js +1118 -0
  142. package/dist/server/types.d.ts +136 -0
  143. package/dist/server/types.js +86 -0
  144. package/dist/stealth/profiles.d.ts +4 -0
  145. package/dist/stealth/profiles.js +259 -0
  146. package/dist/stream.d.ts +44 -0
  147. package/dist/stream.js +151 -0
  148. package/dist/testing/helpers.d.ts +23 -0
  149. package/dist/testing/helpers.js +95 -0
  150. package/dist/testing/index.d.ts +2 -0
  151. package/dist/testing/index.js +2 -0
  152. package/dist/testing/run.d.ts +34 -0
  153. package/dist/testing/run.js +307 -0
  154. package/dist/types.d.ts +1467 -0
  155. package/dist/types.js +61 -0
  156. package/dist/utils/date.d.ts +6 -0
  157. package/dist/utils/date.js +101 -0
  158. package/dist/utils/parse.d.ts +16 -0
  159. package/dist/utils/parse.js +51 -0
  160. package/dist/utils/text.d.ts +4 -0
  161. package/dist/utils/text.js +14 -0
  162. package/dist/utils/transform.d.ts +8 -0
  163. package/dist/utils/transform.js +48 -0
  164. package/package.json +57 -29
  165. package/src/auth.ts +786 -0
  166. package/src/ceremonies/index.ts +8 -2
  167. package/src/choice-token.ts +165 -0
  168. package/src/cli/commands.ts +34 -11
  169. package/src/cli/create.ts +254 -128
  170. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  171. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  172. package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
  173. package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
  174. package/src/cli/templates/provider/README.md.tpl +87 -7
  175. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  176. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  177. package/src/cli/templates/provider/index.ts.tpl +5 -47
  178. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  179. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  180. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  181. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  182. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  183. package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  184. package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  185. package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  186. package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  187. package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  188. package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  189. package/src/cli/templates/provider/start.ts.tpl +1 -1
  190. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  191. package/src/config/loader.ts +1224 -9
  192. package/src/contract-json.ts +75 -0
  193. package/src/contract-serialization.ts +89 -0
  194. package/src/contract-types.ts +52 -0
  195. package/src/contract.ts +216 -0
  196. package/src/define.ts +1820 -70
  197. package/src/errors.ts +27 -0
  198. package/src/i18n/catalog.ts +277 -0
  199. package/src/i18n/index.ts +2 -0
  200. package/src/i18n/keys.ts +64 -0
  201. package/src/index.ts +189 -9
  202. package/src/lint.ts +580 -73
  203. package/src/observability.ts +41 -0
  204. package/src/provider.ts +131 -4
  205. package/src/public-schema-field-lint.ts +237 -0
  206. package/src/runtime/auth-flow.ts +9 -0
  207. package/src/runtime/browser.ts +1054 -51
  208. package/src/runtime/cache.ts +528 -0
  209. package/src/runtime/choice.ts +760 -0
  210. package/src/runtime/executor.ts +32 -3
  211. package/src/runtime/http.ts +980 -195
  212. package/src/runtime/insights.ts +11 -11
  213. package/src/runtime/instrumentation.ts +12 -4
  214. package/src/runtime/key-derivation.ts +1 -1
  215. package/src/runtime/keyring.ts +4 -3
  216. package/src/runtime/proxy-errors.ts +132 -0
  217. package/src/runtime/proxy-telemetry.ts +253 -0
  218. package/src/runtime/redis.ts +116 -0
  219. package/src/runtime/request-options.ts +66 -0
  220. package/src/runtime/state.ts +563 -0
  221. package/src/runtime/stealth.ts +1336 -0
  222. package/src/runtime/stt.ts +629 -0
  223. package/src/runtime/trace.ts +1 -1
  224. package/src/schema.ts +363 -1
  225. package/src/server/serve.ts +1192 -75
  226. package/src/server/types.ts +37 -0
  227. package/src/stream.ts +210 -0
  228. package/src/testing/run.ts +40 -6
  229. package/src/types.ts +1283 -59
  230. package/src/runtime/tls.ts +0 -434
  231. package/src/types/playwright-stealth.d.ts +0 -9
@@ -1,27 +1,49 @@
1
1
  import { createRequire } from "node:module";
2
- import type { LaunchOptions, Page } from "playwright";
2
+ import type { Frame, LaunchOptions, Locator, Page, Request, Route } 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,
14
+ BrowserResourceBody,
15
+ BrowserResourceDecision,
16
+ BrowserResourceMethod,
17
+ BrowserResourcePolicy,
18
+ BrowserResourceRequest,
9
19
  } from "../types";
10
20
 
11
21
  const require = createRequire(import.meta.url);
12
22
  const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
13
23
  const SELECTOR_POLL_INTERVAL_MS = 100;
24
+ const RESOURCE_POLICY_ROUTE_PATTERN = "**/*";
25
+ const DEFAULT_RESOURCE_METHODS = ["GET", "HEAD"] as const;
14
26
 
15
27
  type PlaywrightModule = typeof import("playwright");
16
- type PlaywrightStealthModule = {
17
- stealth(page: unknown): Promise<void>;
28
+ type PlaywrightExtraModule = {
29
+ chromium: PlaywrightModule["chromium"] & { use(plugin: unknown): unknown };
18
30
  };
19
31
 
32
+ type StealthPluginFactory = (options?: {
33
+ enabledEvasions?: Set<string>;
34
+ }) => unknown;
35
+
20
36
  type PoolAcquireResponse = {
37
+ browserContextId?: string;
21
38
  pageId: string;
22
39
  wsEndpoint: string;
23
40
  };
24
41
 
42
+ type PoolReleaseRequest = {
43
+ browserContextId?: string;
44
+ pageId: string;
45
+ };
46
+
25
47
  type JsonRpcId = number;
26
48
 
27
49
  type JsonRpcError = {
@@ -37,35 +59,182 @@ type JsonRpcMessage = {
37
59
  result?: Record<string, unknown>;
38
60
  };
39
61
 
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>;
62
+ type CdpFrameTreeNode = {
63
+ childFrames?: CdpFrameTreeNode[];
64
+ frame: {
65
+ id: string;
66
+ name?: string;
67
+ parentId?: string;
68
+ url?: string;
69
+ };
70
+ };
71
+
72
+ type CdpFetchFulfillParams = {
73
+ readonly requestId: string;
74
+ readonly responseCode: number;
75
+ readonly responseHeaders?: readonly {
76
+ readonly name: string;
77
+ readonly value: string;
78
+ }[];
79
+ readonly body?: string;
54
80
  };
55
81
 
82
+ type BrowserPageContract = BrowserPage;
83
+
84
+ function toResourceBody(
85
+ body: BrowserResourceBody | undefined,
86
+ ): Buffer | string | undefined {
87
+ if (body === undefined || typeof body === "string" || Buffer.isBuffer(body)) {
88
+ return body;
89
+ }
90
+
91
+ if (body instanceof ArrayBuffer) {
92
+ return Buffer.from(new Uint8Array(body));
93
+ }
94
+
95
+ return Buffer.from(body);
96
+ }
97
+
98
+ function isResourceMethod(method: string): method is BrowserResourceMethod {
99
+ return method === "GET" || method === "HEAD";
100
+ }
101
+
102
+ async function toResourceRequest(
103
+ request: Request,
104
+ ): Promise<BrowserResourceRequest | null> {
105
+ const method = request.method().toUpperCase();
106
+ if (!isResourceMethod(method)) {
107
+ return null;
108
+ }
109
+
110
+ return {
111
+ headers: await request.allHeaders(),
112
+ method,
113
+ resourceType: request.resourceType(),
114
+ url: request.url(),
115
+ };
116
+ }
117
+
118
+ function toCdpResourceRequest(
119
+ params: unknown,
120
+ ): { requestId: string; request: BrowserResourceRequest } | null {
121
+ if (!isRecord(params)) {
122
+ return null;
123
+ }
124
+
125
+ const requestId = params.requestId;
126
+ const rawRequest = params.request;
127
+ if (typeof requestId !== "string" || !isRecord(rawRequest)) {
128
+ return null;
129
+ }
130
+
131
+ const url = rawRequest.url;
132
+ const method = String(rawRequest.method ?? "").toUpperCase();
133
+ if (typeof url !== "string" || !isResourceMethod(method)) {
134
+ return null;
135
+ }
136
+
137
+ return {
138
+ requestId,
139
+ request: {
140
+ headers: toCdpResourceHeaders(rawRequest.headers),
141
+ method,
142
+ resourceType:
143
+ typeof params.resourceType === "string" ? params.resourceType : undefined,
144
+ url,
145
+ },
146
+ };
147
+ }
148
+
149
+ function getCdpPausedRequestId(params: unknown): string | null {
150
+ if (!isRecord(params) || typeof params.requestId !== "string") {
151
+ return null;
152
+ }
153
+
154
+ return params.requestId;
155
+ }
156
+
157
+ function toCdpResourceHeaders(value: unknown): Record<string, string> {
158
+ if (!isRecord(value)) {
159
+ return {};
160
+ }
161
+
162
+ const headers: Record<string, string> = {};
163
+ for (const [name, headerValue] of Object.entries(value)) {
164
+ if (typeof headerValue === "string") {
165
+ headers[name] = headerValue;
166
+ }
167
+ }
168
+
169
+ return headers;
170
+ }
171
+
172
+ function matchesResourceRoute(
173
+ match: BrowserResourcePolicy["routes"][number]["match"],
174
+ request: BrowserResourceRequest,
175
+ ): boolean {
176
+ if (typeof match === "string") {
177
+ return request.url === match;
178
+ }
179
+ if (match instanceof RegExp) {
180
+ return match.test(request.url);
181
+ }
182
+
183
+ return match(request);
184
+ }
185
+
186
+ function toCdpFulfillParams(
187
+ requestId: string,
188
+ decision: Extract<BrowserResourceDecision, { readonly action: "fulfill" }>,
189
+ ): CdpFetchFulfillParams {
190
+ const body = toResourceBody(decision.body);
191
+ return {
192
+ ...(body === undefined
193
+ ? {}
194
+ : { body: Buffer.from(body).toString("base64") }),
195
+ ...(decision.headers === undefined
196
+ ? {}
197
+ : {
198
+ responseHeaders: Object.entries(decision.headers).map(
199
+ ([name, value]) => ({ name, value }),
200
+ ),
201
+ }),
202
+ requestId,
203
+ responseCode: decision.status ?? 200,
204
+ };
205
+ }
206
+
207
+ async function fulfillResourceRoute(
208
+ route: Route,
209
+ decision: Extract<BrowserResourceDecision, { readonly action: "fulfill" }>,
210
+ ): Promise<void> {
211
+ const body = toResourceBody(decision.body);
212
+ await route.fulfill({
213
+ ...(body === undefined ? {} : { body }),
214
+ ...(decision.headers === undefined ? {} : { headers: decision.headers }),
215
+ status: decision.status ?? 200,
216
+ });
217
+ }
218
+
56
219
  export type BrowserClientOptions = BrowserOptions & {
220
+ allowedHosts?: string[];
57
221
  cdpUrl?: string;
58
222
  executablePath?: string;
59
223
  extraArgs?: string[];
60
224
  };
61
225
 
62
- type SupportedBrowserClient = BrowserClientContract & {
226
+ type SupportedBrowserClient = {
227
+ readonly engine: BrowserEngine;
63
228
  close(): Promise<void>;
64
229
  newPage(): Promise<BrowserPageContract>;
230
+ rawPage(): Promise<BrowserPageContract>;
231
+ withIsolatedContext<T>(
232
+ handler: (page: BrowserPageContract) => Promise<T>,
233
+ ): Promise<T>;
65
234
  };
66
235
 
67
236
  function getDefaultCdpPoolUrl(env = process.env): string | undefined {
68
- return env.CDP_POOL_URL ?? env.APIFUSE_CDP_POOL_URL;
237
+ return env.APIFUSE__CDP_POOL__URL;
69
238
  }
70
239
 
71
240
  async function importOptionalModule<T extends object>(
@@ -89,11 +258,16 @@ function unwrapModuleDefault<T extends object>(module: T): T {
89
258
  }
90
259
 
91
260
  function isModuleNotFoundError(error: unknown): boolean {
261
+ if (!(error instanceof Error)) {
262
+ return false;
263
+ }
264
+
265
+ const code = "code" in error ? error.code : undefined;
92
266
  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"))
267
+ code === "MODULE_NOT_FOUND" ||
268
+ code === "ERR_MODULE_NOT_FOUND" ||
269
+ error.message.includes("Cannot find module") ||
270
+ error.message.includes("Cannot find package")
97
271
  );
98
272
  }
99
273
 
@@ -118,9 +292,71 @@ function toLaunchOptions(options: BrowserClientOptions): LaunchOptions {
118
292
  };
119
293
  }
120
294
 
295
+ class PlaywrightBrowserLocator implements BrowserLocator {
296
+ constructor(private readonly locator: Locator) {}
297
+
298
+ async click(): Promise<void> {
299
+ await this.locator.click();
300
+ }
301
+
302
+ async fill(text: string): Promise<void> {
303
+ await this.locator.fill(text);
304
+ }
305
+
306
+ async textContent(): Promise<string | null> {
307
+ return await this.locator.textContent();
308
+ }
309
+
310
+ async waitFor(options?: { timeout?: number }): Promise<void> {
311
+ await this.locator.waitFor(options);
312
+ }
313
+ }
314
+
315
+ class PlaywrightBrowserFrame implements BrowserFrame {
316
+ constructor(private readonly frame: Frame) {}
317
+
318
+ get id(): string {
319
+ return this.frame.name() || this.frame.url();
320
+ }
321
+
322
+ get name(): string | undefined {
323
+ const name = this.frame.name();
324
+ return name.length > 0 ? name : undefined;
325
+ }
326
+
327
+ get parentId(): string | undefined {
328
+ const parent = this.frame.parentFrame();
329
+ return parent ? parent.name() || parent.url() : undefined;
330
+ }
331
+
332
+ async url(): Promise<string> {
333
+ return this.frame.url();
334
+ }
335
+
336
+ async title(): Promise<string> {
337
+ return await this.frame.evaluate("document.title");
338
+ }
339
+
340
+ async content(): Promise<string> {
341
+ return await this.frame.content();
342
+ }
343
+
344
+ async evaluate<T>(fn: string | (() => T)): Promise<T> {
345
+ if (typeof fn === "string") {
346
+ return await this.frame.evaluate(fn);
347
+ }
348
+
349
+ return await this.frame.evaluate(fn);
350
+ }
351
+
352
+ locator(selector: string): BrowserLocator {
353
+ return new PlaywrightBrowserLocator(this.frame.locator(selector));
354
+ }
355
+ }
356
+
121
357
  async function loadPlaywright(): Promise<PlaywrightModule> {
122
358
  try {
123
- require("playwright");
359
+ await importOptionalModule<PlaywrightModule>("playwright");
124
360
  } catch (error) {
125
361
  if (isModuleNotFoundError(error)) {
126
362
  throw new ProviderError("Playwright is not installed", {
@@ -148,16 +384,31 @@ async function loadPlaywright(): Promise<PlaywrightModule> {
148
384
  }
149
385
  }
150
386
 
151
- async function loadPlaywrightStealth(): Promise<PlaywrightStealthModule> {
387
+ const playwrightExtraStealthLaunchers = new WeakSet<object>();
388
+
389
+ async function loadPlaywrightExtra(): Promise<PlaywrightExtraModule> {
390
+ try {
391
+ require("playwright");
392
+ } catch (error) {
393
+ if (isModuleNotFoundError(error)) {
394
+ throw new ProviderError("Playwright is not installed", {
395
+ cause: error instanceof Error ? error : undefined,
396
+ fix: "Run: bun add playwright",
397
+ });
398
+ }
399
+
400
+ throw error;
401
+ }
402
+
152
403
  try {
153
404
  return unwrapModuleDefault(
154
- await importOptionalModule<PlaywrightStealthModule>("playwright-stealth"),
405
+ await importOptionalModule<PlaywrightExtraModule>("playwright-extra"),
155
406
  );
156
407
  } catch (error) {
157
408
  if (isModuleNotFoundError(error)) {
158
- throw new ProviderError("playwright-stealth is not installed", {
409
+ throw new ProviderError("playwright-extra is not installed", {
159
410
  cause: error instanceof Error ? error : undefined,
160
- fix: "Run: bun add playwright-stealth",
411
+ fix: "Run: bun add playwright-extra puppeteer-extra-plugin-stealth",
161
412
  });
162
413
  }
163
414
 
@@ -165,6 +416,45 @@ async function loadPlaywrightStealth(): Promise<PlaywrightStealthModule> {
165
416
  }
166
417
  }
167
418
 
419
+ async function loadStealthPluginFactory(): Promise<StealthPluginFactory> {
420
+ try {
421
+ return unwrapModuleDefault(
422
+ await importOptionalModule<StealthPluginFactory>(
423
+ "puppeteer-extra-plugin-stealth",
424
+ ),
425
+ );
426
+ } catch (error) {
427
+ if (isModuleNotFoundError(error)) {
428
+ throw new ProviderError(
429
+ "puppeteer-extra-plugin-stealth is not installed",
430
+ {
431
+ cause: error instanceof Error ? error : undefined,
432
+ fix: "Run: bun add playwright-extra puppeteer-extra-plugin-stealth",
433
+ },
434
+ );
435
+ }
436
+
437
+ throw error;
438
+ }
439
+ }
440
+
441
+ async function loadChromiumLauncher(
442
+ options: BrowserClientOptions,
443
+ ): Promise<PlaywrightModule["chromium"]> {
444
+ if (!(options.stealth ?? true)) {
445
+ return (await loadPlaywright()).chromium;
446
+ }
447
+
448
+ const playwrightExtra = await loadPlaywrightExtra();
449
+ if (!playwrightExtraStealthLaunchers.has(playwrightExtra.chromium)) {
450
+ const createStealthPlugin = await loadStealthPluginFactory();
451
+ playwrightExtra.chromium.use(createStealthPlugin());
452
+ playwrightExtraStealthLaunchers.add(playwrightExtra.chromium);
453
+ }
454
+
455
+ return playwrightExtra.chromium;
456
+ }
457
+
168
458
  async function loadNodriver(): Promise<void> {
169
459
  try {
170
460
  await importOptionalModule("nodriver");
@@ -196,6 +486,7 @@ async function loadSeleniumBase(): Promise<void> {
196
486
  }
197
487
 
198
488
  class PlaywrightBrowserPage implements BrowserPageContract {
489
+ readonly id = "main";
199
490
  readonly pageId?: string;
200
491
 
201
492
  constructor(private readonly page: Page) {}
@@ -232,6 +523,22 @@ class PlaywrightBrowserPage implements BrowserPageContract {
232
523
  await this.page.fill(selector, text);
233
524
  }
234
525
 
526
+ async frames(): Promise<BrowserFrame[]> {
527
+ return this.page.frames().map((frame) => new PlaywrightBrowserFrame(frame));
528
+ }
529
+
530
+ locator(selector: string): BrowserLocator {
531
+ return new PlaywrightBrowserLocator(this.page.locator(selector));
532
+ }
533
+
534
+ async title(): Promise<string> {
535
+ return await this.page.title();
536
+ }
537
+
538
+ async url(): Promise<string> {
539
+ return this.page.url();
540
+ }
541
+
235
542
  async content(): Promise<string> {
236
543
  return await this.page.content();
237
544
  }
@@ -243,6 +550,47 @@ class PlaywrightBrowserPage implements BrowserPageContract {
243
550
  async close(): Promise<void> {
244
551
  await this.page.close();
245
552
  }
553
+
554
+ async withResourcePolicy<T>(
555
+ policy: BrowserResourcePolicy,
556
+ run: () => Promise<T>,
557
+ ): Promise<T> {
558
+ const allowedMethods = new Set(
559
+ policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS,
560
+ );
561
+ const handler = async (route: Route): Promise<void> => {
562
+ const request = await toResourceRequest(route.request());
563
+ if (!request || !allowedMethods.has(request.method)) {
564
+ await route.abort("blockedbyclient");
565
+ return;
566
+ }
567
+
568
+ for (const resourceRoute of policy.routes) {
569
+ if (!matchesResourceRoute(resourceRoute.match, request)) {
570
+ continue;
571
+ }
572
+
573
+ const decision = await resourceRoute.handle(request);
574
+ switch (decision.action) {
575
+ case "fulfill":
576
+ await fulfillResourceRoute(route, decision);
577
+ return;
578
+ case "block":
579
+ await route.abort("blockedbyclient");
580
+ return;
581
+ }
582
+ }
583
+
584
+ await route.abort("blockedbyclient");
585
+ };
586
+
587
+ await this.page.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
588
+ try {
589
+ return await run();
590
+ } finally {
591
+ await this.page.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
592
+ }
593
+ }
246
594
  }
247
595
 
248
596
  class PlaywrightBrowserClient implements SupportedBrowserClient {
@@ -256,10 +604,8 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
256
604
  return this.browser;
257
605
  }
258
606
 
259
- const playwright = await loadPlaywright();
260
- this.browser = await playwright.chromium.launch(
261
- toLaunchOptions(this.options),
262
- );
607
+ const chromium = await loadChromiumLauncher(this.options);
608
+ this.browser = await chromium.launch(toLaunchOptions(this.options));
263
609
  return this.browser;
264
610
  }
265
611
 
@@ -267,14 +613,35 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
267
613
  const browser = await this.ensureBrowser();
268
614
  const page = await browser.newPage();
269
615
 
270
- if (this.options.stealth ?? true) {
271
- const { stealth } = await loadPlaywrightStealth();
272
- await stealth(page);
273
- }
274
-
275
616
  return new PlaywrightBrowserPage(page);
276
617
  }
277
618
 
619
+ async rawPage(): Promise<BrowserPageContract> {
620
+ throw new ProviderError("ctx.browser.rawPage() requires a CDP pool", {
621
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
622
+ fix: "Set APIFUSE__CDP_POOL__URL and use the SDK CDP pool-backed browser runtime. Local Chromium launch is not allowed for rawPage().",
623
+ });
624
+ }
625
+
626
+ async withIsolatedContext<T>(
627
+ handler: (page: BrowserPageContract) => Promise<T>,
628
+ ): Promise<T> {
629
+ const browser = await this.ensureBrowser();
630
+ const context = await browser.newContext();
631
+ const page = await context.newPage();
632
+ const browserPage = new PlaywrightBrowserPage(page);
633
+
634
+ try {
635
+ return await handler(browserPage);
636
+ } finally {
637
+ try {
638
+ await browserPage.close();
639
+ } finally {
640
+ await context.close();
641
+ }
642
+ }
643
+ }
644
+
278
645
  async close(): Promise<void> {
279
646
  const browser = this.browser;
280
647
  this.browser = null;
@@ -287,8 +654,25 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
287
654
  }
288
655
  }
289
656
 
657
+ function normalizeWebSocketEndpoint(endpoint: string): string {
658
+ const url = new URL(endpoint);
659
+ if (url.protocol === "http:") {
660
+ url.protocol = "ws:";
661
+ return url.toString();
662
+ }
663
+ if (url.protocol === "https:") {
664
+ url.protocol = "wss:";
665
+ return url.toString();
666
+ }
667
+ if (url.protocol === "ws:" || url.protocol === "wss:") {
668
+ return endpoint;
669
+ }
670
+ throw new Error(`Unsupported WebSocket endpoint protocol: ${url.protocol}`);
671
+ }
672
+
290
673
  class JsonRpcWebSocketClient {
291
674
  private nextId = 1;
675
+ private readonly endpoint: string;
292
676
  private readonly listeners = new Map<
293
677
  string,
294
678
  Set<(params: unknown) => void>
@@ -303,7 +687,9 @@ class JsonRpcWebSocketClient {
303
687
  private socket?: WebSocket;
304
688
  private socketPromise?: Promise<WebSocket>;
305
689
 
306
- constructor(private readonly endpoint: string) {}
690
+ constructor(endpoint: string) {
691
+ this.endpoint = normalizeWebSocketEndpoint(endpoint);
692
+ }
307
693
 
308
694
  on(method: string, listener: (params: unknown) => void): () => void {
309
695
  const listeners = this.listeners.get(method) ?? new Set();
@@ -426,16 +812,233 @@ class JsonRpcWebSocketClient {
426
812
  }
427
813
  }
428
814
 
815
+ function flattenCdpFrameTree(
816
+ node: CdpFrameTreeNode | undefined,
817
+ out: CdpFrameTreeNode["frame"][] = [],
818
+ ): CdpFrameTreeNode["frame"][] {
819
+ if (!node) {
820
+ return out;
821
+ }
822
+
823
+ out.push(node.frame);
824
+ for (const child of node.childFrames ?? []) {
825
+ flattenCdpFrameTree(child, out);
826
+ }
827
+
828
+ return out;
829
+ }
830
+
831
+ function isRecord(value: unknown): value is Record<string, unknown> {
832
+ return value !== null && typeof value === "object" && !Array.isArray(value);
833
+ }
834
+
835
+ function parsePoolAcquireResponse(value: unknown): PoolAcquireResponse {
836
+ if (
837
+ !isRecord(value) ||
838
+ typeof value.pageId !== "string" ||
839
+ typeof value.wsEndpoint !== "string"
840
+ ) {
841
+ throw new ProviderError("CDP Pool returned an invalid acquire response", {
842
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
843
+ });
844
+ }
845
+
846
+ if (
847
+ value.browserContextId !== undefined &&
848
+ typeof value.browserContextId !== "string"
849
+ ) {
850
+ throw new ProviderError("CDP Pool returned an invalid acquire response", {
851
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
852
+ });
853
+ }
854
+
855
+ return {
856
+ ...(value.browserContextId
857
+ ? { browserContextId: value.browserContextId }
858
+ : {}),
859
+ pageId: value.pageId,
860
+ wsEndpoint: value.wsEndpoint,
861
+ };
862
+ }
863
+
864
+ function parseCdpFrameTreeNode(value: unknown): CdpFrameTreeNode | undefined {
865
+ if (!isRecord(value) || !isRecord(value.frame)) {
866
+ return undefined;
867
+ }
868
+
869
+ const frameId = value.frame.id;
870
+ if (typeof frameId !== "string") {
871
+ return undefined;
872
+ }
873
+
874
+ const childFrames = Array.isArray(value.childFrames)
875
+ ? value.childFrames
876
+ .map(parseCdpFrameTreeNode)
877
+ .filter((child): child is CdpFrameTreeNode => child !== undefined)
878
+ : undefined;
879
+
880
+ return {
881
+ frame: {
882
+ id: frameId,
883
+ name: typeof value.frame.name === "string" ? value.frame.name : undefined,
884
+ parentId:
885
+ typeof value.frame.parentId === "string"
886
+ ? value.frame.parentId
887
+ : undefined,
888
+ url: typeof value.frame.url === "string" ? value.frame.url : undefined,
889
+ },
890
+ ...(childFrames ? { childFrames } : {}),
891
+ };
892
+ }
893
+
894
+ function getCdpExecutionContext(params: unknown): {
895
+ frameId?: string;
896
+ id?: number;
897
+ } {
898
+ if (!isRecord(params) || !isRecord(params.context)) {
899
+ return {};
900
+ }
901
+
902
+ const contextId = params.context.id;
903
+ const auxData = params.context.auxData;
904
+ return {
905
+ frameId:
906
+ isRecord(auxData) && typeof auxData.frameId === "string"
907
+ ? auxData.frameId
908
+ : undefined,
909
+ id: typeof contextId === "number" ? contextId : undefined,
910
+ };
911
+ }
912
+
913
+ class CdpBrowserLocator implements BrowserLocator {
914
+ constructor(
915
+ private readonly frame: {
916
+ evaluate<T>(fn: string | (() => T)): Promise<T>;
917
+ waitForSelector?(
918
+ selector: string,
919
+ options?: { timeout?: number },
920
+ ): Promise<void>;
921
+ },
922
+ private readonly selector: string,
923
+ ) {}
924
+
925
+ async click(): Promise<void> {
926
+ await this.waitFor();
927
+ await this.frame.evaluate(
928
+ `(() => {
929
+ const element = document.querySelector(${JSON.stringify(this.selector)});
930
+ if (!(element instanceof HTMLElement)) {
931
+ throw new Error(${JSON.stringify(`Selector not found: ${this.selector}`)});
932
+ }
933
+ element.click();
934
+ })()`,
935
+ );
936
+ }
937
+
938
+ async fill(text: string): Promise<void> {
939
+ await this.waitFor();
940
+ await this.frame.evaluate(
941
+ `(() => {
942
+ const element = document.querySelector(${JSON.stringify(this.selector)});
943
+ if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
944
+ throw new Error(${JSON.stringify(`Unsupported input target: ${this.selector}`)});
945
+ }
946
+ element.focus();
947
+ element.value = ${JSON.stringify(text)};
948
+ element.dispatchEvent(new Event("input", { bubbles: true }));
949
+ element.dispatchEvent(new Event("change", { bubbles: true }));
950
+ })()`,
951
+ );
952
+ }
953
+
954
+ async textContent(): Promise<string | null> {
955
+ return await this.frame.evaluate<string | null>(
956
+ `document.querySelector(${JSON.stringify(this.selector)})?.textContent ?? null`,
957
+ );
958
+ }
959
+
960
+ async waitFor(options?: { timeout?: number }): Promise<void> {
961
+ if (this.frame.waitForSelector) {
962
+ await this.frame.waitForSelector(this.selector, options);
963
+ return;
964
+ }
965
+
966
+ const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
967
+ const deadline = Date.now() + timeout;
968
+ while (Date.now() < deadline) {
969
+ const exists = await this.frame.evaluate<boolean>(
970
+ `Boolean(document.querySelector(${JSON.stringify(this.selector)}))`,
971
+ );
972
+ if (exists) {
973
+ return;
974
+ }
975
+ await delay(SELECTOR_POLL_INTERVAL_MS);
976
+ }
977
+
978
+ throw new Error(`Timed out waiting for selector: ${this.selector}`);
979
+ }
980
+ }
981
+
982
+ class CdpBrowserFrame implements BrowserFrame {
983
+ constructor(
984
+ readonly id: string,
985
+ private readonly page: CdpPoolBrowserPage,
986
+ private readonly initialUrl = "",
987
+ readonly name?: string,
988
+ readonly parentId?: string,
989
+ ) {}
990
+
991
+ async url(): Promise<string> {
992
+ const evaluatedUrl = await this.evaluate<string | undefined>(
993
+ "window.location.href",
994
+ );
995
+ return evaluatedUrl || this.initialUrl;
996
+ }
997
+
998
+ async title(): Promise<string> {
999
+ return await this.evaluate<string>("document.title");
1000
+ }
1001
+
1002
+ async content(): Promise<string> {
1003
+ return await this.evaluate<string>("document.documentElement.outerHTML");
1004
+ }
1005
+
1006
+ async evaluate<T>(fn: string | (() => T)): Promise<T> {
1007
+ return await this.page.evaluateInFrame<T>(this.id, fn);
1008
+ }
1009
+
1010
+ locator(selector: string): BrowserLocator {
1011
+ return new CdpBrowserLocator(this, selector);
1012
+ }
1013
+
1014
+ async waitForSelector(
1015
+ selector: string,
1016
+ options?: { timeout?: number },
1017
+ ): Promise<void> {
1018
+ await this.page.waitForSelectorInFrame(this.id, selector, options);
1019
+ }
1020
+
1021
+ fallbackUrl(): string {
1022
+ return this.initialUrl;
1023
+ }
1024
+ }
1025
+
429
1026
  class CdpPoolBrowserPage implements BrowserPageContract {
430
1027
  private closed = false;
431
1028
  private initialized = false;
1029
+ private readonly frameExecutionContexts = new Map<string, number>();
432
1030
 
433
1031
  constructor(
434
1032
  readonly pageId: string,
1033
+ private readonly browserContextId: string | undefined,
435
1034
  private readonly pageClient: JsonRpcWebSocketClient,
436
- private readonly release: (pageId: string) => Promise<void>,
1035
+ private readonly release: (request: PoolReleaseRequest) => Promise<void>,
437
1036
  ) {}
438
1037
 
1038
+ get id(): string {
1039
+ return this.pageId;
1040
+ }
1041
+
439
1042
  async goto(url: string): Promise<void> {
440
1043
  await this.initialize();
441
1044
  const startedAt = Date.now();
@@ -457,8 +1060,49 @@ class CdpPoolBrowserPage implements BrowserPageContract {
457
1060
 
458
1061
  async evaluate<T>(fn: string | (() => T)): Promise<T> {
459
1062
  await this.initialize();
1063
+ return await this.evaluateWithContext<T>(fn);
1064
+ }
1065
+
1066
+ async evaluateInFrame<T>(
1067
+ frameId: string,
1068
+ fn: string | (() => T),
1069
+ ): Promise<T> {
1070
+ await this.initialize();
1071
+ const contextId = await this.getFrameExecutionContextId(frameId);
1072
+ return await this.evaluateWithContext<T>(fn, contextId);
1073
+ }
1074
+
1075
+ async waitForSelectorInFrame(
1076
+ frameId: string,
1077
+ selector: string,
1078
+ options?: { timeout?: number },
1079
+ ): Promise<void> {
1080
+ const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
1081
+ const deadline = Date.now() + timeout;
1082
+
1083
+ while (Date.now() < deadline) {
1084
+ const exists = await this.evaluateInFrame<boolean>(
1085
+ frameId,
1086
+ `Boolean(document.querySelector(${JSON.stringify(selector)}))`,
1087
+ );
1088
+
1089
+ if (exists) {
1090
+ return;
1091
+ }
1092
+
1093
+ await delay(SELECTOR_POLL_INTERVAL_MS);
1094
+ }
1095
+
1096
+ throw new Error(`Timed out waiting for selector: ${selector}`);
1097
+ }
1098
+
1099
+ private async evaluateWithContext<T>(
1100
+ fn: string | (() => T),
1101
+ contextId?: number,
1102
+ ): Promise<T> {
460
1103
  const result = await this.pageClient.send("Runtime.evaluate", {
461
1104
  awaitPromise: true,
1105
+ ...(contextId === undefined ? {} : { contextId }),
462
1106
  expression: formatExpression(fn),
463
1107
  returnByValue: true,
464
1108
  });
@@ -539,6 +1183,37 @@ class CdpPoolBrowserPage implements BrowserPageContract {
539
1183
  );
540
1184
  }
541
1185
 
1186
+ async frames(): Promise<BrowserFrame[]> {
1187
+ await this.initialize();
1188
+ const result = await this.pageClient.send("Page.getFrameTree");
1189
+ const frames = flattenCdpFrameTree(parseCdpFrameTreeNode(result.frameTree));
1190
+ return frames.map(
1191
+ (frame) =>
1192
+ new CdpBrowserFrame(
1193
+ frame.id,
1194
+ this,
1195
+ frame.url ?? "",
1196
+ frame.name,
1197
+ frame.parentId,
1198
+ ),
1199
+ );
1200
+ }
1201
+
1202
+ locator(selector: string): BrowserLocator {
1203
+ return new CdpBrowserLocator(this, selector);
1204
+ }
1205
+
1206
+ async title(): Promise<string> {
1207
+ return await this.evaluate<string>("document.title");
1208
+ }
1209
+
1210
+ async url(): Promise<string> {
1211
+ const [mainFrame] = await this.frames();
1212
+ const frameUrl =
1213
+ mainFrame instanceof CdpBrowserFrame ? mainFrame.fallbackUrl() : "";
1214
+ return frameUrl || (await this.evaluate<string>("window.location.href"));
1215
+ }
1216
+
542
1217
  async content(): Promise<string> {
543
1218
  return await this.evaluate<string>("document.documentElement.outerHTML");
544
1219
  }
@@ -562,22 +1237,154 @@ class CdpPoolBrowserPage implements BrowserPageContract {
562
1237
  this.closed = true;
563
1238
 
564
1239
  try {
565
- await this.release(this.pageId);
1240
+ await this.release({
1241
+ ...(this.browserContextId
1242
+ ? { browserContextId: this.browserContextId }
1243
+ : {}),
1244
+ pageId: this.pageId,
1245
+ });
566
1246
  } finally {
567
1247
  await this.pageClient.close();
568
1248
  }
569
1249
  }
570
1250
 
1251
+ async withResourcePolicy<T>(
1252
+ policy: BrowserResourcePolicy,
1253
+ run: () => Promise<T>,
1254
+ ): Promise<T> {
1255
+ const allowedMethods = new Set(
1256
+ policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS,
1257
+ );
1258
+ const handlePausedRequest = (params: unknown): void => {
1259
+ void this.handleResourcePolicyPausedRequest(
1260
+ params,
1261
+ policy,
1262
+ allowedMethods,
1263
+ );
1264
+ };
1265
+
1266
+ const unsubscribe = this.pageClient.on(
1267
+ "Fetch.requestPaused",
1268
+ handlePausedRequest,
1269
+ );
1270
+
1271
+ try {
1272
+ await this.pageClient.send("Fetch.enable", {
1273
+ patterns: [{ requestStage: "Request", urlPattern: "*" }],
1274
+ });
1275
+ } catch (error) {
1276
+ unsubscribe();
1277
+ throw new ProviderError(
1278
+ "CDP browser target does not support BrowserPage.withResourcePolicy()",
1279
+ {
1280
+ cause: error instanceof Error ? error : undefined,
1281
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
1282
+ fix: "Use a Chromium CDP target with the Fetch domain enabled, or use the local Playwright browser runtime.",
1283
+ },
1284
+ );
1285
+ }
1286
+
1287
+ try {
1288
+ return await run();
1289
+ } finally {
1290
+ unsubscribe();
1291
+ await this.pageClient.send("Fetch.disable");
1292
+ }
1293
+ }
1294
+
1295
+ private async handleResourcePolicyPausedRequest(
1296
+ params: unknown,
1297
+ policy: BrowserResourcePolicy,
1298
+ allowedMethods: ReadonlySet<BrowserResourceMethod>,
1299
+ ): Promise<void> {
1300
+ const requestId = getCdpPausedRequestId(params);
1301
+ if (requestId === null) {
1302
+ return;
1303
+ }
1304
+
1305
+ try {
1306
+ const parsed = toCdpResourceRequest(params);
1307
+ if (!parsed || !allowedMethods.has(parsed.request.method)) {
1308
+ await this.failCdpResourceRequest(requestId);
1309
+ return;
1310
+ }
1311
+
1312
+ for (const resourceRoute of policy.routes) {
1313
+ if (!matchesResourceRoute(resourceRoute.match, parsed.request)) {
1314
+ continue;
1315
+ }
1316
+
1317
+ const decision = await resourceRoute.handle(parsed.request);
1318
+ switch (decision.action) {
1319
+ case "fulfill":
1320
+ await this.pageClient.send(
1321
+ "Fetch.fulfillRequest",
1322
+ toCdpFulfillParams(parsed.requestId, decision),
1323
+ );
1324
+ return;
1325
+ case "block":
1326
+ await this.failCdpResourceRequest(parsed.requestId);
1327
+ return;
1328
+ }
1329
+ }
1330
+
1331
+ await this.failCdpResourceRequest(parsed.requestId);
1332
+ } catch {
1333
+ await this.failCdpResourceRequest(requestId);
1334
+ }
1335
+ }
1336
+
1337
+ private async failCdpResourceRequest(requestId: string): Promise<void> {
1338
+ try {
1339
+ await this.pageClient.send("Fetch.failRequest", {
1340
+ errorReason: "BlockedByClient",
1341
+ requestId,
1342
+ });
1343
+ } catch (error) {
1344
+ if (error instanceof Error) {
1345
+ return;
1346
+ }
1347
+ }
1348
+ }
1349
+
571
1350
  private async initialize(): Promise<void> {
572
1351
  if (this.initialized) {
573
1352
  return;
574
1353
  }
575
1354
 
1355
+ this.pageClient.on("Runtime.executionContextCreated", (params) => {
1356
+ const context = getCdpExecutionContext(params);
1357
+ if (context.frameId && context.id !== undefined) {
1358
+ this.frameExecutionContexts.set(context.frameId, context.id);
1359
+ }
1360
+ });
576
1361
  await this.pageClient.send("Page.enable");
577
1362
  await this.pageClient.send("Runtime.enable");
578
1363
  this.initialized = true;
579
1364
  }
580
1365
 
1366
+ private async getFrameExecutionContextId(frameId: string): Promise<number> {
1367
+ const existing = this.frameExecutionContexts.get(frameId);
1368
+ if (existing !== undefined) {
1369
+ return existing;
1370
+ }
1371
+
1372
+ const result = await this.pageClient.send("Page.createIsolatedWorld", {
1373
+ frameId,
1374
+ grantUniveralAccess: true,
1375
+ worldName: "apifuse-provider-sdk",
1376
+ });
1377
+ const contextId = result.executionContextId;
1378
+ if (typeof contextId !== "number") {
1379
+ throw new Error(
1380
+ `Unable to resolve execution context for frame: ${frameId}`,
1381
+ );
1382
+ }
1383
+
1384
+ this.frameExecutionContexts.set(frameId, contextId);
1385
+ return contextId;
1386
+ }
1387
+
581
1388
  private async waitForDocumentReady(
582
1389
  deadline: number,
583
1390
  isLoadEventSeen: () => boolean,
@@ -598,6 +1405,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
598
1405
  }
599
1406
 
600
1407
  class CdpPoolBrowserClient implements SupportedBrowserClient {
1408
+ private readonly allowedHosts: string[];
601
1409
  private readonly poolClient: JsonRpcWebSocketClient;
602
1410
  readonly engine = "playwright-stealth" satisfies BrowserEngine;
603
1411
 
@@ -608,29 +1416,65 @@ class CdpPoolBrowserClient implements SupportedBrowserClient {
608
1416
  });
609
1417
  }
610
1418
 
1419
+ this.allowedHosts = [...new Set(options.allowedHosts ?? [])];
611
1420
  this.poolClient = new JsonRpcWebSocketClient(options.cdpUrl);
612
1421
  }
613
1422
 
614
1423
  async newPage(): Promise<BrowserPageContract> {
615
- const acquireResult = (await this.poolClient.send(
616
- "acquire",
617
- )) as PoolAcquireResponse;
1424
+ return await this.acquirePage({ isolatedContext: true });
1425
+ }
1426
+
1427
+ private async acquirePage(options?: {
1428
+ isolatedContext?: boolean;
1429
+ }): Promise<BrowserPageContract> {
1430
+ const acquireResult = parsePoolAcquireResponse(
1431
+ await this.poolClient.send("acquire", {
1432
+ ...(this.allowedHosts.length > 0
1433
+ ? { allowedHosts: this.allowedHosts }
1434
+ : {}),
1435
+ ...(options?.isolatedContext
1436
+ ? { isolationMode: "browserContext" }
1437
+ : {}),
1438
+ }),
1439
+ );
618
1440
  const pageClient = new JsonRpcWebSocketClient(acquireResult.wsEndpoint);
619
1441
  const page = new CdpPoolBrowserPage(
620
1442
  acquireResult.pageId,
1443
+ acquireResult.browserContextId,
621
1444
  pageClient,
622
- async (pageId) => {
623
- await this.poolClient.send("release", { pageId });
1445
+ async (request) => {
1446
+ await this.poolClient.send("release", request);
624
1447
  },
625
1448
  );
626
1449
 
627
- await page.evaluate(
628
- `window.navigator.webdriver === true ? Object.defineProperty(window.navigator, "webdriver", { configurable: true, get: () => undefined }) : undefined`,
629
- );
1450
+ try {
1451
+ await page.evaluate(
1452
+ `window.navigator.webdriver === true ? Object.defineProperty(window.navigator, "webdriver", { configurable: true, get: () => undefined }) : undefined`,
1453
+ );
1454
+ } catch (error) {
1455
+ await page.close().catch(() => undefined);
1456
+ throw error;
1457
+ }
630
1458
 
631
1459
  return page;
632
1460
  }
633
1461
 
1462
+ async rawPage(): Promise<BrowserPageContract> {
1463
+ return await this.newPage();
1464
+ }
1465
+
1466
+ async withIsolatedContext<T>(
1467
+ handler: (page: BrowserPageContract) => Promise<T>,
1468
+ ): Promise<T> {
1469
+ const page = await this.acquirePage({ isolatedContext: true });
1470
+
1471
+ try {
1472
+ return await handler(page);
1473
+ } finally {
1474
+ await page.close();
1475
+ }
1476
+ }
1477
+
634
1478
  async close(): Promise<void> {
635
1479
  await this.poolClient.close();
636
1480
  }
@@ -655,6 +1499,14 @@ class UnsupportedBrowserEngineClient implements SupportedBrowserClient {
655
1499
  });
656
1500
  }
657
1501
 
1502
+ async rawPage(): Promise<never> {
1503
+ return await this.newPage();
1504
+ }
1505
+
1506
+ async withIsolatedContext<T>(): Promise<T> {
1507
+ return await this.newPage();
1508
+ }
1509
+
658
1510
  async close(): Promise<void> {}
659
1511
  }
660
1512
 
@@ -680,6 +1532,9 @@ function createSeleniumUCClient(): SupportedBrowserClient {
680
1532
 
681
1533
  export class BrowserClient implements BrowserClientContract {
682
1534
  private readonly client: SupportedBrowserClient;
1535
+ private readonly cdpUrl?: string;
1536
+ private activePage?: BrowserPageContract;
1537
+ private readonly activePages = new Set<BrowserPageContract>();
683
1538
  private readonly _engine: BrowserEngine;
684
1539
 
685
1540
  constructor(options: BrowserClientOptions = {}) {
@@ -689,6 +1544,17 @@ export class BrowserClient implements BrowserClientContract {
689
1544
  };
690
1545
  const engine = resolvedOptions.engine ?? "playwright-stealth";
691
1546
  this._engine = engine;
1547
+ this.cdpUrl = resolvedOptions.cdpUrl;
1548
+
1549
+ if (resolvedOptions.requireCdpPool && !resolvedOptions.cdpUrl) {
1550
+ throw new ProviderError(
1551
+ "Managed CDP Pool is required for browser providers in production",
1552
+ {
1553
+ code: "BROWSER_CDP_POOL_REQUIRED",
1554
+ fix: "Set APIFUSE__CDP_POOL__URL for deployed browser providers. Local standalone development may omit it.",
1555
+ },
1556
+ );
1557
+ }
692
1558
 
693
1559
  switch (engine) {
694
1560
  case "nodriver":
@@ -708,13 +1574,150 @@ export class BrowserClient implements BrowserClientContract {
708
1574
  return this._engine;
709
1575
  }
710
1576
 
711
- async newPage(): Promise<unknown> {
712
- return await this.client.newPage();
1577
+ async newPage(): Promise<BrowserPageContract> {
1578
+ const page = await this.client.newPage();
1579
+ return this.activatePage(page);
1580
+ }
1581
+
1582
+ async rawPage(): Promise<BrowserPageContract> {
1583
+ if (!this.cdpUrl) {
1584
+ throw new ProviderError("ctx.browser.rawPage() requires a CDP pool", {
1585
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
1586
+ fix: "Set APIFUSE__CDP_POOL__URL. The SDK escape hatch is CDP pool-backed only and never launches local Chromium.",
1587
+ });
1588
+ }
1589
+
1590
+ const page = await this.client.rawPage();
1591
+ return this.activatePage(page);
1592
+ }
1593
+
1594
+ async withIsolatedContext<T>(
1595
+ handler: (page: BrowserPageContract) => Promise<T>,
1596
+ ): Promise<T> {
1597
+ const previousActivePage = this.activePage;
1598
+ let trackedPage: BrowserPageContract | undefined;
1599
+
1600
+ return await this.client.withIsolatedContext(async (page) => {
1601
+ trackedPage = this.activatePage(page);
1602
+ try {
1603
+ return await handler(trackedPage);
1604
+ } finally {
1605
+ this.activePages.delete(trackedPage);
1606
+ if (this.activePage === trackedPage) {
1607
+ this.activePage = previousActivePage;
1608
+ }
1609
+ }
1610
+ });
1611
+ }
1612
+
1613
+ private activatePage(page: BrowserPageContract): BrowserPageContract {
1614
+ let closed = false;
1615
+ const originalClose = page.close.bind(page);
1616
+ const trackedPage = new Proxy(page, {
1617
+ get: (target, property, receiver) => {
1618
+ if (property === "close") {
1619
+ return async () => {
1620
+ if (closed) return;
1621
+ closed = true;
1622
+ try {
1623
+ await originalClose();
1624
+ } finally {
1625
+ this.activePages.delete(trackedPage);
1626
+ if (this.activePage === trackedPage) {
1627
+ this.activePage = undefined;
1628
+ }
1629
+ }
1630
+ };
1631
+ }
1632
+ const value = Reflect.get(target, property, receiver);
1633
+ return typeof value === "function" ? value.bind(target) : value;
1634
+ },
1635
+ });
1636
+ this.activePages.add(trackedPage);
1637
+ this.activePage = trackedPage;
1638
+ return trackedPage;
1639
+ }
1640
+
1641
+ async solveChallenge(
1642
+ request: BrowserChallengeRequest,
1643
+ ): Promise<BrowserChallengeResult> {
1644
+ if (request.type !== "recaptcha") {
1645
+ throw new ProviderError(
1646
+ `Unsupported browser challenge: ${request.type}`,
1647
+ {
1648
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
1649
+ },
1650
+ );
1651
+ }
1652
+
1653
+ if (this.activePage) {
1654
+ return await solveRecaptchaChallenge(this.activePage, request);
1655
+ }
1656
+
1657
+ const page = await this.client.newPage();
1658
+ try {
1659
+ return await solveRecaptchaChallenge(page, request);
1660
+ } finally {
1661
+ if (this.activePage === page) {
1662
+ this.activePage = undefined;
1663
+ }
1664
+ await page.close();
1665
+ }
713
1666
  }
714
1667
 
715
1668
  async close(): Promise<void> {
716
- await this.client.close();
1669
+ const pages = Array.from(this.activePages);
1670
+ this.activePages.clear();
1671
+ this.activePage = undefined;
1672
+ try {
1673
+ await Promise.all(pages.map((page) => page.close()));
1674
+ } finally {
1675
+ await this.client.close();
1676
+ }
1677
+ }
1678
+ }
1679
+
1680
+ async function solveRecaptchaChallenge(
1681
+ page: BrowserPageContract,
1682
+ request: BrowserChallengeRequest,
1683
+ ): Promise<BrowserChallengeResult> {
1684
+ const timeout = request.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
1685
+ const deadline = Date.now() + timeout;
1686
+
1687
+ while (Date.now() < deadline) {
1688
+ const frames = await page.frames();
1689
+ const recaptchaFrame = await findRecaptchaFrame(frames, request.siteKey);
1690
+ if (recaptchaFrame) {
1691
+ await recaptchaFrame.locator("#recaptcha-anchor").click();
1692
+ return {
1693
+ type: "recaptcha",
1694
+ solved: true,
1695
+ frameUrl: await recaptchaFrame.url(),
1696
+ };
1697
+ }
1698
+
1699
+ await delay(SELECTOR_POLL_INTERVAL_MS);
717
1700
  }
1701
+
1702
+ throw new Error("Timed out waiting for reCAPTCHA iframe");
1703
+ }
1704
+
1705
+ async function findRecaptchaFrame(
1706
+ frames: BrowserFrame[],
1707
+ siteKey?: string,
1708
+ ): Promise<BrowserFrame | undefined> {
1709
+ for (const frame of frames) {
1710
+ const url = await frame.url();
1711
+ const matchesRecaptcha =
1712
+ url.includes("google.com/recaptcha") ||
1713
+ url.includes("recaptcha.net/recaptcha");
1714
+ const matchesSiteKey = !siteKey || url.includes(siteKey);
1715
+ if (matchesRecaptcha && matchesSiteKey) {
1716
+ return frame;
1717
+ }
1718
+ }
1719
+
1720
+ return undefined;
718
1721
  }
719
1722
 
720
1723
  export function createBrowserClient(