@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.51

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 (368) hide show
  1. package/AUTHORING.md +537 -33
  2. package/CHANGELOG.md +205 -1
  3. package/README.md +83 -22
  4. package/SUBMISSION.md +2 -2
  5. package/bin/apifuse-check.ts +165 -11
  6. package/bin/apifuse-dev.ts +57 -13
  7. package/bin/apifuse-migrate-operation-declaration.ts +55 -0
  8. package/bin/apifuse-migrate-shape.ts +202 -0
  9. package/bin/apifuse-pack-check.ts +22 -2
  10. package/bin/apifuse-pack-smoke.ts +57 -2
  11. package/bin/apifuse-pack-types.ts +357 -38
  12. package/bin/apifuse-perf.ts +14 -13
  13. package/bin/apifuse-record.ts +709 -72
  14. package/bin/apifuse-submit-check.ts +2344 -324
  15. package/bin/apifuse-sync-assets.ts +117 -0
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/dist/auth-turn/index.d.ts +3 -3
  18. package/dist/auth-turn/index.js +1 -1
  19. package/dist/auth.d.ts +14 -0
  20. package/dist/auth.js +67 -0
  21. package/dist/ceremonies/index.d.ts +16 -0
  22. package/dist/ceremonies/index.js +141 -36
  23. package/dist/cli/commands.d.ts +1 -1
  24. package/dist/cli/commands.js +27 -0
  25. package/dist/cli/create.d.ts +4 -1
  26. package/dist/cli/create.js +42 -37
  27. package/dist/cli/migrate-operation-declaration.d.ts +59 -0
  28. package/dist/cli/migrate-operation-declaration.js +1249 -0
  29. package/dist/cli/migrate-operation-shape.d.ts +44 -0
  30. package/dist/cli/migrate-operation-shape.js +113 -0
  31. package/dist/cli/migrate-provider-shape.d.ts +52 -0
  32. package/dist/cli/migrate-provider-shape.js +578 -0
  33. package/dist/cli/prompt-assets.d.ts +80 -0
  34. package/dist/cli/prompt-assets.js +743 -0
  35. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  36. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  37. package/dist/cli/templates/provider/README.md.tpl +5 -5
  38. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  39. package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
  40. package/dist/cli/templates/provider/provider.json.tpl +6 -0
  41. package/dist/config/loader.d.ts +179 -16
  42. package/dist/config/loader.js +435 -127
  43. package/dist/contract-serialization.js +4 -8
  44. package/dist/contract-types.d.ts +12 -5
  45. package/dist/contract.js +24 -10
  46. package/dist/declaration-validation.d.ts +33 -0
  47. package/dist/declaration-validation.js +266 -0
  48. package/dist/define.d.ts +66 -29
  49. package/dist/define.js +778 -69
  50. package/dist/dev.d.ts +3 -0
  51. package/dist/dev.js +1 -1
  52. package/dist/engine.d.ts +78 -0
  53. package/dist/engine.js +133 -0
  54. package/dist/error-observability.d.ts +7 -0
  55. package/dist/error-observability.js +61 -0
  56. package/dist/error-resolution.d.ts +4 -0
  57. package/dist/error-resolution.js +122 -0
  58. package/dist/errors.d.ts +33 -0
  59. package/dist/errors.js +40 -0
  60. package/dist/fixture-sanitization.d.ts +28 -0
  61. package/dist/fixture-sanitization.js +227 -0
  62. package/dist/health-scenario.d.ts +1842 -0
  63. package/dist/health-scenario.js +624 -0
  64. package/dist/index.d.ts +21 -10
  65. package/dist/index.js +12 -7
  66. package/dist/lint.d.ts +23 -11
  67. package/dist/lint.js +572 -69
  68. package/dist/native-address.d.ts +43 -0
  69. package/dist/native-address.js +281 -0
  70. package/dist/native-egress-policy.d.ts +31 -0
  71. package/dist/native-egress-policy.js +288 -0
  72. package/dist/observability.d.ts +5 -2
  73. package/dist/observability.js +48 -1
  74. package/dist/provider.d.ts +10 -2
  75. package/dist/provider.js +4 -1
  76. package/dist/runtime/auth-flow.d.ts +5 -1
  77. package/dist/runtime/auth-flow.js +6 -0
  78. package/dist/runtime/browser.d.ts +1 -0
  79. package/dist/runtime/browser.js +492 -49
  80. package/dist/runtime/cache.d.ts +1 -0
  81. package/dist/runtime/cache.js +169 -15
  82. package/dist/runtime/choice-wordlist.d.ts +9 -0
  83. package/dist/runtime/choice-wordlist.js +138 -0
  84. package/dist/runtime/choice.d.ts +13 -1
  85. package/dist/runtime/choice.js +490 -102
  86. package/dist/runtime/chrome149-header-order.d.ts +58 -0
  87. package/dist/runtime/chrome149-header-order.js +289 -0
  88. package/dist/runtime/env.js +12 -0
  89. package/dist/runtime/executor.d.ts +3 -2
  90. package/dist/runtime/executor.js +26 -25
  91. package/dist/runtime/http.d.ts +1 -0
  92. package/dist/runtime/http.js +515 -53
  93. package/dist/runtime/insights.js +2 -2
  94. package/dist/runtime/instrumentation.d.ts +2 -2
  95. package/dist/runtime/instrumentation.js +366 -8
  96. package/dist/runtime/native-network-errors.d.ts +33 -0
  97. package/dist/runtime/native-network-errors.js +69 -0
  98. package/dist/runtime/native-network.d.ts +96 -0
  99. package/dist/runtime/native-network.js +1232 -0
  100. package/dist/runtime/ocr.d.ts +29 -0
  101. package/dist/runtime/ocr.js +440 -0
  102. package/dist/runtime/otlp.d.ts +71 -2
  103. package/dist/runtime/otlp.js +397 -16
  104. package/dist/runtime/proxy-errors.js +6 -2
  105. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  106. package/dist/runtime/proxy-nodemaven.js +146 -0
  107. package/dist/runtime/proxy-telemetry.d.ts +80 -1
  108. package/dist/runtime/proxy-telemetry.js +154 -47
  109. package/dist/runtime/redirects.d.ts +29 -0
  110. package/dist/runtime/redirects.js +36 -0
  111. package/dist/runtime/redis.d.ts +1 -1
  112. package/dist/runtime/redis.js +4 -2
  113. package/dist/runtime/request-options.d.ts +68 -1
  114. package/dist/runtime/request-options.js +548 -0
  115. package/dist/runtime/resolver-config.d.ts +6 -0
  116. package/dist/runtime/resolver-config.js +6 -0
  117. package/dist/runtime/resolver-public.d.ts +1 -0
  118. package/dist/runtime/resolver-public.js +1 -0
  119. package/dist/runtime/resolver-shared.d.ts +14 -0
  120. package/dist/runtime/resolver-shared.js +12 -0
  121. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  122. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  123. package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
  124. package/dist/runtime/resolver-vendors/browser.js +377 -0
  125. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  126. package/dist/runtime/resolver-vendors/capsolver.js +531 -0
  127. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  128. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  129. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  130. package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
  131. package/dist/runtime/resolver-vendors/types.d.ts +94 -0
  132. package/dist/runtime/resolver-vendors/types.js +96 -0
  133. package/dist/runtime/resolver.d.ts +72 -0
  134. package/dist/runtime/resolver.js +771 -0
  135. package/dist/runtime/secrets.d.ts +27 -0
  136. package/dist/runtime/secrets.js +51 -0
  137. package/dist/runtime/state.d.ts +3 -0
  138. package/dist/runtime/state.js +277 -71
  139. package/dist/runtime/stealth-cookies.d.ts +20 -0
  140. package/dist/runtime/stealth-cookies.js +111 -0
  141. package/dist/runtime/stealth.d.ts +41 -5
  142. package/dist/runtime/stealth.js +997 -291
  143. package/dist/runtime/stt.js +1 -12
  144. package/dist/runtime/timeout.d.ts +5 -0
  145. package/dist/runtime/timeout.js +12 -0
  146. package/dist/runtime/trace-config.d.ts +12 -0
  147. package/dist/runtime/trace-config.js +62 -0
  148. package/dist/runtime/trace.d.ts +5 -0
  149. package/dist/runtime/trace.js +43 -10
  150. package/dist/serve.d.ts +1 -1
  151. package/dist/serve.js +1 -1
  152. package/dist/server/error-observability.d.ts +1 -0
  153. package/dist/server/error-observability.js +1 -0
  154. package/dist/server/index.d.ts +5 -2
  155. package/dist/server/index.js +2 -2
  156. package/dist/server/self-test-input-tokens.d.ts +2 -1
  157. package/dist/server/self-test-input-tokens.js +18 -14
  158. package/dist/server/self-test.d.ts +14 -3
  159. package/dist/server/self-test.js +129 -58
  160. package/dist/server/serve-implementation.d.ts +230 -0
  161. package/dist/server/serve-implementation.js +2265 -0
  162. package/dist/server/serve.d.ts +1 -70
  163. package/dist/server/serve.js +1 -1131
  164. package/dist/server/trace-output.d.ts +6 -0
  165. package/dist/server/trace-output.js +79 -0
  166. package/dist/server/types.d.ts +30 -5
  167. package/dist/server/types.js +13 -1
  168. package/dist/stateful/errors.d.ts +19 -0
  169. package/dist/stateful/errors.js +24 -0
  170. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  171. package/dist/stateful/http-provider-event-emitter.js +237 -0
  172. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  173. package/dist/stateful/http-session-owner-registry.js +210 -0
  174. package/dist/stateful/index.d.ts +18 -0
  175. package/dist/stateful/index.js +18 -0
  176. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  177. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  178. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  179. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  180. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  181. package/dist/stateful/provider-event-pipeline.js +1 -0
  182. package/dist/stateful/provider-events.d.ts +101 -0
  183. package/dist/stateful/provider-events.js +289 -0
  184. package/dist/stateful/session-key.d.ts +15 -0
  185. package/dist/stateful/session-key.js +86 -0
  186. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  187. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  188. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  189. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  190. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  191. package/dist/stateful/stateful-provider-adapter.js +287 -0
  192. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  193. package/dist/stateful/stateful-provider-observability.js +161 -0
  194. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  195. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  196. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  197. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  198. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  199. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  200. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  201. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  202. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  203. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  204. package/dist/stateful-signing.d.ts +18 -0
  205. package/dist/stateful-signing.js +27 -0
  206. package/dist/stealth/profiles.d.ts +9 -4
  207. package/dist/stealth/profiles.js +160 -211
  208. package/dist/stream-evidence.d.ts +74 -0
  209. package/dist/stream-evidence.js +785 -0
  210. package/dist/stream.js +7 -1
  211. package/dist/testing/index.d.ts +2 -1
  212. package/dist/testing/index.js +2 -1
  213. package/dist/testing/run.d.ts +32 -2
  214. package/dist/testing/run.js +489 -21
  215. package/dist/trace-sanitization.d.ts +5 -0
  216. package/dist/trace-sanitization.js +45 -0
  217. package/dist/types.d.ts +673 -131
  218. package/dist/types.js +1 -0
  219. package/package.json +45 -5
  220. package/src/auth-turn/index.ts +1 -1
  221. package/src/auth.ts +118 -0
  222. package/src/ceremonies/index.ts +189 -46
  223. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
  224. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
  225. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
  226. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
  227. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +15 -0
  228. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +9 -0
  229. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
  230. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +24 -0
  231. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
  232. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
  233. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
  234. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
  235. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
  236. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
  237. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
  238. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
  239. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
  240. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
  241. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
  242. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
  243. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
  244. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
  245. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
  246. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
  247. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
  248. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
  249. package/src/cli/commands.ts +33 -0
  250. package/src/cli/create.ts +50 -37
  251. package/src/cli/migrate-operation-declaration.ts +1751 -0
  252. package/src/cli/migrate-operation-shape.ts +184 -0
  253. package/src/cli/migrate-provider-shape.ts +772 -0
  254. package/src/cli/prompt-assets.ts +865 -0
  255. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  256. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  257. package/src/cli/templates/provider/README.md.tpl +5 -5
  258. package/src/cli/templates/provider/index.ts.tpl +6 -3
  259. package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
  260. package/src/cli/templates/provider/provider.json.tpl +6 -0
  261. package/src/config/loader.ts +690 -163
  262. package/src/contract-serialization.ts +5 -7
  263. package/src/contract-types.ts +12 -5
  264. package/src/contract.ts +24 -10
  265. package/src/declaration-validation.ts +330 -0
  266. package/src/define.ts +1048 -131
  267. package/src/dev.ts +4 -1
  268. package/src/engine.ts +279 -0
  269. package/src/error-observability.ts +64 -0
  270. package/src/error-resolution.ts +127 -0
  271. package/src/errors.ts +68 -0
  272. package/src/fixture-sanitization.ts +264 -0
  273. package/src/health-scenario.ts +875 -0
  274. package/src/index.ts +220 -13
  275. package/src/lint.ts +698 -90
  276. package/src/native-address.ts +340 -0
  277. package/src/native-egress-policy.ts +358 -0
  278. package/src/observability.ts +51 -1
  279. package/src/provider.ts +161 -3
  280. package/src/runtime/auth-flow.ts +12 -0
  281. package/src/runtime/browser.ts +661 -63
  282. package/src/runtime/cache.ts +189 -14
  283. package/src/runtime/choice-wordlist.ts +145 -0
  284. package/src/runtime/choice.ts +631 -120
  285. package/src/runtime/chrome149-header-order.ts +330 -0
  286. package/src/runtime/env.ts +13 -0
  287. package/src/runtime/executor.ts +43 -31
  288. package/src/runtime/http.ts +641 -61
  289. package/src/runtime/insights.ts +2 -2
  290. package/src/runtime/instrumentation.ts +520 -15
  291. package/src/runtime/native-network-errors.ts +99 -0
  292. package/src/runtime/native-network.ts +1605 -0
  293. package/src/runtime/ocr.ts +523 -0
  294. package/src/runtime/otlp.ts +467 -21
  295. package/src/runtime/proxy-errors.ts +12 -4
  296. package/src/runtime/proxy-nodemaven.ts +221 -0
  297. package/src/runtime/proxy-telemetry.ts +244 -75
  298. package/src/runtime/redirects.ts +66 -0
  299. package/src/runtime/redis.ts +7 -2
  300. package/src/runtime/request-options.ts +680 -1
  301. package/src/runtime/resolver-config.ts +6 -0
  302. package/src/runtime/resolver-public.ts +23 -0
  303. package/src/runtime/resolver-shared.ts +33 -0
  304. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  305. package/src/runtime/resolver-vendors/browser.ts +533 -0
  306. package/src/runtime/resolver-vendors/capsolver.ts +707 -0
  307. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  308. package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
  309. package/src/runtime/resolver-vendors/types.ts +212 -0
  310. package/src/runtime/resolver.ts +1157 -0
  311. package/src/runtime/secrets.ts +64 -0
  312. package/src/runtime/state.ts +394 -77
  313. package/src/runtime/stealth-cookies.ts +132 -0
  314. package/src/runtime/stealth.ts +1280 -336
  315. package/src/runtime/stt.ts +1 -19
  316. package/src/runtime/timeout.ts +18 -0
  317. package/src/runtime/trace-config.ts +78 -0
  318. package/src/runtime/trace.ts +57 -17
  319. package/src/serve.ts +6 -1
  320. package/src/server/error-observability.ts +1 -0
  321. package/src/server/index.ts +34 -2
  322. package/src/server/self-test-input-tokens.ts +29 -14
  323. package/src/server/self-test.ts +191 -68
  324. package/src/server/serve-implementation.ts +3378 -0
  325. package/src/server/serve.ts +1 -1632
  326. package/src/server/trace-output.ts +129 -0
  327. package/src/server/types.ts +13 -1
  328. package/src/stateful/README.md +146 -0
  329. package/src/stateful/errors.ts +35 -0
  330. package/src/stateful/http-provider-event-emitter.ts +314 -0
  331. package/src/stateful/http-session-owner-registry.ts +306 -0
  332. package/src/stateful/index.ts +18 -0
  333. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  334. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  335. package/src/stateful/provider-event-pipeline.ts +61 -0
  336. package/src/stateful/provider-events.ts +462 -0
  337. package/src/stateful/session-key.ts +111 -0
  338. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  339. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  340. package/src/stateful/stateful-provider-adapter.ts +562 -0
  341. package/src/stateful/stateful-provider-observability.ts +261 -0
  342. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  343. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  344. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  345. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  346. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  347. package/src/stateful-signing.ts +46 -0
  348. package/src/stealth/profiles.ts +202 -231
  349. package/src/stream-evidence.ts +988 -0
  350. package/src/stream.ts +8 -1
  351. package/src/testing/index.ts +10 -1
  352. package/src/testing/run.ts +658 -15
  353. package/src/trace-sanitization.ts +63 -0
  354. package/src/types.ts +788 -174
  355. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  356. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  357. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  358. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  359. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  360. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  361. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  362. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  363. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  364. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  365. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  366. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  367. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  368. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env bun
