@apifuse/provider-sdk 2.2.0-beta.4 → 2.2.0-beta.40

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 (303) hide show
  1. package/AUTHORING.md +501 -13
  2. package/CHANGELOG.md +165 -1
  3. package/README.md +72 -14
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +88 -4
  6. package/bin/apifuse-dev.ts +38 -5
  7. package/bin/apifuse-pack-check.ts +22 -2
  8. package/bin/apifuse-pack-smoke.ts +57 -2
  9. package/bin/apifuse-pack-types.ts +356 -38
  10. package/bin/apifuse-perf.ts +14 -13
  11. package/bin/apifuse-record.ts +691 -68
  12. package/bin/apifuse-submit-check.ts +518 -37
  13. package/bin/apifuse-sync-assets.ts +117 -0
  14. package/bin/submit-check-delimited-text.ts +50 -0
  15. package/dist/auth-turn/index.d.ts +3 -3
  16. package/dist/auth-turn/index.js +1 -1
  17. package/dist/auth.d.ts +14 -0
  18. package/dist/auth.js +67 -0
  19. package/dist/ceremonies/index.d.ts +16 -0
  20. package/dist/ceremonies/index.js +141 -36
  21. package/dist/cli/commands.d.ts +1 -1
  22. package/dist/cli/commands.js +8 -0
  23. package/dist/cli/create.d.ts +3 -0
  24. package/dist/cli/create.js +34 -35
  25. package/dist/cli/prompt-assets.d.ts +80 -0
  26. package/dist/cli/prompt-assets.js +743 -0
  27. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  28. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  29. package/dist/cli/templates/provider/README.md.tpl +4 -4
  30. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  31. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  32. package/dist/config/loader.d.ts +177 -16
  33. package/dist/config/loader.js +424 -127
  34. package/dist/contract-serialization.js +4 -8
  35. package/dist/contract-types.d.ts +1 -0
  36. package/dist/contract.js +2 -0
  37. package/dist/declaration-validation.d.ts +32 -0
  38. package/dist/declaration-validation.js +207 -0
  39. package/dist/define.d.ts +51 -25
  40. package/dist/define.js +752 -38
  41. package/dist/error-resolution.d.ts +4 -0
  42. package/dist/error-resolution.js +122 -0
  43. package/dist/errors.d.ts +18 -0
  44. package/dist/errors.js +40 -0
  45. package/dist/fixture-sanitization.d.ts +28 -0
  46. package/dist/fixture-sanitization.js +217 -0
  47. package/dist/health-scenario.d.ts +1842 -0
  48. package/dist/health-scenario.js +624 -0
  49. package/dist/index.d.ts +19 -9
  50. package/dist/index.js +10 -6
  51. package/dist/lint.d.ts +6 -1
  52. package/dist/lint.js +362 -3
  53. package/dist/native-address.d.ts +43 -0
  54. package/dist/native-address.js +281 -0
  55. package/dist/native-egress-policy.d.ts +31 -0
  56. package/dist/native-egress-policy.js +288 -0
  57. package/dist/observability.d.ts +5 -2
  58. package/dist/observability.js +48 -1
  59. package/dist/provider.d.ts +8 -2
  60. package/dist/provider.js +3 -1
  61. package/dist/runtime/auth-flow.d.ts +5 -1
  62. package/dist/runtime/auth-flow.js +6 -0
  63. package/dist/runtime/browser.d.ts +1 -0
  64. package/dist/runtime/browser.js +492 -49
  65. package/dist/runtime/cache.d.ts +1 -0
  66. package/dist/runtime/cache.js +169 -15
  67. package/dist/runtime/choice-wordlist.d.ts +9 -0
  68. package/dist/runtime/choice-wordlist.js +138 -0
  69. package/dist/runtime/choice.d.ts +13 -1
  70. package/dist/runtime/choice.js +490 -102
  71. package/dist/runtime/executor.d.ts +2 -2
  72. package/dist/runtime/executor.js +26 -2
  73. package/dist/runtime/http.d.ts +1 -0
  74. package/dist/runtime/http.js +515 -53
  75. package/dist/runtime/instrumentation.d.ts +2 -2
  76. package/dist/runtime/instrumentation.js +366 -8
  77. package/dist/runtime/native-network-errors.d.ts +33 -0
  78. package/dist/runtime/native-network-errors.js +69 -0
  79. package/dist/runtime/native-network.d.ts +96 -0
  80. package/dist/runtime/native-network.js +1232 -0
  81. package/dist/runtime/ocr.d.ts +29 -0
  82. package/dist/runtime/ocr.js +440 -0
  83. package/dist/runtime/proxy-errors.js +6 -2
  84. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  85. package/dist/runtime/proxy-nodemaven.js +146 -0
  86. package/dist/runtime/proxy-telemetry.d.ts +80 -1
  87. package/dist/runtime/proxy-telemetry.js +154 -47
  88. package/dist/runtime/redirects.d.ts +29 -0
  89. package/dist/runtime/redirects.js +36 -0
  90. package/dist/runtime/redis.d.ts +1 -1
  91. package/dist/runtime/redis.js +4 -2
  92. package/dist/runtime/request-options.d.ts +68 -1
  93. package/dist/runtime/request-options.js +548 -0
  94. package/dist/runtime/resolver-config.d.ts +6 -0
  95. package/dist/runtime/resolver-config.js +6 -0
  96. package/dist/runtime/resolver-public.d.ts +1 -0
  97. package/dist/runtime/resolver-public.js +1 -0
  98. package/dist/runtime/resolver-shared.d.ts +3 -0
  99. package/dist/runtime/resolver-shared.js +12 -0
  100. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  101. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  102. package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
  103. package/dist/runtime/resolver-vendors/browser.js +377 -0
  104. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  105. package/dist/runtime/resolver-vendors/capsolver.js +526 -0
  106. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  107. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  108. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  109. package/dist/runtime/resolver-vendors/twocaptcha.js +407 -0
  110. package/dist/runtime/resolver-vendors/types.d.ts +94 -0
  111. package/dist/runtime/resolver-vendors/types.js +96 -0
  112. package/dist/runtime/resolver.d.ts +60 -0
  113. package/dist/runtime/resolver.js +737 -0
  114. package/dist/runtime/secrets.d.ts +27 -0
  115. package/dist/runtime/secrets.js +51 -0
  116. package/dist/runtime/state.d.ts +3 -0
  117. package/dist/runtime/state.js +277 -71
  118. package/dist/runtime/stealth-cookies.d.ts +20 -0
  119. package/dist/runtime/stealth-cookies.js +111 -0
  120. package/dist/runtime/stealth.d.ts +28 -3
  121. package/dist/runtime/stealth.js +519 -255
  122. package/dist/runtime/stt.js +1 -12
  123. package/dist/runtime/timeout.d.ts +5 -0
  124. package/dist/runtime/timeout.js +12 -0
  125. package/dist/runtime/trace-config.d.ts +12 -0
  126. package/dist/runtime/trace-config.js +61 -0
  127. package/dist/serve.d.ts +1 -1
  128. package/dist/serve.js +1 -1
  129. package/dist/server/index.d.ts +5 -3
  130. package/dist/server/index.js +3 -3
  131. package/dist/server/self-test-input-tokens.d.ts +2 -1
  132. package/dist/server/self-test-input-tokens.js +18 -14
  133. package/dist/server/self-test.d.ts +114 -0
  134. package/dist/server/self-test.js +784 -148
  135. package/dist/server/serve-implementation.d.ts +213 -0
  136. package/dist/server/serve-implementation.js +2173 -0
  137. package/dist/server/serve.d.ts +1 -70
  138. package/dist/server/serve.js +1 -1130
  139. package/dist/server/trace-output.d.ts +4 -0
  140. package/dist/server/trace-output.js +20 -0
  141. package/dist/server/types.d.ts +30 -5
  142. package/dist/server/types.js +13 -1
  143. package/dist/stateful/errors.d.ts +19 -0
  144. package/dist/stateful/errors.js +24 -0
  145. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  146. package/dist/stateful/http-provider-event-emitter.js +237 -0
  147. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  148. package/dist/stateful/http-session-owner-registry.js +210 -0
  149. package/dist/stateful/index.d.ts +18 -0
  150. package/dist/stateful/index.js +18 -0
  151. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  152. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  153. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  154. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  155. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  156. package/dist/stateful/provider-event-pipeline.js +1 -0
  157. package/dist/stateful/provider-events.d.ts +101 -0
  158. package/dist/stateful/provider-events.js +289 -0
  159. package/dist/stateful/session-key.d.ts +15 -0
  160. package/dist/stateful/session-key.js +86 -0
  161. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  162. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  163. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  164. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  165. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  166. package/dist/stateful/stateful-provider-adapter.js +287 -0
  167. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  168. package/dist/stateful/stateful-provider-observability.js +161 -0
  169. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  170. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  171. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  172. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  173. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  174. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  175. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  176. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  177. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  178. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  179. package/dist/stateful-signing.d.ts +18 -0
  180. package/dist/stateful-signing.js +27 -0
  181. package/dist/stealth/profiles.js +16 -7
  182. package/dist/stream-evidence.d.ts +74 -0
  183. package/dist/stream-evidence.js +785 -0
  184. package/dist/stream.js +7 -1
  185. package/dist/testing/index.d.ts +2 -1
  186. package/dist/testing/index.js +2 -1
  187. package/dist/testing/run.d.ts +32 -2
  188. package/dist/testing/run.js +489 -21
  189. package/dist/trace-sanitization.d.ts +5 -0
  190. package/dist/trace-sanitization.js +45 -0
  191. package/dist/types.d.ts +545 -23
  192. package/dist/types.js +1 -0
  193. package/package.json +44 -5
  194. package/src/auth-turn/index.ts +1 -1
  195. package/src/auth.ts +118 -0
  196. package/src/ceremonies/index.ts +189 -46
  197. package/src/cli/commands.ts +10 -0
  198. package/src/cli/create.ts +42 -35
  199. package/src/cli/prompt-assets.ts +865 -0
  200. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  201. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  202. package/src/cli/templates/provider/README.md.tpl +4 -4
  203. package/src/cli/templates/provider/index.ts.tpl +6 -3
  204. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  205. package/src/config/loader.ts +665 -163
  206. package/src/contract-serialization.ts +5 -7
  207. package/src/contract-types.ts +1 -0
  208. package/src/contract.ts +2 -0
  209. package/src/declaration-validation.ts +266 -0
  210. package/src/define.ts +970 -87
  211. package/src/error-resolution.ts +127 -0
  212. package/src/errors.ts +52 -0
  213. package/src/fixture-sanitization.ts +248 -0
  214. package/src/health-scenario.ts +875 -0
  215. package/src/index.ts +204 -8
  216. package/src/lint.ts +408 -4
  217. package/src/native-address.ts +340 -0
  218. package/src/native-egress-policy.ts +358 -0
  219. package/src/observability.ts +51 -1
  220. package/src/provider.ts +133 -0
  221. package/src/runtime/auth-flow.ts +12 -0
  222. package/src/runtime/browser.ts +661 -63
  223. package/src/runtime/cache.ts +189 -14
  224. package/src/runtime/choice-wordlist.ts +145 -0
  225. package/src/runtime/choice.ts +631 -120
  226. package/src/runtime/executor.ts +40 -7
  227. package/src/runtime/http.ts +641 -61
  228. package/src/runtime/instrumentation.ts +520 -15
  229. package/src/runtime/native-network-errors.ts +99 -0
  230. package/src/runtime/native-network.ts +1605 -0
  231. package/src/runtime/ocr.ts +523 -0
  232. package/src/runtime/proxy-errors.ts +12 -4
  233. package/src/runtime/proxy-nodemaven.ts +221 -0
  234. package/src/runtime/proxy-telemetry.ts +244 -75
  235. package/src/runtime/redirects.ts +66 -0
  236. package/src/runtime/redis.ts +7 -2
  237. package/src/runtime/request-options.ts +680 -1
  238. package/src/runtime/resolver-config.ts +6 -0
  239. package/src/runtime/resolver-public.ts +20 -0
  240. package/src/runtime/resolver-shared.ts +17 -0
  241. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  242. package/src/runtime/resolver-vendors/browser.ts +533 -0
  243. package/src/runtime/resolver-vendors/capsolver.ts +700 -0
  244. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  245. package/src/runtime/resolver-vendors/twocaptcha.ts +539 -0
  246. package/src/runtime/resolver-vendors/types.ts +212 -0
  247. package/src/runtime/resolver.ts +1103 -0
  248. package/src/runtime/secrets.ts +64 -0
  249. package/src/runtime/state.ts +394 -77
  250. package/src/runtime/stealth-cookies.ts +132 -0
  251. package/src/runtime/stealth.ts +675 -289
  252. package/src/runtime/stt.ts +1 -19
  253. package/src/runtime/timeout.ts +18 -0
  254. package/src/runtime/trace-config.ts +77 -0
  255. package/src/serve.ts +6 -1
  256. package/src/server/index.ts +37 -2
  257. package/src/server/self-test-input-tokens.ts +29 -14
  258. package/src/server/self-test.ts +1025 -175
  259. package/src/server/serve-implementation.ts +3250 -0
  260. package/src/server/serve.ts +1 -1626
  261. package/src/server/trace-output.ts +32 -0
  262. package/src/server/types.ts +13 -1
  263. package/src/stateful/README.md +146 -0
  264. package/src/stateful/errors.ts +35 -0
  265. package/src/stateful/http-provider-event-emitter.ts +314 -0
  266. package/src/stateful/http-session-owner-registry.ts +306 -0
  267. package/src/stateful/index.ts +18 -0
  268. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  269. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  270. package/src/stateful/provider-event-pipeline.ts +61 -0
  271. package/src/stateful/provider-events.ts +462 -0
  272. package/src/stateful/session-key.ts +111 -0
  273. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  274. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  275. package/src/stateful/stateful-provider-adapter.ts +562 -0
  276. package/src/stateful/stateful-provider-observability.ts +261 -0
  277. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  278. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  279. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  280. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  281. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  282. package/src/stateful-signing.ts +46 -0
  283. package/src/stealth/profiles.ts +17 -7
  284. package/src/stream-evidence.ts +988 -0
  285. package/src/stream.ts +8 -1
  286. package/src/testing/index.ts +10 -1
  287. package/src/testing/run.ts +658 -15
  288. package/src/trace-sanitization.ts +63 -0
  289. package/src/types.ts +660 -38
  290. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  291. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  292. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  293. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  294. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  295. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  296. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  297. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  298. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  299. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  300. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  301. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  302. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  303. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/AUTHORING.md CHANGED
