@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.21

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 (291) hide show
  1. package/AUTHORING.md +487 -0
  2. package/CHANGELOG.md +90 -0
  3. package/README.md +49 -3
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +14 -0
  9. package/bin/apifuse-pack-smoke.ts +36 -81
  10. package/bin/apifuse-pack-types.ts +305 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +659 -111
  13. package/bin/apifuse-submit-check.ts +546 -48
  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-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth.d.ts +2 -2
  21. package/dist/auth.js +9 -18
  22. package/dist/ceremonies/index.d.ts +9 -1
  23. package/dist/ceremonies/index.js +65 -18
  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/config/loader.d.ts +176 -8
  32. package/dist/config/loader.js +424 -95
  33. package/dist/contract-serialization.d.ts +2 -2
  34. package/dist/contract-serialization.js +3 -6
  35. package/dist/contract-types.d.ts +2 -2
  36. package/dist/contract.d.ts +3 -3
  37. package/dist/contract.js +4 -6
  38. package/dist/define.d.ts +9 -1
  39. package/dist/define.js +304 -119
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/error-resolution.d.ts +2 -0
  43. package/dist/error-resolution.js +90 -0
  44. package/dist/errors.d.ts +22 -1
  45. package/dist/errors.js +88 -0
  46. package/dist/fixture-sanitization.d.ts +26 -0
  47. package/dist/fixture-sanitization.js +216 -0
  48. package/dist/i18n/catalog.d.ts +2 -2
  49. package/dist/i18n/catalog.js +4 -10
  50. package/dist/i18n/index.d.ts +2 -2
  51. package/dist/i18n/index.js +2 -2
  52. package/dist/i18n/keys.d.ts +2 -2
  53. package/dist/index.d.ts +46 -42
  54. package/dist/index.js +41 -37
  55. package/dist/lint.d.ts +1 -1
  56. package/dist/lint.js +8 -15
  57. package/dist/native-address.d.ts +43 -0
  58. package/dist/native-address.js +281 -0
  59. package/dist/native-egress-policy.d.ts +31 -0
  60. package/dist/native-egress-policy.js +288 -0
  61. package/dist/observability.d.ts +5 -2
  62. package/dist/observability.js +48 -1
  63. package/dist/provider.d.ts +12 -11
  64. package/dist/provider.js +10 -9
  65. package/dist/public-schema-field-lint.d.ts +1 -1
  66. package/dist/recipes/gov-api.js +1 -1
  67. package/dist/runtime/auth-flow.d.ts +2 -1
  68. package/dist/runtime/auth-flow.js +4 -3
  69. package/dist/runtime/browser.d.ts +1 -1
  70. package/dist/runtime/browser.js +15 -29
  71. package/dist/runtime/cache.d.ts +1 -1
  72. package/dist/runtime/cache.js +4 -8
  73. package/dist/runtime/choice.d.ts +1 -1
  74. package/dist/runtime/choice.js +31 -35
  75. package/dist/runtime/credential.d.ts +1 -1
  76. package/dist/runtime/credential.js +1 -1
  77. package/dist/runtime/env.d.ts +1 -1
  78. package/dist/runtime/executor.d.ts +1 -1
  79. package/dist/runtime/executor.js +31 -4
  80. package/dist/runtime/http.d.ts +2 -2
  81. package/dist/runtime/http.js +387 -47
  82. package/dist/runtime/insights.d.ts +1 -1
  83. package/dist/runtime/insights.js +6 -13
  84. package/dist/runtime/instrumentation.d.ts +2 -2
  85. package/dist/runtime/instrumentation.js +345 -22
  86. package/dist/runtime/keyring.js +1 -1
  87. package/dist/runtime/namespace.js +1 -1
  88. package/dist/runtime/native-network.d.ts +127 -0
  89. package/dist/runtime/native-network.js +1298 -0
  90. package/dist/runtime/otlp.d.ts +1 -1
  91. package/dist/runtime/perf.d.ts +1 -1
  92. package/dist/runtime/provider.d.ts +1 -1
  93. package/dist/runtime/provider.js +1 -2
  94. package/dist/runtime/proxy-errors.d.ts +1 -1
  95. package/dist/runtime/proxy-errors.js +9 -7
  96. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  97. package/dist/runtime/proxy-nodemaven.js +146 -0
  98. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  99. package/dist/runtime/proxy-retry-policy.js +2 -2
  100. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  101. package/dist/runtime/proxy-telemetry.js +55 -52
  102. package/dist/runtime/redirects.d.ts +29 -0
  103. package/dist/runtime/redirects.js +36 -0
  104. package/dist/runtime/redis.d.ts +1 -1
  105. package/dist/runtime/redis.js +2 -4
  106. package/dist/runtime/request-options.d.ts +68 -1
  107. package/dist/runtime/request-options.js +548 -0
  108. package/dist/runtime/secrets.d.ts +27 -0
  109. package/dist/runtime/secrets.js +51 -0
  110. package/dist/runtime/state.d.ts +2 -2
  111. package/dist/runtime/state.js +238 -26
  112. package/dist/runtime/stealth.d.ts +5 -3
  113. package/dist/runtime/stealth.js +423 -143
  114. package/dist/runtime/stt.d.ts +1 -1
  115. package/dist/runtime/stt.js +11 -15
  116. package/dist/runtime/trace.d.ts +2 -2
  117. package/dist/runtime/trace.js +2 -4
  118. package/dist/runtime/waterfall.d.ts +1 -1
  119. package/dist/schema.d.ts +1 -1
  120. package/dist/schema.js +7 -15
  121. package/dist/serve.d.ts +1 -1
  122. package/dist/serve.js +1 -1
  123. package/dist/server/index.d.ts +7 -7
  124. package/dist/server/index.js +6 -6
  125. package/dist/server/self-test-input-tokens.d.ts +2 -1
  126. package/dist/server/self-test-input-tokens.js +18 -14
  127. package/dist/server/self-test-redaction.d.ts +1 -1
  128. package/dist/server/self-test-redaction.js +1 -1
  129. package/dist/server/self-test.d.ts +104 -3
  130. package/dist/server/self-test.js +673 -115
  131. package/dist/server/serve.d.ts +116 -4
  132. package/dist/server/serve.js +799 -128
  133. package/dist/server/types.d.ts +34 -9
  134. package/dist/server/types.js +8 -1
  135. package/dist/stateful/errors.d.ts +14 -0
  136. package/dist/stateful/errors.js +14 -0
  137. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  138. package/dist/stateful/http-provider-event-emitter.js +237 -0
  139. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  140. package/dist/stateful/http-session-owner-registry.js +210 -0
  141. package/dist/stateful/index.d.ts +18 -0
  142. package/dist/stateful/index.js +18 -0
  143. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  144. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  145. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  146. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  147. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  148. package/dist/stateful/provider-event-pipeline.js +1 -0
  149. package/dist/stateful/provider-events.d.ts +101 -0
  150. package/dist/stateful/provider-events.js +289 -0
  151. package/dist/stateful/session-key.d.ts +15 -0
  152. package/dist/stateful/session-key.js +86 -0
  153. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  154. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  155. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  156. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  157. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  158. package/dist/stateful/stateful-provider-adapter.js +287 -0
  159. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  160. package/dist/stateful/stateful-provider-observability.js +161 -0
  161. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  162. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  163. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  164. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  165. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  166. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  167. package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
  168. package/dist/stateful/stateful-provider-session-routing.js +353 -0
  169. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  170. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  171. package/dist/stateful-signing.d.ts +18 -0
  172. package/dist/stateful-signing.js +27 -0
  173. package/dist/stealth/profiles.d.ts +1 -1
  174. package/dist/stealth/profiles.js +5 -14
  175. package/dist/stream-evidence.d.ts +74 -0
  176. package/dist/stream-evidence.js +785 -0
  177. package/dist/stream.d.ts +1 -1
  178. package/dist/testing/index.d.ts +2 -2
  179. package/dist/testing/index.js +2 -2
  180. package/dist/testing/run.d.ts +32 -2
  181. package/dist/testing/run.js +478 -28
  182. package/dist/types.d.ts +342 -14
  183. package/dist/types.js +1 -0
  184. package/dist/user-input.d.ts +30 -0
  185. package/dist/user-input.js +66 -0
  186. package/package.json +16 -5
  187. package/src/auth-turn/index.ts +1 -1
  188. package/src/auth.ts +28 -86
  189. package/src/ceremonies/index.ts +103 -78
  190. package/src/cli/commands.ts +10 -0
  191. package/src/cli/create.ts +42 -35
  192. package/src/cli/prompt-assets.ts +865 -0
  193. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  194. package/src/config/loader.ts +652 -204
  195. package/src/contract-serialization.ts +5 -11
  196. package/src/contract-types.ts +2 -2
  197. package/src/contract.ts +12 -28
  198. package/src/define.ts +499 -498
  199. package/src/dev.ts +4 -9
  200. package/src/error-resolution.ts +91 -0
  201. package/src/errors.ts +110 -5
  202. package/src/fixture-sanitization.ts +247 -0
  203. package/src/i18n/catalog.ts +10 -32
  204. package/src/i18n/index.ts +2 -2
  205. package/src/i18n/keys.ts +5 -11
  206. package/src/index.ts +112 -42
  207. package/src/lint.ts +88 -152
  208. package/src/native-address.ts +340 -0
  209. package/src/native-egress-policy.ts +358 -0
  210. package/src/observability.ts +51 -1
  211. package/src/provider.ts +66 -11
  212. package/src/public-schema-field-lint.ts +7 -33
  213. package/src/recipes/gov-api.ts +2 -5
  214. package/src/runtime/auth-flow.ts +7 -7
  215. package/src/runtime/browser.ts +64 -187
  216. package/src/runtime/cache.ts +20 -67
  217. package/src/runtime/choice.ts +79 -132
  218. package/src/runtime/credential.ts +2 -2
  219. package/src/runtime/env.ts +1 -1
  220. package/src/runtime/executor.ts +43 -20
  221. package/src/runtime/http.ts +494 -57
  222. package/src/runtime/insights.ts +15 -53
  223. package/src/runtime/instrumentation.ts +495 -66
  224. package/src/runtime/keyring.ts +7 -19
  225. package/src/runtime/namespace.ts +2 -7
  226. package/src/runtime/native-network.ts +1686 -0
  227. package/src/runtime/otlp.ts +12 -23
  228. package/src/runtime/perf.ts +1 -1
  229. package/src/runtime/provider.ts +4 -9
  230. package/src/runtime/proxy-errors.ts +29 -42
  231. package/src/runtime/proxy-nodemaven.ts +221 -0
  232. package/src/runtime/proxy-retry-policy.ts +3 -3
  233. package/src/runtime/proxy-telemetry.ts +79 -77
  234. package/src/runtime/redirects.ts +66 -0
  235. package/src/runtime/redis.ts +4 -12
  236. package/src/runtime/request-options.ts +679 -9
  237. package/src/runtime/secrets.ts +64 -0
  238. package/src/runtime/state.ts +353 -133
  239. package/src/runtime/stealth.ts +505 -154
  240. package/src/runtime/stt.ts +38 -94
  241. package/src/runtime/trace.ts +14 -44
  242. package/src/runtime/waterfall.ts +5 -18
  243. package/src/schema.ts +23 -84
  244. package/src/serve.ts +1 -1
  245. package/src/server/index.ts +29 -7
  246. package/src/server/self-test-input-tokens.ts +29 -14
  247. package/src/server/self-test-redaction.ts +2 -2
  248. package/src/server/self-test.ts +857 -132
  249. package/src/server/serve.ts +1151 -328
  250. package/src/server/types.ts +12 -13
  251. package/src/stateful/README.md +146 -0
  252. package/src/stateful/errors.ts +23 -0
  253. package/src/stateful/http-provider-event-emitter.ts +314 -0
  254. package/src/stateful/http-session-owner-registry.ts +306 -0
  255. package/src/stateful/index.ts +18 -0
  256. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  257. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  258. package/src/stateful/provider-event-pipeline.ts +61 -0
  259. package/src/stateful/provider-events.ts +462 -0
  260. package/src/stateful/session-key.ts +111 -0
  261. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  262. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  263. package/src/stateful/stateful-provider-adapter.ts +562 -0
  264. package/src/stateful/stateful-provider-observability.ts +261 -0
  265. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  266. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  267. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  268. package/src/stateful/stateful-provider-session-routing.ts +555 -0
  269. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  270. package/src/stateful-signing.ts +46 -0
  271. package/src/stealth/profiles.ts +10 -26
  272. package/src/stream-evidence.ts +988 -0
  273. package/src/stream.ts +8 -19
  274. package/src/testing/index.ts +10 -2
  275. package/src/testing/run.ts +653 -74
  276. package/src/types.ts +408 -28
  277. package/src/user-input.ts +118 -0
  278. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  279. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  280. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  281. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  282. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  283. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  284. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  285. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  286. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  287. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  288. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  289. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  290. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  291. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/src/define.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  import ms from "ms";
