@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.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 (263) hide show
  1. package/AUTHORING.md +240 -0
  2. package/CHANGELOG.md +93 -0
  3. package/README.md +26 -10
  4. package/SUBMISSION.md +11 -12
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +36 -0
  9. package/bin/apifuse-pack-smoke.ts +22 -81
  10. package/bin/apifuse-pack-types.ts +266 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +53 -70
  13. package/bin/apifuse-submit-check.ts +2177 -353
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
  20. package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  21. package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  22. package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  23. package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  24. package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  25. package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  26. package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  27. package/dist/auth-turn/fixtures/valid/abort.json +8 -0
  28. package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
  29. package/dist/auth-turn/fixtures/valid/complete.json +13 -0
  30. package/dist/auth-turn/fixtures/valid/form.json +14 -0
  31. package/dist/auth-turn/fixtures/valid/message.json +13 -0
  32. package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
  33. package/dist/auth-turn/fixtures/valid/pending.json +5 -0
  34. package/dist/auth-turn/fixtures/valid/poll.json +9 -0
  35. package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
  36. package/dist/auth-turn/fixtures/valid/retry.json +8 -0
  37. package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  38. package/dist/auth-turn/index.d.ts +195 -0
  39. package/dist/auth-turn/index.js +133 -0
  40. package/dist/auth.d.ts +76 -0
  41. package/dist/auth.js +427 -0
  42. package/dist/ceremonies/index.d.ts +1 -1
  43. package/dist/ceremonies/index.js +14 -48
  44. package/dist/cli/commands.d.ts +1 -1
  45. package/dist/cli/commands.js +8 -0
  46. package/dist/cli/create.d.ts +3 -0
  47. package/dist/cli/create.js +47 -33
  48. package/dist/cli/prompt-assets.d.ts +80 -0
  49. package/dist/cli/prompt-assets.js +743 -0
  50. package/dist/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  51. package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  52. package/dist/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
  53. package/dist/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  54. package/dist/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  55. package/dist/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
  56. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  57. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  58. package/dist/cli/templates/provider/AGENTS.md.tpl +96 -0
  59. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  60. package/dist/cli/templates/provider/README.md.tpl +163 -0
  61. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  62. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  63. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  64. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  65. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  66. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  67. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  68. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  69. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  70. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  71. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  72. package/dist/config/loader.d.ts +149 -8
  73. package/dist/config/loader.js +378 -89
  74. package/dist/contract-serialization.d.ts +2 -2
  75. package/dist/contract-serialization.js +3 -6
  76. package/dist/contract-types.d.ts +2 -2
  77. package/dist/contract.d.ts +3 -3
  78. package/dist/contract.js +5 -6
  79. package/dist/define.d.ts +13 -1
  80. package/dist/define.js +245 -178
  81. package/dist/dev.d.ts +1 -1
  82. package/dist/dev.js +1 -1
  83. package/dist/errors.d.ts +4 -1
  84. package/dist/errors.js +48 -0
  85. package/dist/i18n/catalog.d.ts +2 -2
  86. package/dist/i18n/catalog.js +4 -10
  87. package/dist/i18n/index.d.ts +2 -2
  88. package/dist/i18n/index.js +2 -2
  89. package/dist/i18n/keys.d.ts +2 -2
  90. package/dist/index.d.ts +44 -41
  91. package/dist/index.js +39 -36
  92. package/dist/lint.d.ts +1 -0
  93. package/dist/lint.js +35 -15
  94. package/dist/provider.d.ts +11 -9
  95. package/dist/provider.js +9 -8
  96. package/dist/public-schema-field-lint.d.ts +1 -1
  97. package/dist/recipes/gov-api.js +1 -1
  98. package/dist/runtime/auth-flow.d.ts +1 -1
  99. package/dist/runtime/auth-flow.js +4 -2
  100. package/dist/runtime/browser.d.ts +1 -1
  101. package/dist/runtime/browser.js +214 -25
  102. package/dist/runtime/cache.d.ts +1 -1
  103. package/dist/runtime/cache.js +4 -8
  104. package/dist/runtime/choice.d.ts +1 -1
  105. package/dist/runtime/choice.js +31 -35
  106. package/dist/runtime/credential.d.ts +1 -1
  107. package/dist/runtime/credential.js +1 -1
  108. package/dist/runtime/env.d.ts +1 -1
  109. package/dist/runtime/executor.d.ts +1 -1
  110. package/dist/runtime/executor.js +15 -3
  111. package/dist/runtime/http.d.ts +2 -2
  112. package/dist/runtime/http.js +160 -344
  113. package/dist/runtime/insights.d.ts +1 -1
  114. package/dist/runtime/insights.js +6 -13
  115. package/dist/runtime/instrumentation.d.ts +2 -2
  116. package/dist/runtime/instrumentation.js +56 -19
  117. package/dist/runtime/keyring.js +1 -1
  118. package/dist/runtime/namespace.js +1 -1
  119. package/dist/runtime/otlp.d.ts +1 -1
  120. package/dist/runtime/perf.d.ts +1 -1
  121. package/dist/runtime/provider.d.ts +1 -1
  122. package/dist/runtime/provider.js +1 -2
  123. package/dist/runtime/proxy-errors.d.ts +1 -1
  124. package/dist/runtime/proxy-errors.js +9 -7
  125. package/dist/runtime/proxy-nodemaven.d.ts +35 -0
  126. package/dist/runtime/proxy-nodemaven.js +128 -0
  127. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  128. package/dist/runtime/proxy-retry-policy.js +326 -0
  129. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  130. package/dist/runtime/proxy-telemetry.js +55 -52
  131. package/dist/runtime/redis.d.ts +1 -1
  132. package/dist/runtime/redis.js +2 -4
  133. package/dist/runtime/request-options.d.ts +1 -1
  134. package/dist/runtime/secrets.d.ts +27 -0
  135. package/dist/runtime/secrets.js +51 -0
  136. package/dist/runtime/state.d.ts +2 -2
  137. package/dist/runtime/state.js +15 -4
  138. package/dist/runtime/stealth.d.ts +7 -4
  139. package/dist/runtime/stealth.js +257 -215
  140. package/dist/runtime/stt.d.ts +1 -1
  141. package/dist/runtime/stt.js +11 -15
  142. package/dist/runtime/trace.d.ts +2 -2
  143. package/dist/runtime/trace.js +2 -4
  144. package/dist/runtime/waterfall.d.ts +1 -1
  145. package/dist/schema.d.ts +1 -1
  146. package/dist/schema.js +7 -15
  147. package/dist/serve.d.ts +1 -1
  148. package/dist/serve.js +1 -1
  149. package/dist/server/index.d.ts +7 -3
  150. package/dist/server/index.js +6 -2
  151. package/dist/server/self-test-input-tokens.d.ts +1 -0
  152. package/dist/server/self-test-input-tokens.js +37 -0
  153. package/dist/server/self-test-redaction.d.ts +20 -0
  154. package/dist/server/self-test-redaction.js +70 -0
  155. package/dist/server/self-test-token.d.ts +30 -0
  156. package/dist/server/self-test-token.js +50 -0
  157. package/dist/server/self-test.d.ts +199 -0
  158. package/dist/server/self-test.js +1113 -0
  159. package/dist/server/serve.d.ts +14 -3
  160. package/dist/server/serve.js +135 -64
  161. package/dist/server/types.d.ts +10 -9
  162. package/dist/server/types.js +3 -7
  163. package/dist/stealth/profiles.d.ts +1 -1
  164. package/dist/stealth/profiles.js +5 -14
  165. package/dist/stream.d.ts +1 -1
  166. package/dist/testing/index.d.ts +2 -2
  167. package/dist/testing/index.js +2 -2
  168. package/dist/testing/run.d.ts +1 -1
  169. package/dist/testing/run.js +12 -15
  170. package/dist/types.d.ts +237 -1
  171. package/dist/user-input.d.ts +30 -0
  172. package/dist/user-input.js +66 -0
  173. package/package.json +16 -5
  174. package/src/auth-turn/auth-turn.v1.schema.json +89 -0
  175. package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  176. package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  177. package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  178. package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  179. package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  180. package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  181. package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  182. package/src/auth-turn/fixtures/valid/abort.json +8 -0
  183. package/src/auth-turn/fixtures/valid/challenge.json +17 -0
  184. package/src/auth-turn/fixtures/valid/complete.json +13 -0
  185. package/src/auth-turn/fixtures/valid/form.json +14 -0
  186. package/src/auth-turn/fixtures/valid/message.json +13 -0
  187. package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
  188. package/src/auth-turn/fixtures/valid/pending.json +5 -0
  189. package/src/auth-turn/fixtures/valid/poll.json +9 -0
  190. package/src/auth-turn/fixtures/valid/redirect.json +16 -0
  191. package/src/auth-turn/fixtures/valid/retry.json +8 -0
  192. package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  193. package/src/auth-turn/index.ts +177 -0
  194. package/src/auth.ts +728 -0
  195. package/src/ceremonies/index.ts +33 -121
  196. package/src/cli/commands.ts +10 -0
  197. package/src/cli/create.ts +69 -99
  198. package/src/cli/prompt-assets.ts +865 -0
  199. package/src/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  200. package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  201. package/src/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
  202. package/src/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  203. package/src/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  204. package/src/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
  205. package/src/cli/templates/provider/AGENTS.md.tpl +96 -0
  206. package/src/cli/templates/provider/README.md.tpl +7 -4
  207. package/src/config/loader.ts +543 -208
  208. package/src/contract-serialization.ts +5 -11
  209. package/src/contract-types.ts +2 -2
  210. package/src/contract.ts +13 -28
  211. package/src/define.ts +397 -528
  212. package/src/dev.ts +4 -9
  213. package/src/errors.ts +58 -5
  214. package/src/i18n/catalog.ts +10 -32
  215. package/src/i18n/index.ts +2 -2
  216. package/src/i18n/keys.ts +5 -11
  217. package/src/index.ts +64 -41
  218. package/src/lint.ts +122 -159
  219. package/src/provider.ts +40 -9
  220. package/src/public-schema-field-lint.ts +7 -33
  221. package/src/recipes/gov-api.ts +2 -5
  222. package/src/runtime/auth-flow.ts +6 -6
  223. package/src/runtime/browser.ts +320 -151
  224. package/src/runtime/cache.ts +20 -67
  225. package/src/runtime/choice.ts +79 -132
  226. package/src/runtime/credential.ts +2 -2
  227. package/src/runtime/env.ts +1 -1
  228. package/src/runtime/executor.ts +23 -20
  229. package/src/runtime/http.ts +216 -539
  230. package/src/runtime/insights.ts +15 -53
  231. package/src/runtime/instrumentation.ts +78 -65
  232. package/src/runtime/keyring.ts +7 -19
  233. package/src/runtime/namespace.ts +2 -7
  234. package/src/runtime/otlp.ts +12 -23
  235. package/src/runtime/perf.ts +1 -1
  236. package/src/runtime/provider.ts +4 -9
  237. package/src/runtime/proxy-errors.ts +29 -42
  238. package/src/runtime/proxy-nodemaven.ts +178 -0
  239. package/src/runtime/proxy-retry-policy.ts +469 -0
  240. package/src/runtime/proxy-telemetry.ts +79 -77
  241. package/src/runtime/redis.ts +4 -12
  242. package/src/runtime/request-options.ts +4 -13
  243. package/src/runtime/secrets.ts +64 -0
  244. package/src/runtime/state.ts +41 -110
  245. package/src/runtime/stealth.ts +331 -369
  246. package/src/runtime/stt.ts +38 -94
  247. package/src/runtime/trace.ts +14 -44
  248. package/src/runtime/waterfall.ts +5 -18
  249. package/src/schema.ts +23 -84
  250. package/src/serve.ts +1 -1
  251. package/src/server/index.ts +44 -3
  252. package/src/server/self-test-input-tokens.ts +46 -0
  253. package/src/server/self-test-redaction.ts +97 -0
  254. package/src/server/self-test-token.ts +70 -0
  255. package/src/server/self-test.ts +1450 -0
  256. package/src/server/serve.ts +206 -216
  257. package/src/server/types.ts +7 -19
  258. package/src/stealth/profiles.ts +10 -26
  259. package/src/stream.ts +8 -19
  260. package/src/testing/index.ts +2 -2
  261. package/src/testing/run.ts +24 -64
  262. package/src/types.ts +274 -1
  263. package/src/user-input.ts +118 -0
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
- import type { Frame, LaunchOptions, Locator, Page } from "playwright";
2
+ import type { Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
3
3
 
4
- import { ProviderError } from "../errors";
4
+ import { ProviderError } from "../errors.js";
5
5
  import type {
6
6
  BrowserChallengeRequest,
7
7
  BrowserChallengeResult,
@@ -11,20 +11,25 @@ import type {
11
11
  BrowserLocator,
12
12
  BrowserOptions,
13
13
  BrowserPage,
14
- } from "../types";
14
+ BrowserResourceBody,
15
+ BrowserResourceDecision,
16
+ BrowserResourceMethod,
17
+ BrowserResourcePolicy,
18
+ BrowserResourceRequest,
19
+ } from "../types.js";
15
20
 
16
21
  const require = createRequire(import.meta.url);
17
22
  const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
18
23
  const SELECTOR_POLL_INTERVAL_MS = 100;
24
+ const RESOURCE_POLICY_ROUTE_PATTERN = "**/*";
25
+ const DEFAULT_RESOURCE_METHODS = ["GET", "HEAD"] as const;
19
26
 
20
27
  type PlaywrightModule = typeof import("playwright");
21
28
  type PlaywrightExtraModule = {
22
29
  chromium: PlaywrightModule["chromium"] & { use(plugin: unknown): unknown };
23
30
  };
24
31
 
25
- type StealthPluginFactory = (options?: {
26
- enabledEvasions?: Set<string>;
27
- }) => unknown;
32
+ type StealthPluginFactory = (options?: { enabledEvasions?: Set<string> }) => unknown;
28
33
 
29
34
  type PoolAcquireResponse = {
30
35
  browserContextId?: string;
@@ -62,8 +67,147 @@ type CdpFrameTreeNode = {
62
67
  };
63
68
  };
64
69
 
70
+ type CdpFetchFulfillParams = {
71
+ readonly requestId: string;
72
+ readonly responseCode: number;
73
+ readonly responseHeaders?: readonly {
74
+ readonly name: string;
75
+ readonly value: string;
76
+ }[];
77
+ readonly body?: string;
78
+ };
79
+
65
80
  type BrowserPageContract = BrowserPage;
66
81
 
82
+ function toResourceBody(body: BrowserResourceBody | undefined): Buffer | string | undefined {
83
+ if (body === undefined || typeof body === "string" || Buffer.isBuffer(body)) {
84
+ return body;
85
+ }
86
+
87
+ if (body instanceof ArrayBuffer) {
88
+ return Buffer.from(new Uint8Array(body));
89
+ }
90
+
91
+ return Buffer.from(body);
92
+ }
93
+
94
+ function isResourceMethod(method: string): method is BrowserResourceMethod {
95
+ return method === "GET" || method === "HEAD";
96
+ }
97
+
98
+ async function toResourceRequest(request: Request): Promise<BrowserResourceRequest | null> {
99
+ const method = request.method().toUpperCase();
100
+ if (!isResourceMethod(method)) {
101
+ return null;
102
+ }
103
+
104
+ return {
105
+ headers: await request.allHeaders(),
106
+ method,
107
+ resourceType: request.resourceType(),
108
+ url: request.url(),
109
+ };
110
+ }
111
+
112
+ function toCdpResourceRequest(
113
+ params: unknown,
114
+ ): { requestId: string; request: BrowserResourceRequest } | null {
115
+ if (!isRecord(params)) {
116
+ return null;
117
+ }
118
+
119
+ const requestId = params.requestId;
120
+ const rawRequest = params.request;
121
+ if (typeof requestId !== "string" || !isRecord(rawRequest)) {
122
+ return null;
123
+ }
124
+
125
+ const url = rawRequest.url;
126
+ const method = String(rawRequest.method ?? "").toUpperCase();
127
+ if (typeof url !== "string" || !isResourceMethod(method)) {
128
+ return null;
129
+ }
130
+
131
+ return {
132
+ requestId,
133
+ request: {
134
+ headers: toCdpResourceHeaders(rawRequest.headers),
135
+ method,
136
+ resourceType: typeof params.resourceType === "string" ? params.resourceType : undefined,
137
+ url,
138
+ },
139
+ };
140
+ }
141
+
142
+ function getCdpPausedRequestId(params: unknown): string | null {
143
+ if (!isRecord(params) || typeof params.requestId !== "string") {
144
+ return null;
145
+ }
146
+
147
+ return params.requestId;
148
+ }
149
+
150
+ function toCdpResourceHeaders(value: unknown): Record<string, string> {
151
+ if (!isRecord(value)) {
152
+ return {};
153
+ }
154
+
155
+ const headers: Record<string, string> = {};
156
+ for (const [name, headerValue] of Object.entries(value)) {
157
+ if (typeof headerValue === "string") {
158
+ headers[name] = headerValue;
159
+ }
160
+ }
161
+
162
+ return headers;
163
+ }
164
+
165
+ function matchesResourceRoute(
166
+ match: BrowserResourcePolicy["routes"][number]["match"],
167
+ request: BrowserResourceRequest,
168
+ ): boolean {
169
+ if (typeof match === "string") {
170
+ return request.url === match;
171
+ }
172
+ if (match instanceof RegExp) {
173
+ return match.test(request.url);
174
+ }
175
+
176
+ return match(request);
177
+ }
178
+
179
+ function toCdpFulfillParams(
180
+ requestId: string,
181
+ decision: Extract<BrowserResourceDecision, { readonly action: "fulfill" }>,
182
+ ): CdpFetchFulfillParams {
183
+ const body = toResourceBody(decision.body);
184
+ return {
185
+ ...(body === undefined ? {} : { body: Buffer.from(body).toString("base64") }),
186
+ ...(decision.headers === undefined
187
+ ? {}
188
+ : {
189
+ responseHeaders: Object.entries(decision.headers).map(([name, value]) => ({
190
+ name,
191
+ value,
192
+ })),
193
+ }),
194
+ requestId,
195
+ responseCode: decision.status ?? 200,
196
+ };
197
+ }
198
+
199
+ async function fulfillResourceRoute(
200
+ route: Route,
201
+ decision: Extract<BrowserResourceDecision, { readonly action: "fulfill" }>,
202
+ ): Promise<void> {
203
+ const body = toResourceBody(decision.body);
204
+ await route.fulfill({
205
+ ...(body === undefined ? {} : { body }),
206
+ ...(decision.headers === undefined ? {} : { headers: decision.headers }),
207
+ status: decision.status ?? 200,
208
+ });
209
+ }
210
+
67
211
  export type BrowserClientOptions = BrowserOptions & {
68
212
  allowedHosts?: string[];
69
213
  cdpUrl?: string;
@@ -76,18 +220,14 @@ type SupportedBrowserClient = {
76
220
  close(): Promise<void>;
77
221
  newPage(): Promise<BrowserPageContract>;
78
222
  rawPage(): Promise<BrowserPageContract>;
79
- withIsolatedContext<T>(
80
- handler: (page: BrowserPageContract) => Promise<T>,
81
- ): Promise<T>;
223
+ withIsolatedContext<T>(handler: (page: BrowserPageContract) => Promise<T>): Promise<T>;
82
224
  };
83
225
 
84
226
  function getDefaultCdpPoolUrl(env = process.env): string | undefined {
85
227
  return env.APIFUSE__CDP_POOL__URL;
86
228
  }
87
229
 
88
- async function importOptionalModule<T extends object>(
89
- moduleName: string,
90
- ): Promise<T> {
230
+ async function importOptionalModule<T extends object>(moduleName: string): Promise<T> {
91
231
  return (await import(moduleName)) as T;
92
232
  }
93
233
 
@@ -217,9 +357,7 @@ async function loadPlaywright(): Promise<PlaywrightModule> {
217
357
  }
218
358
 
219
359
  try {
220
- return unwrapModuleDefault(
221
- await importOptionalModule<PlaywrightModule>("playwright"),
222
- );
360
+ return unwrapModuleDefault(await importOptionalModule<PlaywrightModule>("playwright"));
223
361
  } catch (error) {
224
362
  if (isModuleNotFoundError(error)) {
225
363
  throw new ProviderError("Playwright is not installed", {
@@ -267,19 +405,14 @@ async function loadPlaywrightExtra(): Promise<PlaywrightExtraModule> {
267
405
  async function loadStealthPluginFactory(): Promise<StealthPluginFactory> {
268
406
  try {
269
407
  return unwrapModuleDefault(
270
- await importOptionalModule<StealthPluginFactory>(
271
- "puppeteer-extra-plugin-stealth",
272
- ),
408
+ await importOptionalModule<StealthPluginFactory>("puppeteer-extra-plugin-stealth"),
273
409
  );
274
410
  } catch (error) {
275
411
  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
- );
412
+ throw new ProviderError("puppeteer-extra-plugin-stealth is not installed", {
413
+ cause: error instanceof Error ? error : undefined,
414
+ fix: "Run: bun add playwright-extra puppeteer-extra-plugin-stealth",
415
+ });
283
416
  }
284
417
 
285
418
  throw error;
@@ -351,10 +484,7 @@ class PlaywrightBrowserPage implements BrowserPageContract {
351
484
  return await this.page.evaluate(fn);
352
485
  }
353
486
 
354
- async waitForSelector(
355
- selector: string,
356
- options?: { timeout?: number },
357
- ): Promise<void> {
487
+ async waitForSelector(selector: string, options?: { timeout?: number }): Promise<void> {
358
488
  await this.page.waitForSelector(selector, options);
359
489
  }
360
490
 
@@ -398,6 +528,42 @@ class PlaywrightBrowserPage implements BrowserPageContract {
398
528
  async close(): Promise<void> {
399
529
  await this.page.close();
400
530
  }
531
+
532
+ async withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T> {
533
+ const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
534
+ const handler = async (route: Route): Promise<void> => {
535
+ const request = await toResourceRequest(route.request());
536
+ if (!request || !allowedMethods.has(request.method)) {
537
+ await route.abort("blockedbyclient");
538
+ return;
539
+ }
540
+
541
+ for (const resourceRoute of policy.routes) {
542
+ if (!matchesResourceRoute(resourceRoute.match, request)) {
543
+ continue;
544
+ }
545
+
546
+ const decision = await resourceRoute.handle(request);
547
+ switch (decision.action) {
548
+ case "fulfill":
549
+ await fulfillResourceRoute(route, decision);
550
+ return;
551
+ case "block":
552
+ await route.abort("blockedbyclient");
553
+ return;
554
+ }
555
+ }
556
+
557
+ await route.abort("blockedbyclient");
558
+ };
559
+
560
+ await this.page.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
561
+ try {
562
+ return await run();
563
+ } finally {
564
+ await this.page.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
565
+ }
566
+ }
401
567
  }
402
568
 
403
569
  class PlaywrightBrowserClient implements SupportedBrowserClient {
@@ -430,9 +596,7 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
430
596
  });
431
597
  }
432
598
 
433
- async withIsolatedContext<T>(
434
- handler: (page: BrowserPageContract) => Promise<T>,
435
- ): Promise<T> {
599
+ async withIsolatedContext<T>(handler: (page: BrowserPageContract) => Promise<T>): Promise<T> {
436
600
  const browser = await this.ensureBrowser();
437
601
  const context = await browser.newContext();
438
602
  const page = await context.newPage();
@@ -480,10 +644,7 @@ function normalizeWebSocketEndpoint(endpoint: string): string {
480
644
  class JsonRpcWebSocketClient {
481
645
  private nextId = 1;
482
646
  private readonly endpoint: string;
483
- private readonly listeners = new Map<
484
- string,
485
- Set<(params: unknown) => void>
486
- >();
647
+ private readonly listeners = new Map<string, Set<(params: unknown) => void>>();
487
648
  private readonly pending = new Map<
488
649
  JsonRpcId,
489
650
  {
@@ -577,9 +738,7 @@ class JsonRpcWebSocketClient {
577
738
  this.pending.delete(payload.id);
578
739
 
579
740
  if (payload.error) {
580
- pending.reject(
581
- new Error(payload.error.message ?? "JSON-RPC command failed"),
582
- );
741
+ pending.reject(new Error(payload.error.message ?? "JSON-RPC command failed"));
583
742
  return;
584
743
  }
585
744
 
@@ -607,11 +766,7 @@ class JsonRpcWebSocketClient {
607
766
  });
608
767
 
609
768
  socket.addEventListener("error", () => {
610
- reject(
611
- new Error(
612
- `Unable to connect to WebSocket endpoint: ${this.endpoint}`,
613
- ),
614
- );
769
+ reject(new Error(`Unable to connect to WebSocket endpoint: ${this.endpoint}`));
615
770
  });
616
771
  });
617
772
 
@@ -650,19 +805,14 @@ function parsePoolAcquireResponse(value: unknown): PoolAcquireResponse {
650
805
  });
651
806
  }
652
807
 
653
- if (
654
- value.browserContextId !== undefined &&
655
- typeof value.browserContextId !== "string"
656
- ) {
808
+ if (value.browserContextId !== undefined && typeof value.browserContextId !== "string") {
657
809
  throw new ProviderError("CDP Pool returned an invalid acquire response", {
658
810
  code: "BROWSER_RUNTIME_UNSUPPORTED",
659
811
  });
660
812
  }
661
813
 
662
814
  return {
663
- ...(value.browserContextId
664
- ? { browserContextId: value.browserContextId }
665
- : {}),
815
+ ...(value.browserContextId ? { browserContextId: value.browserContextId } : {}),
666
816
  pageId: value.pageId,
667
817
  wsEndpoint: value.wsEndpoint,
668
818
  };
@@ -688,10 +838,7 @@ function parseCdpFrameTreeNode(value: unknown): CdpFrameTreeNode | undefined {
688
838
  frame: {
689
839
  id: frameId,
690
840
  name: typeof value.frame.name === "string" ? value.frame.name : undefined,
691
- parentId:
692
- typeof value.frame.parentId === "string"
693
- ? value.frame.parentId
694
- : undefined,
841
+ parentId: typeof value.frame.parentId === "string" ? value.frame.parentId : undefined,
695
842
  url: typeof value.frame.url === "string" ? value.frame.url : undefined,
696
843
  },
697
844
  ...(childFrames ? { childFrames } : {}),
@@ -709,10 +856,7 @@ function getCdpExecutionContext(params: unknown): {
709
856
  const contextId = params.context.id;
710
857
  const auxData = params.context.auxData;
711
858
  return {
712
- frameId:
713
- isRecord(auxData) && typeof auxData.frameId === "string"
714
- ? auxData.frameId
715
- : undefined,
859
+ frameId: isRecord(auxData) && typeof auxData.frameId === "string" ? auxData.frameId : undefined,
716
860
  id: typeof contextId === "number" ? contextId : undefined,
717
861
  };
718
862
  }
@@ -721,10 +865,7 @@ class CdpBrowserLocator implements BrowserLocator {
721
865
  constructor(
722
866
  private readonly frame: {
723
867
  evaluate<T>(fn: string | (() => T)): Promise<T>;
724
- waitForSelector?(
725
- selector: string,
726
- options?: { timeout?: number },
727
- ): Promise<void>;
868
+ waitForSelector?(selector: string, options?: { timeout?: number }): Promise<void>;
728
869
  },
729
870
  private readonly selector: string,
730
871
  ) {}
@@ -796,9 +937,7 @@ class CdpBrowserFrame implements BrowserFrame {
796
937
  ) {}
797
938
 
798
939
  async url(): Promise<string> {
799
- const evaluatedUrl = await this.evaluate<string | undefined>(
800
- "window.location.href",
801
- );
940
+ const evaluatedUrl = await this.evaluate<string | undefined>("window.location.href");
802
941
  return evaluatedUrl || this.initialUrl;
803
942
  }
804
943
 
@@ -818,10 +957,7 @@ class CdpBrowserFrame implements BrowserFrame {
818
957
  return new CdpBrowserLocator(this, selector);
819
958
  }
820
959
 
821
- async waitForSelector(
822
- selector: string,
823
- options?: { timeout?: number },
824
- ): Promise<void> {
960
+ async waitForSelector(selector: string, options?: { timeout?: number }): Promise<void> {
825
961
  await this.page.waitForSelectorInFrame(this.id, selector, options);
826
962
  }
827
963
 
@@ -856,10 +992,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
856
992
 
857
993
  try {
858
994
  await this.pageClient.send("Page.navigate", { url });
859
- await this.waitForDocumentReady(
860
- startedAt + DEFAULT_WAIT_TIMEOUT_MS,
861
- () => loadEventSeen,
862
- );
995
+ await this.waitForDocumentReady(startedAt + DEFAULT_WAIT_TIMEOUT_MS, () => loadEventSeen);
863
996
  } finally {
864
997
  unsubscribe();
865
998
  }
@@ -870,10 +1003,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
870
1003
  return await this.evaluateWithContext<T>(fn);
871
1004
  }
872
1005
 
873
- async evaluateInFrame<T>(
874
- frameId: string,
875
- fn: string | (() => T),
876
- ): Promise<T> {
1006
+ async evaluateInFrame<T>(frameId: string, fn: string | (() => T)): Promise<T> {
877
1007
  await this.initialize();
878
1008
  const contextId = await this.getFrameExecutionContextId(frameId);
879
1009
  return await this.evaluateWithContext<T>(fn, contextId);
@@ -903,10 +1033,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
903
1033
  throw new Error(`Timed out waiting for selector: ${selector}`);
904
1034
  }
905
1035
 
906
- private async evaluateWithContext<T>(
907
- fn: string | (() => T),
908
- contextId?: number,
909
- ): Promise<T> {
1036
+ private async evaluateWithContext<T>(fn: string | (() => T), contextId?: number): Promise<T> {
910
1037
  const result = await this.pageClient.send("Runtime.evaluate", {
911
1038
  awaitPromise: true,
912
1039
  ...(contextId === undefined ? {} : { contextId }),
@@ -916,20 +1043,14 @@ class CdpPoolBrowserPage implements BrowserPageContract {
916
1043
 
917
1044
  if (result.exceptionDetails) {
918
1045
  throw new Error(
919
- String(
920
- (result.exceptionDetails as { text?: string }).text ??
921
- "Browser evaluation failed",
922
- ),
1046
+ String((result.exceptionDetails as { text?: string }).text ?? "Browser evaluation failed"),
923
1047
  );
924
1048
  }
925
1049
 
926
1050
  return (result.result as { value?: T } | undefined)?.value as T;
927
1051
  }
928
1052
 
929
- async waitForSelector(
930
- selector: string,
931
- options?: { timeout?: number },
932
- ): Promise<void> {
1053
+ async waitForSelector(selector: string, options?: { timeout?: number }): Promise<void> {
933
1054
  const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
934
1055
  const deadline = Date.now() + timeout;
935
1056
 
@@ -995,14 +1116,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
995
1116
  const result = await this.pageClient.send("Page.getFrameTree");
996
1117
  const frames = flattenCdpFrameTree(parseCdpFrameTreeNode(result.frameTree));
997
1118
  return frames.map(
998
- (frame) =>
999
- new CdpBrowserFrame(
1000
- frame.id,
1001
- this,
1002
- frame.url ?? "",
1003
- frame.name,
1004
- frame.parentId,
1005
- ),
1119
+ (frame) => new CdpBrowserFrame(frame.id, this, frame.url ?? "", frame.name, frame.parentId),
1006
1120
  );
1007
1121
  }
1008
1122
 
@@ -1016,8 +1130,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1016
1130
 
1017
1131
  async url(): Promise<string> {
1018
1132
  const [mainFrame] = await this.frames();
1019
- const frameUrl =
1020
- mainFrame instanceof CdpBrowserFrame ? mainFrame.fallbackUrl() : "";
1133
+ const frameUrl = mainFrame instanceof CdpBrowserFrame ? mainFrame.fallbackUrl() : "";
1021
1134
  return frameUrl || (await this.evaluate<string>("window.location.href"));
1022
1135
  }
1023
1136
 
@@ -1045,9 +1158,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1045
1158
 
1046
1159
  try {
1047
1160
  await this.release({
1048
- ...(this.browserContextId
1049
- ? { browserContextId: this.browserContextId }
1050
- : {}),
1161
+ ...(this.browserContextId ? { browserContextId: this.browserContextId } : {}),
1051
1162
  pageId: this.pageId,
1052
1163
  });
1053
1164
  } finally {
@@ -1055,6 +1166,93 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1055
1166
  }
1056
1167
  }
1057
1168
 
1169
+ async withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T> {
1170
+ const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
1171
+ const handlePausedRequest = (params: unknown): void => {
1172
+ void this.handleResourcePolicyPausedRequest(params, policy, allowedMethods);
1173
+ };
1174
+
1175
+ const unsubscribe = this.pageClient.on("Fetch.requestPaused", handlePausedRequest);
1176
+
1177
+ try {
1178
+ await this.pageClient.send("Fetch.enable", {
1179
+ patterns: [{ requestStage: "Request", urlPattern: "*" }],
1180
+ });
1181
+ } catch (error) {
1182
+ unsubscribe();
1183
+ throw new ProviderError(
1184
+ "CDP browser target does not support BrowserPage.withResourcePolicy()",
1185
+ {
1186
+ cause: error instanceof Error ? error : undefined,
1187
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
1188
+ fix: "Use a Chromium CDP target with the Fetch domain enabled, or use the local Playwright browser runtime.",
1189
+ },
1190
+ );
1191
+ }
1192
+
1193
+ try {
1194
+ return await run();
1195
+ } finally {
1196
+ unsubscribe();
1197
+ await this.pageClient.send("Fetch.disable");
1198
+ }
1199
+ }
1200
+
1201
+ private async handleResourcePolicyPausedRequest(
1202
+ params: unknown,
1203
+ policy: BrowserResourcePolicy,
1204
+ allowedMethods: ReadonlySet<BrowserResourceMethod>,
1205
+ ): Promise<void> {
1206
+ const requestId = getCdpPausedRequestId(params);
1207
+ if (requestId === null) {
1208
+ return;
1209
+ }
1210
+
1211
+ try {
1212
+ const parsed = toCdpResourceRequest(params);
1213
+ if (!parsed || !allowedMethods.has(parsed.request.method)) {
1214
+ await this.failCdpResourceRequest(requestId);
1215
+ return;
1216
+ }
1217
+
1218
+ for (const resourceRoute of policy.routes) {
1219
+ if (!matchesResourceRoute(resourceRoute.match, parsed.request)) {
1220
+ continue;
1221
+ }
1222
+
1223
+ const decision = await resourceRoute.handle(parsed.request);
1224
+ switch (decision.action) {
1225
+ case "fulfill":
1226
+ await this.pageClient.send(
1227
+ "Fetch.fulfillRequest",
1228
+ toCdpFulfillParams(parsed.requestId, decision),
1229
+ );
1230
+ return;
1231
+ case "block":
1232
+ await this.failCdpResourceRequest(parsed.requestId);
1233
+ return;
1234
+ }
1235
+ }
1236
+
1237
+ await this.failCdpResourceRequest(parsed.requestId);
1238
+ } catch {
1239
+ await this.failCdpResourceRequest(requestId);
1240
+ }
1241
+ }
1242
+
1243
+ private async failCdpResourceRequest(requestId: string): Promise<void> {
1244
+ try {
1245
+ await this.pageClient.send("Fetch.failRequest", {
1246
+ errorReason: "BlockedByClient",
1247
+ requestId,
1248
+ });
1249
+ } catch (error) {
1250
+ if (error instanceof Error) {
1251
+ return;
1252
+ }
1253
+ }
1254
+ }
1255
+
1058
1256
  private async initialize(): Promise<void> {
1059
1257
  if (this.initialized) {
1060
1258
  return;
@@ -1084,9 +1282,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1084
1282
  });
1085
1283
  const contextId = result.executionContextId;
1086
1284
  if (typeof contextId !== "number") {
1087
- throw new Error(
1088
- `Unable to resolve execution context for frame: ${frameId}`,
1089
- );
1285
+ throw new Error(`Unable to resolve execution context for frame: ${frameId}`);
1090
1286
  }
1091
1287
 
1092
1288
  this.frameExecutionContexts.set(frameId, contextId);
@@ -1132,17 +1328,11 @@ class CdpPoolBrowserClient implements SupportedBrowserClient {
1132
1328
  return await this.acquirePage({ isolatedContext: true });
1133
1329
  }
1134
1330
 
1135
- private async acquirePage(options?: {
1136
- isolatedContext?: boolean;
1137
- }): Promise<BrowserPageContract> {
1331
+ private async acquirePage(options?: { isolatedContext?: boolean }): Promise<BrowserPageContract> {
1138
1332
  const acquireResult = parsePoolAcquireResponse(
1139
1333
  await this.poolClient.send("acquire", {
1140
- ...(this.allowedHosts.length > 0
1141
- ? { allowedHosts: this.allowedHosts }
1142
- : {}),
1143
- ...(options?.isolatedContext
1144
- ? { isolationMode: "browserContext" }
1145
- : {}),
1334
+ ...(this.allowedHosts.length > 0 ? { allowedHosts: this.allowedHosts } : {}),
1335
+ ...(options?.isolatedContext ? { isolationMode: "browserContext" } : {}),
1146
1336
  }),
1147
1337
  );
1148
1338
  const pageClient = new JsonRpcWebSocketClient(acquireResult.wsEndpoint);
@@ -1171,9 +1361,7 @@ class CdpPoolBrowserClient implements SupportedBrowserClient {
1171
1361
  return await this.newPage();
1172
1362
  }
1173
1363
 
1174
- async withIsolatedContext<T>(
1175
- handler: (page: BrowserPageContract) => Promise<T>,
1176
- ): Promise<T> {
1364
+ async withIsolatedContext<T>(handler: (page: BrowserPageContract) => Promise<T>): Promise<T> {
1177
1365
  const page = await this.acquirePage({ isolatedContext: true });
1178
1366
 
1179
1367
  try {
@@ -1189,9 +1377,7 @@ class CdpPoolBrowserClient implements SupportedBrowserClient {
1189
1377
  }
1190
1378
 
1191
1379
  class UnsupportedBrowserEngineClient implements SupportedBrowserClient {
1192
- constructor(
1193
- readonly engine: Extract<BrowserEngine, "nodriver" | "selenium-uc">,
1194
- ) {}
1380
+ constructor(readonly engine: Extract<BrowserEngine, "nodriver" | "selenium-uc">) {}
1195
1381
 
1196
1382
  async newPage(): Promise<never> {
1197
1383
  if (this.engine === "nodriver") {
@@ -1218,15 +1404,11 @@ class UnsupportedBrowserEngineClient implements SupportedBrowserClient {
1218
1404
  async close(): Promise<void> {}
1219
1405
  }
1220
1406
 
1221
- function createPlaywrightStealthClient(
1222
- options: BrowserClientOptions = {},
1223
- ): SupportedBrowserClient {
1407
+ function createPlaywrightStealthClient(options: BrowserClientOptions = {}): SupportedBrowserClient {
1224
1408
  return new PlaywrightBrowserClient(options);
1225
1409
  }
1226
1410
 
1227
- function createCdpPoolBrowserClient(
1228
- options: BrowserClientOptions,
1229
- ): SupportedBrowserClient {
1411
+ function createCdpPoolBrowserClient(options: BrowserClientOptions): SupportedBrowserClient {
1230
1412
  return new CdpPoolBrowserClient(options);
1231
1413
  }
1232
1414
 
@@ -1255,13 +1437,10 @@ export class BrowserClient implements BrowserClientContract {
1255
1437
  this.cdpUrl = resolvedOptions.cdpUrl;
1256
1438
 
1257
1439
  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
- );
1440
+ throw new ProviderError("Managed CDP Pool is required for browser providers in production", {
1441
+ code: "BROWSER_CDP_POOL_REQUIRED",
1442
+ fix: "Set APIFUSE__CDP_POOL__URL for deployed browser providers. Local standalone development may omit it.",
1443
+ });
1265
1444
  }
1266
1445
 
1267
1446
  switch (engine) {
@@ -1299,9 +1478,7 @@ export class BrowserClient implements BrowserClientContract {
1299
1478
  return this.activatePage(page);
1300
1479
  }
1301
1480
 
1302
- async withIsolatedContext<T>(
1303
- handler: (page: BrowserPageContract) => Promise<T>,
1304
- ): Promise<T> {
1481
+ async withIsolatedContext<T>(handler: (page: BrowserPageContract) => Promise<T>): Promise<T> {
1305
1482
  const previousActivePage = this.activePage;
1306
1483
  let trackedPage: BrowserPageContract | undefined;
1307
1484
 
@@ -1346,16 +1523,11 @@ export class BrowserClient implements BrowserClientContract {
1346
1523
  return trackedPage;
1347
1524
  }
1348
1525
 
1349
- async solveChallenge(
1350
- request: BrowserChallengeRequest,
1351
- ): Promise<BrowserChallengeResult> {
1526
+ async solveChallenge(request: BrowserChallengeRequest): Promise<BrowserChallengeResult> {
1352
1527
  if (request.type !== "recaptcha") {
1353
- throw new ProviderError(
1354
- `Unsupported browser challenge: ${request.type}`,
1355
- {
1356
- code: "BROWSER_RUNTIME_UNSUPPORTED",
1357
- },
1358
- );
1528
+ throw new ProviderError(`Unsupported browser challenge: ${request.type}`, {
1529
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
1530
+ });
1359
1531
  }
1360
1532
 
1361
1533
  if (this.activePage) {
@@ -1417,8 +1589,7 @@ async function findRecaptchaFrame(
1417
1589
  for (const frame of frames) {
1418
1590
  const url = await frame.url();
1419
1591
  const matchesRecaptcha =
1420
- url.includes("google.com/recaptcha") ||
1421
- url.includes("recaptcha.net/recaptcha");
1592
+ url.includes("google.com/recaptcha") || url.includes("recaptcha.net/recaptcha");
1422
1593
  const matchesSiteKey = !siteKey || url.includes(siteKey);
1423
1594
  if (matchesRecaptcha && matchesSiteKey) {
1424
1595
  return frame;
@@ -1428,8 +1599,6 @@ async function findRecaptchaFrame(
1428
1599
  return undefined;
1429
1600
  }
1430
1601
 
1431
- export function createBrowserClient(
1432
- options: BrowserClientOptions = {},
1433
- ): BrowserClient {
1602
+ export function createBrowserClient(options: BrowserClientOptions = {}): BrowserClient {
1434
1603
  return new BrowserClient(options);
1435
1604
  }