@@ -32,6 +32,45 @@ No retry templates, next-action routing, or other agent choreography: provider p
32
32
 
33
33
  Declare the union in the operation `output` schema (`z.union([CreatedSchema, NeedsInputSchema])`). Reserve hard errors for genuinely unrecoverable flows (payment-gated, unsupported input kinds) and state the concrete reason in the error `message` itself, not only in `details`. Reference implementation: `providers/catchtable` `reserve` in the platform monorepo.
34
34
 
35
+ ### Attempt tokens: the server carries the decisions
36
+
37
+ When a mutation needs more than one user decision (or one decision plus a
38
+ final go/no-go), do not make the agent re-send accumulated state across
39
+ rounds — weak models drop or corrupt it. Split the operation into a
40
+ **prepare/confirm pair** driven by a server-held attempt record:
41
+
42
+ - The prepare operation is non-destructive. A start call takes only the
43
+ scalar intent fields; every response returns a fresh `attempt_token`
44
+ referencing a server-side record (`ctx.choice.issue` with
45
+ `storage.mode: "server"`) that stores every settled decision. Continue
46
+ calls take `attempt_token` plus only the NEW answers.
47
+ - `needs_input` rounds list only the still-pending selections; settled
48
+ decisions may ride along in a display-only field but are never re-sent.
49
+ - When nothing is pending, the prepare operation returns `status: "ready"`
50
+ with a human-readable summary — the consumer's user-facing confirmation.
51
+ - The confirm operation is the only mutation and takes exactly
52
+ `{attempt_token}`. It re-validates everything live before executing and
53
+ returns `needs_input` (fresh token) instead of proceeding when upstream
54
+ drift invalidates a stored decision — never substitute a different option
55
+ for what the user picked.
56
+ - Expired or foreign tokens fail factually (nothing happened; start a new
57
+ attempt with the scalar fields) — no answer salvage from a dead token.
58
+ - **The provider must enforce consumption itself.** `ctx.choice` server
59
+ storage keeps tokens parseable until TTL — `parse` does not invalidate
60
+ them, so a confirm handler that only parses can be replayed into a second
61
+ booking or payment. After a successful execution, record the result under
62
+ the token's digest in `ctx.state` and make replays idempotent: a repeated
63
+ confirm returns the original created payload without touching upstream,
64
+ and later prepare rounds on the consumed token fail factually with the
65
+ existing reference. Record the result only after upstream success, so an
66
+ interrupted confirm stays retryable.
67
+
68
+ The invariant behind all of it: complex flow state is the system's job, not
69
+ the model's. The model carries exactly one opaque key between calls.
70
+ Reference implementations: `providers/catchtable` `reserve`/`reserve-confirm`
71
+ (including the consume-on-success guard) and `providers/modu-parking`
72
+ payment state tokens in the platform monorepo.
73
+
35
74
  ### Description template
