@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.30

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