2
- // @ts-nocheck
3
2
 
4
3
  import { existsSync, readFileSync, statSync } from "node:fs";
5
4
  import { mkdir, writeFile } from "node:fs/promises";
@@ -9,19 +8,63 @@ import { pathToFileURL } from "node:url";
9
8
  import {
10
9
  createBypassProviderCache,
11
10
  createHttpClient,
11
+ createInProcessProviderEngine,
12
+ createOcrClientFromEnv,
12
13
  createProviderChoiceContext,
13
- createStealthClient,
14
+ createProviderEnvironment,
14
15
  createSttClientFromEnv,
16
+ createUnsupportedResolverClient,
15
17
  executeOperation,
16
18
  type HttpClient,
19
+ type HttpResponse,
20
+ type HttpStreamResponse,
17
21
  type ProviderContext,
18
22
  type ProviderDefinition,
23
+ type ProviderEngineBindingCandidates,
19
24
  ProviderError,
25
+ readEngineProxyCredentials,
26
+ type ProviderProxyPolicy,
27
+ type RequestOptions,
20
28
  type StealthClient,
21
29
  TransportError,
22
30
  ValidationError,
23
31
  } from "../src/index.js";
32
+ import type { JsonValue } from "../src/contract-json.js";
33
+ import {
34
+ isSensitiveFixtureKey,
35
+ requestPathForFixture,
36
+ sanitizeDiagnosticText,
37
+ sanitizeFixtureString,
38
+ } from "../src/fixture-sanitization.js";
39
+ import { createResolverClientFromEnv } from "../src/runtime/resolver.js";
40
+ import {
41
+ isSensitiveKey,
42
+ normalizeSensitiveParams,
43
+ parseHttpRequestInvocation,
44
+ REDACTED_QUERY_VALUE,
45
+ redactSensitiveError,
46
+ redactSensitiveText,
47
+ redactUrlQueryParams,
48
+ replaceRequestOptionsInHttpInvocation,
49
+ requestOptionsFromHttpInvocation,
50
+ serializeRequestUrl,
51
+ } from "../src/runtime/request-options.js";
24
52
  import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