36
75
 
37
76
  Every operation `description` MUST be at least 150 characters and follow this structure:
@@ -65,7 +104,98 @@ description:
65
104
 
66
105
  ### Factored operations
67
106
 
68
- Use `defineOperation()` when an operation is large enough to live beside helper functions or in a separate module. It preserves the same type inference as inline `defineProvider()` operations and can be placed directly in the provider `operations` map. `defineProvider()` accepts Zod and Standard Schema v1-compatible schemas. If config validation fails, the SDK names the field to fix, for example `runtime`, `auth.mode`, `operations.<id>.handler`, or `operations.<id>.fixtures.response`.
107
+ `defineProvider(declaration)` returns the builder that accepts `operations`, so
108
+ inline handlers are typed only after capability declarations are fixed. Export
109
+ `ProviderContextOf<typeof buildProvider>` once from the provider entry point.
110
+ Separate operation files import that provider context and call
111
+ `defineOperation<ProviderContext>()({...})`; helpers should accept the one SDK
112
+ client they use rather than the provider context. Zod and Standard Schema v1
113
+ schemas retain input/output inference. Invalid configs name the offending field,
114
+ such as `auth.mode` or `operations.<id>.fixtures.response`.
115
+
116
+ ### Replay-safe fixtures
117
+
118
+ Keep public operation schemas strict: date fields should accept absolute dates,
119
+ not relative tokens. Inside `fixtures.request` only, the SDK resolves `+Nd` and
120
+ `+Nd:YYYYMMDD` (1–365 days ahead) before import-time schema validation and
121
+ stores the resolved request in provider metadata. Health-check case inputs use
122
+ the same resolver when a probe runs. The default calendar is **KST**, including
123
+ the 15:00–23:59 UTC window when KST is already on the next day.
124
+
125
+ `fixtures.recordedAt` is the KST `YYYY-MM-DD` date when the response evidence
126
+ was captured. It must be a real, non-future calendar date. Response date fields
127
+ are expected to align with `recordedAt`, not with the newly resolved request;
128
+ this permits stable recorded evidence alongside a replay-safe request.
129
+
130
+ ```ts
131
+ const FlightInput = z.object({
132
+ departureDate: z.string().date(), // public calls remain absolute-date only
133
+ });
134
+
135
+ const searchFlights = {
136
+ input: FlightInput,
137
+ output: FlightOutput,
138
+ async handler(ctx, input) {
139
+ return fetchAndNormalizeFlights(ctx, input);
140
+ },
141
+ fixtures: {
142
+ request: { departureDate: "+45d" },
143
+ response: recordedFlightResponse, // dates reflect the capture below
144
+ recordedAt: "2026-07-15",
145
+ },
146
+ healthCheckUnsupported: { reason: "Upstream search is cost-bearing." },
147
+ };
148
+ ```
149
+
150
+ For code that explicitly calls the shared resolver, omit the third argument to
151
+ use KST or pass `"UTC"` deliberately:
152
+
153
+ ```ts
154
+ import { resolveHealthCheckInputDateTokens } from "@apifuse/provider-sdk/server";
155
+
156
+ const kstInput = resolveHealthCheckInputDateTokens({ date: "+45d" });
157
+ const utcInput = resolveHealthCheckInputDateTokens({ date: "+45d" }, new Date(), "UTC");
158
+ ```
159
+
160
+ Do not re-resolve a health assertion's dates in UTC when its case input used the
161
+ default KST calendar.
162
+
163
+ ### Real-handler E2E in standard tests
164
+
165
+ `runStandardTests(provider)` validates declarations and fixtures but reports a
166
+ per-operation warning because it has no handler E2E coverage. Opt in with an
167
+ `upstreamStub`: the runner calls each fixture-backed real handler with its
168
+ already-resolved fixture request, routes ProviderContext upstream transports to
169
+ the stub, and validates the result against the output schema. It never compares
170
+ the result to the recorded response because that evidence belongs to
171
+ `recordedAt`.
172
+
173
+ ```ts
174
+ import { runStandardTests } from "@apifuse/provider-sdk/testing";
175
+ import provider from "../index.js";
176
+
177
+ runStandardTests(provider, {
178
+ upstreamStub: ({ transport, method, url }) => {
179
+ if (
180
+ transport === "http" &&
181
+ method === "GET" &&
182
+ url === "https://api.example.test/flights"
183
+ ) {
184
+ return Response.json({ flights: [{ id: "fixture-flight" }] });
185
+ }
186
+ return undefined; // fails the test: live-network passthrough is forbidden
187
+ },
188
+ });
189
+ ```
190
+
191
+ The stub also identifies `stealth`, `browser`, and `native` interactions. Return
192
+ a Web `Response` or `{ status, headers, body }`; an unmatched call fails with
193
+ the operation, transport, and method named in the error. Browser handlers expose
194
+ method-level calls such as `goto`, `evaluate`, and `locator.click`, so provide a
195
+ canned result for each method the handler uses. Native connections similarly
196
+ identify `connectTcp`/`connectTls` and subsequent `write` calls. Direct global
197
+ `fetch` or socket usage is outside this ProviderContext seam and should not be
198
+ used by provider handlers.
69
199
 
