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

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 (290) hide show
  1. package/AUTHORING.md +487 -0
  2. package/CHANGELOG.md +85 -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 +1 -1
  23. package/dist/ceremonies/index.js +8 -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 +208 -125
  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.js +8 -15
  56. package/dist/native-address.d.ts +43 -0
  57. package/dist/native-address.js +281 -0
  58. package/dist/native-egress-policy.d.ts +31 -0
  59. package/dist/native-egress-policy.js +288 -0
  60. package/dist/observability.d.ts +5 -2
  61. package/dist/observability.js +48 -1
  62. package/dist/provider.d.ts +12 -11
  63. package/dist/provider.js +10 -9
  64. package/dist/public-schema-field-lint.d.ts +1 -1
  65. package/dist/recipes/gov-api.js +1 -1
  66. package/dist/runtime/auth-flow.d.ts +1 -1
  67. package/dist/runtime/auth-flow.js +3 -3
  68. package/dist/runtime/browser.d.ts +1 -1
  69. package/dist/runtime/browser.js +15 -29
  70. package/dist/runtime/cache.d.ts +1 -1
  71. package/dist/runtime/cache.js +4 -8
  72. package/dist/runtime/choice.d.ts +1 -1
  73. package/dist/runtime/choice.js +31 -35
  74. package/dist/runtime/credential.d.ts +1 -1
  75. package/dist/runtime/credential.js +1 -1
  76. package/dist/runtime/env.d.ts +1 -1
  77. package/dist/runtime/executor.d.ts +1 -1
  78. package/dist/runtime/executor.js +31 -4
  79. package/dist/runtime/http.d.ts +2 -2
  80. package/dist/runtime/http.js +387 -47
  81. package/dist/runtime/insights.d.ts +1 -1
  82. package/dist/runtime/insights.js +6 -13
  83. package/dist/runtime/instrumentation.d.ts +2 -2
  84. package/dist/runtime/instrumentation.js +345 -22
  85. package/dist/runtime/keyring.js +1 -1
  86. package/dist/runtime/namespace.js +1 -1
  87. package/dist/runtime/native-network.d.ts +127 -0
  88. package/dist/runtime/native-network.js +1298 -0
  89. package/dist/runtime/otlp.d.ts +1 -1
  90. package/dist/runtime/perf.d.ts +1 -1
  91. package/dist/runtime/provider.d.ts +1 -1
  92. package/dist/runtime/provider.js +1 -2
  93. package/dist/runtime/proxy-errors.d.ts +1 -1
  94. package/dist/runtime/proxy-errors.js +9 -7
  95. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  96. package/dist/runtime/proxy-nodemaven.js +146 -0
  97. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  98. package/dist/runtime/proxy-retry-policy.js +2 -2
  99. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  100. package/dist/runtime/proxy-telemetry.js +55 -52
  101. package/dist/runtime/redirects.d.ts +29 -0
  102. package/dist/runtime/redirects.js +36 -0
  103. package/dist/runtime/redis.d.ts +1 -1
  104. package/dist/runtime/redis.js +2 -4
  105. package/dist/runtime/request-options.d.ts +68 -1
  106. package/dist/runtime/request-options.js +548 -0
  107. package/dist/runtime/secrets.d.ts +27 -0
  108. package/dist/runtime/secrets.js +51 -0
  109. package/dist/runtime/state.d.ts +2 -2
  110. package/dist/runtime/state.js +15 -4
  111. package/dist/runtime/stealth.d.ts +5 -3
  112. package/dist/runtime/stealth.js +423 -143
  113. package/dist/runtime/stt.d.ts +1 -1
  114. package/dist/runtime/stt.js +11 -15
  115. package/dist/runtime/trace.d.ts +2 -2
  116. package/dist/runtime/trace.js +2 -4
  117. package/dist/runtime/waterfall.d.ts +1 -1
  118. package/dist/schema.d.ts +1 -1
  119. package/dist/schema.js +7 -15
  120. package/dist/serve.d.ts +1 -1
  121. package/dist/serve.js +1 -1
  122. package/dist/server/index.d.ts +7 -7
  123. package/dist/server/index.js +6 -6
  124. package/dist/server/self-test-input-tokens.d.ts +2 -1
  125. package/dist/server/self-test-input-tokens.js +18 -14
  126. package/dist/server/self-test-redaction.d.ts +1 -1
  127. package/dist/server/self-test-redaction.js +1 -1
  128. package/dist/server/self-test.d.ts +104 -3
  129. package/dist/server/self-test.js +673 -115
  130. package/dist/server/serve.d.ts +115 -4
  131. package/dist/server/serve.js +792 -127
  132. package/dist/server/types.d.ts +30 -9
  133. package/dist/server/types.js +8 -7
  134. package/dist/stateful/errors.d.ts +14 -0
  135. package/dist/stateful/errors.js +14 -0
  136. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  137. package/dist/stateful/http-provider-event-emitter.js +237 -0
  138. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  139. package/dist/stateful/http-session-owner-registry.js +210 -0
  140. package/dist/stateful/index.d.ts +18 -0
  141. package/dist/stateful/index.js +18 -0
  142. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  143. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  144. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  145. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  146. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  147. package/dist/stateful/provider-event-pipeline.js +1 -0
  148. package/dist/stateful/provider-events.d.ts +101 -0
  149. package/dist/stateful/provider-events.js +289 -0
  150. package/dist/stateful/session-key.d.ts +15 -0
  151. package/dist/stateful/session-key.js +86 -0
  152. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  153. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  154. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  155. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  156. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  157. package/dist/stateful/stateful-provider-adapter.js +287 -0
  158. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  159. package/dist/stateful/stateful-provider-observability.js +161 -0
  160. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  161. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  162. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  163. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  164. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  165. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  166. package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
  167. package/dist/stateful/stateful-provider-session-routing.js +353 -0
  168. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  169. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  170. package/dist/stateful-signing.d.ts +18 -0
  171. package/dist/stateful-signing.js +27 -0
  172. package/dist/stealth/profiles.d.ts +1 -1
  173. package/dist/stealth/profiles.js +5 -14
  174. package/dist/stream-evidence.d.ts +74 -0
  175. package/dist/stream-evidence.js +785 -0
  176. package/dist/stream.d.ts +1 -1
  177. package/dist/testing/index.d.ts +2 -2
  178. package/dist/testing/index.js +2 -2
  179. package/dist/testing/run.d.ts +32 -2
  180. package/dist/testing/run.js +477 -33
  181. package/dist/types.d.ts +323 -11
  182. package/dist/types.js +1 -0
  183. package/dist/user-input.d.ts +30 -0
  184. package/dist/user-input.js +66 -0
  185. package/package.json +16 -5
  186. package/src/auth-turn/index.ts +1 -1
  187. package/src/auth.ts +28 -86
  188. package/src/ceremonies/index.ts +24 -78
  189. package/src/cli/commands.ts +10 -0
  190. package/src/cli/create.ts +42 -35
  191. package/src/cli/prompt-assets.ts +865 -0
  192. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  193. package/src/config/loader.ts +652 -204
  194. package/src/contract-serialization.ts +5 -11
  195. package/src/contract-types.ts +2 -2
  196. package/src/contract.ts +12 -28
  197. package/src/define.ts +368 -504
  198. package/src/dev.ts +4 -9
  199. package/src/error-resolution.ts +91 -0
  200. package/src/errors.ts +110 -5
  201. package/src/fixture-sanitization.ts +247 -0
  202. package/src/i18n/catalog.ts +10 -32
  203. package/src/i18n/index.ts +2 -2
  204. package/src/i18n/keys.ts +5 -11
  205. package/src/index.ts +111 -42
  206. package/src/lint.ts +89 -159
  207. package/src/native-address.ts +340 -0
  208. package/src/native-egress-policy.ts +358 -0
  209. package/src/observability.ts +51 -1
  210. package/src/provider.ts +65 -11
  211. package/src/public-schema-field-lint.ts +7 -33
  212. package/src/recipes/gov-api.ts +2 -5
  213. package/src/runtime/auth-flow.ts +5 -7
  214. package/src/runtime/browser.ts +64 -187
  215. package/src/runtime/cache.ts +20 -67
  216. package/src/runtime/choice.ts +79 -132
  217. package/src/runtime/credential.ts +2 -2
  218. package/src/runtime/env.ts +1 -1
  219. package/src/runtime/executor.ts +43 -20
  220. package/src/runtime/http.ts +494 -57
  221. package/src/runtime/insights.ts +15 -53
  222. package/src/runtime/instrumentation.ts +495 -66
  223. package/src/runtime/keyring.ts +7 -19
  224. package/src/runtime/namespace.ts +2 -7
  225. package/src/runtime/native-network.ts +1686 -0
  226. package/src/runtime/otlp.ts +12 -23
  227. package/src/runtime/perf.ts +1 -1
  228. package/src/runtime/provider.ts +4 -9
  229. package/src/runtime/proxy-errors.ts +29 -42
  230. package/src/runtime/proxy-nodemaven.ts +221 -0
  231. package/src/runtime/proxy-retry-policy.ts +3 -3
  232. package/src/runtime/proxy-telemetry.ts +79 -77
  233. package/src/runtime/redirects.ts +66 -0
  234. package/src/runtime/redis.ts +4 -12
  235. package/src/runtime/request-options.ts +679 -9
  236. package/src/runtime/secrets.ts +64 -0
  237. package/src/runtime/state.ts +41 -110
  238. package/src/runtime/stealth.ts +505 -154
  239. package/src/runtime/stt.ts +38 -94
  240. package/src/runtime/trace.ts +14 -44
  241. package/src/runtime/waterfall.ts +5 -18
  242. package/src/schema.ts +23 -84
  243. package/src/serve.ts +1 -1
  244. package/src/server/index.ts +29 -7
  245. package/src/server/self-test-input-tokens.ts +29 -14
  246. package/src/server/self-test-redaction.ts +2 -2
  247. package/src/server/self-test.ts +857 -132
  248. package/src/server/serve.ts +1141 -324
  249. package/src/server/types.ts +12 -19
  250. package/src/stateful/README.md +146 -0
  251. package/src/stateful/errors.ts +23 -0
  252. package/src/stateful/http-provider-event-emitter.ts +314 -0
  253. package/src/stateful/http-session-owner-registry.ts +306 -0
  254. package/src/stateful/index.ts +18 -0
  255. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  256. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  257. package/src/stateful/provider-event-pipeline.ts +61 -0
  258. package/src/stateful/provider-events.ts +462 -0
  259. package/src/stateful/session-key.ts +111 -0
  260. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  261. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  262. package/src/stateful/stateful-provider-adapter.ts +562 -0
  263. package/src/stateful/stateful-provider-observability.ts +261 -0
  264. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  265. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  266. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  267. package/src/stateful/stateful-provider-session-routing.ts +555 -0
  268. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  269. package/src/stateful-signing.ts +46 -0
  270. package/src/stealth/profiles.ts +10 -26
  271. package/src/stream-evidence.ts +988 -0
  272. package/src/stream.ts +8 -19
  273. package/src/testing/index.ts +10 -2
  274. package/src/testing/run.ts +652 -79
  275. package/src/types.ts +369 -11
  276. package/src/user-input.ts +118 -0
  277. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  278. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  279. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  280. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  281. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  282. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  283. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  284. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  285. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  286. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  287. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  288. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  289. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  290. /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"
