@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,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- // @ts-nocheck
2
+ import { readDiagnosticEnv } from "../src/runtime/diagnostic-env.js";
3
3
 
4
4
  import { existsSync, readFileSync, statSync } from "node:fs";
5
5
  import { mkdir, writeFile } from "node:fs/promises";
@@ -8,20 +8,72 @@ import { pathToFileURL } from "node:url";
8
8
 
9
9
  import {
10
10
  createBypassProviderCache,
11
+ createCredentialContext,
11
12
  createHttpClient,
12
- createProviderChoiceContext,
13
- createStealthClient,
13
+ createInProcessProviderEngine,
14
+ createOcrClientFromEnv,
15
+ createHandleContext,
16
+ createProviderEnvironment,
14
17
  createSttClientFromEnv,
15
18
  executeOperation,
16
19
  type HttpClient,
20
+ type HttpResponse,
21
+ type HttpStreamResponse,
17
22
  type ProviderContext,
18
23
  type ProviderDefinition,
24
+ type ProviderEngineBindingCandidates,
19
25
  ProviderError,
26
+ type RequestOptions,
20
27
  type StealthClient,
21
28
  TransportError,
22
29
  ValidationError,
23
30
  } from "../src/index.js";
31
+ import {
32
+ type LocalConnection,
33
+ LOCAL_CONNECTION_ID,
34
+ LOCAL_CONNECTION_SECRETS_ENV,
35
+ providerRequiresConnection,
36
+ resolveLocalConnection,
37
+ } from "../src/cli/local-connection.js";
38
+ import { createCliResolverRuntime } from "../src/cli/resolver-runtime.js";
39
+ import { isAbsoluteUrl } from "../src/runtime/http.js";
40
+ import { getProviderBaseUrl, getProviderStealthBaseUrl } from "../src/runtime/provider.js";
41
+ import { assertProcessEngineModeSupported } from "../src/runtime/engine-mode.js";
42
+ import type { JsonValue } from "../src/contract-json.js";
43
+ import {
44
+ isSensitiveFixtureKey,
45
+ requestPathForFixture,
46
+ sanitizeDiagnosticText,
47
+ sanitizeFixtureString,
48
+ } from "../src/fixture-sanitization.js";
49
+ import {
50
+ isSensitiveKey,
51
+ normalizeSensitiveParams,
52
+ parseHttpRequestInvocation,
53
+ REDACTED_QUERY_VALUE,
54
+ redactSensitiveError,
55
+ redactSensitiveText,
56
+ redactUrlQueryParams,
57
+ replaceRequestOptionsInHttpInvocation,
58
+ requestOptionsFromHttpInvocation,
59
+ serializeRequestUrl,
60
+ } from "../src/runtime/request-options.js";
61
+ import type { ResolverTelemetryCollector } from "../src/runtime/resolver-telemetry.js";
24
62
  import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
63
+ import { createStealthClient } from "../src/runtime/stealth.js";
64
+ import { parseSchema } from "../src/schema.js";
65
+ import {
66
+ captureStreamEvidence,
67
+ createStreamCaptureEnvelope,
68
+ findStreamCaptureGroup,
69
+ findStreamEvidenceRecords,
70
+ hasStreamEvidenceMarker,
71
+ parseStreamEvidenceRecord,
72
+ STREAM_PREVIEW_BYTES,
73
+ type StreamCaptureGroupItem,
74
+ type StreamEvidenceCapture,
75
+ type StreamEvidenceRequest,
76
+ } from "../src/stream-evidence.js";
25
77
 