70
200
  ### Health assertion context
71
201
 
@@ -113,8 +243,9 @@ level, then call `ctx.stt` from operation handlers or auth-flow handlers.
113
243
  ```ts
114
244
  export default defineProvider({
115
245
  id: "example-provider",
116
- // ...metadata, auth, operations, allowedHosts
246
+ // ...metadata, auth, allowedHosts
117
247
  stt: { mode: "required" },
248
+ })({
118
249
  operations: {
119
250
  verifyAudioOtp: {
120
251
  input: z.object({
@@ -234,15 +365,17 @@ const paymentWebviewJourney = defineHealthJourney({
234
365
  ],
235
366
  });
236
367
 
237
- export default defineProvider({
368
+ const buildProvider = defineProvider({
238
369
  id: "example-provider",
239
- // ...metadata, auth, operations, allowedHosts
370
+ // ...metadata, auth, allowedHosts
240
371
  healthJourneys: [paymentWebviewJourney],
241
372
  });
373
+
374
+ export default buildProvider({ operations });
242
375
  ```
243
376
  <!-- @magic-end:sample -->
244
377
 
245
- The journey runner supplies `ctx.gateway`, `ctx.sms.waitForOtp()`, `ctx.journal.sideEffect()`, `ctx.state`, and `ctx.event.operation()` to the optional journey `run` function. Provider authors should keep `run` small: call the covered operations in step order, stop at the declared safe boundary, and let the generated health metadata carry schedule, timeout, required secret, and SMS matcher information to the health monitor.
378
+ The journey runner supplies `ctx.gateway`, `ctx.sms.waitForOtp()`, `ctx.journal.sideEffect()`, `ctx.state`, and `ctx.event.operation()` to the required journey `run` function. Provider authors should keep `run` small: call the covered operations in step order, stop at the declared safe boundary, and let the generated health metadata carry schedule, timeout, required secret, and SMS matcher information to the health monitor.
246
379
 