@@ -74,23 +84,10 @@ interface ProviderImplementationProfile {
74
84
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
75
85
  const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
76
86
  const VALID_RUNTIMES = ["standard", "shared", "browser"] as const;
77
- const VALID_AUTH_MODES = [
78
- "none",
79
- "platform-managed",
80
- "credentials",
81
- "oauth2",
82
- ] as const;
87
+ const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"] as const;
83
88
  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",
93
- ] as const;
89
+ const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"] as const;
90
+ const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"] as const;
94
91
  const VALID_PROVIDER_PROXY_AFFINITIES = [
95
92
  "request",
96
93
  "operation",
@@ -99,25 +96,25 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
99
96
  ] as const;
100
97
  const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
101
98
  const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
99
+ const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
100
+ const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
101
+ // Per-vendor provider-declared credential secrets. A required-mode chain must
102
+ // declare every secret of every credentialed vendor it names, so a missing
103
+ // credential fails at build/validation time rather than during a live outage: a
104
+ // declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
105
+ // exactly the failure class the multi-vendor chain exists to remove. Vendors
106
+ // absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
107
+ // `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
108
+ // no declaration requirement.
109
+ const VENDOR_REQUIRED_SECRETS: Partial<Record<ProviderProxyProvider, readonly string[]>> = {
110
+ smartproxy: [SMARTPROXY_APP_KEY_SECRET],
111
+ nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
112
+ };
102
113
  const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
103
114
  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;
115
+ const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
116
+ const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"] as const;
117
+ const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"] as const;
121
118
  const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
122
119
  const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
123
120
 
