@apifuse/provider-sdk 2.2.0-beta.7 → 2.2.0-beta.70

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 (478) hide show
  1. package/AUTHORING.md +943 -51
  2. package/CHANGELOG.md +273 -1
  3. package/LICENSE +21 -0
  4. package/README.md +117 -29
  5. package/SUBMISSION.md +3 -2
  6. package/bin/apifuse-check.ts +293 -13
  7. package/bin/apifuse-dev.ts +42 -19
  8. package/bin/apifuse-migrate-deployment.ts +200 -0
  9. package/bin/apifuse-migrate-operation-declaration.ts +55 -0
  10. package/bin/apifuse-migrate-shape.ts +202 -0
  11. package/bin/apifuse-pack-check.ts +19 -6
  12. package/bin/apifuse-pack-smoke.ts +57 -2
  13. package/bin/apifuse-pack-types.ts +736 -38
  14. package/bin/apifuse-perf.ts +59 -23
  15. package/bin/apifuse-record.ts +923 -94
  16. package/bin/apifuse-submit-check.ts +2313 -281
  17. package/bin/apifuse-test.ts +3 -1
  18. package/bin/submit-check-delimited-text.ts +50 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth-turn/index.js +1 -1
  21. package/dist/auth.d.ts +14 -0
  22. package/dist/auth.js +67 -0
  23. package/dist/ceremonies/index.d.ts +16 -0
  24. package/dist/ceremonies/index.js +141 -36
  25. package/dist/cli/commands.d.ts +1 -1
  26. package/dist/cli/commands.js +30 -0
  27. package/dist/cli/create.d.ts +1 -1
  28. package/dist/cli/create.js +8 -2
  29. package/dist/cli/deployment-intent.d.ts +178 -0
  30. package/dist/cli/deployment-intent.js +277 -0
  31. package/dist/cli/local-connection.d.ts +111 -0
  32. package/dist/cli/local-connection.js +151 -0
  33. package/dist/cli/migrate-deployment-intent.d.ts +132 -0
  34. package/dist/cli/migrate-deployment-intent.js +1552 -0
  35. package/dist/cli/migrate-operation-declaration.d.ts +70 -0
  36. package/dist/cli/migrate-operation-declaration.js +2206 -0
  37. package/dist/cli/migrate-operation-shape.d.ts +44 -0
  38. package/dist/cli/migrate-operation-shape.js +113 -0
  39. package/dist/cli/migrate-provider-shape.d.ts +56 -0
  40. package/dist/cli/migrate-provider-shape.js +580 -0
  41. package/dist/cli/resolver-runtime.d.ts +21 -0
  42. package/dist/cli/resolver-runtime.js +31 -0
  43. package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +141 -23
  44. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  45. package/dist/cli/templates/provider/README.md.tpl +41 -19
  46. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  47. package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
  48. package/dist/cli/templates/provider/provider.json.tpl +6 -0
  49. package/dist/config/loader.d.ts +202 -16
  50. package/dist/config/loader.js +491 -141
  51. package/dist/contract-serialization.js +4 -8
  52. package/dist/contract-types.d.ts +12 -5
  53. package/dist/contract.js +24 -10
  54. package/dist/declaration-validation.d.ts +34 -0
  55. package/dist/declaration-validation.js +305 -0
  56. package/dist/define.d.ts +71 -29
  57. package/dist/define.js +807 -69
  58. package/dist/dev.d.ts +5 -0
  59. package/dist/dev.js +8 -1
  60. package/dist/engine.d.ts +129 -0
  61. package/dist/engine.js +209 -0
  62. package/dist/error-observability.d.ts +7 -0
  63. package/dist/error-observability.js +61 -0
  64. package/dist/error-resolution.d.ts +18 -0
  65. package/dist/error-resolution.js +243 -0
  66. package/dist/errors.d.ts +74 -0
  67. package/dist/errors.js +99 -0
  68. package/dist/fixture-sanitization.d.ts +28 -0
  69. package/dist/fixture-sanitization.js +371 -0
  70. package/dist/handle-meta.d.ts +90 -0
  71. package/dist/handle-meta.js +73 -0
  72. package/dist/handle.d.ts +226 -0
  73. package/dist/handle.js +345 -0
  74. package/dist/health-scenario.d.ts +1901 -0
  75. package/dist/health-scenario.js +653 -0
  76. package/dist/i18n/catalog.js +21 -1
  77. package/dist/i18n/error-messages.d.ts +86 -0
  78. package/dist/i18n/error-messages.js +192 -0
  79. package/dist/i18n/index.d.ts +1 -0
  80. package/dist/i18n/index.js +1 -0
  81. package/dist/i18n/keys.d.ts +12 -0
  82. package/dist/i18n/keys.js +76 -5
  83. package/dist/i18n/operation-locale-namespace.d.ts +2 -0
  84. package/dist/i18n/operation-locale-namespace.js +7 -0
  85. package/dist/index.d.ts +40 -15
  86. package/dist/index.js +29 -11
  87. package/dist/lint.d.ts +43 -11
  88. package/dist/lint.js +2512 -177
  89. package/dist/native-address.d.ts +43 -0
  90. package/dist/native-address.js +281 -0
  91. package/dist/native-egress-policy.d.ts +31 -0
  92. package/dist/native-egress-policy.js +288 -0
  93. package/dist/observability.d.ts +5 -2
  94. package/dist/observability.js +48 -1
  95. package/dist/provider.d.ts +18 -8
  96. package/dist/provider.js +11 -6
  97. package/dist/runtime/auth-flow.d.ts +6 -1
  98. package/dist/runtime/auth-flow.js +8 -0
  99. package/dist/runtime/bounded-expiring-map.d.ts +21 -0
  100. package/dist/runtime/bounded-expiring-map.js +64 -0
  101. package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
  102. package/dist/runtime/browser-telemetry-binding.js +83 -0
  103. package/dist/runtime/browser-telemetry.d.ts +76 -0
  104. package/dist/runtime/browser-telemetry.js +165 -0
  105. package/dist/runtime/browser.d.ts +4 -0
  106. package/dist/runtime/browser.js +688 -83
  107. package/dist/runtime/cache-telemetry.d.ts +71 -0
  108. package/dist/runtime/cache-telemetry.js +177 -0
  109. package/dist/runtime/cache.d.ts +3 -0
  110. package/dist/runtime/cache.js +245 -23
  111. package/dist/runtime/capability-response.d.ts +3 -0
  112. package/dist/runtime/capability-response.js +56 -0
  113. package/dist/runtime/capability-telemetry.d.ts +46 -0
  114. package/dist/runtime/capability-telemetry.js +68 -0
  115. package/dist/runtime/chrome149-header-order.d.ts +58 -0
  116. package/dist/runtime/chrome149-header-order.js +289 -0
  117. package/dist/runtime/diagnostic-env-values.d.ts +5 -0
  118. package/dist/runtime/diagnostic-env-values.js +57 -0
  119. package/dist/runtime/diagnostic-env.d.ts +14 -0
  120. package/dist/runtime/diagnostic-env.js +44 -0
  121. package/dist/runtime/diagnostic-redactor.d.ts +54 -0
  122. package/dist/runtime/diagnostic-redactor.js +571 -0
  123. package/dist/runtime/egress-lease.d.ts +65 -0
  124. package/dist/runtime/egress-lease.js +236 -0
  125. package/dist/runtime/engine-mode.d.ts +74 -0
  126. package/dist/runtime/engine-mode.js +152 -0
  127. package/dist/runtime/env.js +14 -1
  128. package/dist/runtime/executor.d.ts +3 -2
  129. package/dist/runtime/executor.js +23 -26
  130. package/dist/runtime/handle-wordlist.d.ts +43 -0
  131. package/dist/runtime/handle-wordlist.js +256 -0
  132. package/dist/runtime/handle.d.ts +71 -0
  133. package/dist/runtime/handle.js +908 -0
  134. package/dist/runtime/http-telemetry-binding.d.ts +10 -0
  135. package/dist/runtime/http-telemetry-binding.js +152 -0
  136. package/dist/runtime/http-telemetry-guard.d.ts +16 -0
  137. package/dist/runtime/http-telemetry-guard.js +202 -0
  138. package/dist/runtime/http-telemetry.d.ts +104 -0
  139. package/dist/runtime/http-telemetry.js +190 -0
  140. package/dist/runtime/http.d.ts +34 -0
  141. package/dist/runtime/http.js +680 -121
  142. package/dist/runtime/insights.js +2 -2
  143. package/dist/runtime/instrumentation.d.ts +2 -2
  144. package/dist/runtime/instrumentation.js +462 -19
  145. package/dist/runtime/native-network-errors.d.ts +33 -0
  146. package/dist/runtime/native-network-errors.js +69 -0
  147. package/dist/runtime/native-network.d.ts +105 -0
  148. package/dist/runtime/native-network.js +1433 -0
  149. package/dist/runtime/native-telemetry.d.ts +142 -0
  150. package/dist/runtime/native-telemetry.js +231 -0
  151. package/dist/runtime/ocr-telemetry.d.ts +82 -0
  152. package/dist/runtime/ocr-telemetry.js +192 -0
  153. package/dist/runtime/ocr.d.ts +29 -0
  154. package/dist/runtime/ocr.js +468 -0
  155. package/dist/runtime/otlp.d.ts +71 -2
  156. package/dist/runtime/otlp.js +398 -16
  157. package/dist/runtime/provider.d.ts +7 -0
  158. package/dist/runtime/provider.js +14 -0
  159. package/dist/runtime/proxy-errors.js +6 -2
  160. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  161. package/dist/runtime/proxy-nodemaven.js +147 -0
  162. package/dist/runtime/proxy-retry-policy.d.ts +4 -1
  163. package/dist/runtime/proxy-retry-policy.js +11 -2
  164. package/dist/runtime/proxy-telemetry.d.ts +133 -3
  165. package/dist/runtime/proxy-telemetry.js +218 -70
  166. package/dist/runtime/redirect-hop-headers.d.ts +19 -0
  167. package/dist/runtime/redirect-hop-headers.js +58 -0
  168. package/dist/runtime/redirects.d.ts +61 -0
  169. package/dist/runtime/redirects.js +149 -0
  170. package/dist/runtime/redis.d.ts +1 -1
  171. package/dist/runtime/redis.js +4 -2
  172. package/dist/runtime/request-options.d.ts +75 -1
  173. package/dist/runtime/request-options.js +588 -0
  174. package/dist/runtime/request-telemetry.d.ts +70 -0
  175. package/dist/runtime/request-telemetry.js +229 -0
  176. package/dist/runtime/resolver-config.d.ts +13 -0
  177. package/dist/runtime/resolver-config.js +14 -0
  178. package/dist/runtime/resolver-public.d.ts +3 -0
  179. package/dist/runtime/resolver-public.js +1 -0
  180. package/dist/runtime/resolver-runtime-options.d.ts +14 -0
  181. package/dist/runtime/resolver-runtime-options.js +33 -0
  182. package/dist/runtime/resolver-shared.d.ts +24 -0
  183. package/dist/runtime/resolver-shared.js +69 -0
  184. package/dist/runtime/resolver-span-attributes.d.ts +34 -0
  185. package/dist/runtime/resolver-span-attributes.js +93 -0
  186. package/dist/runtime/resolver-telemetry.d.ts +135 -0
  187. package/dist/runtime/resolver-telemetry.js +204 -0
  188. package/dist/runtime/resolver-usage.d.ts +32 -0
  189. package/dist/runtime/resolver-usage.js +54 -0
  190. package/dist/runtime/resolver-vendors/bindings.d.ts +53 -0
  191. package/dist/runtime/resolver-vendors/bindings.js +45 -0
  192. package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
  193. package/dist/runtime/resolver-vendors/browser.js +423 -0
  194. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  195. package/dist/runtime/resolver-vendors/capsolver.js +544 -0
  196. package/dist/runtime/resolver-vendors/hosts.d.ts +23 -0
  197. package/dist/runtime/resolver-vendors/hosts.js +68 -0
  198. package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
  199. package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
  200. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  201. package/dist/runtime/resolver-vendors/twocaptcha.js +425 -0
  202. package/dist/runtime/resolver-vendors/types.d.ts +125 -0
  203. package/dist/runtime/resolver-vendors/types.js +106 -0
  204. package/dist/runtime/resolver.d.ts +79 -0
  205. package/dist/runtime/resolver.js +1094 -0
  206. package/dist/runtime/state-telemetry.d.ts +79 -0
  207. package/dist/runtime/state-telemetry.js +194 -0
  208. package/dist/runtime/state.d.ts +10 -1
  209. package/dist/runtime/state.js +322 -82
  210. package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
  211. package/dist/runtime/stealth-akamai-sbsd.js +142 -0
  212. package/dist/runtime/stealth-cookies.d.ts +24 -0
  213. package/dist/runtime/stealth-cookies.js +131 -0
  214. package/dist/runtime/stealth-owned-headers.d.ts +39 -0
  215. package/dist/runtime/stealth-owned-headers.js +57 -0
  216. package/dist/runtime/stealth-telemetry.d.ts +92 -0
  217. package/dist/runtime/stealth-telemetry.js +179 -0
  218. package/dist/runtime/stealth.d.ts +45 -6
  219. package/dist/runtime/stealth.js +1775 -320
  220. package/dist/runtime/stt-telemetry.d.ts +77 -0
  221. package/dist/runtime/stt-telemetry.js +176 -0
  222. package/dist/runtime/stt.js +18 -19
  223. package/dist/runtime/timeout.d.ts +5 -0
  224. package/dist/runtime/timeout.js +12 -0
  225. package/dist/runtime/trace-config.d.ts +12 -0
  226. package/dist/runtime/trace-config.js +62 -0
  227. package/dist/runtime/trace.d.ts +17 -0
  228. package/dist/runtime/trace.js +176 -25
  229. package/dist/runtime-boundary-lint.d.ts +137 -0
  230. package/dist/runtime-boundary-lint.js +590 -0
  231. package/dist/schema.d.ts +20 -0
  232. package/dist/schema.js +72 -2
  233. package/dist/serve.d.ts +1 -1
  234. package/dist/serve.js +1 -1
  235. package/dist/server/error-observability.d.ts +1 -0
  236. package/dist/server/error-observability.js +1 -0
  237. package/dist/server/error-stack-frames.d.ts +35 -0
  238. package/dist/server/error-stack-frames.js +219 -0
  239. package/dist/server/index.d.ts +21 -2
  240. package/dist/server/index.js +11 -2
  241. package/dist/server/runtime-diagnostics.d.ts +165 -0
  242. package/dist/server/runtime-diagnostics.js +360 -0
  243. package/dist/server/sdk-version.d.ts +3 -0
  244. package/dist/server/sdk-version.js +19 -0
  245. package/dist/server/self-test-input-tokens.d.ts +2 -1
  246. package/dist/server/self-test-input-tokens.js +18 -14
  247. package/dist/server/self-test-redaction.js +8 -33
  248. package/dist/server/self-test.d.ts +18 -3
  249. package/dist/server/self-test.js +139 -76
  250. package/dist/server/sensitive-values.d.ts +26 -0
  251. package/dist/server/sensitive-values.js +137 -0
  252. package/dist/server/serve-implementation.d.ts +285 -0
  253. package/dist/server/serve-implementation.js +3172 -0
  254. package/dist/server/serve.d.ts +1 -75
  255. package/dist/server/serve.js +1 -1170
  256. package/dist/server/state-backend-report.d.ts +70 -0
  257. package/dist/server/state-backend-report.js +113 -0
  258. package/dist/server/trace-output.d.ts +7 -0
  259. package/dist/server/trace-output.js +111 -0
  260. package/dist/server/types.d.ts +37 -5
  261. package/dist/server/types.js +34 -1
  262. package/dist/stateful/errors.d.ts +19 -0
  263. package/dist/stateful/errors.js +24 -0
  264. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  265. package/dist/stateful/http-provider-event-emitter.js +237 -0
  266. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  267. package/dist/stateful/http-session-owner-registry.js +210 -0
  268. package/dist/stateful/index.d.ts +18 -0
  269. package/dist/stateful/index.js +18 -0
  270. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  271. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  272. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  273. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  274. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  275. package/dist/stateful/provider-event-pipeline.js +1 -0
  276. package/dist/stateful/provider-events.d.ts +101 -0
  277. package/dist/stateful/provider-events.js +289 -0
  278. package/dist/stateful/session-key.d.ts +15 -0
  279. package/dist/stateful/session-key.js +86 -0
  280. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  281. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  282. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  283. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  284. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  285. package/dist/stateful/stateful-provider-adapter.js +287 -0
  286. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  287. package/dist/stateful/stateful-provider-observability.js +161 -0
  288. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  289. package/dist/stateful/stateful-provider-owner-forwarder.js +237 -0
  290. package/dist/stateful/stateful-provider-runtime-context.d.ts +35 -0
  291. package/dist/stateful/stateful-provider-runtime-context.js +61 -0
  292. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  293. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  294. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  295. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  296. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  297. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  298. package/dist/stateful-signing.d.ts +18 -0
  299. package/dist/stateful-signing.js +27 -0
  300. package/dist/stealth/profiles.d.ts +15 -4
  301. package/dist/stealth/profiles.js +169 -211
  302. package/dist/stream-evidence.d.ts +74 -0
  303. package/dist/stream-evidence.js +785 -0
  304. package/dist/stream.js +7 -1
  305. package/dist/testing/index.d.ts +2 -1
  306. package/dist/testing/index.js +2 -1
  307. package/dist/testing/run.d.ts +32 -2
  308. package/dist/testing/run.js +509 -25
  309. package/dist/trace-sanitization.d.ts +6 -0
  310. package/dist/trace-sanitization.js +68 -0
  311. package/dist/types.d.ts +899 -210
  312. package/dist/types.js +1 -0
  313. package/dist/typescript-module.d.ts +39 -0
  314. package/dist/typescript-module.js +72 -0
  315. package/package.json +52 -6
  316. package/src/auth-turn/index.ts +1 -1
  317. package/src/auth.ts +118 -0
  318. package/src/ceremonies/index.ts +189 -46
  319. package/src/cli/commands.ts +37 -0
  320. package/src/cli/create.ts +8 -2
  321. package/src/cli/deployment-intent.ts +364 -0
  322. package/src/cli/local-connection.ts +221 -0
  323. package/src/cli/migrate-deployment-intent.ts +1908 -0
  324. package/src/cli/migrate-operation-declaration.ts +3026 -0
  325. package/src/cli/migrate-operation-shape.ts +184 -0
  326. package/src/cli/migrate-provider-shape.ts +778 -0
  327. package/src/cli/resolver-runtime.ts +72 -0
  328. package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +141 -23
  329. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  330. package/src/cli/templates/provider/README.md.tpl +41 -19
  331. package/src/cli/templates/provider/index.ts.tpl +6 -3
  332. package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
  333. package/src/cli/templates/provider/provider.json.tpl +6 -0
  334. package/src/config/loader.ts +774 -181
  335. package/src/contract-serialization.ts +5 -7
  336. package/src/contract-types.ts +12 -5
  337. package/src/contract.ts +24 -10
  338. package/src/declaration-validation.ts +376 -0
  339. package/src/define.ts +1105 -131
  340. package/src/dev.ts +13 -1
  341. package/src/engine.ts +390 -0
  342. package/src/error-observability.ts +64 -0
  343. package/src/error-resolution.ts +269 -0
  344. package/src/errors.ts +157 -0
  345. package/src/fixture-sanitization.ts +422 -0
  346. package/src/handle-meta.ts +143 -0
  347. package/src/handle.ts +694 -0
  348. package/src/health-scenario.ts +960 -0
  349. package/src/i18n/catalog.ts +23 -1
  350. package/src/i18n/error-messages.ts +255 -0
  351. package/src/i18n/index.ts +1 -0
  352. package/src/i18n/keys.ts +80 -6
  353. package/src/i18n/operation-locale-namespace.ts +10 -0
  354. package/src/index.ts +390 -23
  355. package/src/lint.ts +2975 -234
  356. package/src/native-address.ts +340 -0
  357. package/src/native-egress-policy.ts +358 -0
  358. package/src/observability.ts +51 -1
  359. package/src/provider.ts +207 -27
  360. package/src/runtime/auth-flow.ts +15 -0
  361. package/src/runtime/bounded-expiring-map.ts +67 -0
  362. package/src/runtime/browser-telemetry-binding.ts +92 -0
  363. package/src/runtime/browser-telemetry.ts +289 -0
  364. package/src/runtime/browser.ts +882 -99
  365. package/src/runtime/cache-telemetry.ts +247 -0
  366. package/src/runtime/cache.ts +258 -22
  367. package/src/runtime/capability-response.ts +53 -0
  368. package/src/runtime/capability-telemetry.ts +101 -0
  369. package/src/runtime/chrome149-header-order.ts +330 -0
  370. package/src/runtime/diagnostic-env-values.ts +56 -0
  371. package/src/runtime/diagnostic-env.ts +65 -0
  372. package/src/runtime/diagnostic-redactor.ts +668 -0
  373. package/src/runtime/egress-lease.ts +341 -0
  374. package/src/runtime/engine-mode.ts +233 -0
  375. package/src/runtime/env.ts +15 -1
  376. package/src/runtime/executor.ts +39 -32
  377. package/src/runtime/handle-wordlist.ts +266 -0
  378. package/src/runtime/handle.ts +1180 -0
  379. package/src/runtime/http-telemetry-binding.ts +162 -0
  380. package/src/runtime/http-telemetry-guard.ts +208 -0
  381. package/src/runtime/http-telemetry.ts +292 -0
  382. package/src/runtime/http.ts +896 -150
  383. package/src/runtime/insights.ts +2 -2
  384. package/src/runtime/instrumentation.ts +628 -28
  385. package/src/runtime/native-network-errors.ts +99 -0
  386. package/src/runtime/native-network.ts +1853 -0
  387. package/src/runtime/native-telemetry.ts +366 -0
  388. package/src/runtime/ocr-telemetry.ts +308 -0
  389. package/src/runtime/ocr.ts +564 -0
  390. package/src/runtime/otlp.ts +468 -21
  391. package/src/runtime/provider.ts +15 -0
  392. package/src/runtime/proxy-errors.ts +12 -4
  393. package/src/runtime/proxy-nodemaven.ts +222 -0
  394. package/src/runtime/proxy-retry-policy.ts +12 -1
  395. package/src/runtime/proxy-telemetry.ts +358 -103
  396. package/src/runtime/redirect-hop-headers.ts +71 -0
  397. package/src/runtime/redirects.ts +215 -0
  398. package/src/runtime/redis.ts +7 -2
  399. package/src/runtime/request-options.ts +728 -1
  400. package/src/runtime/request-telemetry.ts +375 -0
  401. package/src/runtime/resolver-config.ts +18 -0
  402. package/src/runtime/resolver-public.ts +44 -0
  403. package/src/runtime/resolver-runtime-options.ts +50 -0
  404. package/src/runtime/resolver-shared.ts +113 -0
  405. package/src/runtime/resolver-span-attributes.ts +139 -0
  406. package/src/runtime/resolver-telemetry.ts +362 -0
  407. package/src/runtime/resolver-usage.ts +99 -0
  408. package/src/runtime/resolver-vendors/bindings.ts +61 -0
  409. package/src/runtime/resolver-vendors/browser.ts +591 -0
  410. package/src/runtime/resolver-vendors/capsolver.ts +727 -0
  411. package/src/runtime/resolver-vendors/hosts.ts +81 -0
  412. package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
  413. package/src/runtime/resolver-vendors/twocaptcha.ts +555 -0
  414. package/src/runtime/resolver-vendors/types.ts +261 -0
  415. package/src/runtime/resolver.ts +1543 -0
  416. package/src/runtime/state-telemetry.ts +298 -0
  417. package/src/runtime/state.ts +435 -89
  418. package/src/runtime/stealth-akamai-sbsd.ts +207 -0
  419. package/src/runtime/stealth-cookies.ts +155 -0
  420. package/src/runtime/stealth-owned-headers.ts +67 -0
  421. package/src/runtime/stealth-telemetry.ts +309 -0
  422. package/src/runtime/stealth.ts +2275 -372
  423. package/src/runtime/stt-telemetry.ts +282 -0
  424. package/src/runtime/stt.ts +96 -91
  425. package/src/runtime/timeout.ts +18 -0
  426. package/src/runtime/trace-config.ts +78 -0
  427. package/src/runtime/trace.ts +257 -35
  428. package/src/runtime-boundary-lint.ts +707 -0
  429. package/src/schema.ts +80 -2
  430. package/src/serve.ts +6 -1
  431. package/src/server/error-observability.ts +1 -0
  432. package/src/server/error-stack-frames.ts +214 -0
  433. package/src/server/index.ts +184 -2
  434. package/src/server/runtime-diagnostics.ts +522 -0
  435. package/src/server/sdk-version.ts +22 -0
  436. package/src/server/self-test-input-tokens.ts +29 -14
  437. package/src/server/self-test-redaction.ts +10 -40
  438. package/src/server/self-test.ts +205 -89
  439. package/src/server/sensitive-values.ts +211 -0
  440. package/src/server/serve-implementation.ts +4596 -0
  441. package/src/server/serve.ts +1 -1685
  442. package/src/server/state-backend-report.ts +185 -0
  443. package/src/server/trace-output.ts +189 -0
  444. package/src/server/types.ts +34 -1
  445. package/src/stateful/README.md +146 -0
  446. package/src/stateful/errors.ts +35 -0
  447. package/src/stateful/http-provider-event-emitter.ts +314 -0
  448. package/src/stateful/http-session-owner-registry.ts +306 -0
  449. package/src/stateful/index.ts +18 -0
  450. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  451. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  452. package/src/stateful/provider-event-pipeline.ts +61 -0
  453. package/src/stateful/provider-events.ts +462 -0
  454. package/src/stateful/session-key.ts +111 -0
  455. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  456. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  457. package/src/stateful/stateful-provider-adapter.ts +562 -0
  458. package/src/stateful/stateful-provider-observability.ts +261 -0
  459. package/src/stateful/stateful-provider-owner-forwarder.ts +310 -0
  460. package/src/stateful/stateful-provider-runtime-context.ts +96 -0
  461. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  462. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  463. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  464. package/src/stateful-signing.ts +46 -0
  465. package/src/stealth/profiles.ts +212 -231
  466. package/src/stream-evidence.ts +988 -0
  467. package/src/stream.ts +8 -1
  468. package/src/testing/index.ts +10 -1
  469. package/src/testing/run.ts +685 -19
  470. package/src/trace-sanitization.ts +110 -0
  471. package/src/types.ts +1014 -273
  472. package/src/typescript-module.ts +100 -0
  473. package/dist/choice-token.d.ts +0 -24
  474. package/dist/choice-token.js +0 -74
  475. package/dist/runtime/choice.d.ts +0 -15
  476. package/dist/runtime/choice.js +0 -413
  477. package/src/choice-token.ts +0 -165
  478. package/src/runtime/choice.ts +0 -686