247
380
  For authenticated journeys, open a fresh connection inside `run` with `ctx.gateway.connect({ input: { ... } })`, execute covered operations with the returned `connectionId`, and disconnect in a `finally` block. Do not require or store long-lived `HEALTH_MONITOR_*_CONNECTION_ID` secrets; those stale connection IDs can hide broken login ceremonies.
248
381
 
@@ -270,6 +403,200 @@ External contributors are expected to submit standalone Provider source plus:
270
403
  Maintainers own monorepo import under `providers/<id>/`, registry generation,
271
404
  deployment projection checks, and release workflows.
272
405
 
406
+ ### Error responses
407
+
408
+ Provider-server failures use a stable public envelope:
409
+
410
+ ```json
411
+ {
412
+ "error": {
413
+ "code": "UPSTREAM_ERROR",
414
+ "message": "The upstream service failed",
415
+ "requestId": "req_123",
416
+ "retryable": true,
417
+ "details": { "providerReason": "temporarily_unavailable" }
418
+ }
419
+ }
420
+ ```
421
+
422
+ `retryable` is always present on responses emitted by the current SDK. Set
423
+ `retryable` in the `ProviderError` options when the provider knows the answer;
424
+ an explicit `true` or `false` wins over the matching operation declaration and
425
+ SDK derivation. When it is omitted, `operations.<id>.docs.errorCodes[].retryable`
426
+ is used for a matching provider-owned code, followed by SDK derivation (which
427
+ defaults ordinary `ProviderError` values to `false`). During stateful rolling
428
+ upgrades, the forwarding client also accepts an older owner response that omits
429
+ `retryable` and treats it as `false` without loosening the emitted response
430
+ contract. Existing optional `fix` guidance is also preserved when a
431
+ `ProviderError` supplies it.
432
+
433
+ `details` belongs exclusively to the provider. The server passes
434
+ `ProviderError.options.details` through verbatim, including strings and arrays,
435
+ and never merges, overwrites, or wraps it. Do not put SDK taxonomy fields there.
436
+ SDK-owned validation and masked-internal-error paths retain their own diagnostic
437
+ details.
438
+
439
+ SDK observability is emitted separately in the
440
+ `X-ApiFuse-Error-Observability` response header as compact, single-line JSON:
441
+
442
+ ```json
443
+ {"category":"upstream_http","taxonomyVersion":"2026-05-26","retryable":true,"upstreamStatus":502}
444
+ ```
445
+
446
+ Treat this header as telemetry, not as provider-controlled public error detail.
447
+ Its category, taxonomy version, retryability, and optional upstream status match
448
+ the structured `provider_request_failed` log event.
449
+
450
+ Declare provider-owned operation failures next to their documentation. The
451
+ server builds a lookup once at startup and applies it to failures from that
452
+ operation:
453
+
454
+ ```ts
455
+ docs: {
456
+ errorCodes: [{
457
+ code: "UPSTREAM_SCHEMA_ERROR",
458
+ status: 502,
459
+ retryable: true,
460
+ description: "The upstream response no longer matches its schema.",
461
+ }],
462
+ },
463
+ handler: async () => {
464
+ throw new ProviderError("Upstream schema changed", {
465
+ code: "UPSTREAM_SCHEMA_ERROR",
466
+ });
467
+ },
468
+ ```
469
+
470
+ `defineProvider` accepts only statuses the server can emit: 400, 401, 404, 429,
471
+ 500, 502, 503, and 504. Invalid declared statuses fail provider definition,
472
+ not a live request. Status selection uses this order:
473
+
474
+ 1. SDK-owned errors retain SDK status semantics. Operation declarations cannot
475
+ override SDK-owned codes, stateful-forwarding failures, Zod/deadline errors,
476
+ or `TransportError` values.
477
+ 2. A matching operation `errorCodes` entry with `status` supplies the status.
478
+ This slot applies to `ValidationError` as well as ordinary `ProviderError`.
479
+ 3. The registered mappings below apply.
480
+ 4. Existing fallbacks apply: `TransportError` 502/504, unregistered input
481
+ `ValidationError` 400 (output validation 500), and other unregistered
482
+ `ProviderError` values 500.
483
+
484
+ The registered mappings are:
485
+
486
+ | Error code or fallback | HTTP status |
487
+ | --- | ---: |
488
+ | `AUTH_REQUIRED`, `reauth_required` | 401 |
489
+ | `MISSING_SECRET` | 400 |
490
+ | `NOT_FOUND`, `not_found`, `NO_DATA` | 404 |
491
+ | `RATE_LIMITED`, `UPSTREAM_RATE_LIMIT`, `LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR` | 429 |
492
+ | `UPSTREAM_ERROR`, `BLOCKED` | 502 |
493
+ | `STT_UNAVAILABLE`, `UNSUPPORTED_STT_BACKEND`, `STATEFUL_FORWARDING_REPLAY_CACHE_FULL` | 503 |
494
+ | Unregistered input `ValidationError` code | 400 |
495
+ | Other unregistered `ProviderError` code | 500 |
496
+
497
+ An unregistered non-validation `ProviderError` code returns HTTP 500 and emits
498
+ the greppable `unregistered_provider_error_code` signal with the code in the
499
+ structured failure log. A matching operation declaration, including one that
500
+ omits `status`, makes the code registered for this signal and may independently
501
+ supply `retryable`. The HTTP 400 `ValidationError` behavior is only the fallback
502
+ when neither an operation status nor a registered mapping applies.
503
+
504
+ Throw the domain `ProviderError` directly. Subclassing or wrapping it as a
505
+ `TransportError` solely to preserve a 5xx response is obsolete; declare the
506
+ domain code's `status` instead. Genuine `TransportError` values remain
507
+ SDK-owned and keep their 502/504 mapping.
508
+
509
+ ### Declared secrets are SDK-enforced
510
+
511
+ Environment/secret presence validation is single-sourced in the SDK. Declare
512
+ every env secret the provider needs in `defineProvider`:
513
+
514
+ ```ts
515
+ secrets: [
516
+ {
517
+ name: "APIFUSE__PROVIDER__MY_PROVIDER__API_KEY",
518
+ required: true,
519
+ description: "Upstream API key from the vendor portal",
520
+ },
521
+ ],
522
+ ```
523
+
524
+ The runtime validates every `required: true` declaration before any operation
525
+ handler or auth-flow handler (except `abort`) runs. When a required secret is
526
+ unset or whitespace-only, the invocation fails with the canonical structured
527
+ error — code `MISSING_SECRET`, HTTP 400, top-level `retryable: false`, and a
528
+ `fix` naming every missing secret — across `/v1/{operation}`, self-test probes,
529
+ `apifuse perf`, and `apifuse record`. Its error-observability header carries the
530
+ `credential_unavailable` category. The server also emits a
531
+ `provider_secrets_missing` warn log at boot so unprovisioned deployments are
532
+ visible immediately without crashing the pod.
533
+
534
+ Provider-local presence re-validation is **deprecated**: do not write
535
+ `requireServiceKey`/`requireApiKey`-style guards that re-check `ctx.env.get()`
536
+ and throw a hand-rolled `CONFIGURATION_ERROR`/`MISSING_SECRET`. Those guards
537
+ are dead weight (the SDK gate runs first) and historically diverged into
538
+ inconsistent error shapes. The `sdk-owned-secret-presence` submit-check rule
539
+ flags them at warn level; acknowledge a deliberate exception with
540
+ `// @apifuse-allow sdk-owned-secret-presence: <reason>`.
541
+
542
+ ```ts
543
+ // Before (deprecated): provider-local double validation
544
+ function requireServiceKey(ctx: ProviderContext): string {
545
+ const value = ctx.env.get(SERVICE_KEY_ENV);
546
+ if (!value?.trim()) {
547
+ throw new ProviderError(`Missing required provider secret: ${SERVICE_KEY_ENV}`, {
548
+ code: "CONFIGURATION_ERROR",
549
+ });
550
+ }
551
+ return value;
552
+ }
553
+
554
+ // After: declare { name: SERVICE_KEY_ENV, required: true } and read directly.
555
+ const serviceKey = ctx.env.get(SERVICE_KEY_ENV);
556
+ ```
557
+
558
+ Note the asymmetry: the gate treats whitespace-only values as missing, but
559
+ `ctx.env.get()` still returns the raw value to handlers — trim at the point of
560
+ use if the upstream is whitespace-sensitive.
561
+
562
+ ### Credentials forced into query parameters
563
+
564
+ Prefer an authorization header or request body whenever the upstream supports
565
+ one. When the upstream requires a credential in the URL query (for example
566
+ `serviceKey`, `confmKey`, or `crtfc_key`), use `sensitiveParams`:
567
+
568
+ ```ts
569
+ const response = await ctx.http.get("/openapi/lookup", {
570
+ params: { pageNo: 1, numOfRows: 100 },
571
+ sensitiveParams: {
572
+ serviceKey: ctx.env.get("APIFUSE__PROVIDER__EXAMPLE__SERVICE_KEY")!,
573
+ },
574
+ });
575
+ ```
576
+
577
+ `sensitiveParams` is merged into the outgoing query like `params`, while its
578
+ values are redacted from SDK transport errors, traces, and `apifuse record`
579
+ fixtures. Do not put query credentials in `params`, and do not hand-build a URL
580
+ containing a key; those paths cannot declare which query values are secret.
581
+
582
+ #### Residual risks
583
+
584
+ Redaction is unconditional in structural positions (declared query keys and
585
+ exact scalar fixture/error fields) for values of every length. In unstructured
586
+ free text, values of four or more characters are replaced as substrings; shorter
587
+ values are replaced only at token boundaries to avoid corrupting unrelated text
588
+ (for example, a secret `api` must not rewrite `rapid`). The residual risk is that
589
+ a sub-four-character secret embedded directly inside a larger alphanumeric token
590
+ can remain in free text. Prefer a higher-entropy credential, or a header/body
591
+ credential channel, whenever the upstream permits it. An empty
592
+ `sensitiveParams: {}` is treated exactly as if the option were omitted.
593
+
594
+ For `session.redirects.run()`, returned hop URLs are diagnostic metadata and
595
+ therefore keep declared query values and common response-only credential keys
596
+ redacted. If a login flow must consume a rotated credential from `Location`,
597
+ inspect it inside `stopWhen`; that callback receives the real hop while callback
598
+ failures are sanitized before propagation.
599
+
273
600
  ### Public local debugging checklist