@@ -135,18 +132,16 @@ function msDurationMs(value: string): number {
135
132
  function parsePositiveMsDuration(value: string): number | undefined {
136
133
  const trimmed = value.trim();
137
134
  if (!MS_DURATION_PATTERN.test(trimmed)) return undefined;
138
- const parsed = ms(
139
- (trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue,
140
- );
135
+ const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue);
141
136
  if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
142
137
  return parsed;
143
138
  }
144
139
 
145
140
  type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
146
- type OperationConfig<
147
- TInput extends SchemaLike,
148
- TOutput extends SchemaLike,
149
- > = Omit<OperationDefinition<TInput, TOutput>, "handler"> & {
141
+ type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
142
+ OperationDefinition<TInput, TOutput>,
143
+ "handler"
144
+ > & {
150
145
  handler(
151
146
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
152
147
  input: InferSchemaOutput<TInput>,
@@ -154,59 +149,44 @@ type OperationConfig<
154
149
  | OperationHandlerResult<InferSchemaOutput<TOutput>>
155
150
  | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
156
151
  };
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
- > =
152
+ type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
153
+ [K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
154
+ ? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
155
+ : never;
156
+ };
157
+ type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
170
158
  | SseOperationConfig<TInput, TOutput>
171
159
  | HttpStreamOperationConfig<TInput, TOutput>
172
160
  | WebSocketOperationConfig<TInput, TOutput>;
173
- type SseOperationConfig<
174
- TInput extends SchemaLike,
175
- TOutput extends SchemaLike,
176
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
161
+ type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
162
+ OperationConfig<TInput, TOutput>,
163
+ "handler" | "transport"
164
+ > & {
177
165
  transport: OperationSseTransport;
178
166
  handler(
179
167
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
180
168
  input: InferSchemaOutput<TInput>,
181
- ):
182
- | AsyncIterable<ProviderStreamEvent>
183
- | Promise<AsyncIterable<ProviderStreamEvent>>;
169
+ ): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
184
170
  };
185
- type HttpStreamOperationConfig<
186
- TInput extends SchemaLike,
187
- TOutput extends SchemaLike,
188
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
171
+ type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
172
+ OperationConfig<TInput, TOutput>,
173
+ "handler" | "transport"
174
+ > & {
189
175
  transport: OperationHttpStreamTransport;
190
176
  handler(
191
177
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
192
178
  input: InferSchemaOutput<TInput>,
193
- ):
194
- | Response
195
- | ReadableStream<Uint8Array>
196
- | Promise<Response | ReadableStream<Uint8Array>>;
179
+ ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
197
180
  };
198
- type WebSocketOperationConfig<
199
- TInput extends SchemaLike,
200
- TOutput extends SchemaLike,
201
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
181
+ type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
182
+ OperationConfig<TInput, TOutput>,
183
+ "handler" | "transport"
184
+ > & {
202
185
  transport: OperationWebSocketTransport;
203
186
  handler(
204
187
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
205
188
  input: InferSchemaOutput<TInput>,
206
- ):
207
- | Response
208
- | ReadableStream<Uint8Array>
209
- | Promise<Response | ReadableStream<Uint8Array>>;
189
+ ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
210
190
  };
211
191
 
212
192
  type AuthStartNoInputGuard<TConfig> = TConfig extends {
@@ -221,13 +201,19 @@ type AuthStartNoInputGuard<TConfig> = TConfig extends {
221
201
  : unknown
222
202
  : unknown;
223
203
 
224
- export interface ProviderConfig<
225
- TOperations extends Record<string, ProviderOperation>,
226
- > {
204
+ export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
227
205
  id: string;
228
206
  version: string;
229
207
  runtime: "standard" | "shared" | "browser";
208
+ /**
209
+ * Optional deployment overrides, passed through verbatim onto the returned
210
+ * provider definition. The SDK types this field but does not deep-validate
211
+ * it — the APIFuse registry builder owns deployment validation and
212
+ * resolves omitted fields against the runtime deployment profiles.
213
+ */
214
+ deployment?: ProviderDeploymentOverrides;
230
215
  allowedHosts?: string[];
216
+ native?: NativeProviderConfig;
231
217
  stealth?: {
232
218
  profile: string;
233
219
  platform: StealthPlatform;
@@ -270,35 +256,24 @@ export interface ProviderConfig<
270
256
  }
271
257
 
272
258
  /** Define one provider operation with schema-driven handler inference. */
273
- export function defineOperation<
274
- TInput extends SchemaLike,
275
- TOutput extends SchemaLike,
276
- >(
259
+ export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
277
260
  operation: OperationConfig<TInput, TOutput>,
278
261
  ): OperationDefinition<TInput, TOutput> {
279
262
  return operation;
280
263
  }
281
264
 
282
265
  /** Define a non-JSON provider operation with explicit transport metadata. */
283
- export function defineStreamOperation<
284
- TInput extends SchemaLike,
285
- TOutput extends SchemaLike,
286
- >(
266
+ export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
287
267
  operation: StreamOperationConfig<TInput, TOutput>,
288
268
  ): OperationDefinition<TInput, TOutput> {
289
269
  return operation;
290
270
  }
291
271
 
292
- function assertObjectConfig(
293
- value: unknown,
294
- ): asserts value is Record<string, unknown> {
272
+ function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
295
273
  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
- );
274
+ throw new ProviderError("defineProvider config must be an object. Offending field: config", {
275
+ fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
276
+ });
302
277
  }
303
278
  }
304
279
  function assertRequiredField(
@@ -336,25 +311,10 @@ function validateProviderShape(config: unknown): void {
336
311
  assertRequiredField(config, "meta", String(config.id));
337
312
  assertRequiredField(config, "operations", String(config.id));
338
313
  if (typeof config.runtime === "string")
339
- assertLiteralField(
340
- config.runtime,
341
- "runtime",
342
- VALID_RUNTIMES,
343
- String(config.id),
344
- );
314
+ assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
345
315
  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
- );
316
+ if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
317
+ assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
358
318
  if (auth && typeof auth === "object" && "exchange" in auth) {
359
319
  throw new ProviderError(
360
320
  `Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
@@ -390,9 +350,7 @@ function validateProviderShape(config: unknown): void {
390
350
  },
391
351
  );
392
352
  }
393
- const accessRecord: Record<string, unknown> = Object.fromEntries(
394
- Object.entries(access),
395
- );
353
+ const accessRecord: Record<string, unknown> = Object.fromEntries(Object.entries(access));
396
354
  for (const key of Object.keys(accessRecord)) {
397
355
  if (key !== "visibility") {
398
356
  throw new ValidationError(`Unknown field "${key}" on access.`, {
@@ -437,31 +395,26 @@ function validateProviderProxy(config: {
437
395
  },
438
396
  );
439
397
  }
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
- );
398
+ rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
399
+ assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
451
400
  if (proxy.provider !== undefined) {
452
- assertLiteralField(
453
- proxy.provider,
454
- "proxy.provider",
455
- VALID_PROVIDER_PROXY_PROVIDERS,
456
- config.id,
457
- );
401
+ assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
402
+ }
403
+ if (proxy.providers !== undefined) {
404
+ if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
405
+ throw new ValidationError(
406
+ `Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`,
407
+ {
408
+ fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
409
+ },
410
+ );
411
+ }
412
+ for (const vendor of proxy.providers) {
413
+ assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
414
+ }
458
415
  }
459
416
  if (proxy.geo !== undefined) {
460
- if (
461
- !proxy.geo ||
462
- typeof proxy.geo !== "object" ||
463
- Array.isArray(proxy.geo)
464
- ) {
417
+ if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
465
418
  throw new ValidationError(
466
419
  `Provider "${config.id}" has invalid proxy.geo: must be an object.`,
467
420
  {
@@ -469,11 +422,7 @@ function validateProviderProxy(config: {
469
422
  },
470
423
  );
471
424
  }
472
- rejectUnknownFields(
473
- proxy.geo,
474
- new Set(["country", "subdivision", "city"]),
475
- "proxy.geo",
476
- );
425
+ rejectUnknownFields(proxy.geo, new Set(["country", "subdivision", "city"]), "proxy.geo");
477
426
  if (proxy.geo.country !== undefined) {
478
427
  assertIsoCountry(proxy.geo.country, "proxy.geo.country");
479
428
  }
@@ -487,11 +436,7 @@ function validateProviderProxy(config: {
487
436
  }
488
437
  }
489
438
  if (proxy.session !== undefined) {
490
- if (
491
- !proxy.session ||
492
- typeof proxy.session !== "object" ||
493
- Array.isArray(proxy.session)
494
- ) {
439
+ if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
495
440
  throw new ValidationError(
496
441
  `Provider "${config.id}" has invalid proxy.session: must be an object.`,
497
442
  {
@@ -501,7 +446,7 @@ function validateProviderProxy(config: {
501
446
  }
502
447
  rejectUnknownFields(
503
448
  proxy.session,
504
- new Set(["affinity", "lifetimeMinutes", "poolSize"]),
449
+ new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]),
505
450
  "proxy.session",
506
451
  );
507
452
  if (proxy.session.affinity !== undefined) {
@@ -513,51 +458,103 @@ function validateProviderProxy(config: {
513
458
  );
514
459
  }
515
460
  const lifetime = proxy.session.lifetimeMinutes;
516
- if (
517
- lifetime !== undefined &&
518
- (!Number.isFinite(lifetime) || lifetime <= 0)
519
- ) {
461
+ if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
520
462
  throw new ValidationError(
521
463
  `Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`,
522
464
  );
523
465
  }
524
466
  const poolSize = proxy.session.poolSize;
467
+ if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
468
+ throw new ValidationError(
469
+ `Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
470
+ );
471
+ }
472
+ const drainLeadSeconds = proxy.session.drainLeadSeconds;
525
473
  if (
526
- poolSize !== undefined &&
527
- (!Number.isInteger(poolSize) || poolSize <= 0)
474
+ drainLeadSeconds !== undefined &&
475
+ (!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)
528
476
  ) {
529
477
  throw new ValidationError(
530
- `Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
478
+ `Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`,
479
+ {
480
+ fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
481
+ },
531
482
  );
532
483
  }
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) {
484
+ // A drain lead longer than the sticky lifetime would fire the expiring
485
+ // event before the session is even established, so the provider would
486
+ // never get a usable window. Reject the contradiction at build time.
487
+ if (
488
+ drainLeadSeconds !== undefined &&
489
+ lifetime !== undefined &&
490
+ Number.isFinite(lifetime) &&
491
+ drainLeadSeconds >= lifetime * 60
492
+ ) {
540
493
  throw new ValidationError(
541
- `Provider "${config.id}" requires Smartproxy egress but does not declare ${SMARTPROXY_APP_KEY_SECRET}.`,
494
+ `Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`,
542
495
  {
543
- fix: `Add secrets: [{ name: "${SMARTPROXY_APP_KEY_SECRET}", required: true }] to the provider.`,
496
+ fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
544
497
  },
545
498
  );
546
499
  }
547
500
  }
501
+ // Every credentialed vendor in a required-mode chain must declare its
502
+ // provider secret(s) so a missing credential fails at build/validation time,
503
+ // not during a live outage. This covers the fallback legs too (not just the
504
+ // first vendor): a declared-but-uncredentialed nodemaven fallback would leave
505
+ // the chain silently down to a single vendor, reintroducing the SPOF the chain
506
+ // removes.
507
+ const vendorChain =
508
+ proxy.providers && proxy.providers.length > 0
509
+ ? proxy.providers
510
+ : proxy.provider
511
+ ? [proxy.provider]
512
+ : [];
513
+ if (proxy.mode === "required") {
514
+ for (const vendor of vendorChain) {
515
+ const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
516
+ if (!requiredSecrets) continue;
517
+ for (const secretName of requiredSecrets) {
518
+ // Match the canonical runtime gate (assertRequiredSecretsPresent /
519
+ // listMissingRequiredSecrets), which enforces only `required === true`
520
+ // declarations. A declaration that omits `required` (defaulting to
521
+ // optional) is skipped at runtime, so accepting it here would pass
522
+ // validation while leaving the credential unenforced until proxy
523
+ // resolution during a live request — the fail-open gap this check exists
524
+ // to close.
525
+ const declared = config.secrets?.some(
526
+ (secret) => secret.name === secretName && secret.required === true,
527
+ );
528
+ if (!declared) {
529
+ throw new ValidationError(
530
+ `Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`,
531
+ {
532
+ fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
533
+ },
534
+ );
535
+ }
536
+ }
537
+ }
538
+ }
539
+ // `decodo`/`custom` are deprecated vendor values (string-union members, so the
540
+ // @deprecated symbol gate can't catch them — warn at validation time instead).
541
+ const deprecatedVendors = vendorChain.filter(
542
+ (vendor) => vendor === "decodo" || vendor === "custom",
543
+ );
544
+ if (deprecatedVendors.length > 0) {
545
+ console.warn(
546
+ `[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.`,
547
+ );
548
+ }
548
549
  }
549
550
 
550
- function validateProviderStt(config: {
551
- id: string;
552
- stt?: ProviderSttConfig;
553
- }): void {
551
+ function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): void {
554
552
  const stt = config.stt;
555
553
  if (stt === undefined) return;
556
554
  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
- );
555
+ throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
556
+ fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
557
+ });
561
558
  }
562
559
  rejectUnknownFields(stt, new Set(["mode"]), "stt");
563
560
  assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
@@ -584,16 +581,9 @@ function validateOperationIds(
584
581
  );
585
582
  }
586
583
  }
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;
584
+ const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
585
+ const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
586
+ const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"] as const;
597
587
 
598
588
  function assertNonEmptyString(
599
589
  value: unknown,
@@ -624,10 +614,7 @@ function validateToolRouterMetadata(
624
614
  },
625
615
  );
626
616
  }
627
- if (
628
- toolRouter.name !== undefined &&
629
- !MCP_TOOL_NAME_REGEX.test(toolRouter.name)
630
- ) {
617
+ if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
631
618
  throw new ValidationError(
632
619
  `Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`,
633
620
  {
@@ -699,10 +686,7 @@ function validateOperationContracts(
699
686
  providerId,
700
687
  );
701
688
  }
702
- if (
703
- contract.lifecycle === "deprecated" ||
704
- contract.lifecycle === "removed"
705
- ) {
689
+ if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
706
690
  if (!contract.deprecation || typeof contract.deprecation !== "object") {
707
691
  throw new ValidationError(
708
692
  `Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`,
@@ -750,10 +734,7 @@ function validateOperationAnnotations(
750
734
  fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
751
735
  },
752
736
  );
753
- if (
754
- timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
755
- timeoutMs > OPERATION_TIMEOUT_MS_MAX
756
- )
737
+ if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
757
738
  throw new ValidationError(
758
739
  `Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`,
759
740
  {
@@ -821,6 +802,36 @@ function validateOperationObservability(
821
802
  }
822
803
  }
823
804
 
805
+ function validateOperationErrorCodes(
806
+ providerId: string,
807
+ operations: Record<string, ProviderOperation>,
808
+ ): void {
809
+ for (const [operationName, operation] of Object.entries(operations)) {
810
+ for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
811
+ if (
812
+ errorCode.status !== undefined &&
813
+ !VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)
814
+ ) {
815
+ const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
816
+ throw new ValidationError(
817
+ `Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`,
818
+ {
819
+ fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
820
+ },
821
+ );
822
+ }
823
+ if (
824
+ errorCode.status !== undefined &&
825
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)
826
+ ) {
827
+ console.warn(
828
+ `[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.`,
829
+ );
830
+ }
831
+ }
832
+ }
833
+ }
834
+
824
835
  const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
825
836
  const SSE_TRANSPORT_FIELDS = new Set([
826
837
  "kind",
@@ -951,12 +962,7 @@ function validateOperationTransports(
951
962
  },
952
963
  );
953
964
  }
954
- assertLiteralField(
955
- kind,
956
- `${fieldPath}.kind`,
957
- VALID_OPERATION_TRANSPORT_KINDS,
958
- providerId,
959
- );
965
+ assertLiteralField(kind, `${fieldPath}.kind`, VALID_OPERATION_TRANSPORT_KINDS, providerId);
960
966
 
961
967
  switch (kind) {
962
968
  case "json":
@@ -988,16 +994,9 @@ function validateOperationTransports(
988
994
  STREAM_MAX_DURATION_MS_MAX,
989
995
  "max duration",
990
996
  );
991
- assertPositiveBytes(
992
- Reflect.get(transport, "maxEventBytes"),
993
- `${fieldPath}.maxEventBytes`,
994
- );
997
+ assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
995
998
  const resumable = Reflect.get(transport, "resumable");
996
- if (
997
- resumable !== undefined &&
998
- resumable !== false &&
999
- resumable !== "last-event-id"
1000
- ) {
999
+ if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
1001
1000
  throw new ValidationError(
1002
1001
  `Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`,
1003
1002
  {
@@ -1017,12 +1016,7 @@ function validateOperationTransports(
1017
1016
  rejectUnknownFields(transport, HTTP_STREAM_TRANSPORT_FIELDS, fieldPath);
1018
1017
  const contentType = Reflect.get(transport, "contentType");
1019
1018
  if (contentType !== undefined) {
1020
- assertNonEmptyString(
1021
- contentType,
1022
- `${fieldPath}.contentType`,
1023
- providerId,
1024
- operationName,
1025
- );
1019
+ assertNonEmptyString(contentType, `${fieldPath}.contentType`, providerId, operationName);
1026
1020
  }
1027
1021
  assertStreamMs(
1028
1022
  Reflect.get(transport, "idleTimeoutMs"),
@@ -1038,10 +1032,7 @@ function validateOperationTransports(
1038
1032
  STREAM_MAX_DURATION_MS_MAX,
1039
1033
  "max duration",
1040
1034
  );
1041
- assertPositiveBytes(
1042
- Reflect.get(transport, "maxChunkBytes"),
1043
- `${fieldPath}.maxChunkBytes`,
1044
- );
1035
+ assertPositiveBytes(Reflect.get(transport, "maxChunkBytes"), `${fieldPath}.maxChunkBytes`);
1045
1036
  break;
1046
1037
  }
1047
1038
  case "websocket": {
@@ -1066,10 +1057,7 @@ function validateOperationTransports(
1066
1057
  );
1067
1058
  }
1068
1059
  for (const subprotocol of subprotocols) {
1069
- if (
1070
- typeof subprotocol !== "string" ||
1071
- !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)
1072
- ) {
1060
+ if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
1073
1061
  throw new ValidationError(
1074
1062
  `Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`,
1075
1063
  {
@@ -1093,10 +1081,7 @@ function validateOperationTransports(
1093
1081
  STREAM_MAX_DURATION_MS_MAX,
1094
1082
  "max duration",
1095
1083
  );
1096
- assertPositiveBytes(
1097
- Reflect.get(transport, "maxFrameBytes"),
1098
- `${fieldPath}.maxFrameBytes`,
1099
- );
1084
+ assertPositiveBytes(Reflect.get(transport, "maxFrameBytes"), `${fieldPath}.maxFrameBytes`);
1100
1085
  break;
1101
1086
  }
1102
1087
  }
@@ -1159,10 +1144,7 @@ function levenshtein(a: string, b: string): number {
1159
1144
  return prev[n] ?? 0;
1160
1145
  }
1161
1146
 
1162
- function suggestField(
1163
- unknown: string,
1164
- candidates: ReadonlySet<string>,
1165
- ): string | undefined {
1147
+ function suggestField(unknown: string, candidates: ReadonlySet<string>): string | undefined {
1166
1148
  let best: string | undefined;
1167
1149
  let bestDist = 3;
1168
1150
  for (const candidate of candidates) {
@@ -1175,11 +1157,7 @@ function suggestField(
1175
1157
  return best;
1176
1158
  }
1177
1159
 
1178
- function rejectUnknownFields(
1179
- value: object,
1180
- allowed: ReadonlySet<string>,
1181
- fieldPath: string,
1182
- ): void {
1160
+ function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldPath: string): void {
1183
1161
  for (const key of Object.keys(value)) {
1184
1162
  if (allowed.has(key)) continue;
1185
1163
  const hint = suggestField(key, allowed);
@@ -1218,23 +1196,12 @@ function validateProviderHealthMonitor(
1218
1196
  field: "healthMonitor" | "healthProbe" = "healthMonitor",
1219
1197
  ): void {
1220
1198
  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
- );
1199
+ if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
1200
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
1201
+ fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
1202
+ });
1232
1203
  const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
1233
- rejectUnknownFields(
1234
- healthMonitorRecord,
1235
- PROVIDER_HEALTH_MONITOR_FIELDS,
1236
- field,
1237
- );
1204
+ rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
1238
1205
  if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
1239
1206
  assertBoundedIntegerMs(
1240
1207
  healthMonitorRecord.defaultProbeTimeoutMs,
@@ -1302,11 +1269,7 @@ function validateProviderHealthMonitor(
1302
1269
 
1303
1270
  const probeOverrides = healthMonitorRecord.probeOverrides;
1304
1271
  if (probeOverrides !== undefined) {
1305
- if (
1306
- !probeOverrides ||
1307
- typeof probeOverrides !== "object" ||
1308
- Array.isArray(probeOverrides)
1309
- )
1272
+ if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
1310
1273
  throw new ValidationError(
1311
1274
  `Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`,
1312
1275
  );
@@ -1372,14 +1335,8 @@ function validateHealthCheckCase(
1372
1335
  ): void {
1373
1336
  const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
1374
1337
  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
- );
1338
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1339
+ rejectUnknownFields(caseValue as Record<string, unknown>, HEALTH_CHECK_CASE_FIELDS, fieldPath);
1383
1340
  const c = caseValue as HealthCheckCase;
1384
1341
  if (typeof c.name !== "string" || c.name.length === 0)
1385
1342
  throw new ValidationError(
@@ -1407,15 +1364,11 @@ function validateHealthCheckCase(
1407
1364
  `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
1365
  );
1409
1366
  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
- );
1367
+ assertBoundedIntegerMs(c.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
1368
+ min: HEALTH_CHECK_TIMEOUT_MS_MIN,
1369
+ max: HEALTH_CHECK_TIMEOUT_MS_MAX,
1370
+ label: "timeout",
1371
+ });
1419
1372
  }
1420
1373
  if (
1421
1374
  c.expectedStatus !== undefined &&
@@ -1431,21 +1384,11 @@ function validateHealthCheckCase(
1431
1384
  );
1432
1385
  }
1433
1386
 
1434
- function validateHealthCheckSuite(
1435
- providerId: string,
1436
- operationName: string,
1437
- suite: unknown,
1438
- ): void {
1387
+ function validateHealthCheckSuite(providerId: string, operationName: string, suite: unknown): void {
1439
1388
  const fieldPath = `operations.${operationName}.healthCheck`;
1440
1389
  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
- );
1390
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1391
+ rejectUnknownFields(suite as Record<string, unknown>, HEALTH_CHECK_SUITE_FIELDS, fieldPath);
1449
1392
  const s = suite as HealthCheckSuite;
1450
1393
  if (!isPositiveMsDurationString(s.interval))
1451
1394
  throw new ValidationError(
@@ -1455,11 +1398,7 @@ function validateHealthCheckSuite(
1455
1398
  },
1456
1399
  );
1457
1400
  if (s.schedule !== undefined) {
1458
- if (
1459
- !s.schedule ||
1460
- typeof s.schedule !== "object" ||
1461
- Array.isArray(s.schedule)
1462
- ) {
1401
+ if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
1463
1402
  throw new ValidationError(
1464
1403
  `Provider "${providerId}" ${fieldPath}.schedule must be an object.`,
1465
1404
  );
@@ -1469,11 +1408,7 @@ function validateHealthCheckSuite(
1469
1408
  `Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`,
1470
1409
  );
1471
1410
  }
1472
- rejectUnknownFields(
1473
- s.schedule,
1474
- new Set(["randomize"]),
1475
- `${fieldPath}.schedule`,
1476
- );
1411
+ rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
1477
1412
  const randomize = Reflect.get(s.schedule, "randomize");
1478
1413
  if (randomize !== undefined) {
1479
1414
  validateScheduleRandomization(
@@ -1484,15 +1419,11 @@ function validateHealthCheckSuite(
1484
1419
  }
1485
1420
  }
1486
1421
  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
- );
1422
+ assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
1423
+ min: HEALTH_CHECK_TIMEOUT_MS_MIN,
1424
+ max: HEALTH_CHECK_TIMEOUT_MS_MAX,
1425
+ label: "timeout",
1426
+ });
1496
1427
  }
1497
1428
  if (s.degradedThresholdMs !== undefined) {
1498
1429
  assertBoundedIntegerMs(
@@ -1505,10 +1436,7 @@ function validateHealthCheckSuite(
1505
1436
  },
1506
1437
  );
1507
1438
  }
1508
- if (
1509
- s.requiresConnection !== undefined &&
1510
- typeof s.requiresConnection !== "boolean"
1511
- )
1439
+ if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
1512
1440
  throw new ValidationError(
1513
1441
  `Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`,
1514
1442
  );
@@ -1540,14 +1468,8 @@ function validateHealthCheckUnsupported(
1540
1468
  unsupported: unknown,
1541
1469
  ): void {
1542
1470
  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
- );
1471
+ if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
1472
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1551
1473
  rejectUnknownFields(
1552
1474
  unsupported as Record<string, unknown>,
1553
1475
  HEALTH_CHECK_UNSUPPORTED_FIELDS,
@@ -1581,12 +1503,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
1581
1503
  "steps",
1582
1504
  "run",
1583
1505
  ]);
1584
- const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
1585
- "kind",
1586
- "interval",
1587
- "jitter",
1588
- "randomize",
1589
- ]);
1506
+ const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
1590
1507
  const HEALTH_JOURNEY_STEP_FIELDS = new Set([
1591
1508
  "id",
1592
1509
  "description",
@@ -1605,10 +1522,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
1605
1522
  "minManualInterval",
1606
1523
  "publicRationale",
1607
1524
  ]);