@@ -1,11 +1,42 @@
1
+ import { readDiagnosticEnv, registerDiagnosticValue } from "./diagnostic-env.js";
2
+ import { AsyncResource } from "node:async_hooks";
3
+ import type {
4
+ BrowserTelemetryPoolAcquireOutcome,
5
+ BrowserTelemetrySink,
6
+ } from "./browser-telemetry.js";
7
+ import { observeTelemetryCallback as observeBrowserTelemetry } from "./http-telemetry-guard.js";
8
+ import { cookieValuesFromHeaders } from "./stealth-cookies.js";
9
+
10
+ function browserTelemetryErrorCode(
11
+ error: unknown,
12
+ ):
13
+ | "BROWSER_CDP_POOL_REQUIRED"
14
+ | "BROWSER_PROXY_INVALID"
15
+ | "BROWSER_RUNTIME_UNSUPPORTED"
16
+ | "BROWSER_CDP_POOL_ERROR"
17
+ | "other" {
18
+ const code =
19
+ error && typeof error === "object" && "code" in error
20
+ ? (error as { code?: unknown }).code
21
+ : undefined;
22
+ if (
23
+ code === "BROWSER_CDP_POOL_REQUIRED" ||
24
+ code === "BROWSER_PROXY_INVALID" ||
25
+ code === "BROWSER_RUNTIME_UNSUPPORTED"
26
+ )
27
+ return code;
28
+ if (typeof code === "number") return "BROWSER_CDP_POOL_ERROR";
29
+ return "other";
30
+ }
1
31
  import { createRequire } from "node:module";