26
78
  type CliArgs = {
27
79
  append: boolean;
@@ -29,6 +81,10 @@ type CliArgs = {
29
81
  operation?: string;
30
82
  params: string;
31
83
  sanitize: boolean;
84
+ /** Repeated `--credential key=value`, in order. */
85
+ credentials: string[];
86
+ /** `--no-credential`: run without a connection even when auth declares one. */
87
+ noCredential: boolean;
32
88
  };
33
89
 
34
90
  type ProviderRuntime = ProviderDefinition;
@@ -39,60 +95,163 @@ const HELP_TEXT = `Usage: apifuse record [path] --operation <operation> --params
39
95
 
40
96
  Calls a real upstream-backed operation through ctx.http or ctx.stealth and writes __fixtures__/raw.json.
41
97
 
98
+ Absolute request URLs need no declaration. A relative URL resolves the way serve resolves it: against
99
+ the operation's upstream.baseUrl, else the provider's first declared one (ctx.stealth then falls back
100
+ to https://<allowedHosts[0]>). A relative ctx.http URL with no base anywhere is refused and names
101
+ the upstream.baseUrl to declare.
102
+
103
+ Streaming responses are recorded as evidence (status, selected headers, full-body SHA-256 and byte
104
+ count, plus a ${STREAM_PREVIEW_BYTES}-byte base64 preview). Test replay is evidence-only: ctx.http.stream exposes the
105
+ preview as its body and the original body_sha256/body_bytes as response metadata.
106
+ When an operation opens multiple streams, all evidence records are saved in stream call order.
107
+ Mixed JSON/stream operations save a tagged call-ordered envelope so snapshot replay can route each response.
108
+ ctx.http.sse() recording is unsupported and fails explicitly.
109
+
42
110
  Options:
43
111
  --operation, -o <name> operation to call
44
112
  --params, -p <json> JSON input passed to the operation (default: {})
45
- --append append to an existing array fixture
113
+ --append preserve the existing fixture and append this capture
46
114
  --sanitize redact common token/header fields (default)
47
- --no-sanitize write the captured upstream payload as-is
115
+ --no-sanitize disable common-field redaction (sensitiveParams are always redacted)
116
+ --credential <k>=<v> credential the gateway would inject (repeatable)
117
+ --no-credential record without a credential even when auth declares one
48
118
  --help, -h show this help
49
119
 
120
+ Credentials:
121
+ A provider whose auth.mode is not "none" reads a credential the gateway
122
+ resolves and injects at invocation time -- for "platform-managed" that is the
123
+ platform-owned key, which is not a provider secret and is not in the
124
+ checkout. Supply it with --credential, or with
125
+ ${LOCAL_CONNECTION_SECRETS_ENV}='{"<key>":"<value>"}'. The keys are the ones
126
+ the provider passes to ctx.credential.get(...).
127
+
50
128
  Example:
51
129
  apifuse record providers/korea-air-quality --operation realtime --params '{"stationName":"jongno"}'`;
52
130
 
53
131
  export async function main() {
132
+ let capture: ReturnType<typeof createCaptureContext> | undefined;
54
133
  try {
55
134
  const args = parseArgs(normalizeArgs(process.argv.slice(2)));
56
135
  const location = resolveProviderLocation(args.providerPath);
57
136
  const provider = await loadProvider(location.rootDir);
137
+ // The capture context attaches in-process; refuse an explicit remote request instead of ignoring it.
138
+ assertProcessEngineModeSupported();
58
139
  const operationName = resolveOperationName(provider, args.operation);
59
140
  const operation = provider.operations[operationName];
60
- const parsedParams = parseParams(operation, args.params);
141
+ const parsedParams = await parseParams(operation, args.params);
142
+
143
+ const connection = resolveRecordConnection(provider, args);
61
144
 
62
- const capture = createCaptureContext(
145
+ capture = createCaptureContext(
63
146
  provider,
64
- resolveOperationBaseUrl(provider, operationName),
147
+ resolveRecordBaseUrls(provider, operationName),
148
+ args.sanitize,
149
+ connection,
65
150
  );
66
151
 
67
152
  console.log(`[apifuse record] Calling ${operationName} on ${provider.id}...`);
68
153
 
69
- const result = await executeOperation(provider, operationName, capture.ctx, parsedParams);
70
- const captured = capture.getCapturedRaw();
154
+ let result: unknown;
155
+ try {
156
+ result = await executeOperation(provider, operationName, capture.ctx, parsedParams);
157
+ } catch (operationError) {
158
+ capture.throwIfRefused();
159
+ let partial: unknown;
160
+ try {
161
+ partial = await capture.getCapturedRaw();
162
+ } catch (finalizationError) {
163
+ throw new StreamRecorderError("Operation and stream finalization both failed.", [
164
+ operationError,
165
+ finalizationError,
166
+ ]);
167
+ }
168
+ const streamCount = findStreamEvidenceRecords(partial).length;
169
+ if (streamCount > 0) {
170
+ throw new StreamRecorderError(
171
+ `Operation failed after finalizing ${streamCount} stream capture${streamCount === 1 ? "" : "s"}.`,
172
+ [operationError],
173
+ );
174
+ }
175
+ throw operationError;
176
+ } finally {
177
+ const telemetryLine = formatResolverTelemetry(
178
+ capture.resolverTelemetry,
179
+ capture.getCapturedSensitiveParams().values,
180
+ );
181
+ if (telemetryLine) console.log(telemetryLine);
182
+ }
183
+ // Checked on success too: an operation that caught the refusal and carried
184
+ // on would otherwise save a fixture missing the request it never made.
185
+ capture.throwIfRefused();
186
+ const captured = await capture.getCapturedRaw();
71
187
 
72
188
  if (captured === undefined) {
73
189
  throw new Error(`No upstream response was captured for ${provider.id}.${operationName}.`);
74
190
  }
75
191
 
76
- const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
192
+ const sensitiveParams = capture.getCapturedSensitiveParams();
193
+ const rawPayload = jsonFixtureValue(captured);
77
194
  const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
78
- const nextPayload = await prepareFixturePayload(fixturePath, rawPayload, args.append);
195
+ const redactedCapture = redactFixture(rawPayload, sensitiveParams, args.sanitize);
196
+ const mergedPayload = await prepareFixturePayload(fixturePath, redactedCapture, args.append);
197
+ // Mandatory query-secret redaction applies to the merged history, including
198
+ // values discovered in older declared-key URL positions. Optional common-
199
+ // field sanitization applies only to this run's new capture so --append does
200
+ // not rewrite deliberately preserved historical fields.
201
+ const historicalSensitiveParams = discoverSensitiveQueryValues(mergedPayload, sensitiveParams);
202
+ const nextPayload = redactFixture(mergedPayload, historicalSensitiveParams, false);
79
203
 
80
204
  await mkdir(dirname(fixturePath), { recursive: true });
81
205
  await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
82
206
 
83
207
  console.log(
84
208
  `[apifuse record] Captured response (${formatBytes(
85
- Buffer.byteLength(JSON.stringify(rawPayload)),
209
+ Buffer.byteLength(JSON.stringify(redactedCapture)),
86
210
  )})`,
87
211
  );
88
212
  console.log(`[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`);
89
213
 
90
214
  void result;
91
215
  } catch (error) {
92
- handleCliError(error);
216
+ handleCliError(error, capture?.getCapturedSensitiveParams().values);
93
217
  }
94
218
  }
95
219
 
220
+ /**
221
+ * The connection this run attaches, or a hard failure when the provider needs
222
+ * one and none is configured.
223
+ *
224
+ * Failing here rather than at the handler is the whole point. Before this, a
225
+ * platform-managed provider recorded with `credential: { mode: "none" }`, so
226
+ * `ctx.credential.get(...)` returned `undefined` and the run died on the
227
+ * provider's own `MISSING_SECRET` — indistinguishable, to the author, from an
228
+ * upstream outage or a bad key. It reported as a capability the recorder
229
+ * lacked only if you already knew to look.
230
+ */
231
+ function resolveRecordConnection(
232
+ provider: ProviderRuntime,
233
+ args: CliArgs,
234
+ ): LocalConnection | undefined {
235
+ if (args.noCredential) {
236
+ if (providerRequiresConnection(provider)) {
237
+ console.warn(
238
+ `[apifuse record] --no-credential: recording ${provider.id} without the credential its auth.mode "${provider.auth?.mode}" declares.`,
239
+ );
240
+ }
241
+ return undefined;
242
+ }
243
+
244
+ const resolution = resolveLocalConnection(provider, {
245
+ overrides: args.credentials,
246
+ });
247
+ if (!resolution.ok)
248
+ throw new ProviderError(resolution.reason, {
249
+ code: "CREDENTIAL_UNAVAILABLE",
250
+ fix: "Supply the credential with --credential or the environment variable named above.",
251
+ });
252
+ return resolution.connection;
253
+ }
254
+
96
255
  function normalizeArgs(argv: string[]): string[] {
97
256
  return argv[0] === "record" ? argv.slice(1) : argv;
98
257
  }
@@ -103,6 +262,8 @@ function parseArgs(argv: string[]): CliArgs {
103
262
  let params = "{}";
104
263
  let sanitize = true;
105
264
  let append = false;
265
+ const credentials: string[] = [];
266
+ let noCredential = false;
106
267
 
107
268
  for (let index = 0; index < argv.length; index += 1) {
108
269
  const arg = argv[index];
@@ -159,6 +320,26 @@ function parseArgs(argv: string[]): CliArgs {
159
320
  continue;
160
321
  }
161
322
 
323
+ if (arg === "--credential") {
324
+ const value = argv[index + 1];
325
+ if (!value) {
326
+ throw new Error("Missing value for --credential.");
327
+ }
328
+ credentials.push(value);
329
+ index += 1;
330
+ continue;
331
+ }
332
+
333
+ if (arg.startsWith("--credential=")) {
334
+ credentials.push(arg.slice("--credential=".length));
335
+ continue;
336
+ }
337
+
338
+ if (arg === "--no-credential") {
339
+ noCredential = true;
340
+ continue;
341
+ }
342
+
162
343
  if (arg.startsWith("-")) {
163
344
  throw new Error(`Unknown option: ${arg}`);
164
345
  }
@@ -171,16 +352,59 @@ function parseArgs(argv: string[]): CliArgs {
171
352
  throw new Error(`Unexpected argument: ${arg}`);
172
353
  }
173
354
 
174
- return { append, providerPath, operation, params, sanitize };
355
+ return {
356
+ append,
357
+ providerPath,
358
+ operation,
359
+ params,
360
+ sanitize,
361
+ credentials,
362
+ noCredential,
363
+ };
364
+ }
365
+
366
+ /**
367
+ * The `resolver` sibling of the server request log, printed once per recorded invocation. The
368
+ * collector redacts at record time with the values registered so far; the mandatory query-secret
369
+ * pass over the final line also covers values captured after a solve and short low-entropy values
370
+ * the diagnostic redactor does not match, the same way handleCliError treats CLI errors.
371
+ */
372
+ export function formatResolverTelemetry(
373
+ collector: ResolverTelemetryCollector,
374
+ sensitiveValues: readonly string[] = [],
375
+ ): string | undefined {
376
+ const resolver = collector.toLogPayload();
377
+ return resolver
378
+ ? `[apifuse record] Resolver telemetry ${redactSensitiveText(
379
+ JSON.stringify({ resolver }),
380
+ sensitiveValues,
381
+ )}`
382
+ : undefined;
175
383
  }
176
384
 
177
- function handleCliError(error: unknown): never {
178
- const message = formatCliError(error);
385
+ function handleCliError(error: unknown, sensitiveValues: readonly string[] = []): never {
386
+ const message = redactSensitiveText(formatCliError(error), sensitiveValues);
179
387
  console.error(`[apifuse record] ${message}`);
180
388
  process.exit(1);
181
389
  }
182
390
 
183
- function formatCliError(error: unknown): string {
391
+ class StreamRecorderError extends Error {
392
+ readonly diagnosticCauses: readonly unknown[];
393
+
394
+ constructor(message: string, diagnosticCauses: readonly unknown[]) {
395
+ super(message, { cause: diagnosticCauses[0] });
396
+ this.name = "StreamRecorderError";
397
+ this.diagnosticCauses = diagnosticCauses;
398
+ }
399
+ }
400
+
401
+ export function formatCliError(error: unknown): string {
402
+ if (error instanceof StreamRecorderError) {
403
+ return [
404
+ sanitizeDiagnosticText(error.message),
405
+ ...error.diagnosticCauses.map((cause) => `cause=${formatDiagnosticCause(cause)}`),
406
+ ].join(" ");
407
+ }
184
408
  if (error instanceof TransportError) {
185
409
  return [
186
410
  error.message,
@@ -201,12 +425,32 @@ function formatCliError(error: unknown): string {
201
425
  }
202
426
 
203
427
  if (error instanceof Error) {
428
+ if (/^Stream capture\b/.test(error.message) && error.cause !== undefined) {
429
+ return `${sanitizeDiagnosticText(error.message)} cause=${formatDiagnosticCause(error.cause)}`;
430
+ }
204
431
  return error.message;
205
432
  }
206
433
 
207
434
  return String(error);
208
435
  }
209
436
 
437
+ function formatDiagnosticCause(cause: unknown): string {
438
+ if (cause instanceof StreamRecorderError) {
439
+ return [
440
+ sanitizeDiagnosticText(cause.message),
441
+ ...cause.diagnosticCauses.map((nested) => `cause=${formatDiagnosticCause(nested)}`),
442
+ ].join(" ");
443
+ }
444
+ if (!(cause instanceof Error)) return sanitizeDiagnosticText(String(cause));
445
+ const code =
446
+ "code" in cause && typeof cause.code === "string"
447
+ ? ` code=${sanitizeDiagnosticText(cause.code)}`
448
+ : "";
449
+ const nestedCause =
450
+ cause.cause === undefined ? "" : ` cause=${formatDiagnosticCause(cause.cause)}`;
451
+ return `${sanitizeDiagnosticText(cause.message)}${code}${nestedCause}`;
452
+ }
453
+
210
454
  function resolveProviderLocation(inputPath?: string) {
211
455
  const originalInput = inputPath ?? process.cwd();
212
456
  const resolvedInput = resolve(process.cwd(), originalInput);
@@ -284,7 +528,10 @@ function resolveOperationName(provider: ProviderRuntime, operationName?: string)
284
528
  return firstOperation;
285
529
  }
286
530
 
287
- function parseParams(operation: ProviderRuntime["operations"][string], value: string): unknown {
531
+ async function parseParams(
532
+ operation: ProviderRuntime["operations"][string],
533
+ value: string,
534
+ ): Promise<unknown> {
288
535
  let parsed: unknown;
289
536
 
290
537
  try {
@@ -295,47 +542,195 @@ function parseParams(operation: ProviderRuntime["operations"][string], value: st
295
542
  );
296
543
  }
297
544
 
298
- return operation.input ? operation.input.parse(parsed) : parsed;
545
+ return operation.input ? parseSchema(operation.input, parsed, "record.params") : parsed;
299
546
  }
300
547
 
301
- function resolveOperationBaseUrl(provider: ProviderRuntime, operationName: string): string {
302
- const baseUrl = provider.operations[operationName]?.upstream?.baseUrl;
303
- if (!baseUrl) {
304
- throw new Error(
305
- `Operation "${operationName}" for provider "${provider.id}" must define upstream.baseUrl.`,
306
- );
307
- }
548
+ /**
549
+ * The bases the recorder's `ctx.http` and `ctx.stealth` resolve relative
550
+ * request URLs against. An absent base means only absolute URLs resolve.
551
+ */
552
+ export type RecordBaseUrls = {
553
+ readonly http?: string;
554
+ readonly stealth?: string;
555
+ };
308
556
 
309
- return baseUrl;
557
+ /**
558
+ * `upstream.baseUrl` only resolves relative request URLs (and seeds the stealth
559
+ * cookie jar's default origin); the recorder uses it for nothing else, so an
560
+ * operation that requests absolute URLs records without one. The operation's
561
+ * own base wins; otherwise the recorder binds the bases `serve` binds, so a
562
+ * relative URL resolves in a recording exactly as it does in production.
563
+ */
564
+ export function resolveRecordBaseUrls(
565
+ provider: ProviderRuntime,
566
+ operationName: string,
567
+ ): RecordBaseUrls {
568
+ const declared = provider.operations[operationName]?.upstream?.baseUrl;
569
+ const own = typeof declared === "string" && declared.length > 0 ? declared : undefined;
570
+ const http = own ?? getProviderBaseUrl(provider);
571
+ const stealth = own ?? getProviderStealthBaseUrl(provider);
572
+ return { ...(http ? { http } : {}), ...(stealth ? { stealth } : {}) };
310
573
  }
311
574
 
312
- function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
313
- let capturedRaw: unknown;
575
+ /** A request the recorder cannot resolve without a declaration; fails the run even if the operation catches it. */
576
+ class RecordRefusalError extends Error {
577
+ constructor(message: string) {
578
+ super(message);
579
+ this.name = "RecordRefusalError";
580
+ }
581
+ }
314
582
 
315
- const http = proxyHttpClient(createHttpClient(baseUrl), (response) => {
316
- capturedRaw = response.data;
317
- });
318
- const stealth = proxyStealthClient(createStealthClient(baseUrl), (response) => {
319
- capturedRaw = normalizeCapturedStealthResponse(response);
583
+ export function createCaptureContext(
584
+ provider: ProviderRuntime,
585
+ baseUrls: RecordBaseUrls,
586
+ sanitize: boolean,
587
+ connection?: LocalConnection,
588
+ ) {
589
+ let refusal: RecordRefusalError | undefined;
590
+ const refuse = (message: string): never => {
591
+ refusal ??= new RecordRefusalError(message);
592
+ throw refusal;
593
+ };
594
+ const assertResolvableHttpUrl = (url: string) => {
595
+ if (baseUrls.http || isAbsoluteUrl(url)) return;
596
+ refuse(
597
+ `ctx.http cannot resolve the relative URL "${requestPathForFixture(url)}": no operation of provider "${provider.id}" declares upstream.baseUrl. Declare upstream.baseUrl on the recorded operation, or request an absolute URL.`,
598
+ );
599
+ };
600
+ let nextCaptureOrder = 0;
601
+ let nextStreamOrdinal = 0;
602
+ let capturedRaw: JsonValue | undefined;
603
+ const rawCaptures: Array<{ order: number; value: JsonValue }> = [];
604
+ const streamCaptures: Array<{
605
+ order: number;
606
+ request: StreamEvidenceRequest;
607
+ capture: StreamEvidenceCapture;
608
+ }> = [];
609
+ let capturedSse: { order: number; method: string; path: string } | undefined;
610
+ const sensitiveParamNames = new Set<string>();
611
+ const sensitiveParamValues = new Set<string>();
612
+ const cache = createBypassProviderCache({ providerId: provider.id });
613
+ const { resolver, resolverTelemetry, registerSensitiveValues } = createCliResolverRuntime(
614
+ provider,
615
+ cache,
616
+ );
617
+ // The injected credential is the most sensitive value in the run, and the
618
+ // recorder prints provider errors and writes fixtures. Seed its VALUES into
619
+ // the redaction set before anything can execute, so an upstream error that
620
+ // echoes the key, and any fixture that captured it, are redacted by the same
621
+ // machinery that already covers declared query secrets.
622
+ //
623
+ // Values only, never the key names: `sensitiveParamNames` means "declared
624
+ // query-key position", and `redactFixture` deliberately skips common-field
625
+ // sanitization for a key that appears in it. Seeding `access_token` there
626
+ // would therefore write a *newly issued* upstream `access_token` — which
627
+ // does not equal the injected value — to raw.json in plaintext. A credential
628
+ // key that really is a query parameter is added to the name set by
629
+ // `captureSensitiveRequestValues` when the provider sends it, which is the
630
+ // only place that claim is true.
631
+ for (const value of Object.values(connection?.secrets ?? {})) {
632
+ if (value.length > 0) sensitiveParamValues.add(value);
633
+ }
634
+ registerSensitiveValues([...sensitiveParamValues]);
635
+
636
+ const captureSensitiveParams = (url: string, options?: SensitiveRequestOptions) => {
637
+ try {
638
+ captureSensitiveRequestValues(url, options, sensitiveParamNames, sensitiveParamValues);
639
+ } finally {
640
+ // Resolver telemetry redacts at record time, so the vendor diagnostics of a later solve
641
+ // must not echo a query secret captured here (same guarantee as handleCliError).
642
+ registerSensitiveValues([...sensitiveParamValues]);
643
+ }
644
+ };
645
+ const getCapturedSensitiveParams = (): CapturedSensitiveParams => ({
646
+ names: [...sensitiveParamNames],
647
+ values: [...sensitiveParamValues],
320
648
  });
321
-
322
- const env = {
323
- get: (key: string) => process.env[key],
649
+ const reserveCaptureOrder = () => {
650
+ nextCaptureOrder += 1;
651
+ return nextCaptureOrder;
324
652
  };
325
- const credential = {
326
- mode: "none" as const,
327
- get: () => undefined,
328
- getAll: () => ({}),
329
- getAccessToken: () => undefined,
330
- getScopes: () => [],
653
+ const retainRawCapture = (order: number, value: unknown) => {
654
+ const json = jsonFixtureValue(value);
655
+ capturedRaw = json;
656
+ rawCaptures.push({ order, value: json });
331
657
  };
658
+
659
+ const http = captureHttpClient(createHttpClient(baseUrls.http), {
660
+ assertResolvableUrl: assertResolvableHttpUrl,
661
+ reserveOrder: reserveCaptureOrder,
662
+ reserveStreamOrdinal: () => {
663
+ nextStreamOrdinal += 1;
664
+ return nextStreamOrdinal;
665
+ },
666
+ onSensitiveParams: captureSensitiveParams,
667
+ onResponse: (order, response) => retainRawCapture(order, response.data),
668
+ onStreamResponse: (order, ordinal, requestUrl, method, response) => {
669
+ const resolvedRequestUrl = new URL(requestUrl, baseUrls.http).toString();
670
+ const request = {
671
+ ordinal,
672
+ method,
673
+ path: requestPathForFixture(resolvedRequestUrl),
674
+ };
675
+ const capture = captureStreamEvidence(response, {
676
+ requestUrl: resolvedRequestUrl,
677
+ request,
678
+ ...(sanitize
679
+ ? {
680
+ sanitizeFixture: (value: JsonValue) =>
681
+ jsonFixtureValue(sanitizeStreamFixture(value, getCapturedSensitiveParams())),
682
+ }
683
+ : {}),
684
+ });
685
+ streamCaptures.push({ order, request, capture });
686
+ return capture.response;
687
+ },
688
+ onSseResponse: (order, requestUrl, method) => {
689
+ capturedSse = {
690
+ order,
691
+ method,
692
+ path: requestPathForFixture(new URL(requestUrl, baseUrls.http).toString()),
693
+ };
694
+ },
695
+ });
696
+ // Without a base `serve` binds no stealth transport either, so every call is refused.
697
+ const refuseStealth = (): never =>
698
+ refuse(
699
+ `ctx.stealth has no base URL: provider "${provider.id}" declares neither upstream.baseUrl nor allowedHosts. Declare allowedHosts on the provider (serve binds ctx.stealth to https://<allowedHosts[0]>) or upstream.baseUrl on the recorded operation.`,
700
+ );
701
+ const stealth: StealthClient = baseUrls.stealth
702
+ ? proxyStealthClient(
703
+ createStealthClient(baseUrls.stealth, {
704
+ ...(provider.stealth ? { stealth: provider.stealth } : {}),
705
+ }),
706
+ captureSensitiveParams,
707
+ (order, response) => retainRawCapture(order, normalizeCapturedStealthResponse(response)),
708
+ reserveCaptureOrder,
709
+ )
710
+ : { fetch: async () => refuseStealth(), createSession: refuseStealth };
711
+
712
+ const providerEnvironment = createProviderEnvironment(
713
+ process.env,
714
+ provider.secrets?.map((secret) => secret.name) ?? [],
715
+ );
716
+ const env = { get: (key: string) => readDiagnosticEnv(key, providerEnvironment) };
717
+ // Built exactly the way `serve` builds it from a gateway-supplied
718
+ // `request.connection`, so the provider takes its production branch rather
719
+ // than the always-undefined one a stub credential forces.
720
+ const credential = createCredentialContext({
721
+ ...(provider.credential?.keys ? { allowedKeys: provider.credential.keys } : {}),
722
+ ...(connection ? { mode: connection.mode, values: connection.secrets } : {}),
723
+ });
332
724
  const state = createMemoryProviderRuntimeState();
333
- const ctx: ProviderContext = {
725
+ const candidates: ProviderEngineBindingCandidates = {
334
726
  env,
335
727
  credential,
336
- request: { headers: {} },
728
+ request: {
729
+ headers: {},
730
+ connectionId: connection ? LOCAL_CONNECTION_ID : "local-record",
731
+ },
337
732
  http,
338
- cache: createBypassProviderCache({ providerId: provider.id }),
733
+ cache,
339
734
  state,
340
735
  stealth,
341
736
  browser: {
@@ -362,70 +757,292 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
362
757
  throw new Error("Auth prompts are not available in apifuse record.");
363
758
  },
364
759
  },
760
+ ocr: createOcrClientFromEnv(provider.ocr),
365
761
  stt: createSttClientFromEnv(provider.stt),
366
- choice: createProviderChoiceContext({
762
+ resolver,
763
+ handle: createHandleContext({
367
764
  providerId: provider.id,
368
- env,
369
- request: { headers: {} },
370
- credential,
765
+ request: {
766
+ headers: {},
767
+ connectionId: connection ? LOCAL_CONNECTION_ID : "local-record",
768
+ },
371
769
  state,
372
770
  }),
373
771
  };
772
+ const ctx = createInProcessProviderEngine().attach({
773
+ provider,
774
+ bindings: candidates,
775
+ }) as ProviderContext;
374
776
 
375
777
  return {
376
778
  ctx,
377
- getCapturedRaw: () => capturedRaw,
779
+ resolverTelemetry,
780
+ throwIfRefused: () => {
781
+ if (refusal) throw refusal;
782
+ },
783
+ getCapturedRaw: async () => {
784
+ if (streamCaptures.length === 0) {
785
+ if (capturedSse) throw unsupportedSseCaptureError(capturedSse);
786
+ return capturedRaw;
787
+ }
788
+
789
+ const settled = await Promise.allSettled(
790
+ streamCaptures.map(async ({ order, request, capture }) => ({
791
+ order,
792
+ request,
793
+ value: redactStreamEvidence(await capture.getEvidence(), getCapturedSensitiveParams()),
794
+ })),
795
+ );
796
+ const failures = settled.flatMap((result, index) =>
797
+ result.status === "rejected"
798
+ ? [
799
+ new StreamRecorderError(
800
+ `Stream finalization failed: method=${streamCaptures[index]!.request.method} path=${streamCaptures[index]!.request.path} ordinal=${streamCaptures[index]!.request.ordinal}.`,
801
+ [result.reason],
802
+ ),
803
+ ]
804
+ : [],
805
+ );
806
+ if (failures.length > 0) {
807
+ throw new StreamRecorderError(
808
+ `${failures.length} stream capture${failures.length === 1 ? "" : "s"} failed to finalize.`,
809
+ failures,
810
+ );
811
+ }
812
+ const evidence = settled.flatMap((result) =>
813
+ result.status === "fulfilled" ? [result.value] : [],
814
+ );
815
+ if (capturedSse) throw unsupportedSseCaptureError(capturedSse);
816
+ const timeline: StreamCaptureGroupItem[] = [...rawCaptures, ...evidence]
817
+ .sort((left, right) => left.order - right.order)
818
+ .map((item) =>
819
+ "request" in item
820
+ ? { kind: "stream" as const, evidence: item.value }
821
+ : { kind: "response" as const, value: item.value },
822
+ );
823
+ return createStreamCaptureEnvelope(timeline);
824
+ },
825
+ getCapturedSensitiveParams,
378
826
  };
379
827
  }
380
828
 
381
- function proxyHttpClient(
382
- client: HttpClient,
383
- onResponse: (response: Awaited<ReturnType<HttpClient["get"]>>) => void,
384
- ): HttpClient {
385
- return new Proxy(client, {
386
- get(target, prop, receiver) {
387
- const value = Reflect.get(target, prop, receiver);
829
+ type CapturedSensitiveParams = {
830
+ names: readonly string[];
831
+ values: readonly string[];
832
+ };
388
833
 
389
- if (typeof value !== "function") {
390
- return value;
391
- }
834
+ type SensitiveRequestOptions = Pick<RequestOptions, "params" | "sensitiveParams">;
835
+
836
+ function captureSensitiveRequestValues(
837
+ url: string,
838
+ options: SensitiveRequestOptions | undefined,
839
+ names: Set<string>,
840
+ values: Set<string>,
841
+ ): void {
842
+ const sensitiveParams = normalizeSensitiveParams(options?.sensitiveParams);
843
+ if (sensitiveParams === undefined) return;
844
+ if (!sensitiveParams || typeof sensitiveParams !== "object" || Array.isArray(sensitiveParams)) {
845
+ throw new TypeError("sensitiveParams must be an object whose values are strings.");
846
+ }
392
847
 
393
- return async (...args: unknown[]) => {
394
- const response = await value.apply(target, args);
395
- onResponse(response);
396
- return response;
397
- };
848
+ const entries = Object.entries(sensitiveParams);
849
+ for (const [key, value] of entries) {
850
+ if (typeof value !== "string") {
851
+ throw new TypeError(`sensitiveParams.${key} must be a string.`);
852
+ }
853
+ names.add(key);
854
+ if (value !== "") values.add(value);
855
+ }
856
+
857
+ let serializedUrl: ReturnType<typeof serializeRequestUrl>;
858
+ try {
859
+ const absoluteUrl = new URL(String(url), "http://apifuse.invalid").toString();
860
+ serializedUrl = serializeRequestUrl(absoluteUrl, options?.params, sensitiveParams);
861
+ } catch (error) {
862
+ const structural = redactUrlQueryParams(String(url), [...names]);
863
+ const safeUrl = redactSensitiveText(structural.redactedUrl, [
864
+ ...values,
865
+ ...structural.sensitiveValues,
866
+ ]);
867
+ const causeKind = error instanceof Error ? error.name : typeof error;
868
+ throw new TypeError(`Cannot securely record sensitiveParams for "${safeUrl}" (${causeKind}).`, {
869
+ cause: redactSensitiveError(
870
+ error,
871
+ [...values, ...structural.sensitiveValues],
872
+ String(url),
873
+ structural.redactedUrl,
874
+ ),
875
+ });
876
+ }
877
+
878
+ for (const value of serializedUrl.sensitiveValues) {
879
+ if (value !== "") values.add(value);
880
+ }
881
+ }
882
+
883
+ function snapshotRequestOptions<T extends SensitiveRequestOptions>(options: T): T {
884
+ return {
885
+ ...options,
886
+ ...(options.params
887
+ ? {
888
+ params: Object.fromEntries(
889
+ Object.entries(options.params).map(([key, value]) => [
890
+ key,
891
+ Array.isArray(value) ? [...value] : value,
892
+ ]),
893
+ ),
894
+ }
895
+ : {}),
896
+ ...(normalizeSensitiveParams(options.sensitiveParams)
897
+ ? { sensitiveParams: { ...options.sensitiveParams } }
898
+ : {}),
899
+ };
900
+ }
901
+
902
+ type HttpCaptureCallbacks = {
903
+ /** Throws before the request when the recorder cannot resolve `url`. */
904
+ assertResolvableUrl(url: string): void;
905
+ reserveOrder(): number;
906
+ reserveStreamOrdinal(): number;
907
+ onSensitiveParams(url: string, options?: RequestOptions): void;
908
+ onResponse(order: number, response: HttpResponse): void;
909
+ onStreamResponse(
910
+ order: number,
911
+ ordinal: number,
912
+ requestUrl: string,
913
+ method: string,
914
+ response: HttpStreamResponse,
915
+ ): HttpStreamResponse;
916
+ onSseResponse(order: number, requestUrl: string, method: string): void;
917
+ };
918
+
919
+ function captureHttpClient(client: HttpClient, callbacks: HttpCaptureCallbacks): HttpClient {
920
+ const captureResponse = async (
921
+ order: number,
922
+ responsePromise: Promise<HttpResponse>,
923
+ ): Promise<HttpResponse> => {
924
+ const response = await responsePromise;
925
+ callbacks.onResponse(order, response);
926
+ return response;
927
+ };
928
+ const captureRequestOptions = (method: PropertyKey, args: unknown[]) => {
929
+ callbacks.assertResolvableUrl(String(args[0]));
930
+ const invocation = parseHttpRequestInvocation(method, args);
931
+ const options = invocation ? requestOptionsFromHttpInvocation(invocation) : undefined;
932
+ if (!invocation || !options) return;
933
+ const snapshot = snapshotRequestOptions(options);
934
+ callbacks.onSensitiveParams(String(args[0]), snapshot);
935
+ replaceRequestOptionsInHttpInvocation(invocation, snapshot);
936
+ };
937
+
938
+ return {
939
+ request: (...args: Parameters<HttpClient["request"]>) => {
940
+ captureRequestOptions("request", args);
941
+ return captureResponse(callbacks.reserveOrder(), client.request(...args));
942
+ },
943
+ get: (...args: Parameters<HttpClient["get"]>) => {
944
+ captureRequestOptions("get", args);
945
+ return captureResponse(callbacks.reserveOrder(), client.get(...args));
946
+ },
947
+ post: (...args: Parameters<HttpClient["post"]>) => {
948
+ captureRequestOptions("post", args);
949
+ return captureResponse(callbacks.reserveOrder(), client.post(...args));
950
+ },
951
+ put: (...args: Parameters<HttpClient["put"]>) => {
952
+ captureRequestOptions("put", args);
953
+ return captureResponse(callbacks.reserveOrder(), client.put(...args));
954
+ },
955
+ delete: (...args: Parameters<HttpClient["delete"]>) => {
956
+ captureRequestOptions("delete", args);
957
+ return captureResponse(callbacks.reserveOrder(), client.delete(...args));
958
+ },
959
+ stream: async (...args: Parameters<HttpClient["stream"]>) => {
960
+ captureRequestOptions("stream", args);
961
+ const order = callbacks.reserveOrder();
962
+ const ordinal = callbacks.reserveStreamOrdinal();
963
+ const method = (args[1]?.method ?? "GET").toUpperCase();
964
+ const response = await client.stream(...args);
965
+ return callbacks.onStreamResponse(order, ordinal, args[0], method, response);
398
966
  },
399
- }) as HttpClient;
967
+ sse: async (...args: Parameters<HttpClient["sse"]>) => {
968
+ captureRequestOptions("sse", args);
969
+ const order = callbacks.reserveOrder();
970
+ const response = await client.sse(...args);
971
+ callbacks.onSseResponse(order, args[0], (args[1]?.method ?? "GET").toUpperCase());
972
+ return response;
973
+ },
974
+ };
400
975
  }
401
976
 
402
977
  type StealthSession = ReturnType<StealthClient["createSession"]>;
403
978
 
404
979
  function proxyStealthClient(
405
980
  client: StealthClient,
406
- onResponse: (response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
981
+ onSensitiveParams: (url: string, options?: SensitiveRequestOptions) => void,
982
+ onResponse: (order: number, response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
983
+ reserveOrder: () => number,
407
984
  ): StealthClient {
408
985
  return {
409
986
  fetch: async (...args: Parameters<StealthClient["fetch"]>) => {
987
+ const order = reserveOrder();
988
+ if (args[1]) args[1] = snapshotRequestOptions(args[1]);
989
+ onSensitiveParams(args[0], args[1]);
410
990
  const response = await client.fetch(...args);
411
- onResponse(response);
991
+ if (response.url) onSensitiveParams(response.url, args[1]);
992
+ onResponse(order, response);
412
993
  return response;
413
994
  },
414
995
  createSession: (...args: Parameters<StealthClient["createSession"]>) =>
415
- proxyStealthSession(client.createSession(...args), onResponse),
996
+ proxyStealthSession(
997
+ client.createSession(...args),
998
+ onSensitiveParams,
999
+ onResponse,
1000
+ reserveOrder,
1001
+ ),
416
1002
  };
417
1003
  }
418
1004
 
419
1005
  function proxyStealthSession(
420
1006
  session: StealthSession,
421
- onResponse: (response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
1007
+ onSensitiveParams: (url: string, options?: SensitiveRequestOptions) => void,
1008
+ onResponse: (order: number, response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
1009
+ reserveOrder: () => number,
422
1010
  ): StealthSession {
423
1011
  return {
424
1012
  fetch: async (...args: Parameters<StealthSession["fetch"]>) => {
1013
+ const order = reserveOrder();
1014
+ if (args[1]) args[1] = snapshotRequestOptions(args[1]);
1015
+ onSensitiveParams(args[0], args[1]);
425
1016
  const response = await session.fetch(...args);
426
- onResponse(response);
1017
+ if (response.url) onSensitiveParams(response.url, args[1]);
1018
+ onResponse(order, response);
1019
+ return response;
1020
+ },
1021
+ replayChallenged: async (...args: Parameters<StealthSession["replayChallenged"]>) => {
1022
+ const order = reserveOrder();
1023
+ const response = await session.replayChallenged(...args);
1024
+ onResponse(order, response);
427
1025
  return response;
428
1026
  },
1027
+ cookies: session.cookies,
1028
+ redirects: {
1029
+ run: async (...args: Parameters<StealthSession["redirects"]["run"]>) => {
1030
+ const order = reserveOrder();
1031
+ args[0] = snapshotRequestOptions(args[0]);
1032
+ onSensitiveParams(args[0].url, args[0]);
1033
+ const callerStopWhen = args[0].stopWhen;
1034
+ args[0].stopWhen = async (hop) => {
1035
+ for (const url of [hop.url, hop.location, hop.nextUrl]) {
1036
+ if (url) onSensitiveParams(url, args[0]);
1037
+ }
1038
+ return callerStopWhen ? await callerStopWhen(hop) : false;
1039
+ };
1040
+ const result = await session.redirects.run(...args);
1041
+ if (result.final.url) onSensitiveParams(result.final.url, args[0]);
1042
+ onResponse(order, result.final);
1043
+ return result;
1044
+ },
1045
+ },
429
1046
  close: () => session.close(),
430
1047
  };
431
1048
  }
@@ -438,31 +1055,193 @@ function normalizeCapturedStealthResponse(response: Awaited<ReturnType<StealthCl
438
1055
  }
439
1056
  }
440
1057
 
441
- function sanitizeFixture(value: unknown): unknown {
1058
+ function redactStreamEvidence(
1059
+ evidence: Awaited<ReturnType<StreamEvidenceCapture["getEvidence"]>>,
1060
+ sensitiveParams: CapturedSensitiveParams,
1061
+ ): Awaited<ReturnType<StreamEvidenceCapture["getEvidence"]>> {
1062
+ return parseStreamEvidenceRecord(redactFixture(evidence, sensitiveParams, false));
1063
+ }
1064
+
1065
+ function sanitizeStreamFixture(value: unknown, sensitiveParams: CapturedSensitiveParams): unknown {
1066
+ if (typeof value === "string") {
1067
+ if (value !== "" && sensitiveParams.values.includes(value)) return REDACTED_QUERY_VALUE;
1068
+ return sanitizeFixtureString(redactFixtureText(value, sensitiveParams));
1069
+ }
442
1070
  if (Array.isArray(value)) {
443
- return value.map((item) => sanitizeFixture(item));
1071
+ return value.map((item) => sanitizeStreamFixture(item, sensitiveParams));
444
1072
  }
445
-
446
1073
  if (!value || typeof value !== "object") {
447
- return value;
1074
+ return redactFixture(value, sensitiveParams, false);
448
1075
  }
449
1076
 
450
- const entries = Object.entries(value as MutableRecord).map(([key, entryValue]) => {
451
- if (isSensitiveKey(key)) {
452
- return [key, "[REDACTED]"] as const;
453
- }
1077
+ const result: MutableRecord = Object.create(null) as MutableRecord;
1078
+ for (const [key, entryValue] of Object.entries(value as MutableRecord)) {
1079
+ const redactedKey = sensitiveParams.values.includes(key)
1080
+ ? REDACTED_QUERY_VALUE
1081
+ : redactFixtureText(key, sensitiveParams);
1082
+ const uniqueKey = collisionSafeKey(result, redactedKey);
1083
+ result[uniqueKey] =
1084
+ isSensitiveFixtureKey(key) && !sensitiveParams.names.includes(key)
1085
+ ? REDACTED_QUERY_VALUE
1086
+ : sanitizeStreamFixture(entryValue, sensitiveParams);
1087
+ }
1088
+ return result;
1089
+ }
454
1090
 
455
- return [key, sanitizeFixture(entryValue)] as const;
456
- });
1091
+ function redactStreamPreview(
1092
+ bodyPreviewBase64: string,
1093
+ sensitiveParams: CapturedSensitiveParams,
1094
+ ): { bodyPreviewBase64: string; changed: boolean } {
1095
+ const preview = Buffer.from(bodyPreviewBase64, "base64");
1096
+ if (preview.byteLength === 0) return { bodyPreviewBase64, changed: false };
1097
+
1098
+ const text = new TextDecoder().decode(preview);
1099
+ let redactedText = redactFixtureText(text, sensitiveParams);
1100
+ const trimmed = text.trimEnd();
1101
+ try {
1102
+ const parsed = JSON.parse(trimmed) as unknown;
1103
+ const structurallyRedacted = JSON.stringify(redactFixture(parsed, sensitiveParams, false));
1104
+ if (structurallyRedacted !== JSON.stringify(parsed)) redactedText = structurallyRedacted;
1105
+ } catch {
1106
+ // Non-JSON previews still use the shared free-text sensitive-value policy.
1107
+ }
1108
+ if (redactedText === text) return { bodyPreviewBase64, changed: false };
457
1109
 
458
- return Object.fromEntries(entries);
1110
+ const fitted = Buffer.alloc(preview.byteLength, 0x20);
1111
+ let redactedBytes = Buffer.from(redactedText);
1112
+ if (redactedBytes.byteLength > fitted.byteLength) {
1113
+ redactedBytes = Buffer.from(REDACTED_QUERY_VALUE);
1114
+ }
1115
+ redactedBytes.copy(fitted, 0, 0, Math.min(redactedBytes.byteLength, fitted.byteLength));
1116
+ return { bodyPreviewBase64: fitted.toString("base64"), changed: true };
459
1117
  }
460
1118
 
461
- function isSensitiveKey(key: string): boolean {
462
- return /authorization|token|api[-_]?key/i.test(key);
1119
+ function redactFixture(
1120
+ value: unknown,
1121
+ sensitiveParams: CapturedSensitiveParams,
1122
+ sanitizeCommonFields: boolean,
1123
+ ): unknown {
1124
+ if (typeof value === "string") {
1125
+ if (value !== "" && sensitiveParams.values.includes(value)) return REDACTED_QUERY_VALUE;
1126
+ return redactFixtureText(value, sensitiveParams);
1127
+ }
1128
+
1129
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1130
+ return sensitiveParams.values.includes(String(value)) ? REDACTED_QUERY_VALUE : value;
1131
+ }
1132
+
1133
+ if (Array.isArray(value)) {
1134
+ return value.map((item) => redactFixture(item, sensitiveParams, sanitizeCommonFields));
1135
+ }
1136
+
1137
+ if (!value || typeof value !== "object") return value;
1138
+
1139
+ const result: MutableRecord = Object.create(null) as MutableRecord;
1140
+ for (const [key, entryValue] of Object.entries(value as MutableRecord)) {
1141
+ const redactedKey = sensitiveParams.values.includes(key)
1142
+ ? REDACTED_QUERY_VALUE
1143
+ : redactFixtureText(key, sensitiveParams);
1144
+ const uniqueKey = collisionSafeKey(result, redactedKey);
1145
+ result[uniqueKey] =
1146
+ sanitizeCommonFields &&
1147
+ (isSensitiveKey(key) || isSensitiveFixtureKey(key)) &&
1148
+ !sensitiveParams.names.includes(key)
1149
+ ? REDACTED_QUERY_VALUE
1150
+ : redactFixture(entryValue, sensitiveParams, sanitizeCommonFields);
1151
+ }
1152
+ const record = value as MutableRecord;
1153
+ if (record.__apifuse_stream__ === true && typeof record.body_preview_base64 === "string") {
1154
+ const preview = redactStreamPreview(record.body_preview_base64, sensitiveParams);
1155
+ result.body_preview_base64 = preview.bodyPreviewBase64;
1156
+ if (preview.changed) result.preview_sanitized = true;
1157
+ }
1158
+ return result;
1159
+ }
1160
+
1161
+ function redactFixtureText(text: string, sensitiveParams: CapturedSensitiveParams): string {
1162
+ // Shared free-text policy: long values are unconditional substrings; values
1163
+ // shorter than four characters require token boundaries. Exact scalar echoes
1164
+ // and declared query-key positions are structurally redacted for every length.
1165
+ let redacted = redactSensitiveText(text, sensitiveParams.values);
1166
+ for (const name of sensitiveParams.names) {
1167
+ let componentEncodedName = name;
1168
+ try {
1169
+ componentEncodedName = encodeURIComponent(name);
1170
+ } catch {
1171
+ // Lone surrogates remain covered by the raw and form-encoded variants.
1172
+ }
1173
+ const keyVariants = new Set([
1174
+ name,
1175
+ componentEncodedName,
1176
+ new URLSearchParams({ [name]: "" }).toString().slice(0, -1),
1177
+ ]);
1178
+ for (const key of keyVariants) {
1179
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1180
+ const queryValue = new RegExp(`(^|[?&])(${escapedKey}=)[^&#\\s"]*`, "g");
1181
+ redacted = redacted.replace(queryValue, (_match, prefix, assignment) => {
1182
+ return `${prefix}${assignment}${REDACTED_QUERY_VALUE}`;
1183
+ });
1184
+ }
1185
+ }
1186
+ return redacted;
1187
+ }
1188
+
1189
+ function discoverSensitiveQueryValues(
1190
+ value: unknown,
1191
+ sensitiveParams: CapturedSensitiveParams,
1192
+ ): CapturedSensitiveParams {
1193
+ const values = new Set(sensitiveParams.values);
1194
+ const absoluteUrl = /\b[A-Za-z][A-Za-z\d+.-]*:\/\/[^\s<>"']+/g;
1195
+ const discoverFromUrl = (url: string) => {
1196
+ for (const discovered of redactUrlQueryParams(url, sensitiveParams.names).sensitiveValues) {
1197
+ if (discovered !== "" && discovered !== REDACTED_QUERY_VALUE) values.add(discovered);
1198
+ }
1199
+ };
1200
+ const visitText = (text: string) => {
1201
+ if (!/\s/.test(text)) {
1202
+ try {
1203
+ new URL(text, "http://apifuse.invalid");
1204
+ discoverFromUrl(text);
1205
+ return;
1206
+ } catch {
1207
+ // Fall through to extracting absolute URL spans from prose.
1208
+ }
1209
+ }
1210
+ for (const match of text.matchAll(absoluteUrl)) {
1211
+ try {
1212
+ new URL(match[0]);
1213
+ discoverFromUrl(match[0]);
1214
+ } catch {
1215
+ // Ignore URI-like prose that is not a parseable URL.
1216
+ }
1217
+ }
1218
+ };
1219
+ const visit = (current: unknown): void => {
1220
+ if (typeof current === "string") {
1221
+ visitText(current);
1222
+ return;
1223
+ }
1224
+ if (Array.isArray(current)) {
1225
+ for (const item of current) visit(item);
1226
+ return;
1227
+ }
1228
+ if (!current || typeof current !== "object") return;
1229
+ for (const [key, entryValue] of Object.entries(current as MutableRecord)) {
1230
+ visitText(key);
1231
+ visit(entryValue);
1232
+ }
1233
+ };
1234
+ visit(value);
1235
+ return { names: sensitiveParams.names, values: [...values] };
463
1236
  }
464
1237
 
465
- async function prepareFixturePayload(
1238
+ function collisionSafeKey(record: MutableRecord, preferredKey: string): string {
1239
+ if (!(preferredKey in record)) return preferredKey;
1240
+ let suffix = 2;
1241
+ while (`${preferredKey}#${suffix}` in record) suffix += 1;
1242
+ return `${preferredKey}#${suffix}`;
1243
+ }
1244
+ export async function prepareFixturePayload(
466
1245
  fixturePath: string,
467
1246
  payload: unknown,
468
1247
  append: boolean,
@@ -471,15 +1250,45 @@ async function prepareFixturePayload(
471
1250
  return payload;
472
1251
  }
473
1252
 
1253
+ let fixtureSource: string;
474
1254
  try {
475
- const existing = JSON.parse(readFileSync(fixturePath, "utf8")) as unknown;
476
- if (Array.isArray(existing)) {
477
- return [...existing, payload];
478
- }
479
- } catch {
480
- // Fall through to overwrite with the new payload.
1255
+ fixtureSource = readFileSync(fixturePath, "utf8");
1256
+ } catch (error) {
1257
+ throw new Error(
1258
+ `Cannot append to existing fixture "${fixturePath}" because it could not be read: ${
1259
+ error instanceof Error ? error.message : String(error)
1260
+ }. Fix its permissions or delete it, then run apifuse record --append again.`,
1261
+ );
481
1262
  }
482
1263
 
1264
+ let existing: unknown;
1265
+ try {
1266
+ existing = JSON.parse(fixtureSource) as unknown;
1267
+ } catch (error) {
1268
+ throw new Error(
1269
+ `Cannot append to corrupt fixture "${fixturePath}" because it is not valid JSON: ${
1270
+ error instanceof Error ? error.message : String(error)
1271
+ }. Fix or delete the fixture, then run apifuse record --append again.`,
1272
+ );
1273
+ }
1274
+
1275
+ if (hasStreamEvidenceMarker(existing)) {
1276
+ const evidence = parseStreamEvidenceRecord(existing);
1277
+ return [createStreamCaptureEnvelope([{ kind: "stream", evidence }]), payload];
1278
+ }
1279
+ if (Array.isArray(existing)) {
1280
+ if (existing.some((item) => hasStreamEvidenceMarker(item))) {
1281
+ const legacyGroup = findStreamCaptureGroup(existing);
1282
+ if (legacyGroup) {
1283
+ const prefix = existing.slice(0, -legacyGroup.items.length);
1284
+ return [...prefix, createStreamCaptureEnvelope(legacyGroup.items), payload];
1285
+ }
1286
+ }
1287
+ return [...existing, payload];
1288
+ }
1289
+ if (existing !== null) {
1290
+ return [existing, payload];
1291
+ }
483
1292
  return payload;
484
1293
  }
485
1294
 
@@ -491,4 +1300,24 @@ function formatBytes(bytes: number): string {
491
1300
  return `${(bytes / 1024).toFixed(1)} KB`;
492
1301
  }
493
1302
 
494
- await main();
1303
+ function jsonFixtureValue(value: unknown): JsonValue {
1304
+ const serialized = JSON.stringify(value);
1305
+ if (serialized === undefined) {
1306
+ throw new Error("Captured upstream response is not JSON-serializable.");
1307
+ }
1308
+ return JSON.parse(serialized) as JsonValue;
1309
+ }
1310
+
1311
+ function unsupportedSseCaptureError(request: {
1312
+ order: number;
1313
+ method: string;
1314
+ path: string;
1315
+ }): Error {
1316
+ return new Error(
1317
+ `apifuse record does not support ctx.http.sse(): method=${request.method} path=${request.path} call=${request.order}.`,
1318
+ );
1319
+ }
1320
+
1321
+ if (import.meta.main) {
1322
+ await main();
1323
+ }