2
2
 
3
- import { ProviderError, ValidationError } from "./errors";
4
- import { safeParseSchemaSync } from "./schema";
3
+ import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
4
+ import { ProviderError, ValidationError } from "./errors.js";
5
+ import {
6
+ NativeEgressPolicyValidationError,
7
+ validateNativeProviderConfig,
8
+ } from "./native-egress-policy.js";
9
+ import { safeParseSchemaSync } from "./schema.js";
10
+ import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
5
11
  import type {
6
12
  AuthConfig,
7
13
  BrowserEngine,
@@ -20,10 +26,13 @@ import type {
20
26
  OperationSseTransport,
21
27
  OperationTransport,
22
28
  OperationWebSocketTransport,
29
+ NativeProviderConfig,
23
30
  ProviderAccessConfig,
24
31
  ProviderDefinition,
32
+ ProviderDeploymentOverrides,
25
33
  ProviderHealthMonitorConfig,
26
34
  ProviderProxyConfig,
35
+ ProviderProxyProvider,
27
36
  ProviderPublicProfile,
28
37
  ProviderReviewed,
29
38
  ProviderSecretDeclaration,
@@ -32,7 +41,7 @@ import type {
32
41
  SchemaLike,
33
42
  SmsOtpMatcherDefinition,
34
43
  StealthPlatform,
35
- } from "./types";
44
+ } from "./types.js";
36
45
  import {
37
46
  HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
38
47
  HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
@@ -48,7 +57,8 @@ import {
48
57
  STREAM_IDLE_TIMEOUT_MS_MIN,
49
58
  STREAM_MAX_DURATION_MS_MAX,
50
59
  STREAM_MAX_DURATION_MS_MIN,
51
- } from "./types";
60
+ VALID_OPERATION_ERROR_STATUSES,
61
+ } from "./types.js";
52
62
 
53
63
  type ProviderImplementationSourceAccess =
54
64
  | "official_api"
@@ -79,18 +89,34 @@ const VALID_AUTH_MODES = [
79
89
  "platform-managed",
80
90
  "credentials",
81
91
  "oauth2",
92
+ "oauth2_proxied",
82
93
  ] as const;
83
- const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
84
- const VALID_PROVIDER_PROXY_MODES = [
85
- "disabled",
86
- "optional",
87
- "required",
88
- ] as const;
89
- const VALID_PROVIDER_PROXY_PROVIDERS = [
90
- "smartproxy",
91
- "decodo",
92
- "custom",
94
+ const PROXIED_OAUTH_REQUIRED_FIELDS = [
95
+ "authorizeUrl",
96
+ "tokenUrl",
97
+ "customScheme",
98
+ "rewriteProfile",
99
+ "clientIdEnvKey",
93
100
  ] as const;
101
+ const PROXIED_OAUTH_ALLOWED_FIELDS = new Set([
102
+ ...PROXIED_OAUTH_REQUIRED_FIELDS,
103
+ "pkce",
104
+ "authorizeParams",
105
+ "tokenParams",
106
+ ]);
107
+ const PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS = new Set([
108
+ "client_id",
109
+ "response_type",
110
+ "state",
111
+ "code_challenge",
112
+ "code_challenge_method",
113
+ ]);
114
+ const PROXIED_OAUTH_PROFILE_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
115
+ const PROXIED_OAUTH_ENV_KEY_REGEX = /^[A-Z][A-Z0-9_]*__[A-Z0-9_]+$/;
116
+ const CUSTOM_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+$/;
117
+ const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
118
+ const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"] as const;
119
+ const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"] as const;
94
120
  const VALID_PROVIDER_PROXY_AFFINITIES = [
95
121
  "request",
96
122
  "operation",
@@ -99,25 +125,25 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
99
125
  ] as const;
100
126
  const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
101
127
  const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
128
+ const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
129
+ const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
130
+ // Per-vendor provider-declared credential secrets. A required-mode chain must
131
+ // declare every secret of every credentialed vendor it names, so a missing
132
+ // credential fails at build/validation time rather than during a live outage: a
133
+ // declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
134
+ // exactly the failure class the multi-vendor chain exists to remove. Vendors
135
+ // absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
136
+ // `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
137
+ // no declaration requirement.
138
+ const VENDOR_REQUIRED_SECRETS: Partial<Record<ProviderProxyProvider, readonly string[]>> = {
139
+ smartproxy: [SMARTPROXY_APP_KEY_SECRET],
140
+ nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
141
+ };
102
142
  const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
103
143
  const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
104
- const VALID_OPERATION_RISK_CLASSES = [
105
- "read",
106
- "write",
107
- "destructive",
108
- "external-send",
109
- ] as const;
110
- const VALID_OPERATION_APPROVAL_POLICIES = [
111
- "never",
112
- "risk-based",
113
- "always",
114
- ] as const;
115
- const VALID_OPERATION_TRANSPORT_KINDS = [
116
- "json",
117
- "sse",
118
- "http-stream",
119
- "websocket",
120
- ] as const;
144
+ const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
145
+ const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"] as const;
146
+ const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"] as const;
121
147
  const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
122
148
  const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
123
149
 
@@ -135,18 +161,16 @@ function msDurationMs(value: string): number {
135
161
  function parsePositiveMsDuration(value: string): number | undefined {
136
162
  const trimmed = value.trim();
137
163
  if (!MS_DURATION_PATTERN.test(trimmed)) return undefined;
138
- const parsed = ms(
139
- (trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue,
140
- );
164
+ const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue);
141
165
  if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
142
166
  return parsed;
143
167
  }
144
168
 
145
169
  type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
146
- type OperationConfig<
147
- TInput extends SchemaLike,
148
- TOutput extends SchemaLike,
149
- > = Omit<OperationDefinition<TInput, TOutput>, "handler"> & {
170
+ type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
171
+ OperationDefinition<TInput, TOutput>,
172
+ "handler"
173
+ > & {
150
174
  handler(
151
175
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
152
176
  input: InferSchemaOutput<TInput>,
@@ -154,59 +178,44 @@ type OperationConfig<
154
178
  | OperationHandlerResult<InferSchemaOutput<TOutput>>
155
179
  | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
156
180
  };
157
- type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> =
158
- {
159
- [K in keyof TOperations]: TOperations[K] extends OperationDefinition<
160
- infer TInput,
161
- infer TOutput
162
- >
163
- ? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
164
- : never;
165
- };
166
- type StreamOperationConfig<
167
- TInput extends SchemaLike,
168
- TOutput extends SchemaLike,
169
- > =
181
+ type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
182
+ [K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
183
+ ? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
184
+ : never;
185
+ };
186
+ type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
170
187
  | SseOperationConfig<TInput, TOutput>
171
188
  | HttpStreamOperationConfig<TInput, TOutput>
172
189
  | WebSocketOperationConfig<TInput, TOutput>;
173
- type SseOperationConfig<
174
- TInput extends SchemaLike,
175
- TOutput extends SchemaLike,
176
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
190
+ type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
191
+ OperationConfig<TInput, TOutput>,
192
+ "handler" | "transport"
193
+ > & {
177
194
  transport: OperationSseTransport;
178
195
  handler(
179
196
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
180
197
  input: InferSchemaOutput<TInput>,
181
- ):
182
- | AsyncIterable<ProviderStreamEvent>
183
- | Promise<AsyncIterable<ProviderStreamEvent>>;
198
+ ): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
184
199
  };
185
- type HttpStreamOperationConfig<
186
- TInput extends SchemaLike,
187
- TOutput extends SchemaLike,
188
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
200
+ type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
201
+ OperationConfig<TInput, TOutput>,
202
+ "handler" | "transport"
203
+ > & {
189
204
  transport: OperationHttpStreamTransport;
190
205
  handler(
191
206
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
192
207
  input: InferSchemaOutput<TInput>,
193
- ):
194
- | Response
195
- | ReadableStream<Uint8Array>
196
- | Promise<Response | ReadableStream<Uint8Array>>;
208
+ ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
197
209
  };
198
- type WebSocketOperationConfig<
199
- TInput extends SchemaLike,
200
- TOutput extends SchemaLike,
201
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
210
+ type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
211
+ OperationConfig<TInput, TOutput>,
212
+ "handler" | "transport"
213
+ > & {
202
214
  transport: OperationWebSocketTransport;
203
215
  handler(
204
216
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
205
217
  input: InferSchemaOutput<TInput>,
206
- ):
207
- | Response
208
- | ReadableStream<Uint8Array>
209
- | Promise<Response | ReadableStream<Uint8Array>>;
218
+ ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
210
219
  };
211
220
 
212
221
  type AuthStartNoInputGuard<TConfig> = TConfig extends {
@@ -221,13 +230,19 @@ type AuthStartNoInputGuard<TConfig> = TConfig extends {
221
230
  : unknown
222
231
  : unknown;
223
232
 
224
- export interface ProviderConfig<
225
- TOperations extends Record<string, ProviderOperation>,
226
- > {
233
+ export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
227
234
  id: string;
228
235
  version: string;
229
236
  runtime: "standard" | "shared" | "browser";
237
+ /**
238
+ * Optional deployment overrides, passed through verbatim onto the returned
239
+ * provider definition. The SDK types this field but does not deep-validate
240
+ * it — the APIFuse registry builder owns deployment validation and
241
+ * resolves omitted fields against the runtime deployment profiles.
242
+ */
243
+ deployment?: ProviderDeploymentOverrides;
230
244
  allowedHosts?: string[];
245
+ native?: NativeProviderConfig;
231
246
  stealth?: {
232
247
  profile: string;
233
248
  platform: StealthPlatform;
@@ -270,35 +285,24 @@ export interface ProviderConfig<
270
285
  }
271
286
 
272
287
  /** Define one provider operation with schema-driven handler inference. */
273
- export function defineOperation<
274
- TInput extends SchemaLike,
275
- TOutput extends SchemaLike,
276
- >(
288
+ export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
277
289
  operation: OperationConfig<TInput, TOutput>,
278
290
  ): OperationDefinition<TInput, TOutput> {
279
291
  return operation;
280
292
  }
281
293
 
282
294
  /** Define a non-JSON provider operation with explicit transport metadata. */
283
- export function defineStreamOperation<
284
- TInput extends SchemaLike,
285
- TOutput extends SchemaLike,
286
- >(
295
+ export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
287
296
  operation: StreamOperationConfig<TInput, TOutput>,
288
297
  ): OperationDefinition<TInput, TOutput> {
289
298
  return operation;
290
299
  }
291
300
 
292
- function assertObjectConfig(
293
- value: unknown,
294
- ): asserts value is Record<string, unknown> {
301
+ function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
295
302
  if (!value || typeof value !== "object") {
296
- throw new ProviderError(
297
- "defineProvider config must be an object. Offending field: config",
298
- {
299
- fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
300
- },
301
- );
303
+ throw new ProviderError("defineProvider config must be an object. Offending field: config", {
304
+ fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
305
+ });
302
306
  }
303
307
  }
304
308
  function assertRequiredField(
@@ -328,6 +332,111 @@ function assertLiteralField<TValue extends string>(
328
332
  );
329
333
  }
330
334
  }
335
+
336
+ function validateProxiedOAuthParams(
337
+ value: unknown,
338
+ field: "authorizeParams" | "tokenParams",
339
+ providerId: string,
340
+ ): void {
341
+ if (value === undefined) return;
342
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
343
+ throw new ValidationError(
344
+ `Provider "${providerId}" auth.proxied.${field} must be an object of string values.`,
345
+ );
346
+ }
347
+ for (const [key, paramValue] of Object.entries(value)) {
348
+ if (!key.trim() || typeof paramValue !== "string") {
349
+ throw new ValidationError(
350
+ `Provider "${providerId}" auth.proxied.${field} must contain non-empty keys and string values.`,
351
+ );
352
+ }
353
+ if (
354
+ field === "authorizeParams" &&
355
+ PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())
356
+ ) {
357
+ throw new ValidationError(
358
+ `Provider "${providerId}" auth.proxied.authorizeParams cannot override reserved parameter "${key}".`,
359
+ );
360
+ }
361
+ }
362
+ }
363
+
364
+ function validateProxiedOAuthAuth(auth: Record<string, unknown>, providerId: string): void {
365
+ const proxied = auth.proxied;
366
+ if (auth.mode !== "oauth2_proxied") {
367
+ if (proxied !== undefined) {
368
+ throw new ValidationError(
369
+ `Provider "${providerId}" auth.proxied is only valid when auth.mode is "oauth2_proxied".`,
370
+ );
371
+ }
372
+ return;
373
+ }
374
+ if (!proxied || typeof proxied !== "object" || Array.isArray(proxied)) {
375
+ throw new ValidationError(
376
+ `Provider "${providerId}" with auth.mode "oauth2_proxied" must declare auth.proxied.`,
377
+ );
378
+ }
379
+ const config = Object.fromEntries(Object.entries(proxied));
380
+ for (const key of Object.keys(config)) {
381
+ if (!PROXIED_OAUTH_ALLOWED_FIELDS.has(key)) {
382
+ throw new ValidationError(
383
+ `Provider "${providerId}" has unknown auth.proxied field "${key}".`,
384
+ );
385
+ }
386
+ }
387
+ for (const field of PROXIED_OAUTH_REQUIRED_FIELDS) {
388
+ if (typeof config[field] !== "string" || !config[field].trim()) {
389
+ throw new ValidationError(
390
+ `Provider "${providerId}" auth.proxied.${field} must be a non-empty string.`,
391
+ );
392
+ }
393
+ }
394
+ for (const field of ["authorizeUrl", "tokenUrl"] as const) {
395
+ try {
396
+ const endpoint = new URL(String(config[field]));
397
+ if (
398
+ endpoint.protocol !== "https:" ||
399
+ endpoint.username ||
400
+ endpoint.password ||
401
+ endpoint.hash
402
+ ) {
403
+ throw new Error("invalid endpoint");
404
+ }
405
+ } catch {
406
+ throw new ValidationError(
407
+ `Provider "${providerId}" auth.proxied.${field} must be an absolute HTTPS URL without credentials or a fragment.`,
408
+ );
409
+ }
410
+ }
411
+ const customScheme = String(config.customScheme);
412
+ if (
413
+ !CUSTOM_SCHEME_REGEX.test(customScheme) ||
414
+ customScheme.toLowerCase().startsWith("http://") ||
415
+ customScheme.toLowerCase().startsWith("https://")
416
+ ) {
417
+ throw new ValidationError(
418
+ `Provider "${providerId}" auth.proxied.customScheme must be a non-HTTP custom-scheme URL prefix.`,
419
+ );
420
+ }
421
+ if (!PROXIED_OAUTH_PROFILE_REGEX.test(String(config.rewriteProfile))) {
422
+ throw new ValidationError(
423
+ `Provider "${providerId}" auth.proxied.rewriteProfile must be a kebab-case profile name.`,
424
+ );
425
+ }
426
+ if (!PROXIED_OAUTH_ENV_KEY_REGEX.test(String(config.clientIdEnvKey))) {
427
+ throw new ValidationError(
428
+ `Provider "${providerId}" auth.proxied.clientIdEnvKey must be an APIFuse-style uppercase environment key.`,
429
+ );
430
+ }
431
+ if (config.pkce !== undefined && config.pkce !== "S256" && config.pkce !== "none") {
432
+ throw new ValidationError(
433
+ `Provider "${providerId}" auth.proxied.pkce must be "S256" or "none".`,
434
+ );
435
+ }
436
+ validateProxiedOAuthParams(config.authorizeParams, "authorizeParams", providerId);
437
+ validateProxiedOAuthParams(config.tokenParams, "tokenParams", providerId);
438
+ }
439
+
331
440
  function validateProviderShape(config: unknown): void {
332
441
  assertObjectConfig(config);
333
442
  assertRequiredField(config, "id");
@@ -336,25 +445,13 @@ function validateProviderShape(config: unknown): void {
336
445
  assertRequiredField(config, "meta", String(config.id));
337
446
  assertRequiredField(config, "operations", String(config.id));
338
447
  if (typeof config.runtime === "string")
339
- assertLiteralField(
340
- config.runtime,
341
- "runtime",
342
- VALID_RUNTIMES,
343
- String(config.id),
344
- );
448
+ assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
345
449
  const auth = config.auth;
346
- if (
347
- auth &&
348
- typeof auth === "object" &&
349
- "mode" in auth &&
350
- typeof auth.mode === "string"
351
- )
352
- assertLiteralField(
353
- auth.mode,
354
- "auth.mode",
355
- VALID_AUTH_MODES,
356
- String(config.id),
357
- );
450
+ if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
451
+ assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
452
+ if (auth && typeof auth === "object" && !Array.isArray(auth)) {
453
+ validateProxiedOAuthAuth(Object.fromEntries(Object.entries(auth)), String(config.id));
454
+ }
358
455
  if (auth && typeof auth === "object" && "exchange" in auth) {
359
456
  throw new ProviderError(
360
457
  `Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
@@ -390,9 +487,7 @@ function validateProviderShape(config: unknown): void {
390
487
  },
391
488
  );
392
489
  }
393
- const accessRecord: Record<string, unknown> = Object.fromEntries(
394
- Object.entries(access),
395
- );
490
+ const accessRecord: Record<string, unknown> = Object.fromEntries(Object.entries(access));
396
491
  for (const key of Object.keys(accessRecord)) {
397
492
  if (key !== "visibility") {
398
493
  throw new ValidationError(`Unknown field "${key}" on access.`, {
@@ -437,31 +532,26 @@ function validateProviderProxy(config: {
437
532
  },
438
533
  );
439
534
  }
440
- rejectUnknownFields(
441
- proxy,
442
- new Set(["mode", "provider", "geo", "session"]),
443
- "proxy",
444
- );
445
- assertLiteralField(
446
- proxy.mode,
447
- "proxy.mode",
448
- VALID_PROVIDER_PROXY_MODES,
449
- config.id,
450
- );
535
+ rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
536
+ assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
451
537
  if (proxy.provider !== undefined) {
452
- assertLiteralField(
453
- proxy.provider,
454
- "proxy.provider",
455
- VALID_PROVIDER_PROXY_PROVIDERS,
456
- config.id,
457
- );
538
+ assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
539
+ }
540
+ if (proxy.providers !== undefined) {
541
+ if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
542
+ throw new ValidationError(
543
+ `Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`,
544
+ {
545
+ fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
546
+ },
547
+ );
548
+ }
549
+ for (const vendor of proxy.providers) {
550
+ assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
551
+ }
458
552
  }
459
553
  if (proxy.geo !== undefined) {
460
- if (
461
- !proxy.geo ||
462
- typeof proxy.geo !== "object" ||
463
- Array.isArray(proxy.geo)
464
- ) {
554
+ if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
465
555
  throw new ValidationError(
466
556
  `Provider "${config.id}" has invalid proxy.geo: must be an object.`,
467
557
  {
@@ -469,11 +559,7 @@ function validateProviderProxy(config: {
469
559
  },
470
560
  );
471
561
  }
472
- rejectUnknownFields(
473
- proxy.geo,
474
- new Set(["country", "subdivision", "city"]),
475
- "proxy.geo",
476
- );
562
+ rejectUnknownFields(proxy.geo, new Set(["country", "subdivision", "city"]), "proxy.geo");
477
563
  if (proxy.geo.country !== undefined) {
478
564
  assertIsoCountry(proxy.geo.country, "proxy.geo.country");
479
565
  }
@@ -487,11 +573,7 @@ function validateProviderProxy(config: {
487
573
  }
488
574
  }
489
575
  if (proxy.session !== undefined) {
490
- if (
491
- !proxy.session ||
492
- typeof proxy.session !== "object" ||
493
- Array.isArray(proxy.session)
494
- ) {
576
+ if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
495
577
  throw new ValidationError(
496
578
  `Provider "${config.id}" has invalid proxy.session: must be an object.`,
497
579
  {
@@ -501,7 +583,7 @@ function validateProviderProxy(config: {
501
583
  }
502
584
  rejectUnknownFields(
503
585
  proxy.session,
504
- new Set(["affinity", "lifetimeMinutes", "poolSize"]),
586
+ new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]),
505
587
  "proxy.session",
506
588
  );
507
589
  if (proxy.session.affinity !== undefined) {
@@ -513,51 +595,103 @@ function validateProviderProxy(config: {
513
595
  );
514
596
  }
515
597
  const lifetime = proxy.session.lifetimeMinutes;
516
- if (
517
- lifetime !== undefined &&
518
- (!Number.isFinite(lifetime) || lifetime <= 0)
519
- ) {
598
+ if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
520
599
  throw new ValidationError(
521
600
  `Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`,
522
601
  );
523
602
  }
524
603
  const poolSize = proxy.session.poolSize;
604
+ if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
605
+ throw new ValidationError(
606
+ `Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
607
+ );
608
+ }
609
+ const drainLeadSeconds = proxy.session.drainLeadSeconds;
525
610
  if (
526
- poolSize !== undefined &&
527
- (!Number.isInteger(poolSize) || poolSize <= 0)
611
+ drainLeadSeconds !== undefined &&
612
+ (!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)
528
613
  ) {
529
614
  throw new ValidationError(
530
- `Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
615
+ `Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`,
616
+ {
617
+ fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
618
+ },
531
619
  );
532
620
  }
533
- }
534
- if (proxy.mode === "required" && proxy.provider === "smartproxy") {
535
- const hasSmartproxySecret = config.secrets?.some(
536
- (secret) =>
537
- secret.name === SMARTPROXY_APP_KEY_SECRET && secret.required !== false,
538
- );
539
- if (!hasSmartproxySecret) {
621
+ // A drain lead longer than the sticky lifetime would fire the expiring
622
+ // event before the session is even established, so the provider would
623
+ // never get a usable window. Reject the contradiction at build time.
624
+ if (
625
+ drainLeadSeconds !== undefined &&
626
+ lifetime !== undefined &&
627
+ Number.isFinite(lifetime) &&
628
+ drainLeadSeconds >= lifetime * 60
629
+ ) {
540
630
  throw new ValidationError(
541
- `Provider "${config.id}" requires Smartproxy egress but does not declare ${SMARTPROXY_APP_KEY_SECRET}.`,
631
+ `Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`,
542
632
  {
543
- fix: `Add secrets: [{ name: "${SMARTPROXY_APP_KEY_SECRET}", required: true }] to the provider.`,
633
+ fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
544
634
  },
545
635
  );
546
636
  }
547
637
  }
638
+ // Every credentialed vendor in a required-mode chain must declare its
639
+ // provider secret(s) so a missing credential fails at build/validation time,
640
+ // not during a live outage. This covers the fallback legs too (not just the
641
+ // first vendor): a declared-but-uncredentialed nodemaven fallback would leave
642
+ // the chain silently down to a single vendor, reintroducing the SPOF the chain
643
+ // removes.
644
+ const vendorChain =
645
+ proxy.providers && proxy.providers.length > 0
646
+ ? proxy.providers
647
+ : proxy.provider
648
+ ? [proxy.provider]
649
+ : [];
650
+ if (proxy.mode === "required") {
651
+ for (const vendor of vendorChain) {
652
+ const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
653
+ if (!requiredSecrets) continue;
654
+ for (const secretName of requiredSecrets) {
655
+ // Match the canonical runtime gate (assertRequiredSecretsPresent /
656
+ // listMissingRequiredSecrets), which enforces only `required === true`
657
+ // declarations. A declaration that omits `required` (defaulting to
658
+ // optional) is skipped at runtime, so accepting it here would pass
659
+ // validation while leaving the credential unenforced until proxy
660
+ // resolution during a live request — the fail-open gap this check exists
661
+ // to close.
662
+ const declared = config.secrets?.some(
663
+ (secret) => secret.name === secretName && secret.required === true,
664
+ );
665
+ if (!declared) {
666
+ throw new ValidationError(
667
+ `Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`,
668
+ {
669
+ fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
670
+ },
671
+ );
672
+ }
673
+ }
674
+ }
675
+ }
676
+ // `decodo`/`custom` are deprecated vendor values (string-union members, so the
677
+ // @deprecated symbol gate can't catch them — warn at validation time instead).
678
+ const deprecatedVendors = vendorChain.filter(
679
+ (vendor) => vendor === "decodo" || vendor === "custom",
680
+ );
681
+ if (deprecatedVendors.length > 0) {
682
+ console.warn(
683
+ `[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven", or the APIFUSE__PROXY__URL bring-your-own escape hatch.`,
684
+ );
685
+ }
548
686
  }
549
687
 
550
- function validateProviderStt(config: {
551
- id: string;
552
- stt?: ProviderSttConfig;
553
- }): void {
688
+ function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): void {
554
689
  const stt = config.stt;
555
690
  if (stt === undefined) return;
556
691
  if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
557
- throw new ValidationError(
558
- `Provider "${config.id}" has invalid stt: must be an object.`,
559
- { fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.` },
560
- );
692
+ throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
693
+ fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
694
+ });
561
695
  }
562
696
  rejectUnknownFields(stt, new Set(["mode"]), "stt");
563
697
  assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
@@ -584,16 +718,9 @@ function validateOperationIds(
584
718
  );
585
719
  }
586
720
  }
587
- const OPERATION_CONTRACT_VERSION_REGEX =
588
- /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
589
- const OPERATION_SENSITIVE_PATH_REGEX =
590
- /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
591
- const VALID_OPERATION_LIFECYCLES = [
592
- "stable",
593
- "beta",
594
- "deprecated",
595
- "removed",
596
- ] as const;
721
+ const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
722
+ const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
723
+ const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"] as const;
597
724
 
598
725
  function assertNonEmptyString(
599
726
  value: unknown,
@@ -624,10 +751,7 @@ function validateToolRouterMetadata(
624
751
  },
625
752
  );
626
753
  }
627
- if (
628
- toolRouter.name !== undefined &&
629
- !MCP_TOOL_NAME_REGEX.test(toolRouter.name)
630
- ) {
754
+ if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
631
755
  throw new ValidationError(
632
756
  `Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`,
633
757
  {
@@ -699,10 +823,7 @@ function validateOperationContracts(
699
823
  providerId,
700
824
  );
701
825
  }
702
- if (
703
- contract.lifecycle === "deprecated" ||
704
- contract.lifecycle === "removed"
705
- ) {
826
+ if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
706
827
  if (!contract.deprecation || typeof contract.deprecation !== "object") {
707
828
  throw new ValidationError(
708
829
  `Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`,
@@ -750,10 +871,7 @@ function validateOperationAnnotations(
750
871
  fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
751
872
  },
752
873
  );
753
- if (
754
- timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
755
- timeoutMs > OPERATION_TIMEOUT_MS_MAX
756
- )
874
+ if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
757
875
  throw new ValidationError(
758
876
  `Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`,
759
877
  {
@@ -821,6 +939,36 @@ function validateOperationObservability(
821
939
  }
822
940
  }
823
941
 
942
+ function validateOperationErrorCodes(
943
+ providerId: string,
944
+ operations: Record<string, ProviderOperation>,
945
+ ): void {
946
+ for (const [operationName, operation] of Object.entries(operations)) {
947
+ for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
948
+ if (
949
+ errorCode.status !== undefined &&
950
+ !VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)
951
+ ) {
952
+ const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
953
+ throw new ValidationError(
954
+ `Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`,
955
+ {
956
+ fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
957
+ },
958
+ );
959
+ }
960
+ if (
961
+ errorCode.status !== undefined &&
962
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)
963
+ ) {
964
+ console.warn(
965
+ `[provider-sdk] Provider "${providerId}" operation "${operationName}" declares status ${errorCode.status} for SDK-owned error code "${errorCode.code}"; the declared status is documentation-only and will be ignored at runtime.`,
966
+ );
967
+ }
968
+ }
969
+ }
970
+ }
971
+
824
972
  const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
825
973
  const SSE_TRANSPORT_FIELDS = new Set([
826
974
  "kind",
@@ -951,12 +1099,7 @@ function validateOperationTransports(
951
1099
  },
952
1100
  );
953
1101
  }
954
- assertLiteralField(
955
- kind,
956
- `${fieldPath}.kind`,
957
- VALID_OPERATION_TRANSPORT_KINDS,
958
- providerId,
959
- );
1102
+ assertLiteralField(kind, `${fieldPath}.kind`, VALID_OPERATION_TRANSPORT_KINDS, providerId);
960
1103
 
961
1104
  switch (kind) {
962
1105
  case "json":
@@ -988,16 +1131,9 @@ function validateOperationTransports(
988
1131
  STREAM_MAX_DURATION_MS_MAX,
989
1132
  "max duration",
990
1133
  );
991
- assertPositiveBytes(
992
- Reflect.get(transport, "maxEventBytes"),
993
- `${fieldPath}.maxEventBytes`,
994
- );
1134
+ assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
995
1135
  const resumable = Reflect.get(transport, "resumable");
996
- if (
997
- resumable !== undefined &&
998
- resumable !== false &&
999
- resumable !== "last-event-id"
1000
- ) {
1136
+ if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
1001
1137
  throw new ValidationError(
1002
1138
  `Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`,
1003
1139
  {
@@ -1017,12 +1153,7 @@ function validateOperationTransports(
1017
1153
  rejectUnknownFields(transport, HTTP_STREAM_TRANSPORT_FIELDS, fieldPath);
1018
1154
  const contentType = Reflect.get(transport, "contentType");
1019
1155
  if (contentType !== undefined) {
1020
- assertNonEmptyString(
1021
- contentType,
1022
- `${fieldPath}.contentType`,
1023
- providerId,
1024
- operationName,
1025
- );
1156
+ assertNonEmptyString(contentType, `${fieldPath}.contentType`, providerId, operationName);
1026
1157
  }
1027
1158
  assertStreamMs(
1028
1159
  Reflect.get(transport, "idleTimeoutMs"),
@@ -1038,10 +1169,7 @@ function validateOperationTransports(
1038
1169
  STREAM_MAX_DURATION_MS_MAX,
1039
1170
  "max duration",
1040
1171
  );
1041
- assertPositiveBytes(
1042
- Reflect.get(transport, "maxChunkBytes"),
1043
- `${fieldPath}.maxChunkBytes`,
1044
- );
1172
+ assertPositiveBytes(Reflect.get(transport, "maxChunkBytes"), `${fieldPath}.maxChunkBytes`);
1045
1173
  break;
1046
1174
  }
1047
1175
  case "websocket": {
@@ -1066,10 +1194,7 @@ function validateOperationTransports(
1066
1194
  );
1067
1195
  }
1068
1196
  for (const subprotocol of subprotocols) {
1069
- if (
1070
- typeof subprotocol !== "string" ||
1071
- !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)
1072
- ) {
1197
+ if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
1073
1198
  throw new ValidationError(
1074
1199
  `Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`,
1075
1200
  {
@@ -1093,10 +1218,7 @@ function validateOperationTransports(
1093
1218
  STREAM_MAX_DURATION_MS_MAX,
1094
1219
  "max duration",
1095
1220
  );
1096
- assertPositiveBytes(
1097
- Reflect.get(transport, "maxFrameBytes"),
1098
- `${fieldPath}.maxFrameBytes`,
1099
- );
1221
+ assertPositiveBytes(Reflect.get(transport, "maxFrameBytes"), `${fieldPath}.maxFrameBytes`);
1100
1222
  break;
1101
1223
  }
1102
1224
  }
@@ -1159,10 +1281,7 @@ function levenshtein(a: string, b: string): number {
1159
1281
  return prev[n] ?? 0;
1160
1282
  }
1161
1283
 
1162
- function suggestField(
1163
- unknown: string,
1164
- candidates: ReadonlySet<string>,
1165
- ): string | undefined {
1284
+ function suggestField(unknown: string, candidates: ReadonlySet<string>): string | undefined {
1166
1285
  let best: string | undefined;
1167
1286
  let bestDist = 3;
1168
1287
  for (const candidate of candidates) {
@@ -1175,11 +1294,7 @@ function suggestField(
1175
1294
  return best;
1176
1295
  }
1177
1296
 
1178
- function rejectUnknownFields(
1179
- value: object,
1180
- allowed: ReadonlySet<string>,
1181
- fieldPath: string,
1182
- ): void {
1297
+ function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldPath: string): void {
1183
1298
  for (const key of Object.keys(value)) {
1184
1299
  if (allowed.has(key)) continue;
1185
1300
  const hint = suggestField(key, allowed);
@@ -1218,23 +1333,12 @@ function validateProviderHealthMonitor(
1218
1333
  field: "healthMonitor" | "healthProbe" = "healthMonitor",
1219
1334
  ): void {
1220
1335
  if (healthMonitor === undefined) return;
1221
- if (
1222
- !healthMonitor ||
1223
- typeof healthMonitor !== "object" ||
1224
- Array.isArray(healthMonitor)
1225
- )
1226
- throw new ValidationError(
1227
- `Provider "${providerId}" has invalid ${field}: must be an object.`,
1228
- {
1229
- fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
1230
- },
1231
- );
1336
+ if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
1337
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
1338
+ fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
1339
+ });
1232
1340
  const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
1233
- rejectUnknownFields(
1234
- healthMonitorRecord,
1235
- PROVIDER_HEALTH_MONITOR_FIELDS,
1236
- field,
1237
- );
1341
+ rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
1238
1342
  if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
1239
1343
  assertBoundedIntegerMs(
1240
1344
  healthMonitorRecord.defaultProbeTimeoutMs,
@@ -1302,11 +1406,7 @@ function validateProviderHealthMonitor(
1302
1406
 
1303
1407
  const probeOverrides = healthMonitorRecord.probeOverrides;
1304
1408
  if (probeOverrides !== undefined) {
1305
- if (
1306
- !probeOverrides ||
1307
- typeof probeOverrides !== "object" ||
1308
- Array.isArray(probeOverrides)
1309
- )
1409
+ if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
1310
1410
  throw new ValidationError(
1311
1411
  `Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`,
1312
1412
  );
@@ -1372,14 +1472,8 @@ function validateHealthCheckCase(
1372
1472
  ): void {
1373
1473
  const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
1374
1474
  if (!caseValue || typeof caseValue !== "object" || Array.isArray(caseValue))
1375
- throw new ValidationError(
1376
- `Provider "${providerId}" ${fieldPath} must be an object.`,
1377
- );
1378
- rejectUnknownFields(
1379
- caseValue as Record<string, unknown>,
1380
- HEALTH_CHECK_CASE_FIELDS,
1381
- fieldPath,
1382
- );
1475
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1476
+ rejectUnknownFields(caseValue as Record<string, unknown>, HEALTH_CHECK_CASE_FIELDS, fieldPath);
1383
1477
  const c = caseValue as HealthCheckCase;
1384
1478
  if (typeof c.name !== "string" || c.name.length === 0)
1385
1479
  throw new ValidationError(
@@ -1407,15 +1501,11 @@ function validateHealthCheckCase(
1407
1501
  `Provider "${providerId}" ${fieldPath}.degradedThresholdMs must be an integer degraded threshold in [${HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN}, ${HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX}] ms.`,
1408
1502
  );
1409
1503
  if (c.timeoutMs !== undefined) {
1410
- assertBoundedIntegerMs(
1411
- c.timeoutMs,
1412
- `Provider "${providerId}" ${fieldPath}.timeoutMs`,
1413
- {
1414
- min: HEALTH_CHECK_TIMEOUT_MS_MIN,
1415
- max: HEALTH_CHECK_TIMEOUT_MS_MAX,
1416
- label: "timeout",
1417
- },
1418
- );
1504
+ assertBoundedIntegerMs(c.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
1505
+ min: HEALTH_CHECK_TIMEOUT_MS_MIN,
1506
+ max: HEALTH_CHECK_TIMEOUT_MS_MAX,
1507
+ label: "timeout",
1508
+ });
1419
1509
  }
1420
1510
  if (
1421
1511
  c.expectedStatus !== undefined &&
@@ -1431,21 +1521,11 @@ function validateHealthCheckCase(
1431
1521
  );
1432
1522
  }
1433
1523
 
1434
- function validateHealthCheckSuite(
1435
- providerId: string,
1436
- operationName: string,
1437
- suite: unknown,
1438
- ): void {
1524
+ function validateHealthCheckSuite(providerId: string, operationName: string, suite: unknown): void {
1439
1525
  const fieldPath = `operations.${operationName}.healthCheck`;
1440
1526
  if (!suite || typeof suite !== "object" || Array.isArray(suite))
1441
- throw new ValidationError(
1442
- `Provider "${providerId}" ${fieldPath} must be an object.`,
1443
- );
1444
- rejectUnknownFields(
1445
- suite as Record<string, unknown>,
1446
- HEALTH_CHECK_SUITE_FIELDS,
1447
- fieldPath,
1448
- );
1527
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1528
+ rejectUnknownFields(suite as Record<string, unknown>, HEALTH_CHECK_SUITE_FIELDS, fieldPath);
1449
1529
  const s = suite as HealthCheckSuite;
1450
1530
  if (!isPositiveMsDurationString(s.interval))
1451
1531
  throw new ValidationError(
@@ -1455,11 +1535,7 @@ function validateHealthCheckSuite(
1455
1535
  },
1456
1536
  );
1457
1537
  if (s.schedule !== undefined) {
1458
- if (
1459
- !s.schedule ||
1460
- typeof s.schedule !== "object" ||
1461
- Array.isArray(s.schedule)
1462
- ) {
1538
+ if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
1463
1539
  throw new ValidationError(
1464
1540
  `Provider "${providerId}" ${fieldPath}.schedule must be an object.`,
1465
1541
  );
@@ -1469,11 +1545,7 @@ function validateHealthCheckSuite(
1469
1545
  `Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`,
1470
1546
  );
1471
1547
  }
1472
- rejectUnknownFields(
1473
- s.schedule,
1474
- new Set(["randomize"]),
1475
- `${fieldPath}.schedule`,
1476
- );
1548
+ rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
1477
1549
  const randomize = Reflect.get(s.schedule, "randomize");
1478
1550
  if (randomize !== undefined) {
1479
1551
  validateScheduleRandomization(
@@ -1484,15 +1556,11 @@ function validateHealthCheckSuite(
1484
1556
  }
1485
1557
  }
1486
1558
  if (s.timeoutMs !== undefined) {
1487
- assertBoundedIntegerMs(
1488
- s.timeoutMs,
1489
- `Provider "${providerId}" ${fieldPath}.timeoutMs`,
1490
- {
1491
- min: HEALTH_CHECK_TIMEOUT_MS_MIN,
1492
- max: HEALTH_CHECK_TIMEOUT_MS_MAX,
1493
- label: "timeout",
1494
- },
1495
- );
1559
+ assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
1560
+ min: HEALTH_CHECK_TIMEOUT_MS_MIN,
1561
+ max: HEALTH_CHECK_TIMEOUT_MS_MAX,
1562
+ label: "timeout",
1563
+ });
1496
1564
  }
1497
1565
  if (s.degradedThresholdMs !== undefined) {
1498
1566
  assertBoundedIntegerMs(
@@ -1505,10 +1573,7 @@ function validateHealthCheckSuite(
1505
1573
  },
1506
1574
  );
1507
1575
  }
1508
- if (
1509
- s.requiresConnection !== undefined &&
1510
- typeof s.requiresConnection !== "boolean"
1511
- )
1576
+ if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
1512
1577
  throw new ValidationError(
1513
1578
  `Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`,
1514
1579
  );
@@ -1540,14 +1605,8 @@ function validateHealthCheckUnsupported(
1540
1605
  unsupported: unknown,
1541
1606
  ): void {
1542
1607
  const fieldPath = `operations.${operationName}.healthCheckUnsupported`;
1543
- if (
1544
- !unsupported ||
1545
- typeof unsupported !== "object" ||
1546
- Array.isArray(unsupported)
1547
- )
1548
- throw new ValidationError(
1549
- `Provider "${providerId}" ${fieldPath} must be an object.`,
1550
- );
1608
+ if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
1609
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1551
1610
  rejectUnknownFields(
1552
1611
  unsupported as Record<string, unknown>,
1553
1612
  HEALTH_CHECK_UNSUPPORTED_FIELDS,
@@ -1581,12 +1640,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
1581
1640
  "steps",
1582
1641
  "run",
1583
1642
  ]);
1584
- const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
1585
- "kind",
1586
- "interval",
1587
- "jitter",
1588
- "randomize",
1589
- ]);
1643
+ const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
1590
1644
  const HEALTH_JOURNEY_STEP_FIELDS = new Set([
1591
1645
  "id",
1592
1646
  "description",
@@ -1605,10 +1659,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
1605
1659
  "minManualInterval",
1606
1660
  "publicRationale",
1607
1661
  ]);
1608
- const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
1609
- "enabled",
1610
- "reason",
1611
- ]);
1662
+ const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
1612
1663
  const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
1613
1664
  "enabled",
1614
1665
  "requiresAcknowledgement",
@@ -1628,32 +1679,18 @@ function validateHealthJourneyManualTrigger(
1628
1679
  manualTrigger: unknown,
1629
1680
  ): void {
1630
1681
  const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
1631
- if (
1632
- !manualTrigger ||
1633
- typeof manualTrigger !== "object" ||
1634
- Array.isArray(manualTrigger)
1635
- ) {
1682
+ if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
1636
1683
  throw new ValidationError(
1637
1684
  `Provider "${providerId}" ${fieldPath} must be an object when present.`,
1638
1685
  );
1639
1686
  }
1640
- rejectUnknownFields(
1641
- manualTrigger,
1642
- HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS,
1643
- fieldPath,
1644
- );
1687
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
1645
1688
  const enabled = Reflect.get(manualTrigger, "enabled");
1646
1689
  if (typeof enabled !== "boolean") {
1647
- throw new ValidationError(
1648
- `Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`,
1649
- );
1690
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`);
1650
1691
  }
1651
1692
  if (enabled === false) {
1652
- rejectUnknownFields(
1653
- manualTrigger,
1654
- HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS,
1655
- fieldPath,
1656
- );
1693
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS, fieldPath);
1657
1694
  if (
1658
1695
  Reflect.get(manualTrigger, "reason") !== undefined &&
1659
1696
  (typeof Reflect.get(manualTrigger, "reason") !== "string" ||
@@ -1665,25 +1702,15 @@ function validateHealthJourneyManualTrigger(
1665
1702
  }
1666
1703
  return;
1667
1704
  }
1668
- rejectUnknownFields(
1669
- manualTrigger,
1670
- HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS,
1671
- fieldPath,
1672
- );
1673
- const requiresAcknowledgement = Reflect.get(
1674
- manualTrigger,
1675
- "requiresAcknowledgement",
1676
- );
1705
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS, fieldPath);
1706
+ const requiresAcknowledgement = Reflect.get(manualTrigger, "requiresAcknowledgement");
1677
1707
  if (typeof requiresAcknowledgement !== "boolean") {
1678
1708
  throw new ValidationError(
1679
1709
  `Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`,
1680
1710
  );
1681
1711
  }
1682
1712
  const risk = Reflect.get(manualTrigger, "risk");
1683
- if (
1684
- typeof risk !== "string" ||
1685
- !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)
1686
- ) {
1713
+ if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
1687
1714
  throw new ValidationError(
1688
1715
  `Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`,
1689
1716
  );
@@ -1694,10 +1721,7 @@ function validateHealthJourneyManualTrigger(
1694
1721
  );
1695
1722
  }
1696
1723
  const minManualInterval = Reflect.get(manualTrigger, "minManualInterval");
1697
- assertIsoDuration(
1698
- minManualInterval,
1699
- `Provider "${providerId}" ${fieldPath}.minManualInterval`,
1700
- );
1724
+ assertIsoDuration(minManualInterval, `Provider "${providerId}" ${fieldPath}.minManualInterval`);
1701
1725
  if (isoDurationMs(minManualInterval) <= 0) {
1702
1726
  throw new ValidationError(
1703
1727
  `Provider "${providerId}" ${fieldPath}.minManualInterval must be a positive duration.`,
@@ -1728,18 +1752,14 @@ const SMS_ORIGIN_FIELDS_BY_KIND: Record<string, ReadonlySet<string>> = {
1728
1752
  e164: new Set(["kind", "value", "display"]),
1729
1753
  nationalServiceCode: new Set(["kind", "country", "value", "display"]),
1730
1754
  };
1731
- const DURATION_RE =
1732
- /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
1755
+ const DURATION_RE = /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
1733
1756
  const E164_RE = /^\+[1-9]\d{1,14}$/;
1734
1757
  const ISO_COUNTRY_RE = /^[A-Z]{2}$/;
1735
1758
  const NATIONAL_SERVICE_CODE_RE = /^[0-9]{2,15}$/;
1736
1759
  const BCP47_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
1737
1760
  const JOURNEY_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1738
1761
 
1739
- function assertIsoDuration(
1740
- value: unknown,
1741
- fieldPath: string,
1742
- ): asserts value is string {
1762
+ function assertIsoDuration(value: unknown, fieldPath: string): asserts value is string {
1743
1763
  if (typeof value !== "string" || !DURATION_RE.test(value)) {
1744
1764
  throw new ValidationError(
1745
1765
  `${fieldPath} must be an ISO 8601 duration for example PT8H or PT2M30S.`,
@@ -1754,15 +1774,10 @@ function isoDurationMs(value: string): number {
1754
1774
  const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
1755
1775
  const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
1756
1776
  const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
1757
- return (
1758
- days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000
1759
- );
1777
+ return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
1760
1778
  }
1761
1779
 
1762
- function scheduleRandomizationMs(
1763
- randomize: unknown,
1764
- fieldPath: string,
1765
- ): number {
1780
+ function scheduleRandomizationMs(randomize: unknown, fieldPath: string): number {
1766
1781
  const mode = Reflect.get(randomize as object, "mode");
1767
1782
  switch (mode) {
1768
1783
  case "centered": {
@@ -1776,9 +1791,7 @@ function scheduleRandomizationMs(
1776
1791
  return isoDurationMs(maxDelay);
1777
1792
  }
1778
1793
  default:
1779
- throw new ValidationError(
1780
- `${fieldPath}.mode must be "centered" or "delayed".`,
1781
- );
1794
+ throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
1782
1795
  }
1783
1796
  }
1784
1797
 
@@ -1792,25 +1805,18 @@ function validateScheduleRandomization(
1792
1805
  }
1793
1806
  const mode = Reflect.get(randomize, "mode");
1794
1807
  const allowedFields =
1795
- mode === "centered"
1796
- ? new Set(["mode", "maxOffset"])
1797
- : new Set(["mode", "maxDelay"]);
1808
+ mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
1798
1809
  rejectUnknownFields(randomize, allowedFields, fieldPath);
1799
1810
  const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
1800
1811
  if (offsetMs <= 0) {
1801
1812
  throw new ValidationError(`${fieldPath} duration must be positive.`);
1802
1813
  }
1803
1814
  if (offsetMs >= intervalMs) {
1804
- throw new ValidationError(
1805
- `${fieldPath} duration must be shorter than schedule interval.`,
1806
- );
1815
+ throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
1807
1816
  }
1808
1817
  }
1809
1818
 
1810
- function assertIsoCountry(
1811
- value: unknown,
1812
- fieldPath: string,
1813
- ): asserts value is string {
1819
+ function assertIsoCountry(value: unknown, fieldPath: string): asserts value is string {
1814
1820
  if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
1815
1821
  throw new ValidationError(
1816
1822
  `${fieldPath} must be an ISO 3166-1 alpha-2 country code for example KR.`,
@@ -1833,9 +1839,7 @@ function normalizeIntervalDuration(input: string): string {
1833
1839
  ? durationMs / 3_600_000
1834
1840
  : durationMs / 86_400_000;
1835
1841
  if (!Number.isInteger(amount) || amount <= 0) {
1836
- throw new ValidationError(
1837
- `Journey schedule interval must be a positive duration.`,
1838
- );
1842
+ throw new ValidationError(`Journey schedule interval must be a positive duration.`);
1839
1843
  }
1840
1844
  if (unit === "s") return `PT${amount}S`;
1841
1845
  if (unit === "m") return `PT${amount}M`;
@@ -1894,11 +1898,7 @@ function countCapturingGroups(pattern: RegExp): number {
1894
1898
  if (inCharacterClass || char !== "(") continue;
1895
1899
  const next = source[i + 1];
1896
1900
  if (next === "?" && source[i + 2] !== "<") continue;
1897
- if (
1898
- next === "?" &&
1899
- source[i + 2] === "<" &&
1900
- (source[i + 3] === "=" || source[i + 3] === "!")
1901
- )
1901
+ if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
1902
1902
  continue;
1903
1903
  count += 1;
1904
1904
  }
@@ -1917,9 +1917,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
1917
1917
  }
1918
1918
  const kind = Reflect.get(origin, "kind");
1919
1919
  if (kind !== "e164" && kind !== "nationalServiceCode") {
1920
- throw new ValidationError(
1921
- `${fieldPath}.kind must be "e164" or "nationalServiceCode".`,
1922
- );
1920
+ throw new ValidationError(`${fieldPath}.kind must be "e164" or "nationalServiceCode".`);
1923
1921
  }
1924
1922
  rejectUnknownFields(origin, SMS_ORIGIN_FIELDS_BY_KIND[kind], fieldPath);
1925
1923
  if (kind === "e164") {
@@ -1946,9 +1944,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
1946
1944
  Reflect.get(origin, "display") !== undefined &&
1947
1945
  typeof Reflect.get(origin, "display") !== "string"
1948
1946
  ) {
1949
- throw new ValidationError(
1950
- `${fieldPath}.display must be a string when present.`,
1951
- );
1947
+ throw new ValidationError(`${fieldPath}.display must be a string when present.`);
1952
1948
  }
1953
1949
  }
1954
1950
 
@@ -1962,9 +1958,7 @@ function validateSmsOtpMatcher(
1962
1958
  rejectUnknownFields(matcher, SMS_OTP_MATCHER_FIELDS, fieldPath);
1963
1959
  const matcherId = Reflect.get(matcher, "id");
1964
1960
  if (typeof matcherId !== "string" || !JOURNEY_ID_RE.test(matcherId)) {
1965
- throw new ValidationError(
1966
- `${fieldPath}.id must be a kebab-case identifier.`,
1967
- );
1961
+ throw new ValidationError(`${fieldPath}.id must be a kebab-case identifier.`);
1968
1962
  }
1969
1963
  assertIsoCountry(Reflect.get(matcher, "country"), `${fieldPath}.country`);
1970
1964
  if (
@@ -1972,24 +1966,18 @@ function validateSmsOtpMatcher(
1972
1966
  (typeof Reflect.get(matcher, "locale") !== "string" ||
1973
1967
  !BCP47_RE.test(Reflect.get(matcher, "locale")))
1974
1968
  ) {
1975
- throw new ValidationError(
1976
- `${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`,
1977
- );
1969
+ throw new ValidationError(`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`);
1978
1970
  }
1979
1971
  if (
1980
1972
  Reflect.get(matcher, "phoneNumber") !== undefined &&
1981
1973
  (typeof Reflect.get(matcher, "phoneNumber") !== "string" ||
1982
1974
  !E164_RE.test(Reflect.get(matcher, "phoneNumber")))
1983
1975
  ) {
1984
- throw new ValidationError(
1985
- `${fieldPath}.phoneNumber must be an ITU-T E.164 number.`,
1986
- );
1976
+ throw new ValidationError(`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`);
1987
1977
  }
1988
1978
  const origins = Reflect.get(matcher, "origins");
1989
1979
  if (!Array.isArray(origins) || origins.length === 0) {
1990
- throw new ValidationError(
1991
- `${fieldPath}.origins must be a non-empty array.`,
1992
- );
1980
+ throw new ValidationError(`${fieldPath}.origins must be a non-empty array.`);
1993
1981
  }
1994
1982
  for (const [index, origin] of origins.entries()) {
1995
1983
  validateSmsOrigin(origin, `${fieldPath}.origins[${index}]`);
@@ -2010,10 +1998,7 @@ function validateSmsOtpMatcher(
2010
1998
  );
2011
1999
  }
2012
2000
  const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
2013
- if (
2014
- countCapturingGroups(regex) !== 1 &&
2015
- Reflect.get(code, "capture") === undefined
2016
- ) {
2001
+ if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
2017
2002
  throw new ValidationError(
2018
2003
  `${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`,
2019
2004
  );
@@ -2023,20 +2008,12 @@ function validateSmsOtpMatcher(
2023
2008
  typeof Reflect.get(code, "capture") !== "string" &&
2024
2009
  typeof Reflect.get(code, "capture") !== "number"
2025
2010
  ) {
2026
- throw new ValidationError(
2027
- `${fieldPath}.code.capture must be a string or number when present.`,
2028
- );
2011
+ throw new ValidationError(`${fieldPath}.code.capture must be a string or number when present.`);
2029
2012
  }
2030
2013
  assertIsoDuration(Reflect.get(matcher, "maxAge"), `${fieldPath}.maxAge`);
2031
- assertIsoDuration(
2032
- Reflect.get(matcher, "waitTimeout"),
2033
- `${fieldPath}.waitTimeout`,
2034
- );
2014
+ assertIsoDuration(Reflect.get(matcher, "waitTimeout"), `${fieldPath}.waitTimeout`);
2035
2015
  if (Reflect.get(matcher, "clockSkew") !== undefined)
2036
- assertIsoDuration(
2037
- Reflect.get(matcher, "clockSkew"),
2038
- `${fieldPath}.clockSkew`,
2039
- );
2016
+ assertIsoDuration(Reflect.get(matcher, "clockSkew"), `${fieldPath}.clockSkew`);
2040
2017
  }
2041
2018
 
2042
2019
  export function defineSmsOtpMatcher(
@@ -2068,9 +2045,7 @@ export function defineSmsOtpMatcher(
2068
2045
  return matcher;
2069
2046
  }
2070
2047
 
2071
- export function defineHealthJourney(
2072
- config: HealthJourneyDefinition,
2073
- ): HealthJourneyDefinition {
2048
+ export function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition {
2074
2049
  return config;
2075
2050
  }
2076
2051
 
@@ -2081,22 +2056,15 @@ function validateHealthJourneySchedule(
2081
2056
  ): void {
2082
2057
  const fieldPath = `healthJourneys.${journeyId}.schedule`;
2083
2058
  if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) {
2084
- throw new ValidationError(
2085
- `Provider "${providerId}" ${fieldPath} must be an object.`,
2086
- );
2059
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
2087
2060
  }
2088
2061
  rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
2089
2062
  if (Reflect.get(schedule, "kind") !== "interval")
2090
- throw new ValidationError(
2091
- `Provider "${providerId}" ${fieldPath}.kind must be "interval".`,
2092
- );
2063
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
2093
2064
  const interval = Reflect.get(schedule, "interval");
2094
2065
  assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
2095
2066
  const randomize = Reflect.get(schedule, "randomize");
2096
- if (
2097
- Reflect.get(schedule, "jitter") !== undefined &&
2098
- randomize !== undefined
2099
- ) {
2067
+ if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
2100
2068
  throw new ValidationError(
2101
2069
  `Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`,
2102
2070
  );
@@ -2123,17 +2091,13 @@ function validateHealthJourneys(
2123
2091
  const covered = new Set<string>();
2124
2092
  if (healthJourneys === undefined) return covered;
2125
2093
  if (!Array.isArray(healthJourneys)) {
2126
- throw new ValidationError(
2127
- `Provider "${providerId}" healthJourneys must be an array.`,
2128
- );
2094
+ throw new ValidationError(`Provider "${providerId}" healthJourneys must be an array.`);
2129
2095
  }
2130
2096
  const journeyIds = new Set<string>();
2131
2097
  for (const [index, journey] of healthJourneys.entries()) {
2132
2098
  const prefix = `healthJourneys[${index}]`;
2133
2099
  if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
2134
- throw new ValidationError(
2135
- `Provider "${providerId}" ${prefix} must be an object.`,
2136
- );
2100
+ throw new ValidationError(`Provider "${providerId}" ${prefix} must be an object.`);
2137
2101
  }
2138
2102
  rejectUnknownFields(journey, HEALTH_JOURNEY_FIELDS, prefix);
2139
2103
  if (typeof journey.id !== "string" || !JOURNEY_ID_RE.test(journey.id)) {
@@ -2147,10 +2111,7 @@ function validateHealthJourneys(
2147
2111
  );
2148
2112
  journeyIds.add(journey.id);
2149
2113
  validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
2150
- if (
2151
- !Array.isArray(journey.coversOperations) ||
2152
- journey.coversOperations.length === 0
2153
- ) {
2114
+ if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
2154
2115
  throw new ValidationError(
2155
2116
  `Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`,
2156
2117
  );
@@ -2185,10 +2146,7 @@ function validateHealthJourneys(
2185
2146
  `Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers must be an array.`,
2186
2147
  );
2187
2148
  for (const [matcherIndex, matcher] of journey.smsMatchers.entries()) {
2188
- validateSmsOtpMatcher(
2189
- matcher,
2190
- `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`,
2191
- );
2149
+ validateSmsOtpMatcher(matcher, `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`);
2192
2150
  if (matcherIds.has(matcher.id))
2193
2151
  throw new ValidationError(
2194
2152
  `Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers has duplicate matcher id "${matcher.id}".`,
@@ -2199,9 +2157,7 @@ function validateHealthJourneys(
2199
2157
  for (const [stepIndex, step] of journey.steps.entries()) {
2200
2158
  const stepPath = `healthJourneys.${journey.id}.steps[${stepIndex}]`;
2201
2159
  if (!step || typeof step !== "object" || Array.isArray(step))
2202
- throw new ValidationError(
2203
- `Provider "${providerId}" ${stepPath} must be an object.`,
2204
- );
2160
+ throw new ValidationError(`Provider "${providerId}" ${stepPath} must be an object.`);
2205
2161
  rejectUnknownFields(step, HEALTH_JOURNEY_STEP_FIELDS, stepPath);
2206
2162
  if (typeof step.id !== "string" || !JOURNEY_ID_RE.test(step.id))
2207
2163
  throw new ValidationError(
@@ -2211,20 +2167,13 @@ function validateHealthJourneys(
2211
2167
  throw new ValidationError(
2212
2168
  `Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`,
2213
2169
  );
2214
- if (
2215
- step.usesSmsMatcher !== undefined &&
2216
- !matcherIds.has(step.usesSmsMatcher)
2217
- )
2170
+ if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
2218
2171
  throw new ValidationError(
2219
2172
  `Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`,
2220
2173
  );
2221
2174
  }
2222
2175
  if (journey.manualTrigger !== undefined)
2223
- validateHealthJourneyManualTrigger(
2224
- providerId,
2225
- journey.id,
2226
- journey.manualTrigger,
2227
- );
2176
+ validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
2228
2177
  if (journey.timeout !== undefined)
2229
2178
  assertIsoDuration(
2230
2179
  journey.timeout,
@@ -2270,23 +2219,10 @@ function validateOperationHealthChecks(
2270
2219
  fix: `Remove either operations.${operationName}.healthCheck or operations.${operationName}.healthCheckUnsupported.`,
2271
2220
  },
2272
2221
  );
2273
- if (hasCheck)
2274
- validateHealthCheckSuite(
2275
- providerId,
2276
- operationName,
2277
- operation.healthCheck,
2278
- );
2222
+ if (hasCheck) validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
2279
2223
  if (hasUnsupported)
2280
- validateHealthCheckUnsupported(
2281
- providerId,
2282
- operationName,
2283
- operation.healthCheckUnsupported,
2284
- );
2285
- if (
2286
- !hasCheck &&
2287
- !hasUnsupported &&
2288
- !journeyCoveredOperations.has(operationName)
2289
- )
2224
+ validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
2225
+ if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
2290
2226
  throw new ValidationError(
2291
2227
  `Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`,
2292
2228
  {
@@ -2308,6 +2244,26 @@ function validateOperationFixtures(
2308
2244
  fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
2309
2245
  },
2310
2246
  );
2247
+ if (operation.fixtures?.recordedAt !== undefined) {
2248
+ const recordedAt = operation.fixtures.recordedAt;
2249
+ const parsed =
2250
+ typeof recordedAt === "string"
2251
+ ? new Date(`${recordedAt}T00:00:00.000Z`)
2252
+ : new Date(Number.NaN);
2253
+ const isCalendarDate =
2254
+ typeof recordedAt === "string" &&
2255
+ /^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
2256
+ !Number.isNaN(parsed.getTime()) &&
2257
+ parsed.toISOString().slice(0, 10) === recordedAt;
2258
+ const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
2259
+ if (!isCalendarDate || recordedAt > kstToday)
2260
+ throw new ValidationError(
2261
+ `Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`,
2262
+ {
2263
+ fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
2264
+ },
2265
+ );
2266
+ }
2311
2267
  if (operation.fixtures?.request !== undefined) {
2312
2268
  const result = safeParseSchemaSync(
2313
2269
  operation.input,
@@ -2341,6 +2297,44 @@ function validateOperationFixtures(
2341
2297
  }
2342
2298
  }
2343
2299
 
2300
+ function resolveOperationFixtureRequests<TOperations extends Record<string, ProviderOperation>>(
2301
+ operations: TOperations,
2302
+ ): TOperations {
2303
+ let changed = false;
2304
+ const resolvedOperations = Object.fromEntries(
2305
+ Object.entries(operations).map(([operationName, operation]) => {
2306
+ if (operation.fixtures?.request === undefined) return [operationName, operation];
2307
+ const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
2308
+ if (request === operation.fixtures.request) return [operationName, operation];
2309
+ changed = true;
2310
+ return [
2311
+ operationName,
2312
+ {
2313
+ ...operation,
2314
+ fixtures: {
2315
+ ...operation.fixtures,
2316
+ request,
2317
+ },
2318
+ },
2319
+ ];
2320
+ }),
2321
+ ) as TOperations;
2322
+ return changed ? resolvedOperations : operations;
2323
+ }
2324
+
2325
+ /**
2326
+ * Shallow shape guard only: the `deployment` object is passed through
2327
+ * verbatim and deliberately not deep-validated by the SDK — the APIFuse
2328
+ * registry builder owns deployment validation and profile resolution.
2329
+ */
2330
+ function validateProviderDeployment(providerId: string, deployment: unknown): void {
2331
+ if (deployment === undefined) return;
2332
+ if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
2333
+ throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
2334
+ fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
2335
+ });
2336
+ }
2337
+
2344
2338
  export function defineProvider<
2345
2339
  TOperations extends Record<string, ProviderOperation>,
2346
2340
  TConfig extends ProviderConfig<TOperations>,
@@ -2348,20 +2342,19 @@ export function defineProvider<
2348
2342
  config: TConfig & AuthStartNoInputGuard<TConfig>,
2349
2343
  ): ProviderDefinition & { operations: OperationMapConfig<TOperations> } {
2350
2344
  validateProviderShape(config);
2345
+ const operations = resolveOperationFixtureRequests(config.operations);
2351
2346
  if (!CONNECTOR_ID_REGEX.test(config.id))
2352
2347
  throw new ProviderError(`Invalid provider id: "${config.id}"`, {
2353
2348
  fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
2354
2349
  });
2355
2350
  if (Object.keys(config.operations).length === 0)
2356
- throw new ProviderError(
2357
- `Provider "${config.id}" must define at least one operation`,
2358
- {
2359
- fix: "Add at least one operation to the operations object",
2360
- },
2361
- );
2351
+ throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
2352
+ fix: "Add at least one operation to the operations object",
2353
+ });
2362
2354
  validateOperationIds(config.id, config.operations);
2363
2355
  validateOperationAnnotations(config.id, config.operations);
2364
2356
  validateOperationObservability(config.id, config.operations);
2357
+ validateOperationErrorCodes(config.id, config.operations);
2365
2358
  validateOperationTransports(config.id, config.operations);
2366
2359
  validateOperationContracts(config.id, config.operations);
2367
2360
  validateToolRouterMetadata(config.id, config.operations);
@@ -2370,11 +2363,7 @@ export function defineProvider<
2370
2363
  config.operations,
2371
2364
  config.healthJourneys,
2372
2365
  );
2373
- validateOperationHealthChecks(
2374
- config.id,
2375
- config.operations,
2376
- journeyCoveredOperations,
2377
- );
2366
+ validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
2378
2367
  if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
2379
2368
  throw new ValidationError(
2380
2369
  `Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
@@ -2387,7 +2376,15 @@ export function defineProvider<
2387
2376
  config.healthProbe ?? config.healthMonitor,
2388
2377
  config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
2389
2378
  );
2390
- validateOperationFixtures(config.id, config.operations);
2379
+ validateOperationFixtures(config.id, operations);
2380
+ validateProviderDeployment(config.id, config.deployment);
2381
+ try {
2382
+ validateNativeProviderConfig(config.native);
2383
+ } catch (error) {
2384
+ if (error instanceof NativeEgressPolicyValidationError)
2385
+ throw new ValidationError(error.message);
2386
+ throw error;
2387
+ }
2391
2388
  validateProviderProxy(config);
2392
2389
  validateProviderStt(config);
2393
2390
  if (config.runtime === "browser" && !config.browser)
@@ -2406,7 +2403,11 @@ export function defineProvider<
2406
2403
  id: config.id,
2407
2404
  version: config.version,
2408
2405
  runtime: config.runtime,
2406
+ // Verbatim passthrough: deployment validation and profile resolution
2407
+ // are owned by the APIFuse registry builder, not the SDK.
2408
+ deployment: config.deployment,
2409
2409
  allowedHosts: config.allowedHosts,
2410
+ native: config.native,
2410
2411
  stealth: config.stealth,
2411
2412
  proxy: config.proxy,
2412
2413
  stt: config.stt,
@@ -2418,7 +2419,7 @@ export function defineProvider<
2418
2419
  credential: config.credential,
2419
2420
  context: config.context,
2420
2421
  meta: config.meta,
2421
- operations: config.operations,
2422
+ operations,
2422
2423
  // Transitional healthMonitor → healthProbe alias: mirror whichever field
2423
2424
  // was declared onto both so old and new consumers keep working.
2424
2425
  healthMonitor: config.healthMonitor ?? config.healthProbe,