2
- import type { Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
32
+ import type { CDPSession, Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
3
33
 
4
34
  import { ProviderError } from "../errors.js";
5
35
  import type {
6
36
  BrowserChallengeRequest,
7
37
  BrowserChallengeResult,
8
38
  BrowserClient as BrowserClientContract,
39
+ BrowserCookie,
9
40
  BrowserEngine,
10
41
  BrowserFrame,
11
42
  BrowserLocator,
@@ -42,19 +73,35 @@ type PoolReleaseRequest = {
42
73
  pageId: string;
43
74
  };
44
75
 
45
- type JsonRpcId = number;
76
+ type WebSocketCommandId = number;
46
77
 
47
- type JsonRpcError = {
78
+ type WebSocketCommandError = {
48
79
  code?: number;
49
80
  message?: string;
50
81
  };
51
82
 
52
- type JsonRpcMessage = {
53
- error?: JsonRpcError;
54
- id?: JsonRpcId;
83
+ type WebSocketCommandMessage = {
84
+ error?: WebSocketCommandError;
85
+ id?: WebSocketCommandId;
86
+ jsonrpc?: "2.0";
55
87
  method?: string;
56
88
  params?: Record<string, unknown>;
57
89
  result?: Record<string, unknown>;
90
+ sessionId?: string;
91
+ };
92
+
93
+ type CdpCommandFrame = {
94
+ id: WebSocketCommandId;
95
+ method: string;
96
+ params: Record<string, unknown>;
97
+ sessionId?: string;
98
+ };
99
+
100
+ type JsonRpcCommandFrame = {
101
+ id: WebSocketCommandId;
102
+ jsonrpc: "2.0";
103
+ method: string;
104
+ params: Record<string, unknown>;
58
105
  };
59
106
 
60
107
  type CdpFrameTreeNode = {
@@ -70,10 +117,25 @@ type CdpFrameTreeNode = {
70
117
  type CdpFetchFulfillParams = {
71
118
  readonly requestId: string;
72
119
  readonly responseCode: number;
73
- readonly responseHeaders?: readonly {
120
+ readonly responseHeaders?: {
121
+ readonly name: string;
122
+ readonly value: string;
123
+ }[];
124
+ readonly body?: string;
125
+ };
126
+
127
+ type CdpPausedDocumentResponse = {
128
+ readonly hasBody: boolean;
129
+ readonly requestId: string;
130
+ readonly responseCode: number;
131
+ readonly responseHeaders: readonly {
74
132
  readonly name: string;
75
133
  readonly value: string;
76
134
  }[];
135
+ };
136
+
137
+ type CdpResponseBody = {
138
+ readonly base64Encoded?: boolean;
77
139
  readonly body?: string;
78
140
  };
79
141
 
@@ -92,7 +154,7 @@ function toResourceBody(body: BrowserResourceBody | undefined): Buffer | string
92
154
  }
93
155
 
94
156
  function isResourceMethod(method: string): method is BrowserResourceMethod {
95
- return method === "GET" || method === "HEAD";
157
+ return method === "GET" || method === "HEAD" || method === "POST";
96
158
  }
97
159
 
98
160
  async function toResourceRequest(request: Request): Promise<BrowserResourceRequest | null> {
@@ -147,6 +209,85 @@ function getCdpPausedRequestId(params: unknown): string | null {
147
209
  return params.requestId;
148
210
  }
149
211
 
212
+ function isCdpResponseStage(params: unknown): boolean {
213
+ return (
214
+ isRecord(params) &&
215
+ (typeof params.responseStatusCode === "number" ||
216
+ typeof params.responseErrorReason === "string")
217
+ );
218
+ }
219
+
220
+ function toCdpPausedDocumentResponse(params: unknown): CdpPausedDocumentResponse | null {
221
+ if (
222
+ !isRecord(params) ||
223
+ typeof params.requestId !== "string" ||
224
+ typeof params.responseStatusCode !== "number" ||
225
+ params.resourceType !== "Document" ||
226
+ !isRecord(params.request)
227
+ ) {
228
+ return null;
229
+ }
230
+
231
+ const responseCode = params.responseStatusCode;
232
+ const responseHeaders = Array.isArray(params.responseHeaders)
233
+ ? params.responseHeaders.flatMap((header) => {
234
+ if (
235
+ !isRecord(header) ||
236
+ typeof header.name !== "string" ||
237
+ typeof header.value !== "string"
238
+ ) {
239
+ return [];
240
+ }
241
+ return [{ name: header.name, value: header.value }];
242
+ })
243
+ : [];
244
+
245
+ return {
246
+ hasBody:
247
+ String(params.request.method ?? "").toUpperCase() !== "HEAD" &&
248
+ responseCode >= 200 &&
249
+ (responseCode < 300 || responseCode >= 400) &&
250
+ responseCode !== 204 &&
251
+ responseCode !== 205 &&
252
+ responseCode !== 304,
253
+ requestId: params.requestId,
254
+ responseCode,
255
+ responseHeaders,
256
+ };
257
+ }
258
+
259
+ function toCdpDocumentFulfillParams(
260
+ response: CdpPausedDocumentResponse,
261
+ body: CdpResponseBody,
262
+ contentSecurityPolicy: string,
263
+ ): CdpFetchFulfillParams {
264
+ const responseBody = typeof body.body === "string" ? body.body : "";
265
+ return {
266
+ body: body.base64Encoded ? responseBody : Buffer.from(responseBody).toString("base64"),
267
+ requestId: response.requestId,
268
+ responseCode: response.responseCode,
269
+ responseHeaders: [
270
+ ...response.responseHeaders,
271
+ { name: "Content-Security-Policy", value: contentSecurityPolicy },
272
+ ],
273
+ };
274
+ }
275
+
276
+ function resourcePolicyFetchPatterns(policy: BrowserResourcePolicy) {
277
+ return [
278
+ { requestStage: "Request" as const, urlPattern: "*" },
279
+ ...(policy.documentContentSecurityPolicy
280
+ ? [
281
+ {
282
+ requestStage: "Response" as const,
283
+ resourceType: "Document" as const,
284
+ urlPattern: "*",
285
+ },
286
+ ]
287
+ : []),
288
+ ];
289
+ }
290
+
150
291
  function toCdpResourceHeaders(value: unknown): Record<string, string> {
151
292
  if (!isRecord(value)) {
152
293
  return {};
@@ -208,11 +349,172 @@ async function fulfillResourceRoute(
208
349
  });
209
350
  }
210
351
 
352
+ async function decideResourceRequest(
353
+ policy: BrowserResourcePolicy,
354
+ request: BrowserResourceRequest,
355
+ ): Promise<BrowserResourceDecision> {
356
+ for (const resourceRoute of policy.routes) {
357
+ if (matchesResourceRoute(resourceRoute.match, request)) {
358
+ return await resourceRoute.handle(request);
359
+ }
360
+ }
361
+
362
+ return { action: "block" };
363
+ }
364
+
365
+ async function abortResourceRoute(route: Route): Promise<void> {
366
+ await route.abort("blockedbyclient");
367
+ }
368
+
369
+ async function handleCdpResourceRequest(
370
+ session: CDPSession,
371
+ policy: BrowserResourcePolicy,
372
+ allowedMethods: ReadonlySet<BrowserResourceMethod>,
373
+ params: unknown,
374
+ ): Promise<void> {
375
+ const paused = toCdpResourceRequest(params);
376
+ const requestId = paused?.requestId ?? getCdpPausedRequestId(params);
377
+ if (!requestId) return;
378
+
379
+ try {
380
+ if (!paused || !allowedMethods.has(paused.request.method)) {
381
+ await session.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId });
382
+ return;
383
+ }
384
+
385
+ const decision = await decideResourceRequest(policy, paused.request);
386
+ switch (decision.action) {
387
+ case "continue":
388
+ await session.send("Fetch.continueRequest", { requestId });
389
+ return;
390
+ case "fulfill":
391
+ await session.send("Fetch.fulfillRequest", toCdpFulfillParams(requestId, decision));
392
+ return;
393
+ case "block":
394
+ await session.send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId });
395
+ return;
396
+ }
397
+ } catch {
398
+ await session
399
+ .send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId })
400
+ .catch(() => undefined);
401
+ }
402
+ }
403
+
404
+ async function handleCdpDocumentResponse(
405
+ session: CDPSession,
406
+ contentSecurityPolicy: string,
407
+ params: unknown,
408
+ ): Promise<void> {
409
+ const requestId = getCdpPausedRequestId(params);
410
+ if (!requestId) return;
411
+
412
+ try {
413
+ const response = toCdpPausedDocumentResponse(params);
414
+ if (!response) throw new Error("CDP document response metadata is unavailable");
415
+ if (!response.hasBody) {
416
+ await session.send("Fetch.continueResponse", {
417
+ requestId,
418
+ responseCode: response.responseCode,
419
+ responseHeaders: [...response.responseHeaders],
420
+ });
421
+ return;
422
+ }
423
+
424
+ const body = await session.send("Fetch.getResponseBody", {
425
+ requestId: response.requestId,
426
+ });
427
+ await session.send(
428
+ "Fetch.fulfillRequest",
429
+ toCdpDocumentFulfillParams(response, body, contentSecurityPolicy),
430
+ );
431
+ } catch {
432
+ await session
433
+ .send("Fetch.failRequest", { errorReason: "BlockedByClient", requestId })
434
+ .catch(() => undefined);
435
+ }
436
+ }
437
+
438
+ function getCdpAuthRequiredRequestId(params: unknown): string | null {
439
+ if (!isRecord(params) || typeof params.requestId !== "string") {
440
+ return null;
441
+ }
442
+
443
+ return params.requestId;
444
+ }
445
+
446
+ function isCdpProxyAuthChallenge(params: unknown): boolean {
447
+ return (
448
+ isRecord(params) && isRecord(params.authChallenge) && params.authChallenge.source === "Proxy"
449
+ );
450
+ }
451
+
452
+ async function handleCdpAuthRequired(
453
+ session: CDPSession,
454
+ proxy: PlaywrightProxy & { username: string; password: string },
455
+ authAttempts: Set<string>,
456
+ params: unknown,
457
+ telemetry?: BrowserTelemetrySink,
458
+ ): Promise<void> {
459
+ const requestId = getCdpAuthRequiredRequestId(params);
460
+ if (!requestId) return;
461
+
462
+ // Chromium can emit another challenge after rejected credentials. Cancel a
463
+ // retry so incorrect credentials fail promptly instead of looping forever.
464
+ const response =
465
+ isCdpProxyAuthChallenge(params) && !authAttempts.has(requestId)
466
+ ? (() => {
467
+ authAttempts.add(requestId);
468
+ observeBrowserTelemetry(telemetry, () => telemetry?.recordProxyAuthChallenge());
469
+ return {
470
+ authChallengeResponse: {
471
+ password: proxy.password,
472
+ response: "ProvideCredentials" as const,
473
+ username: proxy.username,
474
+ },
475
+ requestId,
476
+ };
477
+ })()
478
+ : {
479
+ authChallengeResponse: { response: "CancelAuth" as const },
480
+ requestId,
481
+ };
482
+
483
+ await session.send("Fetch.continueWithAuth", response).catch(() => undefined);
484
+ }
485
+
486
+ function createResourcePolicyHandler(
487
+ policy: BrowserResourcePolicy,
488
+ allowedMethods: ReadonlySet<BrowserResourceMethod>,
489
+ ): (route: Route) => Promise<void> {
490
+ return async (route: Route): Promise<void> => {
491
+ const request = await toResourceRequest(route.request());
492
+ if (!request || !allowedMethods.has(request.method)) {
493
+ await abortResourceRoute(route);
494
+ return;
495
+ }
496
+
497
+ const decision = await decideResourceRequest(policy, request);
498
+ switch (decision.action) {
499
+ case "continue":
500
+ await route.continue();
501
+ return;
502
+ case "fulfill":
503
+ await fulfillResourceRoute(route, decision);
504
+ return;
505
+ case "block":
506
+ await abortResourceRoute(route);
507
+ }
508
+ };
509
+ }
510
+
211
511
  export type BrowserClientOptions = BrowserOptions & {
212
512
  allowedHosts?: string[];
213
513
  cdpUrl?: string;
214
514
  executablePath?: string;
215
515
  extraArgs?: string[];
516
+ serviceWorkers?: "allow" | "block";
517
+ telemetry?: BrowserTelemetrySink;
216
518
  };
217
519
 
218
520
  type SupportedBrowserClient = {
@@ -224,7 +526,7 @@ type SupportedBrowserClient = {
224
526
  };
225
527
 
226
528
  function getDefaultCdpPoolUrl(env = process.env): string | undefined {
227
- return env.APIFUSE__CDP_POOL__URL;
529
+ return readDiagnosticEnv("APIFUSE__CDP_POOL__URL", env);
228
530
  }
229
531
 
230
532
  async function importOptionalModule<T extends object>(moduleName: string): Promise<T> {
@@ -271,12 +573,72 @@ function formatExpression<T>(fn: string | (() => T)): string {
271
573
  return `(${fn.toString()})()`;
272
574
  }
273
575
 
274
- function toLaunchOptions(options: BrowserClientOptions): LaunchOptions {
576
+ function toLaunchOptions(
577
+ options: BrowserClientOptions,
578
+ proxy: LaunchOptions["proxy"],
579
+ ): LaunchOptions {
275
580
  return {
276
- args: options.extraArgs,
581
+ // `extraArgs` is optional, but playwright-extra's stealth evasions mutate
582
+ // `options.args` unguarded (navigator.webdriver does
583
+ // `options.args.findIndex(...)` in beforeLaunch). Forwarding `undefined`
584
+ // therefore crashes every stealth launch that omits extraArgs with
585
+ // "TypeError: undefined is not an object (evaluating 'options.args.findIndex')".
586
+ // Always hand the launcher a concrete array.
587
+ args: options.extraArgs ?? [],
277
588
  executablePath: options.executablePath,
278
589
  headless: options.headless ?? true,
279
- proxy: options.proxy ? { server: options.proxy } : undefined,
590
+ proxy,
591
+ };
592
+ }
593
+
594
+ type PlaywrightProxy = NonNullable<LaunchOptions["proxy"]>;
595
+
596
+ function hasProxyCredentials(
597
+ proxy: LaunchOptions["proxy"] | undefined,
598
+ ): proxy is PlaywrightProxy & { username: string; password: string } {
599
+ return proxy?.username !== undefined && proxy.password !== undefined;
600
+ }
601
+
602
+ function toPlaywrightProxy(proxy: string | undefined): LaunchOptions["proxy"] {
603
+ if (!proxy) return undefined;
604
+
605
+ const schemeEnd = proxy.indexOf("://");
606
+ const authorityStart = schemeEnd >= 0 ? schemeEnd + 3 : 0;
607
+ const remainder = proxy.slice(authorityStart);
608
+ const authorityEnd = remainder.search(/[/?#]/);
609
+ const authority = remainder.slice(0, authorityEnd >= 0 ? authorityEnd : undefined);
610
+ if (!authority.includes("@")) {
611
+ return { server: proxy };
612
+ }
613
+
614
+ let parsed: URL;
615
+ try {
616
+ parsed = new URL(proxy);
617
+ } catch {
618
+ throw new ProviderError("Browser proxy URL is invalid", {
619
+ code: "BROWSER_PROXY_INVALID",
620
+ fix: "Use a valid proxy URL.",
621
+ });
622
+ }
623
+
624
+ let username: string;
625
+ let password: string;
626
+ try {
627
+ username = decodeURIComponent(parsed.username);
628
+ password = decodeURIComponent(parsed.password);
629
+ } catch {
630
+ throw new ProviderError("Browser proxy credentials use invalid percent-encoding", {
631
+ code: "BROWSER_PROXY_INVALID",
632
+ fix: "Percent-encode the proxy username and password as URL userinfo.",
633
+ });
634
+ }
635
+ registerDiagnosticValue(username);
636
+ registerDiagnosticValue(password);
637
+
638
+ return {
639
+ server: `${parsed.protocol}//${parsed.host}`,
640
+ username,
641
+ password,
280
642
  };
281
643
  }
282
644
 
@@ -470,10 +832,24 @@ class PlaywrightBrowserPage implements BrowserPageContract {
470
832
  readonly id = "main";
471
833
  readonly pageId?: string;
472
834
 
473
- constructor(private readonly page: Page) {}
835
+ constructor(
836
+ private readonly page: Page,
837
+ private readonly proxy: LaunchOptions["proxy"] = undefined,
838
+ private readonly telemetry?: BrowserTelemetrySink,
839
+ ) {}
474
840
 
475
- async goto(url: string): Promise<void> {
476
- await this.page.goto(url);
841
+ async goto(
842
+ url: string,
843
+ options?: { readonly timeout?: number; readonly waitUntil?: "load" | "domcontentloaded" },
844
+ ): Promise<void> {
845
+ const response = await this.page.goto(url, options);
846
+ const headers = await response?.headersArray().catch(() => []);
847
+ for (const header of headers ?? []) {
848
+ if (header.name.toLowerCase() === "set-cookie") {
849
+ registerDiagnosticValue(header.value);
850
+ for (const value of cookieValuesFromHeaders(header.value)) registerDiagnosticValue(value);
851
+ }
852
+ }
477
853
  }
478
854
 
479
855
  async evaluate<T>(fn: string | (() => T)): Promise<T> {
@@ -484,6 +860,10 @@ class PlaywrightBrowserPage implements BrowserPageContract {
484
860
  return await this.page.evaluate(fn);
485
861
  }
486
862
 
863
+ async userAgent(): Promise<string> {
864
+ return await this.evaluate<string>("navigator.userAgent");
865
+ }
866
+
487
867
  async waitForSelector(selector: string, options?: { timeout?: number }): Promise<void> {
488
868
  await this.page.waitForSelector(selector, options);
489
869
  }
@@ -529,45 +909,110 @@ class PlaywrightBrowserPage implements BrowserPageContract {
529
909
  await this.page.close();
530
910
  }
531
911
 
912
+ async cookies(): Promise<readonly BrowserCookie[]> {
913
+ const cookies = (await this.page.context().cookies()).map(toBrowserCookie);
914
+ for (const cookie of cookies) registerDiagnosticValue(cookie.value);
915
+ return cookies;
916
+ }
917
+
532
918
  async withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T> {
533
919
  const allowedMethods = new Set(policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS);
920
+ const policyHandler = createResourcePolicyHandler(policy, allowedMethods);
921
+ const context = this.page.context();
534
922
  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");
923
+ try {
924
+ // Context routing sees a popup's first request before the late `page`
925
+ // event. Fail every secondary page closed so no redirect can escape
926
+ // the per-page CDP interception while that session is being attached.
927
+ if (route.request().frame().page() !== this.page) {
928
+ await abortResourceRoute(route);
929
+ return;
930
+ }
931
+ } catch {
932
+ await abortResourceRoute(route);
538
933
  return;
539
934
  }
540
935
 
541
- for (const resourceRoute of policy.routes) {
542
- if (!matchesResourceRoute(resourceRoute.match, request)) {
543
- continue;
544
- }
936
+ await policyHandler(route);
937
+ };
938
+ const sessions = new Set<CDPSession>();
939
+ const pendingAttachments = new Set<Promise<void>>();
940
+ const proxy = hasProxyCredentials(this.proxy) ? this.proxy : undefined;
941
+ let active = true;
942
+ const attachPage = async (page: Page): Promise<void> => {
943
+ const session = await context.newCDPSession(page);
944
+ if (!active) {
945
+ await session.detach();
946
+ return;
947
+ }
545
948
 
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;
949
+ const onRequestPaused = (params: unknown) => {
950
+ if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
951
+ void handleCdpDocumentResponse(session, policy.documentContentSecurityPolicy, params);
952
+ return;
554
953
  }
954
+ void handleCdpResourceRequest(session, policy, allowedMethods, params);
955
+ };
956
+ const authAttempts = new Set<string>();
957
+ const onAuthRequired = (params: unknown) => {
958
+ if (!proxy) return;
959
+ void handleCdpAuthRequired(session, proxy, authAttempts, params, this.telemetry);
960
+ };
961
+ session.on("Fetch.requestPaused", onRequestPaused);
962
+ if (proxy) {
963
+ session.on("Fetch.authRequired", onAuthRequired);
964
+ }
965
+ try {
966
+ await session.send("Fetch.enable", {
967
+ ...(proxy ? { handleAuthRequests: true } : {}),
968
+ patterns: resourcePolicyFetchPatterns(policy),
969
+ });
970
+ sessions.add(session);
971
+ } catch (error) {
972
+ session.off("Fetch.requestPaused", onRequestPaused);
973
+ if (proxy) {
974
+ session.off("Fetch.authRequired", onAuthRequired);
975
+ }
976
+ await session.detach().catch(() => undefined);
977
+ throw error;
555
978
  }
556
-
557
- await route.abort("blockedbyclient");
979
+ };
980
+ const onPage = (page: Page) => {
981
+ const attachment = attachPage(page)
982
+ .catch(async () => {
983
+ await page.close().catch(() => undefined);
984
+ })
985
+ .finally(() => pendingAttachments.delete(attachment));
986
+ pendingAttachments.add(attachment);
558
987
  };
559
988
 
560
- await this.page.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
989
+ await context.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
561
990
  try {
562
- return await run();
991
+ context.on("page", onPage);
992
+ try {
993
+ await attachPage(this.page);
994
+ return await run();
995
+ } finally {
996
+ context.off("page", onPage);
997
+ }
563
998
  } finally {
564
- await this.page.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
999
+ active = false;
1000
+ await context.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
1001
+ await Promise.allSettled(pendingAttachments);
1002
+ await Promise.all(
1003
+ [...sessions].map(async (session) => {
1004
+ await session.send("Fetch.disable").catch(() => undefined);
1005
+ await session.detach().catch(() => undefined);
1006
+ }),
1007
+ );
565
1008
  }
566
1009
  }
567
1010
  }
568
1011
 
569
1012
  class PlaywrightBrowserClient implements SupportedBrowserClient {
570
1013
  private browser: import("playwright").Browser | null = null;
1014
+ private parsedProxy: LaunchOptions["proxy"] | undefined;
1015
+ private proxyParsed = false;
571
1016
  readonly engine = "playwright-stealth" satisfies BrowserEngine;
572
1017
 
573
1018
  constructor(private readonly options: BrowserClientOptions = {}) {}
@@ -578,15 +1023,23 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
578
1023
  }
579
1024
 
580
1025
  const chromium = await loadChromiumLauncher(this.options);
581
- this.browser = await chromium.launch(toLaunchOptions(this.options));
1026
+ if (!this.proxyParsed) {
1027
+ this.parsedProxy = toPlaywrightProxy(this.options.proxy);
1028
+ this.proxyParsed = true;
1029
+ }
1030
+ this.browser = await chromium.launch(toLaunchOptions(this.options, this.parsedProxy));
582
1031
  return this.browser;
583
1032
  }
584
1033
 
585
1034
  async newPage(): Promise<BrowserPageContract> {
1035
+ if (!this.options.cdpUrl)
1036
+ observeBrowserTelemetry(this.options.telemetry, () =>
1037
+ this.options.telemetry?.recordPoolAcquire("not_configured"),
1038
+ );
586
1039
  const browser = await this.ensureBrowser();
587
1040
  const page = await browser.newPage();
588
1041
 
589
- return new PlaywrightBrowserPage(page);
1042
+ return new PlaywrightBrowserPage(page, this.parsedProxy, this.options.telemetry);
590
1043
  }
591
1044
 
592
1045
  async rawPage(): Promise<BrowserPageContract> {
@@ -597,10 +1050,14 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
597
1050
  }
598
1051
 
599
1052
  async withIsolatedContext<T>(handler: (page: BrowserPageContract) => Promise<T>): Promise<T> {
1053
+ if (!this.options.cdpUrl)
1054
+ observeBrowserTelemetry(this.options.telemetry, () =>
1055
+ this.options.telemetry?.recordPoolAcquire("not_configured"),
1056
+ );
600
1057
  const browser = await this.ensureBrowser();
601
- const context = await browser.newContext();
1058
+ const context = await browser.newContext({ serviceWorkers: this.options.serviceWorkers });
602
1059
  const page = await context.newPage();
603
- const browserPage = new PlaywrightBrowserPage(page);
1060
+ const browserPage = new PlaywrightBrowserPage(page, this.parsedProxy, this.options.telemetry);
604
1061
 
605
1062
  try {
606
1063
  return await handler(browserPage);
@@ -641,12 +1098,12 @@ function normalizeWebSocketEndpoint(endpoint: string): string {
641
1098
  throw new Error(`Unsupported WebSocket endpoint protocol: ${url.protocol}`);
642
1099
  }
643
1100
 
644
- class JsonRpcWebSocketClient {
1101
+ abstract class WebSocketCommandClient {
645
1102
  private nextId = 1;
646
1103
  private readonly endpoint: string;
647
1104
  private readonly listeners = new Map<string, Set<(params: unknown) => void>>();
648
1105
  private readonly pending = new Map<
649
- JsonRpcId,
1106
+ WebSocketCommandId,
650
1107
  {
651
1108
  reject: (reason?: unknown) => void;
652
1109
  resolve: (value: Record<string, unknown>) => void;
@@ -654,6 +1111,7 @@ class JsonRpcWebSocketClient {
654
1111
  >();
655
1112
  private socket?: WebSocket;
656
1113
  private socketPromise?: Promise<WebSocket>;
1114
+ private closed = false;
657
1115
 
658
1116
  constructor(endpoint: string) {
659
1117
  this.endpoint = normalizeWebSocketEndpoint(endpoint);
@@ -677,23 +1135,18 @@ class JsonRpcWebSocketClient {
677
1135
  params: Record<string, unknown> = {},
678
1136
  ): Promise<Record<string, unknown>> {
679
1137
  const socket = await this.getSocket();
1138
+ if (this.closed) throw new Error(`WebSocket closed: ${this.endpoint}`);
680
1139
  const id = this.nextId++;
681
1140
 
682
1141
  return await new Promise<Record<string, unknown>>((resolve, reject) => {
683
1142
  this.pending.set(id, { resolve, reject });
684
1143
 
685
- socket.send(
686
- JSON.stringify({
687
- id,
688
- jsonrpc: "2.0",
689
- method,
690
- params,
691
- }),
692
- );
1144
+ socket.send(JSON.stringify(this.createCommandFrame(id, method, params)));
693
1145
  });
694
1146
  }
695
1147
 
696
1148
  async close(): Promise<void> {
1149
+ this.closed = true;
697
1150
  for (const pending of this.pending.values()) {
698
1151
  pending.reject(new Error(`WebSocket closed: ${this.endpoint}`));
699
1152
  }
@@ -705,7 +1158,14 @@ class JsonRpcWebSocketClient {
705
1158
  this.socketPromise = undefined;
706
1159
  }
707
1160
 
1161
+ protected abstract createCommandFrame(
1162
+ id: WebSocketCommandId,
1163
+ method: string,
1164
+ params: Record<string, unknown>,
1165
+ ): CdpCommandFrame | JsonRpcCommandFrame;
1166
+
708
1167
  private async getSocket(): Promise<WebSocket> {
1168
+ if (this.closed) throw new Error(`WebSocket closed: ${this.endpoint}`);
709
1169
  if (this.socket?.readyState === WebSocket.OPEN) {
710
1170
  return this.socket;
711
1171
  }
@@ -718,6 +1178,11 @@ class JsonRpcWebSocketClient {
718
1178
  const socket = new WebSocket(this.endpoint);
719
1179
 
720
1180
  socket.addEventListener("open", () => {
1181
+ if (this.closed) {
1182
+ socket.close();
1183
+ reject(new Error(`WebSocket closed: ${this.endpoint}`));
1184
+ return;
1185
+ }
721
1186
  this.socket = socket;
722
1187
  resolve(socket);
723
1188
  });
@@ -727,7 +1192,7 @@ class JsonRpcWebSocketClient {
727
1192
  typeof event.data === "string"
728
1193
  ? event.data
729
1194
  : Buffer.from(event.data as ArrayBufferLike).toString("utf8");
730
- const payload = JSON.parse(rawData) as JsonRpcMessage;
1195
+ const payload = JSON.parse(rawData) as WebSocketCommandMessage;
731
1196
 
732
1197
  if (typeof payload.id === "number") {
733
1198
  const pending = this.pending.get(payload.id);
@@ -738,7 +1203,11 @@ class JsonRpcWebSocketClient {
738
1203
  this.pending.delete(payload.id);
739
1204
 
740
1205
  if (payload.error) {
741
- pending.reject(new Error(payload.error.message ?? "JSON-RPC command failed"));
1206
+ const error = new Error(payload.error.message ?? "JSON-RPC command failed");
1207
+ if (typeof payload.error.code === "number") {
1208
+ Object.assign(error, { code: payload.error.code });
1209
+ }
1210
+ pending.reject(error);
742
1211
  return;
743
1212
  }
744
1213
 
@@ -774,6 +1243,38 @@ class JsonRpcWebSocketClient {
774
1243
  }
775
1244
  }
776
1245
 
1246
+ class CdpWebSocketClient extends WebSocketCommandClient {
1247
+ constructor(
1248
+ endpoint: string,
1249
+ private readonly sessionId?: string,
1250
+ ) {
1251
+ super(endpoint);
1252
+ }
1253
+
1254
+ protected createCommandFrame(
1255
+ id: WebSocketCommandId,
1256
+ method: string,
1257
+ params: Record<string, unknown>,
1258
+ ): CdpCommandFrame {
1259
+ return {
1260
+ id,
1261
+ method,
1262
+ params,
1263
+ ...(this.sessionId === undefined ? {} : { sessionId: this.sessionId }),
1264
+ };
1265
+ }
1266
+ }
1267
+
1268
+ class JsonRpcWebSocketClient extends WebSocketCommandClient {
1269
+ protected createCommandFrame(
1270
+ id: WebSocketCommandId,
1271
+ method: string,
1272
+ params: Record<string, unknown>,
1273
+ ): JsonRpcCommandFrame {
1274
+ return { id, jsonrpc: "2.0", method, params };
1275
+ }
1276
+ }
1277
+
777
1278
  function flattenCdpFrameTree(
778
1279
  node: CdpFrameTreeNode | undefined,
779
1280
  out: CdpFrameTreeNode["frame"][] = [],
@@ -794,6 +1295,64 @@ function isRecord(value: unknown): value is Record<string, unknown> {
794
1295
  return value !== null && typeof value === "object" && !Array.isArray(value);
795
1296
  }
796
1297
 
1298
+ function isBrowserCookieSameSite(value: unknown): value is BrowserCookie["sameSite"] {
1299
+ return value === "Strict" || value === "Lax" || value === "None";
1300
+ }
1301
+
1302
+ function toBrowserCookie(cookie: {
1303
+ readonly name: string;
1304
+ readonly value: string;
1305
+ readonly domain: string;
1306
+ readonly path: string;
1307
+ readonly expires?: number;
1308
+ readonly httpOnly: boolean;
1309
+ readonly secure: boolean;
1310
+ readonly sameSite?: unknown;
1311
+ }): BrowserCookie {
1312
+ return {
1313
+ name: cookie.name,
1314
+ value: cookie.value,
1315
+ domain: cookie.domain,
1316
+ path: cookie.path,
1317
+ ...(cookie.expires !== undefined && cookie.expires > 0 ? { expires: cookie.expires } : {}),
1318
+ httpOnly: cookie.httpOnly,
1319
+ secure: cookie.secure,
1320
+ ...(isBrowserCookieSameSite(cookie.sameSite) ? { sameSite: cookie.sameSite } : {}),
1321
+ };
1322
+ }
1323
+
1324
+ function parseCdpCookies(value: unknown): readonly BrowserCookie[] {
1325
+ if (!Array.isArray(value)) {
1326
+ throw new Error("CDP Network.getCookies returned an invalid cookie list");
1327
+ }
1328
+
1329
+ return value.map((cookie) => {
1330
+ if (
1331
+ !isRecord(cookie) ||
1332
+ typeof cookie.name !== "string" ||
1333
+ typeof cookie.value !== "string" ||
1334
+ typeof cookie.domain !== "string" ||
1335
+ typeof cookie.path !== "string" ||
1336
+ typeof cookie.expires !== "number" ||
1337
+ typeof cookie.httpOnly !== "boolean" ||
1338
+ typeof cookie.secure !== "boolean"
1339
+ ) {
1340
+ throw new Error("CDP Network.getCookies returned an invalid cookie");
1341
+ }
1342
+
1343
+ return toBrowserCookie({
1344
+ name: cookie.name,
1345
+ value: cookie.value,
1346
+ domain: cookie.domain,
1347
+ path: cookie.path,
1348
+ expires: cookie.expires,
1349
+ httpOnly: cookie.httpOnly,
1350
+ secure: cookie.secure,
1351
+ sameSite: cookie.sameSite,
1352
+ });
1353
+ });
1354
+ }
1355
+
797
1356
  function parsePoolAcquireResponse(value: unknown): PoolAcquireResponse {
798
1357
  if (
799
1358
  !isRecord(value) ||
@@ -818,6 +1377,36 @@ function parsePoolAcquireResponse(value: unknown): PoolAcquireResponse {
818
1377
  };
819
1378
  }
820
1379
 
1380
+ function poolAcquireOutcome(error: unknown): BrowserTelemetryPoolAcquireOutcome {
1381
+ const code =
1382
+ error instanceof Error && typeof (error as Error & { code?: unknown }).code === "number"
1383
+ ? (error as Error & { code: number }).code
1384
+ : undefined;
1385
+ switch (code) {
1386
+ case -32001:
1387
+ return "queue_full";
1388
+ case -32002:
1389
+ return "timed_out";
1390
+ case -32003:
1391
+ return "shutting_down";
1392
+ case -32004:
1393
+ return "unknown_lease";
1394
+ case -32005:
1395
+ return "unknown_method";
1396
+ case -32006:
1397
+ return "missing_allowed_hosts";
1398
+ }
1399
+ if (error instanceof Error && /Unable to connect|WebSocket closed/.test(error.message))
1400
+ return "transport_failure";
1401
+ return "other";
1402
+ }
1403
+
1404
+ function poolAcquireCode(error: unknown): number | undefined {
1405
+ return error instanceof Error && typeof (error as Error & { code?: unknown }).code === "number"
1406
+ ? (error as Error & { code: number }).code
1407
+ : undefined;
1408
+ }
1409
+
821
1410
  function parseCdpFrameTreeNode(value: unknown): CdpFrameTreeNode | undefined {
822
1411
  if (!isRecord(value) || !isRecord(value.frame)) {
823
1412
  return undefined;
@@ -861,6 +1450,18 @@ function getCdpExecutionContext(params: unknown): {
861
1450
  };
862
1451
  }
863
1452
 
1453
+ function getCdpDestroyedExecutionContextId(params: unknown): number | undefined {
1454
+ if (!isRecord(params)) {
1455
+ return undefined;
1456
+ }
1457
+
1458
+ return typeof params.executionContextId === "number" ? params.executionContextId : undefined;
1459
+ }
1460
+
1461
+ function isMissingExecutionContextError(error: unknown): boolean {
1462
+ return error instanceof Error && /\b(?:cannot|failed to) find context\b/i.test(error.message);
1463
+ }
1464
+
864
1465
  class CdpBrowserLocator implements BrowserLocator {
865
1466
  constructor(
866
1467
  private readonly frame: {
@@ -967,14 +1568,14 @@ class CdpBrowserFrame implements BrowserFrame {
967
1568
  }
968
1569
 
969
1570
  class CdpPoolBrowserPage implements BrowserPageContract {
970
- private closed = false;
1571
+ private closePromise?: Promise<void>;
971
1572
  private initialized = false;
972
1573
  private readonly frameExecutionContexts = new Map<string, number>();
973
1574
 
974
1575
  constructor(
975
1576
  readonly pageId: string,
976
1577
  private readonly browserContextId: string | undefined,
977
- private readonly pageClient: JsonRpcWebSocketClient,
1578
+ private readonly pageClient: CdpWebSocketClient,
978
1579
  private readonly release: (request: PoolReleaseRequest) => Promise<void>,
979
1580
  ) {}
980
1581
 
@@ -982,17 +1583,29 @@ class CdpPoolBrowserPage implements BrowserPageContract {
982
1583
  return this.pageId;
983
1584
  }
984
1585
 
985
- async goto(url: string): Promise<void> {
1586
+ async goto(
1587
+ url: string,
1588
+ options?: { readonly timeout?: number; readonly waitUntil?: "load" | "domcontentloaded" },
1589
+ ): Promise<void> {
986
1590
  await this.initialize();
987
1591
  const startedAt = Date.now();
988
- let loadEventSeen = false;
989
- const unsubscribe = this.pageClient.on("Page.loadEventFired", () => {
990
- loadEventSeen = true;
1592
+ const timeout = options?.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
1593
+ const waitUntil = options?.waitUntil ?? "load";
1594
+ let expectedEventSeen = false;
1595
+ const eventName =
1596
+ waitUntil === "domcontentloaded" ? "Page.domContentEventFired" : "Page.loadEventFired";
1597
+ const unsubscribe = this.pageClient.on(eventName, () => {
1598
+ expectedEventSeen = true;
991
1599
  });
992
1600
 
993
1601
  try {
994
- await this.pageClient.send("Page.navigate", { url });
995
- await this.waitForDocumentReady(startedAt + DEFAULT_WAIT_TIMEOUT_MS, () => loadEventSeen);
1602
+ const navigation = (await this.pageClient.send("Page.navigate", { url })) as {
1603
+ readonly errorText?: unknown;
1604
+ };
1605
+ if (typeof navigation.errorText === "string" && navigation.errorText.length > 0) {
1606
+ throw new Error(`Page.navigate failed: ${navigation.errorText} at ${url}`);
1607
+ }
1608
+ await this.waitForDocumentReady(startedAt + timeout, () => expectedEventSeen, waitUntil);
996
1609
  } finally {
997
1610
  unsubscribe();
998
1611
  }
@@ -1003,10 +1616,27 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1003
1616
  return await this.evaluateWithContext<T>(fn);
1004
1617
  }
1005
1618
 
1619
+ async userAgent(): Promise<string> {
1620
+ return await this.evaluate<string>("navigator.userAgent");
1621
+ }
1622
+
1006
1623
  async evaluateInFrame<T>(frameId: string, fn: string | (() => T)): Promise<T> {
1007
1624
  await this.initialize();
1008
1625
  const contextId = await this.getFrameExecutionContextId(frameId);
1009
- return await this.evaluateWithContext<T>(fn, contextId);
1626
+ try {
1627
+ return await this.evaluateWithContext<T>(fn, contextId);
1628
+ } catch (error) {
1629
+ if (!isMissingExecutionContextError(error)) {
1630
+ throw error;
1631
+ }
1632
+
1633
+ if (this.frameExecutionContexts.get(frameId) === contextId) {
1634
+ this.frameExecutionContexts.delete(frameId);
1635
+ }
1636
+
1637
+ const refreshedContextId = await this.getFrameExecutionContextId(frameId);
1638
+ return await this.evaluateWithContext<T>(fn, refreshedContextId);
1639
+ }
1010
1640
  }
1011
1641
 
1012
1642
  async waitForSelectorInFrame(
@@ -1149,21 +1779,34 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1149
1779
  return Buffer.from(String(result.data ?? ""), "base64");
1150
1780
  }
1151
1781
 
1782
+ async cookies(): Promise<readonly BrowserCookie[]> {
1783
+ await this.initialize();
1784
+ const result = await this.pageClient.send("Network.getCookies");
1785
+ const cookies = parseCdpCookies(result.cookies);
1786
+ for (const cookie of cookies) registerDiagnosticValue(cookie.value);
1787
+ return cookies;
1788
+ }
1789
+
1152
1790
  async close(): Promise<void> {
1153
- if (this.closed) {
1791
+ // Only the first caller observes the release outcome; repeat calls stay
1792
+ // idempotent (as they were before the promise was memoized) instead of
1793
+ // replaying a cached rejection, and never re-issue a release against a
1794
+ // target the pool has already disposed.
1795
+ if (this.closePromise) {
1796
+ await this.closePromise.catch(() => undefined);
1154
1797
  return;
1155
1798
  }
1156
-
1157
- this.closed = true;
1158
-
1159
- try {
1160
- await this.release({
1161
- ...(this.browserContextId ? { browserContextId: this.browserContextId } : {}),
1162
- pageId: this.pageId,
1163
- });
1164
- } finally {
1165
- await this.pageClient.close();
1166
- }
1799
+ this.closePromise = (async () => {
1800
+ try {
1801
+ await this.release({
1802
+ ...(this.browserContextId ? { browserContextId: this.browserContextId } : {}),
1803
+ pageId: this.pageId,
1804
+ });
1805
+ } finally {
1806
+ await this.pageClient.close();
1807
+ }
1808
+ })();
1809
+ await this.closePromise;
1167
1810
  }
1168
1811
 
1169
1812
  async withResourcePolicy<T>(policy: BrowserResourcePolicy, run: () => Promise<T>): Promise<T> {
@@ -1176,7 +1819,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1176
1819
 
1177
1820
  try {
1178
1821
  await this.pageClient.send("Fetch.enable", {
1179
- patterns: [{ requestStage: "Request", urlPattern: "*" }],
1822
+ patterns: resourcePolicyFetchPatterns(policy),
1180
1823
  });
1181
1824
  } catch (error) {
1182
1825
  unsubscribe();
@@ -1193,8 +1836,19 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1193
1836
  try {
1194
1837
  return await run();
1195
1838
  } finally {
1196
- unsubscribe();
1197
- await this.pageClient.send("Fetch.disable");
1839
+ try {
1840
+ if (this.closePromise) {
1841
+ // Keep interception active until release settles. The release failure
1842
+ // stays observable through close() itself; swallowing it here keeps
1843
+ // this finally from replacing run()'s own outcome, and still avoids a
1844
+ // reconnect or a disable against a disposed target.
1845
+ await this.closePromise.catch(() => undefined);
1846
+ } else {
1847
+ await this.pageClient.send("Fetch.disable");
1848
+ }
1849
+ } finally {
1850
+ unsubscribe();
1851
+ }
1198
1852
  }
1199
1853
  }
1200
1854
 
@@ -1209,6 +1863,28 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1209
1863
  }
1210
1864
 
1211
1865
  try {
1866
+ if (isCdpResponseStage(params) && policy.documentContentSecurityPolicy) {
1867
+ const response = toCdpPausedDocumentResponse(params);
1868
+ if (!response) throw new Error("CDP document response metadata is unavailable");
1869
+ if (!response.hasBody) {
1870
+ await this.pageClient.send("Fetch.continueResponse", {
1871
+ requestId,
1872
+ responseCode: response.responseCode,
1873
+ responseHeaders: [...response.responseHeaders],
1874
+ });
1875
+ return;
1876
+ }
1877
+
1878
+ const body = await this.pageClient.send("Fetch.getResponseBody", {
1879
+ requestId: response.requestId,
1880
+ });
1881
+ await this.pageClient.send(
1882
+ "Fetch.fulfillRequest",
1883
+ toCdpDocumentFulfillParams(response, body, policy.documentContentSecurityPolicy),
1884
+ );
1885
+ return;
1886
+ }
1887
+
1212
1888
  const parsed = toCdpResourceRequest(params);
1213
1889
  if (!parsed || !allowedMethods.has(parsed.request.method)) {
1214
1890
  await this.failCdpResourceRequest(requestId);
@@ -1222,6 +1898,11 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1222
1898
 
1223
1899
  const decision = await resourceRoute.handle(parsed.request);
1224
1900
  switch (decision.action) {
1901
+ case "continue":
1902
+ await this.pageClient.send("Fetch.continueRequest", {
1903
+ requestId: parsed.requestId,
1904
+ });
1905
+ return;
1225
1906
  case "fulfill":
1226
1907
  await this.pageClient.send(
1227
1908
  "Fetch.fulfillRequest",
@@ -1264,8 +1945,46 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1264
1945
  this.frameExecutionContexts.set(context.frameId, context.id);
1265
1946
  }
1266
1947
  });
1948
+ this.pageClient.on("Runtime.executionContextDestroyed", (params) => {
1949
+ const destroyedContextId = getCdpDestroyedExecutionContextId(params);
1950
+ if (destroyedContextId === undefined) {
1951
+ return;
1952
+ }
1953
+
1954
+ for (const [frameId, contextId] of this.frameExecutionContexts) {
1955
+ if (contextId === destroyedContextId) {
1956
+ this.frameExecutionContexts.delete(frameId);
1957
+ }
1958
+ }
1959
+ });
1960
+ this.pageClient.on("Runtime.executionContextsCleared", () => {
1961
+ this.frameExecutionContexts.clear();
1962
+ });
1963
+ const registerResponseCookies = (params: unknown) => {
1964
+ if (!isRecord(params)) return;
1965
+ const headers = Array.isArray(params.responseHeaders)
1966
+ ? params.responseHeaders
1967
+ : isRecord(params.headers)
1968
+ ? Object.entries(params.headers).map(([name, value]) => ({ name, value }))
1969
+ : [];
1970
+ for (const header of headers) {
1971
+ if (
1972
+ isRecord(header) &&
1973
+ typeof header.name === "string" &&
1974
+ header.name.toLowerCase() === "set-cookie" &&
1975
+ typeof header.value === "string"
1976
+ ) {
1977
+ registerDiagnosticValue(header.value);
1978
+ for (const value of cookieValuesFromHeaders(header.value)) registerDiagnosticValue(value);
1979
+ }
1980
+ }
1981
+ };
1982
+ const scopedCookieRegistration = AsyncResource.bind(registerResponseCookies);
1983
+ this.pageClient.on("Network.responseReceived", scopedCookieRegistration);
1984
+ this.pageClient.on("Network.responseReceivedExtraInfo", scopedCookieRegistration);
1267
1985
  await this.pageClient.send("Page.enable");
1268
1986
  await this.pageClient.send("Runtime.enable");
1987
+ await this.pageClient.send("Network.enable").catch(() => undefined);
1269
1988
  this.initialized = true;
1270
1989
  }
1271
1990
 
@@ -1291,12 +2010,16 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1291
2010
 
1292
2011
  private async waitForDocumentReady(
1293
2012
  deadline: number,
1294
- isLoadEventSeen: () => boolean,
2013
+ isExpectedEventSeen: () => boolean,
2014
+ waitUntil: "load" | "domcontentloaded",
1295
2015
  ): Promise<void> {
1296
2016
  while (Date.now() < deadline) {
1297
2017
  const readyState = await this.evaluate<string>("document.readyState");
1298
- if (readyState === "complete" || readyState === "interactive") {
1299
- if (isLoadEventSeen() || readyState === "complete") {
2018
+ const documentReady =
2019
+ readyState === "complete" ||
2020
+ (waitUntil === "domcontentloaded" && readyState === "interactive");
2021
+ if (documentReady) {
2022
+ if (isExpectedEventSeen() || readyState === "complete") {
1300
2023
  return;
1301
2024
  }
1302
2025
  }
@@ -1311,6 +2034,7 @@ class CdpPoolBrowserPage implements BrowserPageContract {
1311
2034
  class CdpPoolBrowserClient implements SupportedBrowserClient {
1312
2035
  private readonly allowedHosts: string[];
1313
2036
  private readonly poolClient: JsonRpcWebSocketClient;
2037
+ private readonly telemetry?: BrowserTelemetrySink;
1314
2038
  readonly engine = "playwright-stealth" satisfies BrowserEngine;
1315
2039
 
1316
2040
  constructor(options: BrowserClientOptions) {
@@ -1321,6 +2045,7 @@ class CdpPoolBrowserClient implements SupportedBrowserClient {
1321
2045
  }
1322
2046
 
1323
2047
  this.allowedHosts = [...new Set(options.allowedHosts ?? [])];
2048
+ this.telemetry = options.telemetry;
1324
2049
  this.poolClient = new JsonRpcWebSocketClient(options.cdpUrl);
1325
2050
  }
1326
2051
 
@@ -1329,13 +2054,30 @@ class CdpPoolBrowserClient implements SupportedBrowserClient {
1329
2054
  }
1330
2055
 
1331
2056
  private async acquirePage(options?: { isolatedContext?: boolean }): Promise<BrowserPageContract> {
1332
- const acquireResult = parsePoolAcquireResponse(
1333
- await this.poolClient.send("acquire", {
1334
- ...(this.allowedHosts.length > 0 ? { allowedHosts: this.allowedHosts } : {}),
1335
- ...(options?.isolatedContext ? { isolationMode: "browserContext" } : {}),
1336
- }),
1337
- );
1338
- const pageClient = new JsonRpcWebSocketClient(acquireResult.wsEndpoint);
2057
+ let acquireResult: PoolAcquireResponse;
2058
+ try {
2059
+ acquireResult = parsePoolAcquireResponse(
2060
+ await this.poolClient.send("acquire", {
2061
+ ...(this.allowedHosts.length > 0 ? { allowedHosts: this.allowedHosts } : {}),
2062
+ ...(options?.isolatedContext ? { isolationMode: "browserContext" } : {}),
2063
+ }),
2064
+ );
2065
+ observeBrowserTelemetry(this.telemetry, () => this.telemetry?.recordPoolAcquire("ok"));
2066
+ } catch (error) {
2067
+ observeBrowserTelemetry(this.telemetry, () =>
2068
+ this.telemetry?.recordPoolAcquire(poolAcquireOutcome(error)),
2069
+ );
2070
+ observeBrowserTelemetry(this.telemetry, () =>
2071
+ this.telemetry?.recordError("BROWSER_CDP_POOL_ERROR"),
2072
+ );
2073
+ const code = poolAcquireCode(error);
2074
+ if (code !== undefined && ![-32001, -32002, -32003, -32004, -32005, -32006].includes(code))
2075
+ observeBrowserTelemetry(this.telemetry, () =>
2076
+ this.telemetry?.recordPoolAcquireUnknownCode?.(code),
2077
+ );
2078
+ throw error;
2079
+ }
2080
+ const pageClient = new CdpWebSocketClient(acquireResult.wsEndpoint);
1339
2081
  const page = new CdpPoolBrowserPage(
1340
2082
  acquireResult.pageId,
1341
2083
  acquireResult.browserContextId,
@@ -1426,6 +2168,7 @@ export class BrowserClient implements BrowserClientContract {
1426
2168
  private activePage?: BrowserPageContract;
1427
2169
  private readonly activePages = new Set<BrowserPageContract>();
1428
2170
  private readonly _engine: BrowserEngine;
2171
+ private readonly telemetry?: BrowserTelemetrySink;
1429
2172
 
1430
2173
  constructor(options: BrowserClientOptions = {}) {
1431
2174
  const resolvedOptions = {
@@ -1434,6 +2177,7 @@ export class BrowserClient implements BrowserClientContract {
1434
2177
  };
1435
2178
  const engine = resolvedOptions.engine ?? "playwright-stealth";
1436
2179
  this._engine = engine;
2180
+ this.telemetry = resolvedOptions.telemetry;
1437
2181
  this.cdpUrl = resolvedOptions.cdpUrl;
1438
2182
 
1439
2183
  if (resolvedOptions.requireCdpPool && !resolvedOptions.cdpUrl) {
@@ -1462,23 +2206,46 @@ export class BrowserClient implements BrowserClientContract {
1462
2206
  }
1463
2207
 
1464
2208
  async newPage(): Promise<BrowserPageContract> {
1465
- const page = await this.client.newPage();
1466
- return this.activatePage(page);
2209
+ observeBrowserTelemetry(this.telemetry, () => this.telemetry?.recordEngine(this._engine));
2210
+ try {
2211
+ const page = await this.client.newPage();
2212
+ return this.activatePage(page);
2213
+ } catch (error) {
2214
+ observeBrowserTelemetry(this.telemetry, () =>
2215
+ this.telemetry?.recordError(browserTelemetryErrorCode(error)),
2216
+ );
2217
+ throw error;
2218
+ }
1467
2219
  }
1468
2220
 
1469
2221
  async rawPage(): Promise<BrowserPageContract> {
2222
+ observeBrowserTelemetry(this.telemetry, () => this.telemetry?.recordEngine(this._engine));
1470
2223
  if (!this.cdpUrl) {
2224
+ observeBrowserTelemetry(this.telemetry, () =>
2225
+ this.telemetry?.recordPoolAcquire("not_configured"),
2226
+ );
2227
+ observeBrowserTelemetry(this.telemetry, () =>
2228
+ this.telemetry?.recordError("BROWSER_RUNTIME_UNSUPPORTED"),
2229
+ );
1471
2230
  throw new ProviderError("ctx.browser.rawPage() requires a CDP pool", {
1472
2231
  code: "BROWSER_RUNTIME_UNSUPPORTED",
1473
2232
  fix: "Set APIFUSE__CDP_POOL__URL. The SDK escape hatch is CDP pool-backed only and never launches local Chromium.",
1474
2233
  });
1475
2234
  }
1476
2235
 
1477
- const page = await this.client.rawPage();
1478
- return this.activatePage(page);
2236
+ try {
2237
+ const page = await this.client.rawPage();
2238
+ return this.activatePage(page);
2239
+ } catch (error) {
2240
+ observeBrowserTelemetry(this.telemetry, () =>
2241
+ this.telemetry?.recordError(browserTelemetryErrorCode(error)),
2242
+ );
2243
+ throw error;
2244
+ }
1479
2245
  }
1480
2246
 
1481
2247
  async withIsolatedContext<T>(handler: (page: BrowserPageContract) => Promise<T>): Promise<T> {
2248
+ observeBrowserTelemetry(this.telemetry, () => this.telemetry?.recordEngine(this._engine));
1482
2249
  const previousActivePage = this.activePage;
1483
2250
  let trackedPage: BrowserPageContract | undefined;
1484
2251
 
@@ -1496,22 +2263,34 @@ export class BrowserClient implements BrowserClientContract {
1496
2263
  }
1497
2264
 
1498
2265
  private activatePage(page: BrowserPageContract): BrowserPageContract {
1499
- let closed = false;
2266
+ let closePromise: Promise<void> | undefined;
1500
2267
  const originalClose = page.close.bind(page);
1501
2268
  const trackedPage = new Proxy(page, {
1502
2269
  get: (target, property, receiver) => {
1503
2270
  if (property === "close") {
1504
2271
  return async () => {
1505
- if (closed) return;
1506
- closed = true;
1507
- try {
1508
- await originalClose();
1509
- } finally {
1510
- this.activePages.delete(trackedPage);
1511
- if (this.activePage === trackedPage) {
1512
- this.activePage = undefined;
1513
- }
2272
+ // Repeat calls resolve on the first outcome instead of replaying a
2273
+ // cached rejection, so a failed release cannot make close() — or
2274
+ // BrowserClient.close(), which closes every tracked page — throw
2275
+ // forever.
2276
+ if (closePromise) {
2277
+ await closePromise.catch(() => undefined);
2278
+ return;
1514
2279
  }
2280
+ closePromise = (async () => {
2281
+ // Bookkeeping runs even when the release fails: the page is gone
2282
+ // either way, so leaving it in activePages would leak it and let
2283
+ // solveChallenge reuse a disposed target.
2284
+ try {
2285
+ await originalClose();
2286
+ } finally {
2287
+ this.activePages.delete(trackedPage);
2288
+ if (this.activePage === trackedPage) {
2289
+ this.activePage = undefined;
2290
+ }
2291
+ }
2292
+ })();
2293
+ await closePromise;
1515
2294
  };
1516
2295
  }
1517
2296
  const value = Reflect.get(target, property, receiver);
@@ -1524,7 +2303,11 @@ export class BrowserClient implements BrowserClientContract {
1524
2303
  }
1525
2304
 
1526
2305
  async solveChallenge(request: BrowserChallengeRequest): Promise<BrowserChallengeResult> {
2306
+ observeBrowserTelemetry(this.telemetry, () => this.telemetry?.recordEngine(this._engine));
1527
2307
  if (request.type !== "recaptcha") {
2308
+ observeBrowserTelemetry(this.telemetry, () =>
2309
+ this.telemetry?.recordError("BROWSER_RUNTIME_UNSUPPORTED"),
2310
+ );
1528
2311
  throw new ProviderError(`Unsupported browser challenge: ${request.type}`, {
1529
2312
  code: "BROWSER_RUNTIME_UNSUPPORTED",
1530
2313
  });