1608
- const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
1609
- "enabled",
1610
- "reason",
1611
- ]);
1525
+ const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
1612
1526
  const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
1613
1527
  "enabled",
1614
1528
  "requiresAcknowledgement",
@@ -1628,32 +1542,18 @@ function validateHealthJourneyManualTrigger(
1628
1542
  manualTrigger: unknown,
1629
1543
  ): void {
1630
1544
  const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
1631
- if (
1632
- !manualTrigger ||
1633
- typeof manualTrigger !== "object" ||
1634
- Array.isArray(manualTrigger)
1635
- ) {
1545
+ if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
1636
1546
  throw new ValidationError(
1637
1547
  `Provider "${providerId}" ${fieldPath} must be an object when present.`,
1638
1548
  );
1639
1549
  }
1640
- rejectUnknownFields(
1641
- manualTrigger,
1642
- HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS,
1643
- fieldPath,
1644
- );
1550
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
1645
1551
  const enabled = Reflect.get(manualTrigger, "enabled");
1646
1552
  if (typeof enabled !== "boolean") {
1647
- throw new ValidationError(
1648
- `Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`,
1649
- );
1553
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`);
1650
1554
  }
1651
1555
  if (enabled === false) {
1652
- rejectUnknownFields(
1653
- manualTrigger,
1654
- HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS,
1655
- fieldPath,
1656
- );
1556
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS, fieldPath);
1657
1557
  if (
1658
1558
  Reflect.get(manualTrigger, "reason") !== undefined &&
1659
1559
  (typeof Reflect.get(manualTrigger, "reason") !== "string" ||
@@ -1665,25 +1565,15 @@ function validateHealthJourneyManualTrigger(
1665
1565
  }
1666
1566
  return;
1667
1567
  }
1668
- rejectUnknownFields(
1669
- manualTrigger,
1670
- HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS,
1671
- fieldPath,
1672
- );
1673
- const requiresAcknowledgement = Reflect.get(
1674
- manualTrigger,
1675
- "requiresAcknowledgement",
1676
- );
1568
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS, fieldPath);
1569
+ const requiresAcknowledgement = Reflect.get(manualTrigger, "requiresAcknowledgement");
1677
1570
  if (typeof requiresAcknowledgement !== "boolean") {
1678
1571
  throw new ValidationError(
1679
1572
  `Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`,
1680
1573
  );
1681
1574
  }
1682
1575
  const risk = Reflect.get(manualTrigger, "risk");
1683
- if (
1684
- typeof risk !== "string" ||
1685
- !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)
1686
- ) {
1576
+ if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
1687
1577
  throw new ValidationError(
1688
1578
  `Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`,
1689
1579
  );
@@ -1694,10 +1584,7 @@ function validateHealthJourneyManualTrigger(
1694
1584
  );
1695
1585
  }
1696
1586
  const minManualInterval = Reflect.get(manualTrigger, "minManualInterval");
1697
- assertIsoDuration(
1698
- minManualInterval,
1699
- `Provider "${providerId}" ${fieldPath}.minManualInterval`,
1700
- );
1587
+ assertIsoDuration(minManualInterval, `Provider "${providerId}" ${fieldPath}.minManualInterval`);
1701
1588
  if (isoDurationMs(minManualInterval) <= 0) {
1702
1589
  throw new ValidationError(
1703
1590
  `Provider "${providerId}" ${fieldPath}.minManualInterval must be a positive duration.`,
@@ -1728,18 +1615,14 @@ const SMS_ORIGIN_FIELDS_BY_KIND: Record<string, ReadonlySet<string>> = {
1728
1615
  e164: new Set(["kind", "value", "display"]),
1729
1616
  nationalServiceCode: new Set(["kind", "country", "value", "display"]),
1730
1617
  };
1731
- const DURATION_RE =
1732
- /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
1618
+ const DURATION_RE = /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
1733
1619
  const E164_RE = /^\+[1-9]\d{1,14}$/;
1734
1620
  const ISO_COUNTRY_RE = /^[A-Z]{2}$/;
1735
1621
  const NATIONAL_SERVICE_CODE_RE = /^[0-9]{2,15}$/;
1736
1622
  const BCP47_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
1737
1623
  const JOURNEY_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1738
1624
 
1739
- function assertIsoDuration(
1740
- value: unknown,
1741
- fieldPath: string,
1742
- ): asserts value is string {
1625
+ function assertIsoDuration(value: unknown, fieldPath: string): asserts value is string {
1743
1626
  if (typeof value !== "string" || !DURATION_RE.test(value)) {
1744
1627
  throw new ValidationError(
1745
1628
  `${fieldPath} must be an ISO 8601 duration for example PT8H or PT2M30S.`,
@@ -1754,15 +1637,10 @@ function isoDurationMs(value: string): number {
1754
1637
  const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
1755
1638
  const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
1756
1639
  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
- );
1640
+ return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
1760
1641
  }
1761
1642
 
1762
- function scheduleRandomizationMs(
1763
- randomize: unknown,
1764
- fieldPath: string,
1765
- ): number {
1643
+ function scheduleRandomizationMs(randomize: unknown, fieldPath: string): number {
1766
1644
  const mode = Reflect.get(randomize as object, "mode");
1767
1645
  switch (mode) {
1768
1646
  case "centered": {
@@ -1776,9 +1654,7 @@ function scheduleRandomizationMs(
1776
1654
  return isoDurationMs(maxDelay);
1777
1655
  }
1778
1656
  default:
1779
- throw new ValidationError(
1780
- `${fieldPath}.mode must be "centered" or "delayed".`,
1781
- );
1657
+ throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
1782
1658
  }
1783
1659
  }
1784
1660
 
@@ -1792,25 +1668,18 @@ function validateScheduleRandomization(
1792
1668
  }
1793
1669
  const mode = Reflect.get(randomize, "mode");
1794
1670
  const allowedFields =
1795
- mode === "centered"
1796
- ? new Set(["mode", "maxOffset"])
1797
- : new Set(["mode", "maxDelay"]);
1671
+ mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
1798
1672
  rejectUnknownFields(randomize, allowedFields, fieldPath);
1799
1673
  const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
1800
1674
  if (offsetMs <= 0) {
1801
1675
  throw new ValidationError(`${fieldPath} duration must be positive.`);
1802
1676
  }
1803
1677
  if (offsetMs >= intervalMs) {
1804
- throw new ValidationError(
1805
- `${fieldPath} duration must be shorter than schedule interval.`,
1806
- );
1678
+ throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
1807
1679
  }
1808
1680
  }
1809
1681
 
1810
- function assertIsoCountry(
1811
- value: unknown,
1812
- fieldPath: string,
1813
- ): asserts value is string {
1682
+ function assertIsoCountry(value: unknown, fieldPath: string): asserts value is string {
1814
1683
  if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
1815
1684
  throw new ValidationError(
1816
1685
  `${fieldPath} must be an ISO 3166-1 alpha-2 country code for example KR.`,
@@ -1833,9 +1702,7 @@ function normalizeIntervalDuration(input: string): string {
1833
1702
  ? durationMs / 3_600_000
1834
1703
  : durationMs / 86_400_000;
1835
1704
  if (!Number.isInteger(amount) || amount <= 0) {
1836
- throw new ValidationError(
1837
- `Journey schedule interval must be a positive duration.`,
1838
- );
1705
+ throw new ValidationError(`Journey schedule interval must be a positive duration.`);
1839
1706
  }
1840
1707
  if (unit === "s") return `PT${amount}S`;
1841
1708
  if (unit === "m") return `PT${amount}M`;
@@ -1894,11 +1761,7 @@ function countCapturingGroups(pattern: RegExp): number {
1894
1761
  if (inCharacterClass || char !== "(") continue;
1895
1762
  const next = source[i + 1];
1896
1763
  if (next === "?" && source[i + 2] !== "<") continue;
1897
- if (
1898
- next === "?" &&
1899
- source[i + 2] === "<" &&
1900
- (source[i + 3] === "=" || source[i + 3] === "!")
1901
- )
1764
+ if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
1902
1765
  continue;
1903
1766
  count += 1;
1904
1767
  }
@@ -1917,9 +1780,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
1917
1780
  }
1918
1781
  const kind = Reflect.get(origin, "kind");
1919
1782
  if (kind !== "e164" && kind !== "nationalServiceCode") {
1920
- throw new ValidationError(
1921
- `${fieldPath}.kind must be "e164" or "nationalServiceCode".`,
1922
- );
1783
+ throw new ValidationError(`${fieldPath}.kind must be "e164" or "nationalServiceCode".`);
1923
1784
  }
1924
1785
  rejectUnknownFields(origin, SMS_ORIGIN_FIELDS_BY_KIND[kind], fieldPath);
1925
1786
  if (kind === "e164") {
@@ -1946,9 +1807,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
1946
1807
  Reflect.get(origin, "display") !== undefined &&
1947
1808
  typeof Reflect.get(origin, "display") !== "string"
1948
1809
  ) {
1949
- throw new ValidationError(
1950
- `${fieldPath}.display must be a string when present.`,
1951
- );
1810
+ throw new ValidationError(`${fieldPath}.display must be a string when present.`);
1952
1811
  }
1953
1812
  }
1954
1813
 
@@ -1962,9 +1821,7 @@ function validateSmsOtpMatcher(
1962
1821
  rejectUnknownFields(matcher, SMS_OTP_MATCHER_FIELDS, fieldPath);
1963
1822
  const matcherId = Reflect.get(matcher, "id");
1964
1823
  if (typeof matcherId !== "string" || !JOURNEY_ID_RE.test(matcherId)) {
1965
- throw new ValidationError(
1966
- `${fieldPath}.id must be a kebab-case identifier.`,
1967
- );
1824
+ throw new ValidationError(`${fieldPath}.id must be a kebab-case identifier.`);
1968
1825
  }
1969
1826
  assertIsoCountry(Reflect.get(matcher, "country"), `${fieldPath}.country`);
1970
1827
  if (
@@ -1972,24 +1829,18 @@ function validateSmsOtpMatcher(
1972
1829
  (typeof Reflect.get(matcher, "locale") !== "string" ||
1973
1830
  !BCP47_RE.test(Reflect.get(matcher, "locale")))
1974
1831
  ) {
1975
- throw new ValidationError(
1976
- `${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`,
1977
- );
1832
+ throw new ValidationError(`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`);
1978
1833
  }
1979
1834
  if (
1980
1835
  Reflect.get(matcher, "phoneNumber") !== undefined &&
1981
1836
  (typeof Reflect.get(matcher, "phoneNumber") !== "string" ||
1982
1837
  !E164_RE.test(Reflect.get(matcher, "phoneNumber")))
1983
1838
  ) {
1984
- throw new ValidationError(
1985
- `${fieldPath}.phoneNumber must be an ITU-T E.164 number.`,
1986
- );
1839
+ throw new ValidationError(`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`);
1987
1840
  }
1988
1841
  const origins = Reflect.get(matcher, "origins");
1989
1842
  if (!Array.isArray(origins) || origins.length === 0) {
1990
- throw new ValidationError(
1991
- `${fieldPath}.origins must be a non-empty array.`,
1992
- );
1843
+ throw new ValidationError(`${fieldPath}.origins must be a non-empty array.`);
1993
1844
  }
1994
1845
  for (const [index, origin] of origins.entries()) {
1995
1846
  validateSmsOrigin(origin, `${fieldPath}.origins[${index}]`);
@@ -2010,10 +1861,7 @@ function validateSmsOtpMatcher(
2010
1861
  );
2011
1862
  }
2012
1863
  const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
2013
- if (
2014
- countCapturingGroups(regex) !== 1 &&
2015
- Reflect.get(code, "capture") === undefined
2016
- ) {
1864
+ if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
2017
1865
  throw new ValidationError(
2018
1866
  `${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`,
2019
1867
  );
@@ -2023,20 +1871,12 @@ function validateSmsOtpMatcher(
2023
1871
  typeof Reflect.get(code, "capture") !== "string" &&
2024
1872
  typeof Reflect.get(code, "capture") !== "number"
2025
1873
  ) {
2026
- throw new ValidationError(
2027
- `${fieldPath}.code.capture must be a string or number when present.`,
2028
- );
1874
+ throw new ValidationError(`${fieldPath}.code.capture must be a string or number when present.`);
2029
1875
  }
2030
1876
  assertIsoDuration(Reflect.get(matcher, "maxAge"), `${fieldPath}.maxAge`);
2031
- assertIsoDuration(
2032
- Reflect.get(matcher, "waitTimeout"),
2033
- `${fieldPath}.waitTimeout`,
2034
- );
1877
+ assertIsoDuration(Reflect.get(matcher, "waitTimeout"), `${fieldPath}.waitTimeout`);
2035
1878
  if (Reflect.get(matcher, "clockSkew") !== undefined)
2036
- assertIsoDuration(
2037
- Reflect.get(matcher, "clockSkew"),
2038
- `${fieldPath}.clockSkew`,
2039
- );
1879
+ assertIsoDuration(Reflect.get(matcher, "clockSkew"), `${fieldPath}.clockSkew`);
2040
1880
  }
2041
1881
 
2042
1882
  export function defineSmsOtpMatcher(
@@ -2068,9 +1908,7 @@ export function defineSmsOtpMatcher(
2068
1908
  return matcher;
2069
1909
  }
2070
1910
 
2071
- export function defineHealthJourney(
2072
- config: HealthJourneyDefinition,
2073
- ): HealthJourneyDefinition {
1911
+ export function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition {
2074
1912
  return config;
2075
1913
  }
2076
1914
 
@@ -2081,22 +1919,15 @@ function validateHealthJourneySchedule(
2081
1919
  ): void {
2082
1920
  const fieldPath = `healthJourneys.${journeyId}.schedule`;
2083
1921
  if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) {
2084
- throw new ValidationError(
2085
- `Provider "${providerId}" ${fieldPath} must be an object.`,
2086
- );
1922
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
2087
1923
  }
2088
1924
  rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
2089
1925
  if (Reflect.get(schedule, "kind") !== "interval")
2090
- throw new ValidationError(
2091
- `Provider "${providerId}" ${fieldPath}.kind must be "interval".`,
2092
- );
1926
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
2093
1927
  const interval = Reflect.get(schedule, "interval");
2094
1928
  assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
2095
1929
  const randomize = Reflect.get(schedule, "randomize");
2096
- if (
2097
- Reflect.get(schedule, "jitter") !== undefined &&
2098
- randomize !== undefined
2099
- ) {
1930
+ if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
2100
1931
  throw new ValidationError(
2101
1932
  `Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`,
2102
1933
  );
@@ -2123,17 +1954,13 @@ function validateHealthJourneys(
2123
1954
  const covered = new Set<string>();
2124
1955
  if (healthJourneys === undefined) return covered;
2125
1956
  if (!Array.isArray(healthJourneys)) {
2126
- throw new ValidationError(
2127
- `Provider "${providerId}" healthJourneys must be an array.`,
2128
- );
1957
+ throw new ValidationError(`Provider "${providerId}" healthJourneys must be an array.`);
2129
1958
  }
2130
1959
  const journeyIds = new Set<string>();
2131
1960
  for (const [index, journey] of healthJourneys.entries()) {
2132
1961
  const prefix = `healthJourneys[${index}]`;
2133
1962
  if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
2134
- throw new ValidationError(
2135
- `Provider "${providerId}" ${prefix} must be an object.`,
2136
- );
1963
+ throw new ValidationError(`Provider "${providerId}" ${prefix} must be an object.`);
2137
1964
  }
2138
1965
  rejectUnknownFields(journey, HEALTH_JOURNEY_FIELDS, prefix);
2139
1966
  if (typeof journey.id !== "string" || !JOURNEY_ID_RE.test(journey.id)) {
@@ -2147,10 +1974,7 @@ function validateHealthJourneys(
2147
1974
  );
2148
1975
  journeyIds.add(journey.id);
2149
1976
  validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
2150
- if (
2151
- !Array.isArray(journey.coversOperations) ||
2152
- journey.coversOperations.length === 0
2153
- ) {
1977
+ if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
2154
1978
  throw new ValidationError(
2155
1979
  `Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`,
2156
1980
  );
@@ -2185,10 +2009,7 @@ function validateHealthJourneys(
2185
2009
  `Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers must be an array.`,
2186
2010
  );
2187
2011
  for (const [matcherIndex, matcher] of journey.smsMatchers.entries()) {
2188
- validateSmsOtpMatcher(
2189
- matcher,
2190
- `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`,
2191
- );
2012
+ validateSmsOtpMatcher(matcher, `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`);
2192
2013
  if (matcherIds.has(matcher.id))
2193
2014
  throw new ValidationError(
2194
2015
  `Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers has duplicate matcher id "${matcher.id}".`,
@@ -2199,9 +2020,7 @@ function validateHealthJourneys(
2199
2020
  for (const [stepIndex, step] of journey.steps.entries()) {
2200
2021
  const stepPath = `healthJourneys.${journey.id}.steps[${stepIndex}]`;
2201
2022
  if (!step || typeof step !== "object" || Array.isArray(step))
2202
- throw new ValidationError(
2203
- `Provider "${providerId}" ${stepPath} must be an object.`,
2204
- );
2023
+ throw new ValidationError(`Provider "${providerId}" ${stepPath} must be an object.`);
2205
2024
  rejectUnknownFields(step, HEALTH_JOURNEY_STEP_FIELDS, stepPath);
2206
2025
  if (typeof step.id !== "string" || !JOURNEY_ID_RE.test(step.id))
2207
2026
  throw new ValidationError(
@@ -2211,20 +2030,13 @@ function validateHealthJourneys(
2211
2030
  throw new ValidationError(
2212
2031
  `Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`,
2213
2032
  );
2214
- if (
2215
- step.usesSmsMatcher !== undefined &&
2216
- !matcherIds.has(step.usesSmsMatcher)
2217
- )
2033
+ if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
2218
2034
  throw new ValidationError(
2219
2035
  `Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`,
2220
2036
  );
2221
2037
  }
2222
2038
  if (journey.manualTrigger !== undefined)
2223
- validateHealthJourneyManualTrigger(
2224
- providerId,
2225
- journey.id,
2226
- journey.manualTrigger,
2227
- );
2039
+ validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
2228
2040
  if (journey.timeout !== undefined)
2229
2041
  assertIsoDuration(
2230
2042
  journey.timeout,
@@ -2270,23 +2082,10 @@ function validateOperationHealthChecks(
2270
2082
  fix: `Remove either operations.${operationName}.healthCheck or operations.${operationName}.healthCheckUnsupported.`,
2271
2083
  },
2272
2084
  );
2273
- if (hasCheck)
2274
- validateHealthCheckSuite(
2275
- providerId,
2276
- operationName,
2277
- operation.healthCheck,
2278
- );
2085
+ if (hasCheck) validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
2279
2086
  if (hasUnsupported)
2280
- validateHealthCheckUnsupported(
2281
- providerId,
2282
- operationName,
2283
- operation.healthCheckUnsupported,
2284
- );
2285
- if (
2286
- !hasCheck &&
2287
- !hasUnsupported &&
2288
- !journeyCoveredOperations.has(operationName)
2289
- )
2087
+ validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
2088
+ if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
2290
2089
  throw new ValidationError(
2291
2090
  `Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`,
2292
2091
  {
@@ -2308,6 +2107,26 @@ function validateOperationFixtures(
2308
2107
  fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
2309
2108
  },
2310
2109
  );
2110
+ if (operation.fixtures?.recordedAt !== undefined) {
2111
+ const recordedAt = operation.fixtures.recordedAt;
2112
+ const parsed =
2113
+ typeof recordedAt === "string"
2114
+ ? new Date(`${recordedAt}T00:00:00.000Z`)
2115
+ : new Date(Number.NaN);
2116
+ const isCalendarDate =
2117
+ typeof recordedAt === "string" &&
2118
+ /^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
2119
+ !Number.isNaN(parsed.getTime()) &&
2120
+ parsed.toISOString().slice(0, 10) === recordedAt;
2121
+ const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
2122
+ if (!isCalendarDate || recordedAt > kstToday)
2123
+ throw new ValidationError(
2124
+ `Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`,
2125
+ {
2126
+ fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
2127
+ },
2128
+ );
2129
+ }
2311
2130
  if (operation.fixtures?.request !== undefined) {
2312
2131
  const result = safeParseSchemaSync(
2313
2132
  operation.input,
@@ -2341,6 +2160,44 @@ function validateOperationFixtures(
2341
2160
  }
2342
2161
  }
2343
2162
 
2163
+ function resolveOperationFixtureRequests<TOperations extends Record<string, ProviderOperation>>(
2164
+ operations: TOperations,
2165
+ ): TOperations {
2166
+ let changed = false;
2167
+ const resolvedOperations = Object.fromEntries(
2168
+ Object.entries(operations).map(([operationName, operation]) => {
2169
+ if (operation.fixtures?.request === undefined) return [operationName, operation];
2170
+ const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
2171
+ if (request === operation.fixtures.request) return [operationName, operation];
2172
+ changed = true;
2173
+ return [
2174
+ operationName,
2175
+ {
2176
+ ...operation,
2177
+ fixtures: {
2178
+ ...operation.fixtures,
2179
+ request,
2180
+ },
2181
+ },
2182
+ ];
2183
+ }),
2184
+ ) as TOperations;
2185
+ return changed ? resolvedOperations : operations;
2186
+ }
2187
+
2188
+ /**
2189
+ * Shallow shape guard only: the `deployment` object is passed through
2190
+ * verbatim and deliberately not deep-validated by the SDK — the APIFuse
2191
+ * registry builder owns deployment validation and profile resolution.
2192
+ */
2193
+ function validateProviderDeployment(providerId: string, deployment: unknown): void {
2194
+ if (deployment === undefined) return;
2195
+ if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
2196
+ throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
2197
+ fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
2198
+ });
2199
+ }
2200
+
2344
2201
  export function defineProvider<
2345
2202
  TOperations extends Record<string, ProviderOperation>,
2346
2203
  TConfig extends ProviderConfig<TOperations>,
@@ -2348,20 +2205,19 @@ export function defineProvider<
2348
2205
  config: TConfig & AuthStartNoInputGuard<TConfig>,
2349
2206
  ): ProviderDefinition & { operations: OperationMapConfig<TOperations> } {
2350
2207
  validateProviderShape(config);
2208
+ const operations = resolveOperationFixtureRequests(config.operations);
2351
2209
  if (!CONNECTOR_ID_REGEX.test(config.id))
2352
2210
  throw new ProviderError(`Invalid provider id: "${config.id}"`, {
2353
2211
  fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
2354
2212
  });
2355
2213
  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
- );
2214
+ throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
2215
+ fix: "Add at least one operation to the operations object",
2216
+ });
2362
2217
  validateOperationIds(config.id, config.operations);
2363
2218
  validateOperationAnnotations(config.id, config.operations);
2364
2219
  validateOperationObservability(config.id, config.operations);
2220
+ validateOperationErrorCodes(config.id, config.operations);
2365
2221
  validateOperationTransports(config.id, config.operations);
2366
2222
  validateOperationContracts(config.id, config.operations);
2367
2223
  validateToolRouterMetadata(config.id, config.operations);
@@ -2370,11 +2226,7 @@ export function defineProvider<
2370
2226
  config.operations,
2371
2227
  config.healthJourneys,
2372
2228
  );
2373
- validateOperationHealthChecks(
2374
- config.id,
2375
- config.operations,
2376
- journeyCoveredOperations,
2377
- );
2229
+ validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
2378
2230
  if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
2379
2231
  throw new ValidationError(
2380
2232
  `Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
@@ -2387,7 +2239,15 @@ export function defineProvider<
2387
2239
  config.healthProbe ?? config.healthMonitor,
2388
2240
  config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
2389
2241
  );
2390
- validateOperationFixtures(config.id, config.operations);
2242
+ validateOperationFixtures(config.id, operations);
2243
+ validateProviderDeployment(config.id, config.deployment);
2244
+ try {
2245
+ validateNativeProviderConfig(config.native);
2246
+ } catch (error) {
2247
+ if (error instanceof NativeEgressPolicyValidationError)
2248
+ throw new ValidationError(error.message);
2249
+ throw error;
2250
+ }
2391
2251
  validateProviderProxy(config);
2392
2252
  validateProviderStt(config);
2393
2253
  if (config.runtime === "browser" && !config.browser)
@@ -2406,7 +2266,11 @@ export function defineProvider<
2406
2266
  id: config.id,
2407
2267
  version: config.version,
2408
2268
  runtime: config.runtime,
2269
+ // Verbatim passthrough: deployment validation and profile resolution
2270
+ // are owned by the APIFuse registry builder, not the SDK.
2271
+ deployment: config.deployment,
2409
2272
  allowedHosts: config.allowedHosts,
2273
+ native: config.native,
2410
2274
  stealth: config.stealth,
2411
2275
  proxy: config.proxy,
2412
2276
  stt: config.stt,
@@ -2418,7 +2282,7 @@ export function defineProvider<
2418
2282
  credential: config.credential,
2419
2283
  context: config.context,
2420
2284
  meta: config.meta,
2421
- operations: config.operations,
2285
+ operations,
2422
2286
  // Transitional healthMonitor → healthProbe alias: mirror whichever field
2423
2287
  // was declared onto both so old and new consumers keep working.
2424
2288
  healthMonitor: config.healthMonitor ?? config.healthProbe,