53
+ import { createStealthClient } from "../src/runtime/stealth.js";
54
+ import { parseSchema } from "../src/schema.js";
55
+ import { getStealthProfile } from "../src/stealth/profiles.js";
56
+ import {
57
+ captureStreamEvidence,
58
+ createStreamCaptureEnvelope,
59
+ findStreamCaptureGroup,
60
+ findStreamEvidenceRecords,
61
+ hasStreamEvidenceMarker,
62
+ parseStreamEvidenceRecord,
63
+ STREAM_PREVIEW_BYTES,
64
+ type StreamCaptureGroupItem,
65
+ type StreamEvidenceCapture,
66
+ type StreamEvidenceRequest,
67
+ } from "../src/stream-evidence.js";
25
68
 
26
69
  type CliArgs = {
27
70
  append: boolean;
@@ -39,57 +82,95 @@ const HELP_TEXT = `Usage: apifuse record [path] --operation <operation> --params
39
82
 
40
83
  Calls a real upstream-backed operation through ctx.http or ctx.stealth and writes __fixtures__/raw.json.
41
84
 
85
+ Streaming responses are recorded as evidence (status, selected headers, full-body SHA-256 and byte
86
+ count, plus a ${STREAM_PREVIEW_BYTES}-byte base64 preview). Test replay is evidence-only: ctx.http.stream exposes the
87
+ preview as its body and the original body_sha256/body_bytes as response metadata.
88
+ When an operation opens multiple streams, all evidence records are saved in stream call order.
89
+ Mixed JSON/stream operations save a tagged call-ordered envelope so snapshot replay can route each response.
90
+ ctx.http.sse() recording is unsupported and fails explicitly.
91
+
42
92
  Options:
43
93
  --operation, -o <name> operation to call
44
94
  --params, -p <json> JSON input passed to the operation (default: {})
45
- --append append to an existing array fixture
95
+ --append preserve the existing fixture and append this capture
46
96
  --sanitize redact common token/header fields (default)
47
- --no-sanitize write the captured upstream payload as-is
97
+ --no-sanitize disable common-field redaction (sensitiveParams are always redacted)
48
98
  --help, -h show this help
49
99
 
50
100
  Example:
51
101
  apifuse record providers/korea-air-quality --operation realtime --params '{"stationName":"jongno"}'`;
52
102
 
53
103
  export async function main() {
104
+ let capture: ReturnType<typeof createCaptureContext> | undefined;
54
105
  try {
55
106
  const args = parseArgs(normalizeArgs(process.argv.slice(2)));
56
107
  const location = resolveProviderLocation(args.providerPath);
57
108
  const provider = await loadProvider(location.rootDir);
58
109
  const operationName = resolveOperationName(provider, args.operation);
59
110
  const operation = provider.operations[operationName];
60
- const parsedParams = parseParams(operation, args.params);
111
+ const parsedParams = await parseParams(operation, args.params);
61
112
 
62
- const capture = createCaptureContext(
113
+ capture = createCaptureContext(
63
114
  provider,
64
115
  resolveOperationBaseUrl(provider, operationName),
116
+ args.sanitize,
65
117
  );
66
118
 
67
119
  console.log(`[apifuse record] Calling ${operationName} on ${provider.id}...`);
68
120
 
69
- const result = await executeOperation(provider, operationName, capture.ctx, parsedParams);
70
- const captured = capture.getCapturedRaw();
121
+ let result: unknown;
122
+ try {
123
+ result = await executeOperation(provider, operationName, capture.ctx, parsedParams);
124
+ } catch (operationError) {
125
+ let partial: unknown;
126
+ try {
127
+ partial = await capture.getCapturedRaw();
128
+ } catch (finalizationError) {
129
+ throw new StreamRecorderError("Operation and stream finalization both failed.", [
130
+ operationError,
131
+ finalizationError,
132
+ ]);
133
+ }
134
+ const streamCount = findStreamEvidenceRecords(partial).length;
135
+ if (streamCount > 0) {
136
+ throw new StreamRecorderError(
137
+ `Operation failed after finalizing ${streamCount} stream capture${streamCount === 1 ? "" : "s"}.`,
138
+ [operationError],
139
+ );
140
+ }
141
+ throw operationError;
142
+ }
143
+ const captured = await capture.getCapturedRaw();
71
144
 
72
145
  if (captured === undefined) {
73
146
  throw new Error(`No upstream response was captured for ${provider.id}.${operationName}.`);
74
147
  }
75
148
 
76
- const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
149
+ const sensitiveParams = capture.getCapturedSensitiveParams();
150
+ const rawPayload = jsonFixtureValue(captured);
77
151
  const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
78
- const nextPayload = await prepareFixturePayload(fixturePath, rawPayload, args.append);
152
+ const redactedCapture = redactFixture(rawPayload, sensitiveParams, args.sanitize);
153
+ const mergedPayload = await prepareFixturePayload(fixturePath, redactedCapture, args.append);
154
+ // Mandatory query-secret redaction applies to the merged history, including
155
+ // values discovered in older declared-key URL positions. Optional common-
156
+ // field sanitization applies only to this run's new capture so --append does
157
+ // not rewrite deliberately preserved historical fields.
158
+ const historicalSensitiveParams = discoverSensitiveQueryValues(mergedPayload, sensitiveParams);
159
+ const nextPayload = redactFixture(mergedPayload, historicalSensitiveParams, false);
79
160
 
80
161
  await mkdir(dirname(fixturePath), { recursive: true });
81
162
  await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
82
163
 
83
164
  console.log(
84
165
  `[apifuse record] Captured response (${formatBytes(
85
- Buffer.byteLength(JSON.stringify(rawPayload)),
166
+ Buffer.byteLength(JSON.stringify(redactedCapture)),
86
167
  )})`,
87
168
  );
88
169
  console.log(`[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`);
89
170
 
90
171
  void result;
91
172
  } catch (error) {
92
- handleCliError(error);
173
+ handleCliError(error, capture?.getCapturedSensitiveParams().values);
93
174
  }
94
175
  }
95
176
 
@@ -174,13 +255,29 @@ function parseArgs(argv: string[]): CliArgs {
174
255
  return { append, providerPath, operation, params, sanitize };
175
256
  }
176
257
 
177
- function handleCliError(error: unknown): never {
178
- const message = formatCliError(error);
258
+ function handleCliError(error: unknown, sensitiveValues: readonly string[] = []): never {
259
+ const message = redactSensitiveText(formatCliError(error), sensitiveValues);
179
260
  console.error(`[apifuse record] ${message}`);
180
261
  process.exit(1);
181
262
  }
182
263
 
183
- function formatCliError(error: unknown): string {
264
+ class StreamRecorderError extends Error {
265
+ readonly diagnosticCauses: readonly unknown[];
266
+
267
+ constructor(message: string, diagnosticCauses: readonly unknown[]) {
268
+ super(message, { cause: diagnosticCauses[0] });
269
+ this.name = "StreamRecorderError";
270
+ this.diagnosticCauses = diagnosticCauses;
271
+ }
272
+ }
273
+
274
+ export function formatCliError(error: unknown): string {
275
+ if (error instanceof StreamRecorderError) {
276
+ return [
277
+ sanitizeDiagnosticText(error.message),
278
+ ...error.diagnosticCauses.map((cause) => `cause=${formatDiagnosticCause(cause)}`),
279
+ ].join(" ");
280
+ }
184
281
  if (error instanceof TransportError) {
185
282
  return [
186
283
  error.message,
@@ -201,12 +298,32 @@ function formatCliError(error: unknown): string {
201
298
  }
202
299
 
203
300
  if (error instanceof Error) {
301
+ if (/^Stream capture\b/.test(error.message) && error.cause !== undefined) {
302
+ return `${sanitizeDiagnosticText(error.message)} cause=${formatDiagnosticCause(error.cause)}`;
303
+ }
204
304
  return error.message;
205
305
  }
206
306
 
207
307
  return String(error);
208
308
  }
209
309
 
310
+ function formatDiagnosticCause(cause: unknown): string {
311
+ if (cause instanceof StreamRecorderError) {
312
+ return [
313
+ sanitizeDiagnosticText(cause.message),
314
+ ...cause.diagnosticCauses.map((nested) => `cause=${formatDiagnosticCause(nested)}`),
315
+ ].join(" ");
316
+ }
317
+ if (!(cause instanceof Error)) return sanitizeDiagnosticText(String(cause));
318
+ const code =
319
+ "code" in cause && typeof cause.code === "string"
320
+ ? ` code=${sanitizeDiagnosticText(cause.code)}`
321
+ : "";
322
+ const nestedCause =
323
+ cause.cause === undefined ? "" : ` cause=${formatDiagnosticCause(cause.cause)}`;
324
+ return `${sanitizeDiagnosticText(cause.message)}${code}${nestedCause}`;
325
+ }
326
+
210
327
  function resolveProviderLocation(inputPath?: string) {
211
328
  const originalInput = inputPath ?? process.cwd();
212
329
  const resolvedInput = resolve(process.cwd(), originalInput);
@@ -284,7 +401,10 @@ function resolveOperationName(provider: ProviderRuntime, operationName?: string)
284
401
  return firstOperation;
285
402
  }
286
403
 
287
- function parseParams(operation: ProviderRuntime["operations"][string], value: string): unknown {
404
+ async function parseParams(
405
+ operation: ProviderRuntime["operations"][string],
406
+ value: string,
407
+ ): Promise<unknown> {
288
408
  let parsed: unknown;
289
409
 
290
410
  try {
@@ -295,7 +415,7 @@ function parseParams(operation: ProviderRuntime["operations"][string], value: st
295
415
  );
296
416
  }
297
417
 
298
- return operation.input ? operation.input.parse(parsed) : parsed;
418
+ return operation.input ? parseSchema(operation.input, parsed, "record.params") : parsed;
299
419
  }
300
420
 
301
421
  function resolveOperationBaseUrl(provider: ProviderRuntime, operationName: string): string {
@@ -309,19 +429,99 @@ function resolveOperationBaseUrl(provider: ProviderRuntime, operationName: strin
309
429
  return baseUrl;
310
430
  }
311
431
 
312
- function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
313
- let capturedRaw: unknown;
432
+ function resolveNativeProxyPolicy(provider: ProviderDefinition): ProviderProxyPolicy | undefined {
433
+ if (typeof provider.proxy === "object") return provider.proxy;
434
+ if (provider.proxy === true) return { mode: "optional" };
435
+ if (provider.proxy === false) return { mode: "disabled" };
436
+ return undefined;
437
+ }
314
438
 
315
- const http = proxyHttpClient(createHttpClient(baseUrl), (response) => {
316
- capturedRaw = response.data;
439
+ export function createCaptureContext(
440
+ provider: ProviderRuntime,
441
+ baseUrl: string,
442
+ sanitize: boolean,
443
+ ) {
444
+ let nextCaptureOrder = 0;
445
+ let nextStreamOrdinal = 0;
446
+ let capturedRaw: JsonValue | undefined;
447
+ const rawCaptures: Array<{ order: number; value: JsonValue }> = [];
448
+ const streamCaptures: Array<{
449
+ order: number;
450
+ request: StreamEvidenceRequest;
451
+ capture: StreamEvidenceCapture;
452
+ }> = [];
453
+ let capturedSse: { order: number; method: string; path: string } | undefined;
454
+ const sensitiveParamNames = new Set<string>();
455
+ const sensitiveParamValues = new Set<string>();
456
+ const captureSensitiveParams = (url: string, options?: SensitiveRequestOptions) => {
457
+ captureSensitiveRequestValues(url, options, sensitiveParamNames, sensitiveParamValues);
458
+ };
459
+ const getCapturedSensitiveParams = (): CapturedSensitiveParams => ({
460
+ names: [...sensitiveParamNames],
461
+ values: [...sensitiveParamValues],
317
462
  });
318
- const stealth = proxyStealthClient(createStealthClient(baseUrl), (response) => {
319
- capturedRaw = normalizeCapturedStealthResponse(response);
463
+ const reserveCaptureOrder = () => {
464
+ nextCaptureOrder += 1;
465
+ return nextCaptureOrder;
466
+ };
467
+ const retainRawCapture = (order: number, value: unknown) => {
468
+ const json = jsonFixtureValue(value);
469
+ capturedRaw = json;
470
+ rawCaptures.push({ order, value: json });
471
+ };
472
+
473
+ const http = captureHttpClient(createHttpClient(baseUrl), {
474
+ reserveOrder: reserveCaptureOrder,
475
+ reserveStreamOrdinal: () => {
476
+ nextStreamOrdinal += 1;
477
+ return nextStreamOrdinal;
478
+ },
479
+ onSensitiveParams: captureSensitiveParams,
480
+ onResponse: (order, response) => retainRawCapture(order, response.data),
481
+ onStreamResponse: (order, ordinal, requestUrl, method, response) => {
482
+ const resolvedRequestUrl = new URL(requestUrl, baseUrl).toString();
483
+ const request = {
484
+ ordinal,
485
+ method,
486
+ path: requestPathForFixture(resolvedRequestUrl),
487
+ };
488
+ const capture = captureStreamEvidence(response, {
489
+ requestUrl: resolvedRequestUrl,
490
+ request,
491
+ ...(sanitize
492
+ ? {
493
+ sanitizeFixture: (value: JsonValue) =>
494
+ jsonFixtureValue(sanitizeStreamFixture(value, getCapturedSensitiveParams())),
495
+ }
496
+ : {}),
497
+ });
498
+ streamCaptures.push({ order, request, capture });
499
+ return capture.response;
500
+ },
501
+ onSseResponse: (order, requestUrl, method) => {
502
+ capturedSse = {
503
+ order,
504
+ method,
505
+ path: requestPathForFixture(new URL(requestUrl, baseUrl).toString()),
506
+ };
507
+ },
320
508
  });
509
+ const stealth = proxyStealthClient(
510
+ createStealthClient(baseUrl, {
511
+ ...(provider.stealth ? { stealth: provider.stealth } : {}),
512
+ }),
513
+ captureSensitiveParams,
514
+ (order, response) => retainRawCapture(order, normalizeCapturedStealthResponse(response)),
515
+ reserveCaptureOrder,
516
+ );
321
517
 
322
- const env = {
323
- get: (key: string) => process.env[key],
324
- };
518
+ const providerEnvironment = createProviderEnvironment(
519
+ process.env,
520
+ provider.secrets?.map((secret) => secret.name) ?? [],
521
+ );
522
+ const env = { get: (key: string) => providerEnvironment[key] };
523
+ const engineEnv = { get: (key: string) => process.env[key] };
524
+ const engineCredentials = readEngineProxyCredentials();
325
525
  const credential = {
326
526
  mode: "none" as const,
327
527
  get: () => undefined,
@@ -330,12 +530,15 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
330
530
  getScopes: () => [],
331
531
  };
332
532
  const state = createMemoryProviderRuntimeState();
333
- const ctx: ProviderContext = {
533
+ const cache = createBypassProviderCache({ providerId: provider.id });
534
+ const proxyPolicy = resolveNativeProxyPolicy(provider);
535
+ const stealthProfile = provider.stealth ? getStealthProfile(provider.stealth) : undefined;
536
+ const candidates: ProviderEngineBindingCandidates = {
334
537
  env,
335
538
  credential,
336
539
  request: { headers: {} },
337
540
  http,
338
- cache: createBypassProviderCache({ providerId: provider.id }),
541
+ cache,
339
542
  state,
340
543
  stealth,
341
544
  browser: {
@@ -362,70 +565,292 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
362
565
  throw new Error("Auth prompts are not available in apifuse record.");
363
566
  },
364
567
  },
568
+ ocr: createOcrClientFromEnv(provider.ocr),
365
569
  stt: createSttClientFromEnv(provider.stt),
570
+ resolver: provider.resolver
571
+ ? createResolverClientFromEnv(provider.resolver, engineCredentials, {
572
+ allowedHosts: provider.allowedHosts,
573
+ cache,
574
+ ...(proxyPolicy
575
+ ? {
576
+ proxyIntent: {
577
+ mode: proxyPolicy.mode,
578
+ upstream: { proxy: provider.proxy },
579
+ ...(stealthProfile ? { userAgent: stealthProfile.userAgent } : {}),
580
+ },
581
+ }
582
+ : {}),
583
+ })
584
+ : createUnsupportedResolverClient("Provider does not declare resolver capability"),
366
585
  choice: createProviderChoiceContext({
367
586
  providerId: provider.id,
368
- env,
587
+ env: engineEnv,
369
588
  request: { headers: {} },
370
589
  credential,
371
590
  state,
372
591
  }),
373
592
  };
593
+ const ctx = createInProcessProviderEngine().attach({
594
+ provider,
595
+ bindings: candidates,
596
+ }) as ProviderContext;
374
597
 
375
598
  return {
376
599
  ctx,
377
- getCapturedRaw: () => capturedRaw,
600
+ getCapturedRaw: async () => {
601
+ if (streamCaptures.length === 0) {
602
+ if (capturedSse) throw unsupportedSseCaptureError(capturedSse);
603
+ return capturedRaw;
604
+ }
605
+
606
+ const settled = await Promise.allSettled(
607
+ streamCaptures.map(async ({ order, request, capture }) => ({
608
+ order,
609
+ request,
610
+ value: redactStreamEvidence(await capture.getEvidence(), getCapturedSensitiveParams()),
611
+ })),
612
+ );
613
+ const failures = settled.flatMap((result, index) =>
614
+ result.status === "rejected"
615
+ ? [
616
+ new StreamRecorderError(
617
+ `Stream finalization failed: method=${streamCaptures[index]!.request.method} path=${streamCaptures[index]!.request.path} ordinal=${streamCaptures[index]!.request.ordinal}.`,
618
+ [result.reason],
619
+ ),
620
+ ]
621
+ : [],
622
+ );
623
+ if (failures.length > 0) {
624
+ throw new StreamRecorderError(
625
+ `${failures.length} stream capture${failures.length === 1 ? "" : "s"} failed to finalize.`,
626
+ failures,
627
+ );
628
+ }
629
+ const evidence = settled.flatMap((result) =>
630
+ result.status === "fulfilled" ? [result.value] : [],
631
+ );
632
+ if (capturedSse) throw unsupportedSseCaptureError(capturedSse);
633
+ const timeline: StreamCaptureGroupItem[] = [...rawCaptures, ...evidence]
634
+ .sort((left, right) => left.order - right.order)
635
+ .map((item) =>
636
+ "request" in item
637
+ ? { kind: "stream" as const, evidence: item.value }
638
+ : { kind: "response" as const, value: item.value },
639
+ );
640
+ return createStreamCaptureEnvelope(timeline);
641
+ },
642
+ getCapturedSensitiveParams,
378
643
  };
379
644
  }
380
645
 
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);
646
+ type CapturedSensitiveParams = {
647
+ names: readonly string[];
648
+ values: readonly string[];
649
+ };
388
650
 
389
- if (typeof value !== "function") {
390
- return value;
391
- }
651
+ type SensitiveRequestOptions = Pick<RequestOptions, "params" | "sensitiveParams">;
652
+
653
+ function captureSensitiveRequestValues(
654
+ url: string,
655
+ options: SensitiveRequestOptions | undefined,
656
+ names: Set<string>,
657
+ values: Set<string>,
658
+ ): void {
659
+ const sensitiveParams = normalizeSensitiveParams(options?.sensitiveParams);
660
+ if (sensitiveParams === undefined) return;
661
+ if (!sensitiveParams || typeof sensitiveParams !== "object" || Array.isArray(sensitiveParams)) {
662
+ throw new TypeError("sensitiveParams must be an object whose values are strings.");
663
+ }
392
664
 
393
- return async (...args: unknown[]) => {
394
- const response = await value.apply(target, args);
395
- onResponse(response);
396
- return response;
397
- };
665
+ const entries = Object.entries(sensitiveParams);
666
+ for (const [key, value] of entries) {
667
+ if (typeof value !== "string") {
668
+ throw new TypeError(`sensitiveParams.${key} must be a string.`);
669
+ }
670
+ names.add(key);
671
+ if (value !== "") values.add(value);
672
+ }
673
+
674
+ let serializedUrl: ReturnType<typeof serializeRequestUrl>;
675
+ try {
676
+ const absoluteUrl = new URL(String(url), "http://apifuse.invalid").toString();
677
+ serializedUrl = serializeRequestUrl(absoluteUrl, options?.params, sensitiveParams);
678
+ } catch (error) {
679
+ const structural = redactUrlQueryParams(String(url), [...names]);
680
+ const safeUrl = redactSensitiveText(structural.redactedUrl, [
681
+ ...values,
682
+ ...structural.sensitiveValues,
683
+ ]);
684
+ const causeKind = error instanceof Error ? error.name : typeof error;
685
+ throw new TypeError(`Cannot securely record sensitiveParams for "${safeUrl}" (${causeKind}).`, {
686
+ cause: redactSensitiveError(
687
+ error,
688
+ [...values, ...structural.sensitiveValues],
689
+ String(url),
690
+ structural.redactedUrl,
691
+ ),
692
+ });
693
+ }
694
+
695
+ for (const value of serializedUrl.sensitiveValues) {
696
+ if (value !== "") values.add(value);
697
+ }
698
+ }
699
+
700
+ function snapshotRequestOptions<T extends SensitiveRequestOptions>(options: T): T {
701
+ return {
702
+ ...options,
703
+ ...(options.params
704
+ ? {
705
+ params: Object.fromEntries(
706
+ Object.entries(options.params).map(([key, value]) => [
707
+ key,
708
+ Array.isArray(value) ? [...value] : value,
709
+ ]),
710
+ ),
711
+ }
712
+ : {}),
713
+ ...(normalizeSensitiveParams(options.sensitiveParams)
714
+ ? { sensitiveParams: { ...options.sensitiveParams } }
715
+ : {}),
716
+ };
717
+ }
718
+
719
+ type HttpCaptureCallbacks = {
720
+ reserveOrder(): number;
721
+ reserveStreamOrdinal(): number;
722
+ onSensitiveParams(url: string, options?: RequestOptions): void;
723
+ onResponse(order: number, response: HttpResponse): void;
724
+ onStreamResponse(
725
+ order: number,
726
+ ordinal: number,
727
+ requestUrl: string,
728
+ method: string,
729
+ response: HttpStreamResponse,
730
+ ): HttpStreamResponse;
731
+ onSseResponse(order: number, requestUrl: string, method: string): void;
732
+ };
733
+
734
+ function captureHttpClient(client: HttpClient, callbacks: HttpCaptureCallbacks): HttpClient {
735
+ const captureResponse = async (
736
+ order: number,
737
+ responsePromise: Promise<HttpResponse>,
738
+ ): Promise<HttpResponse> => {
739
+ const response = await responsePromise;
740
+ callbacks.onResponse(order, response);
741
+ return response;
742
+ };
743
+ const captureRequestOptions = (method: PropertyKey, args: unknown[]) => {
744
+ const invocation = parseHttpRequestInvocation(method, args);
745
+ const options = invocation ? requestOptionsFromHttpInvocation(invocation) : undefined;
746
+ if (!invocation || !options) return;
747
+ const snapshot = snapshotRequestOptions(options);
748
+ callbacks.onSensitiveParams(String(args[0]), snapshot);
749
+ replaceRequestOptionsInHttpInvocation(invocation, snapshot);
750
+ };
751
+
752
+ return {
753
+ request: (...args: Parameters<HttpClient["request"]>) => {
754
+ captureRequestOptions("request", args);
755
+ return captureResponse(callbacks.reserveOrder(), client.request(...args));
756
+ },
757
+ get: (...args: Parameters<HttpClient["get"]>) => {
758
+ captureRequestOptions("get", args);
759
+ return captureResponse(callbacks.reserveOrder(), client.get(...args));
760
+ },
761
+ post: (...args: Parameters<HttpClient["post"]>) => {
762
+ captureRequestOptions("post", args);
763
+ return captureResponse(callbacks.reserveOrder(), client.post(...args));
764
+ },
765
+ put: (...args: Parameters<HttpClient["put"]>) => {
766
+ captureRequestOptions("put", args);
767
+ return captureResponse(callbacks.reserveOrder(), client.put(...args));
768
+ },
769
+ delete: (...args: Parameters<HttpClient["delete"]>) => {
770
+ captureRequestOptions("delete", args);
771
+ return captureResponse(callbacks.reserveOrder(), client.delete(...args));
772
+ },
773
+ stream: async (...args: Parameters<HttpClient["stream"]>) => {
774
+ captureRequestOptions("stream", args);
775
+ const order = callbacks.reserveOrder();
776
+ const ordinal = callbacks.reserveStreamOrdinal();
777
+ const method = (args[1]?.method ?? "GET").toUpperCase();
778
+ const response = await client.stream(...args);
779
+ return callbacks.onStreamResponse(order, ordinal, args[0], method, response);
780
+ },
781
+ sse: async (...args: Parameters<HttpClient["sse"]>) => {
782
+ captureRequestOptions("sse", args);
783
+ const order = callbacks.reserveOrder();
784
+ const response = await client.sse(...args);
785
+ callbacks.onSseResponse(order, args[0], (args[1]?.method ?? "GET").toUpperCase());
786
+ return response;
398
787
  },
399
- }) as HttpClient;
788
+ };
400
789
  }
401
790
 
402
791
  type StealthSession = ReturnType<StealthClient["createSession"]>;
403
792
 
404
793
  function proxyStealthClient(
405
794
  client: StealthClient,
406
- onResponse: (response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
795
+ onSensitiveParams: (url: string, options?: SensitiveRequestOptions) => void,
796
+ onResponse: (order: number, response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
797
+ reserveOrder: () => number,
407
798
  ): StealthClient {
408
799
  return {
409
800
  fetch: async (...args: Parameters<StealthClient["fetch"]>) => {
801
+ const order = reserveOrder();
802
+ if (args[1]) args[1] = snapshotRequestOptions(args[1]);
803
+ onSensitiveParams(args[0], args[1]);
410
804
  const response = await client.fetch(...args);
411
- onResponse(response);
805
+ if (response.url) onSensitiveParams(response.url, args[1]);
806
+ onResponse(order, response);
412
807
  return response;
413
808
  },
414
809
  createSession: (...args: Parameters<StealthClient["createSession"]>) =>
415
- proxyStealthSession(client.createSession(...args), onResponse),
810
+ proxyStealthSession(
811
+ client.createSession(...args),
812
+ onSensitiveParams,
813
+ onResponse,
814
+ reserveOrder,
815
+ ),
416
816
  };
417
817
  }
418
818
 
419
819
  function proxyStealthSession(
420
820
  session: StealthSession,
421
- onResponse: (response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
821
+ onSensitiveParams: (url: string, options?: SensitiveRequestOptions) => void,
822
+ onResponse: (order: number, response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
823
+ reserveOrder: () => number,
422
824
  ): StealthSession {
423
825
  return {
424
826
  fetch: async (...args: Parameters<StealthSession["fetch"]>) => {
827
+ const order = reserveOrder();
828
+ if (args[1]) args[1] = snapshotRequestOptions(args[1]);
829
+ onSensitiveParams(args[0], args[1]);
425
830
  const response = await session.fetch(...args);
426
- onResponse(response);
831
+ if (response.url) onSensitiveParams(response.url, args[1]);
832
+ onResponse(order, response);
427
833
  return response;
428
834
  },
835
+ cookies: session.cookies,
836
+ redirects: {
837
+ run: async (...args: Parameters<StealthSession["redirects"]["run"]>) => {
838
+ const order = reserveOrder();
839
+ args[0] = snapshotRequestOptions(args[0]);
840
+ onSensitiveParams(args[0].url, args[0]);
841
+ const callerStopWhen = args[0].stopWhen;
842
+ args[0].stopWhen = async (hop) => {
843
+ for (const url of [hop.url, hop.location, hop.nextUrl]) {
844
+ if (url) onSensitiveParams(url, args[0]);
845
+ }
846
+ return callerStopWhen ? await callerStopWhen(hop) : false;
847
+ };
848
+ const result = await session.redirects.run(...args);
849
+ if (result.final.url) onSensitiveParams(result.final.url, args[0]);
850
+ onResponse(order, result.final);
851
+ return result;
852
+ },
853
+ },
429
854
  close: () => session.close(),
430
855
  };
431
856
  }
@@ -438,31 +863,193 @@ function normalizeCapturedStealthResponse(response: Awaited<ReturnType<StealthCl
438
863
  }
439
864
  }
440
865
 
441
- function sanitizeFixture(value: unknown): unknown {
866
+ function redactStreamEvidence(
867
+ evidence: Awaited<ReturnType<StreamEvidenceCapture["getEvidence"]>>,
868
+ sensitiveParams: CapturedSensitiveParams,
869
+ ): Awaited<ReturnType<StreamEvidenceCapture["getEvidence"]>> {
870
+ return parseStreamEvidenceRecord(redactFixture(evidence, sensitiveParams, false));
871
+ }
872
+
873
+ function sanitizeStreamFixture(value: unknown, sensitiveParams: CapturedSensitiveParams): unknown {
874
+ if (typeof value === "string") {
875
+ if (value !== "" && sensitiveParams.values.includes(value)) return REDACTED_QUERY_VALUE;
876
+ return sanitizeFixtureString(redactFixtureText(value, sensitiveParams));
877
+ }
442
878
  if (Array.isArray(value)) {
443
- return value.map((item) => sanitizeFixture(item));
879
+ return value.map((item) => sanitizeStreamFixture(item, sensitiveParams));
444
880
  }
445
-
446
881
  if (!value || typeof value !== "object") {
447
- return value;
882
+ return redactFixture(value, sensitiveParams, false);
448
883
  }
449
884
 
450
- const entries = Object.entries(value as MutableRecord).map(([key, entryValue]) => {
451
- if (isSensitiveKey(key)) {
452
- return [key, "[REDACTED]"] as const;
453
- }
885
+ const result: MutableRecord = Object.create(null) as MutableRecord;
886
+ for (const [key, entryValue] of Object.entries(value as MutableRecord)) {
887
+ const redactedKey = sensitiveParams.values.includes(key)
888
+ ? REDACTED_QUERY_VALUE
889
+ : redactFixtureText(key, sensitiveParams);
890
+ const uniqueKey = collisionSafeKey(result, redactedKey);
891
+ result[uniqueKey] =
892
+ isSensitiveFixtureKey(key) && !sensitiveParams.names.includes(key)
893
+ ? REDACTED_QUERY_VALUE
894
+ : sanitizeStreamFixture(entryValue, sensitiveParams);
895
+ }
896
+ return result;
897
+ }
454
898
 
455
- return [key, sanitizeFixture(entryValue)] as const;
456
- });
899
+ function redactStreamPreview(
900
+ bodyPreviewBase64: string,
901
+ sensitiveParams: CapturedSensitiveParams,
902
+ ): { bodyPreviewBase64: string; changed: boolean } {
903
+ const preview = Buffer.from(bodyPreviewBase64, "base64");
904
+ if (preview.byteLength === 0) return { bodyPreviewBase64, changed: false };
905
+
906
+ const text = new TextDecoder().decode(preview);
907
+ let redactedText = redactFixtureText(text, sensitiveParams);
908
+ const trimmed = text.trimEnd();
909
+ try {
910
+ const parsed = JSON.parse(trimmed) as unknown;
911
+ const structurallyRedacted = JSON.stringify(redactFixture(parsed, sensitiveParams, false));
912
+ if (structurallyRedacted !== JSON.stringify(parsed)) redactedText = structurallyRedacted;
913
+ } catch {
914
+ // Non-JSON previews still use the shared free-text sensitive-value policy.
915
+ }
916
+ if (redactedText === text) return { bodyPreviewBase64, changed: false };
917
+
918
+ const fitted = Buffer.alloc(preview.byteLength, 0x20);
919
+ let redactedBytes = Buffer.from(redactedText);
920
+ if (redactedBytes.byteLength > fitted.byteLength) {
921
+ redactedBytes = Buffer.from(REDACTED_QUERY_VALUE);
922
+ }
923
+ redactedBytes.copy(fitted, 0, 0, Math.min(redactedBytes.byteLength, fitted.byteLength));
924
+ return { bodyPreviewBase64: fitted.toString("base64"), changed: true };
925
+ }
926
+
927
+ function redactFixture(
928
+ value: unknown,
929
+ sensitiveParams: CapturedSensitiveParams,
930
+ sanitizeCommonFields: boolean,
931
+ ): unknown {
932
+ if (typeof value === "string") {
933
+ if (value !== "" && sensitiveParams.values.includes(value)) return REDACTED_QUERY_VALUE;
934
+ return redactFixtureText(value, sensitiveParams);
935
+ }
936
+
937
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
938
+ return sensitiveParams.values.includes(String(value)) ? REDACTED_QUERY_VALUE : value;
939
+ }
457
940
 
458
- return Object.fromEntries(entries);
941
+ if (Array.isArray(value)) {
942
+ return value.map((item) => redactFixture(item, sensitiveParams, sanitizeCommonFields));
943
+ }
944
+
945
+ if (!value || typeof value !== "object") return value;
946
+
947
+ const result: MutableRecord = Object.create(null) as MutableRecord;
948
+ for (const [key, entryValue] of Object.entries(value as MutableRecord)) {
949
+ const redactedKey = sensitiveParams.values.includes(key)
950
+ ? REDACTED_QUERY_VALUE
951
+ : redactFixtureText(key, sensitiveParams);
952
+ const uniqueKey = collisionSafeKey(result, redactedKey);
953
+ result[uniqueKey] =
954
+ sanitizeCommonFields &&
955
+ (isSensitiveKey(key) || isSensitiveFixtureKey(key)) &&
956
+ !sensitiveParams.names.includes(key)
957
+ ? REDACTED_QUERY_VALUE
958
+ : redactFixture(entryValue, sensitiveParams, sanitizeCommonFields);
959
+ }
960
+ const record = value as MutableRecord;
961
+ if (record.__apifuse_stream__ === true && typeof record.body_preview_base64 === "string") {
962
+ const preview = redactStreamPreview(record.body_preview_base64, sensitiveParams);
963
+ result.body_preview_base64 = preview.bodyPreviewBase64;
964
+ if (preview.changed) result.preview_sanitized = true;
965
+ }
966
+ return result;
459
967
  }
460
968
 
461
- function isSensitiveKey(key: string): boolean {
462
- return /authorization|token|api[-_]?key/i.test(key);
969
+ function redactFixtureText(text: string, sensitiveParams: CapturedSensitiveParams): string {
970
+ // Shared free-text policy: long values are unconditional substrings; values
971
+ // shorter than four characters require token boundaries. Exact scalar echoes
972
+ // and declared query-key positions are structurally redacted for every length.
973
+ let redacted = redactSensitiveText(text, sensitiveParams.values);
974
+ for (const name of sensitiveParams.names) {
975
+ let componentEncodedName = name;
976
+ try {
977
+ componentEncodedName = encodeURIComponent(name);
978
+ } catch {
979
+ // Lone surrogates remain covered by the raw and form-encoded variants.
980
+ }
981
+ const keyVariants = new Set([
982
+ name,
983
+ componentEncodedName,
984
+ new URLSearchParams({ [name]: "" }).toString().slice(0, -1),
985
+ ]);
986
+ for (const key of keyVariants) {
987
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
988
+ const queryValue = new RegExp(`(^|[?&])(${escapedKey}=)[^&#\\s"]*`, "g");
989
+ redacted = redacted.replace(queryValue, (_match, prefix, assignment) => {
990
+ return `${prefix}${assignment}${REDACTED_QUERY_VALUE}`;
991
+ });
992
+ }
993
+ }
994
+ return redacted;
463
995
  }
464
996
 
465
- async function prepareFixturePayload(
997
+ function discoverSensitiveQueryValues(
998
+ value: unknown,
999
+ sensitiveParams: CapturedSensitiveParams,
1000
+ ): CapturedSensitiveParams {
1001
+ const values = new Set(sensitiveParams.values);
1002
+ const absoluteUrl = /\b[A-Za-z][A-Za-z\d+.-]*:\/\/[^\s<>"']+/g;
1003
+ const discoverFromUrl = (url: string) => {
1004
+ for (const discovered of redactUrlQueryParams(url, sensitiveParams.names).sensitiveValues) {
1005
+ if (discovered !== "" && discovered !== REDACTED_QUERY_VALUE) values.add(discovered);
1006
+ }
1007
+ };
1008
+ const visitText = (text: string) => {
1009
+ if (!/\s/.test(text)) {
1010
+ try {
1011
+ new URL(text, "http://apifuse.invalid");
1012
+ discoverFromUrl(text);
1013
+ return;
1014
+ } catch {
1015
+ // Fall through to extracting absolute URL spans from prose.
1016
+ }
1017
+ }
1018
+ for (const match of text.matchAll(absoluteUrl)) {
1019
+ try {
1020
+ new URL(match[0]);
1021
+ discoverFromUrl(match[0]);
1022
+ } catch {
1023
+ // Ignore URI-like prose that is not a parseable URL.
1024
+ }
1025
+ }
1026
+ };
1027
+ const visit = (current: unknown): void => {
1028
+ if (typeof current === "string") {
1029
+ visitText(current);
1030
+ return;
1031
+ }
1032
+ if (Array.isArray(current)) {
1033
+ for (const item of current) visit(item);
1034
+ return;
1035
+ }
1036
+ if (!current || typeof current !== "object") return;
1037
+ for (const [key, entryValue] of Object.entries(current as MutableRecord)) {
1038
+ visitText(key);
1039
+ visit(entryValue);
1040
+ }
1041
+ };
1042
+ visit(value);
1043
+ return { names: sensitiveParams.names, values: [...values] };
1044
+ }
1045
+
1046
+ function collisionSafeKey(record: MutableRecord, preferredKey: string): string {
1047
+ if (!(preferredKey in record)) return preferredKey;
1048
+ let suffix = 2;
1049
+ while (`${preferredKey}#${suffix}` in record) suffix += 1;
1050
+ return `${preferredKey}#${suffix}`;
1051
+ }
1052
+ export async function prepareFixturePayload(
466
1053
  fixturePath: string,
467
1054
  payload: unknown,
468
1055
  append: boolean,
@@ -471,15 +1058,45 @@ async function prepareFixturePayload(
471
1058
  return payload;
472
1059
  }
473
1060
 
1061
+ let fixtureSource: string;
474
1062
  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.
1063
+ fixtureSource = readFileSync(fixturePath, "utf8");
1064
+ } catch (error) {
1065
+ throw new Error(
1066
+ `Cannot append to existing fixture "${fixturePath}" because it could not be read: ${
1067
+ error instanceof Error ? error.message : String(error)
1068
+ }. Fix its permissions or delete it, then run apifuse record --append again.`,
1069
+ );
481
1070
  }
482
1071
 
1072
+ let existing: unknown;
1073
+ try {
1074
+ existing = JSON.parse(fixtureSource) as unknown;
1075
+ } catch (error) {
1076
+ throw new Error(
1077
+ `Cannot append to corrupt fixture "${fixturePath}" because it is not valid JSON: ${
1078
+ error instanceof Error ? error.message : String(error)
1079
+ }. Fix or delete the fixture, then run apifuse record --append again.`,
1080
+ );
1081
+ }
1082
+
1083
+ if (hasStreamEvidenceMarker(existing)) {
1084
+ const evidence = parseStreamEvidenceRecord(existing);
1085
+ return [createStreamCaptureEnvelope([{ kind: "stream", evidence }]), payload];
1086
+ }
1087
+ if (Array.isArray(existing)) {
1088
+ if (existing.some((item) => hasStreamEvidenceMarker(item))) {
1089
+ const legacyGroup = findStreamCaptureGroup(existing);
1090
+ if (legacyGroup) {
1091
+ const prefix = existing.slice(0, -legacyGroup.items.length);
1092
+ return [...prefix, createStreamCaptureEnvelope(legacyGroup.items), payload];
1093
+ }
1094
+ }
1095
+ return [...existing, payload];
1096
+ }
1097
+ if (existing !== null) {
1098
+ return [existing, payload];
1099
+ }
483
1100
  return payload;
484
1101
  }
485
1102
 
@@ -491,4 +1108,24 @@ function formatBytes(bytes: number): string {
491
1108
  return `${(bytes / 1024).toFixed(1)} KB`;
492
1109
  }
493
1110
 
494
- await main();
1111
+ function jsonFixtureValue(value: unknown): JsonValue {
1112
+ const serialized = JSON.stringify(value);
1113
+ if (serialized === undefined) {
1114
+ throw new Error("Captured upstream response is not JSON-serializable.");
1115
+ }
1116
+ return JSON.parse(serialized) as JsonValue;
1117
+ }
1118
+
1119
+ function unsupportedSseCaptureError(request: {
1120
+ order: number;
1121
+ method: string;
1122
+ path: string;
1123
+ }): Error {
1124
+ return new Error(
1125
+ `apifuse record does not support ctx.http.sse(): method=${request.method} path=${request.path} call=${request.order}.`,
1126
+ );
1127
+ }
1128
+
1129
+ if (import.meta.main) {
1130
+ await main();
1131
+ }