274
601
 
275
602
  - Operation smoke requests use the provider server envelope:
@@ -287,7 +614,7 @@ deployment projection checks, and release workflows.
287
614
  HTML, or upstream `Error` objects.
288
615
 
289
616
  ```ts
290
- export default defineProvider({
617
+ const buildProvider = defineProvider({
291
618
  id: "example-provider",
292
619
  version: "1.0.0",
293
620
  runtime: "standard",
@@ -321,8 +648,10 @@ export default defineProvider({
321
648
  },
322
649
  },
323
650
  credential: { keys: ["cookie"] },
324
- // ...metadata and operations
651
+ // ...metadata
325
652
  });
653
+
654
+ export default buildProvider({ operations });
326
655
  ```
327
656
 
328
657
  - Credentials auth providers should use `defineCredentialsAuth()` instead of
@@ -349,15 +678,17 @@ const credentialsAuth = defineCredentialsAuth({
349
678
  },
350
679
  });
351
680
 
352
- export default defineProvider({
681
+ const buildProvider = defineProvider({
353
682
  id: "example-provider",
354
683
  version: "1.0.0",
355
684
  runtime: "standard",
356
685
  auth: credentialsAuth.auth,
357
686
  credential: credentialsAuth.credential,
358
687
  context: credentialsAuth.context,
359
- // ...metadata and operations
688
+ // ...metadata
360
689
  });
690
+
691
+ export default buildProvider({ operations });
361
692
  ```
362
693
 
363
694
  For OTP, MFA, CAPTCHA handoff, or user-approved login, return a challenge from
@@ -417,14 +748,171 @@ const credentialsAuth = defineCredentialsAuth({
417
748
  request's `context`.
418
749
  - Stealth/browser providers may require local runtime setup outside Provider code:
419
750
  keep access-sensitive operations on `ctx.stealth.fetch()` with an SDK stealth
420
- `profile`; the TypeScript runtime uses `impit` behind that interface, so do
751
+ `profile`; the TypeScript runtime uses `wreq-js` behind that interface, so do
421
752
  not add per-operation JA3, HTTP/2 SETTINGS, or pseudo-header tuning. `ctx.stealth`
422
- supports Chrome/Firefox-style profiles; use `browser.engine:
423
- "playwright-stealth"` for Safari-specific or real browser Providers
424
- (`nodriver` is Python-runtime only); install local browser assets with
753
+ supports Chrome, Firefox, and Safari profiles; use `ctx.browser` when a
754
+ Provider needs real browser execution. TypeScript browser Providers use
755
+ `browser.engine: "playwright-stealth"` (`nodriver` is Python-runtime only);
756
+ install local browser assets with
425
757
  `bunx playwright install chromium`, or set
426
758
  `APIFUSE__CDP_POOL__URL` for remote browser debugging.
427
759
 
760
+ ### Native gateway adapter migration
761
+
762
+ Native proxy resolution supports both HTTP CONNECT and SOCKS5 without
763
+ terminating origin TLS. The built-in vendor order follows `proxy.providers`
764
+ exactly: `smartproxy` means the `api.smartproxy.org` allocation vendor (raw
765
+ `ip:port` endpoints), while `nodemaven` is the credentialed gateway. It is not
766
+ the company formerly called Smartproxy; that separate company is represented
767
+ by the deprecated `decodo` name.
768
+
769
+ Custom `gatewaySynthesizers` must now accept the selected `protocol` and the
770
+ injected `credentials` resolver on `NativeGatewayProxySynthesisInput`.
771
+ Synthesizers may be async because allocation vendors perform network I/O. They
772
+ may return a proxy, `undefined` when they do not implement the offered vendor,
773
+ or `{ kind: "skipped", reason }` so an exhausted required chain can explain an
774
+ absent credential, unsupported protocol, or allocation failure. Accordingly,
775
+ `resolveNativeGatewayProxy(...)` must now be awaited.
776
+
777
+ Callers that do not supply custom synthesizers or credentials keep env-backed
778
+ behavior. Hosts that already have an allowlisted `EnvContext` can inject it
779
+ explicitly, and vault-backed or per-tenant hosts can supply their own resolver:
780
+
781
+ ```ts
782
+ import {
783
+ createEnvVendorCredentialResolver,
784
+ createNativeNetworkClient,
785
+ } from "@apifuse/provider-sdk";
786
+
787
+ const network = createNativeNetworkClient({
788
+ proxyPolicy: { mode: "required", providers: ["smartproxy", "nodemaven"] },
789
+ credentials: createEnvVendorCredentialResolver(ctx.env),
790
+ // Optional advanced override; omit for each vendor's default.
791
+ proxyProtocol: "socks5",
792
+ });
793
+ ```
794
+
795
+ Never include credential values in adapter skip messages or thrown errors. The
796
+ SDK redacts built-in proxy URL userinfo, CONNECT authentication, and allocator
797
+ causes, but a custom adapter remains responsible for not publishing secrets in
798
+ its own diagnostics.
799
+
800
+ ### Limiting stealth response bodies
801
+
802
+ Set `maxBodyBytes` on `ctx.stealth.fetch()` or `session.redirects.run()` when an
803
+ upstream response has a known safe maximum. The limit is opt-in and counts
804
+ decoded bytes as `wreq-js` streams them. It applies to every redirect hop, uses a
805
+ parseable `Content-Length` for an early rejection, and still enforces the limit
806
+ incrementally when the header is absent or inaccurate. Exceeding the limit
807
+ aborts the response and throws a non-retryable `TransportError` with code
808
+ `response_too_large`.
809
+
810
+ Pass the limit to the transport instead of checking `Content-Length` in provider
811
+ code:
812
+
813
+ ```ts
814
+ const response = await ctx.stealth.fetch("/api/search", {
815
+ params: { query: input.query },
816
+ maxBodyBytes: 2 * 1024 * 1024,
817
+ })
818
+ ```
819
+
820
+ ### Persisting stealth session cookies
821
+
822
+ Persist `session.cookies.serialize()` as JSON when an authenticated session must
823
+ survive a restart or move to another replica. The returned
824
+ `StealthCookieStoreV1` has an explicit version and retains every cookie together
825
+ with its Domain, Path, Secure, expiry, host-only, and other cookie attributes.
826
+ Restore it with `session.cookies.deserialize()`. Unsupported future versions
827
+ fail explicitly instead of being accepted as a partial cookie jar.
828
+
829
+ Credential values are strings, so stringify the store at the credential
830
+ boundary and parse it when rebuilding the session:
831
+
832
+ ```ts
833
+ // After login (including any redirects across sibling hosts):
834
+ const result = await session.redirects.run({ url: loginUrl });
835
+ return {
836
+ credential: {
837
+ cookieStore: JSON.stringify(result.cookieStore),
838
+ },
839
+ };
840
+
841
+ // In a later operation or replica:
842
+ const persisted = ctx.credential.get("cookieStore");
843
+ if (persisted) {
844
+ session.cookies.deserialize(JSON.parse(persisted));
845
+ }
846
+ ```
847
+
848
+ `snapshot()` and `restore()` remain only for backward compatibility with flat
849
+ `Record<string, string>` credentials. `snapshot()` enumerates cookies across all
850
+ hosts and paths, but the flat shape is inherently lossy: duplicate names
851
+ collapse and Domain, Path, Secure, expiry, and host-only attributes cannot be
852
+ represented. `restore()` therefore recreates host-only `Path=/` cookies on the
853
+ session base origin. Do not use the flat form for new persistence code. Cookie
854
+ headers remain origin-filtered: use `toHeader(url)` for a particular request and
855
+ never build a request header from serialized or snapshotted persistence data.
856
+
857
+ ### Recording and replaying streaming responses
858
+
859
+ `apifuse record` passes responses returned by `ctx.http.stream()` directly to the operation
860
+ handler while incrementally capturing a bounded preview. If the handler returns or cancels its
861
+ reader before EOF, the recorder drains the retained upstream reader before writing a JSON evidence
862
+ record to `__fixtures__/raw.json`. The record contains the status, success
863
+ flag, `content-type`/`content-length`/`content-disposition` headers when present, the
864
+ full body SHA-256 and byte count, and a base64 preview up to the configured stream preview limit.
865
+ Textual previews are decoded and passed through the fixture sanitizer before base64 encoding.
866
+ Classification uses both the declared content type and the preview bytes, so missing or incorrect
867
+ content-type headers do not bypass sanitization. PEM private-key blocks and long high-entropy
868
+ tokens in otherwise unstructured text are redacted as well. If the full preview is not valid UTF-8,
869
+ the entire lossy-decoded preview is scanned and matching decodable byte windows are sanitized. Only a
870
+ magic-number-confirmed binary preview with no textual-secret pattern anywhere in the preview bypasses
871
+ sanitization; other undecodable data fails closed.
872
+ Sanitized previews carry `preview_sanitized: true`, plus a
873
+ `preview_redaction_reason` when capture had to fail closed. The original hash and byte count always
874
+ describe upstream bytes, not a sanitized preview.
875
+
876
+ Each record includes query-free request provenance (`method`, `path`, and a one-based stream call
877
+ ordinal). Provenance never stores the origin, URL userinfo, query, or fragment. Every retained path
878
+ segment is scrubbed before persistence: credential-key segments, values following those keys,
879
+ known token shapes, and long high-entropy opaque segments become `[REDACTED]`. If an operation
880
+ opens multiple streams, the recorder finalizes every retained reader and
881
+ writes all evidence records in stream call order. Stream invocations use a tagged capture envelope
882
+ whose items distinguish stream evidence from ordinary JSON responses. Evidence-only snapshot replay
883
+ consumes that exact call order and fails immediately when evidence is exhausted or a call kind is
884
+ reordered. When request provenance is present, replay also rejects method or path changes (relative
885
+ URLs are resolved against the recorded path prefix); ordinals remain diagnostic and are not matched.
886
+ Appended fixtures replay a stream envelope only when it is the latest invocation. SSE
887
+ recording remains unsupported and fails explicitly instead of retaining an unrelated earlier
888
+ response.
889
+
890
+ #### Residual risks
891
+
892
+ - Credential-path sanitization decodes each URL path segment once. Double-encoded separators or
893
+ values such as `%252F` are not decoded recursively, so they can conceal a credential-shaped
894
+ segment from the recorder. This single-pass policy keeps path handling deterministic and avoids
895
+ interpreting ambiguous or intentionally layered encodings differently from the upstream. Never
896
+ place credentials in URL paths, and review recorded provenance before committing fixtures.
897
+ - Primitive strings embedded in prose are redacted only when they match the current PEM,
898
+ credential-assignment, known-token, or entropy heuristics. Other secret formats can remain because
899
+ blanket redaction of ordinary strings would destroy useful fixture content and create broad false
900
+ positives. Keep secrets under credential-named structured fields where possible and manually
901
+ inspect sanitized fixture text before committing it.
902
+
903
+ Stream fixture replay in `runStandardTests(..., { snapshot: true })` is evidence-only:
904
+ `ctx.http.stream()` returns a usable stream containing exactly the recorded preview,
905
+ not a fabricated full body. The replay response also carries runtime metadata
906
+ `evidence_only: true`, `body_sha256`, `body_bytes`, and the optional preview sanitization fields
907
+ for assertions about the original capture. Do not assert that the replay body hashes to
908
+ `body_sha256` when `body_bytes`
909
+ exceeds the decoded preview length or `preview_sanitized` is present; use the metadata for
910
+ full-body integrity and limit body-content assertions to the preview.
911
+
912
+ Golden snapshot suites can set `requireSnapshot: true` so a missing committed snapshot fails instead
913
+ of being created implicitly. Regenerate intentional changes with
914
+ `bun test --update-snapshots`; review and commit the resulting `transform.snap.json` file.
915
+
428
916
  ### Running the pre-submission report
429
917
 
430
918
  ```bash