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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/AUTHORING.md +493 -5
  2. package/CHANGELOG.md +131 -1
  3. package/README.md +52 -6
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +106 -62
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +63 -55
  8. package/bin/apifuse-pack-check.ts +22 -2
  9. package/bin/apifuse-pack-smoke.ts +78 -82
  10. package/bin/apifuse-pack-types.ts +583 -0
  11. package/bin/apifuse-perf.ts +59 -140
  12. package/bin/apifuse-record.ts +698 -113
  13. package/bin/apifuse-submit-check.ts +517 -44
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml.ts +1 -1
  18. package/dist/auth-turn/index.d.ts +4 -4
  19. package/dist/auth-turn/index.js +1 -1
  20. package/dist/auth.d.ts +16 -2
  21. package/dist/auth.js +76 -18
  22. package/dist/ceremonies/index.d.ts +9 -1
  23. package/dist/ceremonies/index.js +117 -29
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/cli/templates/provider/README.md.tpl +4 -4
  32. package/dist/config/loader.d.ts +176 -17
  33. package/dist/config/loader.js +434 -161
  34. package/dist/contract-serialization.d.ts +2 -2
  35. package/dist/contract-serialization.js +7 -14
  36. package/dist/contract-types.d.ts +3 -2
  37. package/dist/contract.d.ts +3 -3
  38. package/dist/contract.js +6 -6
  39. package/dist/declaration-validation.d.ts +23 -0
  40. package/dist/declaration-validation.js +159 -0
  41. package/dist/define.d.ts +13 -1
  42. package/dist/define.js +391 -122
  43. package/dist/dev.d.ts +1 -1
  44. package/dist/dev.js +1 -1
  45. package/dist/error-resolution.d.ts +4 -0
  46. package/dist/error-resolution.js +123 -0
  47. package/dist/errors.d.ts +19 -1
  48. package/dist/errors.js +41 -3
  49. package/dist/fixture-sanitization.d.ts +26 -0
  50. package/dist/fixture-sanitization.js +216 -0
  51. package/dist/i18n/catalog.d.ts +2 -2
  52. package/dist/i18n/catalog.js +4 -10
  53. package/dist/i18n/index.d.ts +2 -2
  54. package/dist/i18n/index.js +2 -2
  55. package/dist/i18n/keys.d.ts +2 -2
  56. package/dist/index.d.ts +50 -42
  57. package/dist/index.js +41 -37
  58. package/dist/lint.d.ts +6 -1
  59. package/dist/lint.js +370 -18
  60. package/dist/native-address.d.ts +43 -0
  61. package/dist/native-address.js +281 -0
  62. package/dist/native-egress-policy.d.ts +31 -0
  63. package/dist/native-egress-policy.js +288 -0
  64. package/dist/observability.d.ts +5 -2
  65. package/dist/observability.js +48 -1
  66. package/dist/provider.d.ts +13 -11
  67. package/dist/provider.js +10 -9
  68. package/dist/public-schema-field-lint.d.ts +1 -1
  69. package/dist/recipes/gov-api.js +1 -1
  70. package/dist/runtime/auth-flow.d.ts +3 -1
  71. package/dist/runtime/auth-flow.js +8 -3
  72. package/dist/runtime/browser.d.ts +1 -1
  73. package/dist/runtime/browser.js +138 -40
  74. package/dist/runtime/cache.d.ts +2 -1
  75. package/dist/runtime/cache.js +173 -23
  76. package/dist/runtime/choice-wordlist.d.ts +9 -0
  77. package/dist/runtime/choice-wordlist.js +138 -0
  78. package/dist/runtime/choice.d.ts +14 -1
  79. package/dist/runtime/choice.js +566 -101
  80. package/dist/runtime/credential.d.ts +1 -1
  81. package/dist/runtime/credential.js +1 -1
  82. package/dist/runtime/env.d.ts +1 -1
  83. package/dist/runtime/executor.d.ts +1 -1
  84. package/dist/runtime/executor.js +25 -3
  85. package/dist/runtime/http.d.ts +3 -2
  86. package/dist/runtime/http.js +517 -55
  87. package/dist/runtime/insights.d.ts +1 -1
  88. package/dist/runtime/insights.js +6 -13
  89. package/dist/runtime/instrumentation.d.ts +2 -2
  90. package/dist/runtime/instrumentation.js +371 -23
  91. package/dist/runtime/keyring.js +1 -1
  92. package/dist/runtime/namespace.js +1 -1
  93. package/dist/runtime/native-network-errors.d.ts +33 -0
  94. package/dist/runtime/native-network-errors.js +69 -0
  95. package/dist/runtime/native-network.d.ts +96 -0
  96. package/dist/runtime/native-network.js +1232 -0
  97. package/dist/runtime/ocr.d.ts +29 -0
  98. package/dist/runtime/ocr.js +440 -0
  99. package/dist/runtime/otlp.d.ts +1 -1
  100. package/dist/runtime/perf.d.ts +1 -1
  101. package/dist/runtime/provider.d.ts +1 -1
  102. package/dist/runtime/provider.js +1 -2
  103. package/dist/runtime/proxy-errors.d.ts +1 -1
  104. package/dist/runtime/proxy-errors.js +9 -7
  105. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  106. package/dist/runtime/proxy-nodemaven.js +146 -0
  107. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  108. package/dist/runtime/proxy-retry-policy.js +2 -2
  109. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  110. package/dist/runtime/proxy-telemetry.js +58 -52
  111. package/dist/runtime/redirects.d.ts +29 -0
  112. package/dist/runtime/redirects.js +36 -0
  113. package/dist/runtime/redis.d.ts +1 -1
  114. package/dist/runtime/redis.js +5 -5
  115. package/dist/runtime/request-options.d.ts +68 -1
  116. package/dist/runtime/request-options.js +548 -0
  117. package/dist/runtime/resolver-config.d.ts +6 -0
  118. package/dist/runtime/resolver-config.js +6 -0
  119. package/dist/runtime/resolver-public.d.ts +1 -0
  120. package/dist/runtime/resolver-public.js +1 -0
  121. package/dist/runtime/resolver-shared.d.ts +3 -0
  122. package/dist/runtime/resolver-shared.js +12 -0
  123. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  124. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  125. package/dist/runtime/resolver-vendors/browser.d.ts +20 -0
  126. package/dist/runtime/resolver-vendors/browser.js +282 -0
  127. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  128. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  129. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  130. package/dist/runtime/resolver-vendors/twocaptcha.js +368 -0
  131. package/dist/runtime/resolver-vendors/types.d.ts +83 -0
  132. package/dist/runtime/resolver-vendors/types.js +69 -0
  133. package/dist/runtime/resolver.d.ts +59 -0
  134. package/dist/runtime/resolver.js +705 -0
  135. package/dist/runtime/secrets.d.ts +27 -0
  136. package/dist/runtime/secrets.js +51 -0
  137. package/dist/runtime/state.d.ts +5 -2
  138. package/dist/runtime/state.js +280 -74
  139. package/dist/runtime/stealth-cookies.d.ts +20 -0
  140. package/dist/runtime/stealth-cookies.js +111 -0
  141. package/dist/runtime/stealth.d.ts +30 -5
  142. package/dist/runtime/stealth.js +523 -259
  143. package/dist/runtime/stt.d.ts +1 -1
  144. package/dist/runtime/stt.js +12 -27
  145. package/dist/runtime/timeout.d.ts +5 -0
  146. package/dist/runtime/timeout.js +12 -0
  147. package/dist/runtime/trace.d.ts +2 -2
  148. package/dist/runtime/trace.js +2 -4
  149. package/dist/runtime/waterfall.d.ts +1 -1
  150. package/dist/schema.d.ts +1 -1
  151. package/dist/schema.js +7 -15
  152. package/dist/serve.d.ts +1 -1
  153. package/dist/serve.js +1 -1
  154. package/dist/server/index.d.ts +7 -7
  155. package/dist/server/index.js +6 -6
  156. package/dist/server/self-test-input-tokens.d.ts +2 -1
  157. package/dist/server/self-test-input-tokens.js +18 -14
  158. package/dist/server/self-test-redaction.d.ts +1 -1
  159. package/dist/server/self-test-redaction.js +1 -1
  160. package/dist/server/self-test.d.ts +117 -3
  161. package/dist/server/self-test.js +787 -151
  162. package/dist/server/serve-implementation.d.ts +210 -0
  163. package/dist/server/serve-implementation.js +2078 -0
  164. package/dist/server/serve.d.ts +1 -70
  165. package/dist/server/serve.js +1 -1143
  166. package/dist/server/types.d.ts +34 -9
  167. package/dist/server/types.js +8 -1
  168. package/dist/stateful/errors.d.ts +19 -0
  169. package/dist/stateful/errors.js +24 -0
  170. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  171. package/dist/stateful/http-provider-event-emitter.js +237 -0
  172. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  173. package/dist/stateful/http-session-owner-registry.js +210 -0
  174. package/dist/stateful/index.d.ts +18 -0
  175. package/dist/stateful/index.js +18 -0
  176. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  177. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  178. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  179. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  180. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  181. package/dist/stateful/provider-event-pipeline.js +1 -0
  182. package/dist/stateful/provider-events.d.ts +101 -0
  183. package/dist/stateful/provider-events.js +289 -0
  184. package/dist/stateful/session-key.d.ts +15 -0
  185. package/dist/stateful/session-key.js +86 -0
  186. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  187. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  188. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  189. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  190. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  191. package/dist/stateful/stateful-provider-adapter.js +287 -0
  192. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  193. package/dist/stateful/stateful-provider-observability.js +161 -0
  194. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  195. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  196. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  197. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  198. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  199. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  200. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  201. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  202. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  203. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  204. package/dist/stateful-signing.d.ts +18 -0
  205. package/dist/stateful-signing.js +27 -0
  206. package/dist/stealth/profiles.d.ts +1 -1
  207. package/dist/stealth/profiles.js +21 -21
  208. package/dist/stream-evidence.d.ts +74 -0
  209. package/dist/stream-evidence.js +785 -0
  210. package/dist/stream.d.ts +1 -1
  211. package/dist/stream.js +7 -1
  212. package/dist/testing/index.d.ts +3 -2
  213. package/dist/testing/index.js +3 -2
  214. package/dist/testing/run.d.ts +32 -2
  215. package/dist/testing/run.js +488 -28
  216. package/dist/types.d.ts +566 -19
  217. package/dist/types.js +1 -0
  218. package/dist/user-input.d.ts +30 -0
  219. package/dist/user-input.js +66 -0
  220. package/package.json +42 -7
  221. package/src/auth-turn/index.ts +2 -2
  222. package/src/auth.ts +146 -86
  223. package/src/ceremonies/index.ts +167 -92
  224. package/src/cli/commands.ts +10 -0
  225. package/src/cli/create.ts +42 -35
  226. package/src/cli/prompt-assets.ts +865 -0
  227. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  228. package/src/cli/templates/provider/README.md.tpl +4 -4
  229. package/src/config/loader.ts +667 -289
  230. package/src/contract-serialization.ts +10 -18
  231. package/src/contract-types.ts +3 -2
  232. package/src/contract.ts +14 -28
  233. package/src/declaration-validation.ts +202 -0
  234. package/src/define.ts +631 -495
  235. package/src/dev.ts +4 -9
  236. package/src/error-resolution.ts +128 -0
  237. package/src/errors.ts +56 -11
  238. package/src/fixture-sanitization.ts +247 -0
  239. package/src/i18n/catalog.ts +10 -32
  240. package/src/i18n/index.ts +2 -2
  241. package/src/i18n/keys.ts +5 -11
  242. package/src/index.ts +158 -44
  243. package/src/lint.ts +488 -154
  244. package/src/native-address.ts +340 -0
  245. package/src/native-egress-policy.ts +358 -0
  246. package/src/observability.ts +51 -1
  247. package/src/provider.ts +66 -11
  248. package/src/public-schema-field-lint.ts +7 -33
  249. package/src/recipes/gov-api.ts +2 -5
  250. package/src/runtime/auth-flow.ts +13 -7
  251. package/src/runtime/browser.ts +252 -207
  252. package/src/runtime/cache.ts +209 -81
  253. package/src/runtime/choice-wordlist.ts +145 -0
  254. package/src/runtime/choice.ts +758 -197
  255. package/src/runtime/credential.ts +2 -2
  256. package/src/runtime/env.ts +1 -1
  257. package/src/runtime/executor.ts +37 -19
  258. package/src/runtime/http.ts +645 -65
  259. package/src/runtime/insights.ts +15 -53
  260. package/src/runtime/instrumentation.ts +530 -67
  261. package/src/runtime/keyring.ts +7 -19
  262. package/src/runtime/namespace.ts +2 -7
  263. package/src/runtime/native-network-errors.ts +99 -0
  264. package/src/runtime/native-network.ts +1605 -0
  265. package/src/runtime/ocr.ts +523 -0
  266. package/src/runtime/otlp.ts +12 -23
  267. package/src/runtime/perf.ts +1 -1
  268. package/src/runtime/provider.ts +4 -9
  269. package/src/runtime/proxy-errors.ts +29 -42
  270. package/src/runtime/proxy-nodemaven.ts +221 -0
  271. package/src/runtime/proxy-retry-policy.ts +3 -3
  272. package/src/runtime/proxy-telemetry.ts +84 -77
  273. package/src/runtime/redirects.ts +66 -0
  274. package/src/runtime/redis.ts +10 -13
  275. package/src/runtime/request-options.ts +679 -9
  276. package/src/runtime/resolver-config.ts +6 -0
  277. package/src/runtime/resolver-public.ts +18 -0
  278. package/src/runtime/resolver-shared.ts +17 -0
  279. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  280. package/src/runtime/resolver-vendors/browser.ts +408 -0
  281. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  282. package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
  283. package/src/runtime/resolver-vendors/types.ts +173 -0
  284. package/src/runtime/resolver.ts +1060 -0
  285. package/src/runtime/secrets.ts +64 -0
  286. package/src/runtime/state.ts +399 -161
  287. package/src/runtime/stealth-cookies.ts +132 -0
  288. package/src/runtime/stealth.ts +681 -295
  289. package/src/runtime/stt.ts +39 -113
  290. package/src/runtime/timeout.ts +18 -0
  291. package/src/runtime/trace.ts +14 -44
  292. package/src/runtime/waterfall.ts +5 -18
  293. package/src/schema.ts +23 -84
  294. package/src/serve.ts +6 -1
  295. package/src/server/index.ts +30 -7
  296. package/src/server/self-test-input-tokens.ts +29 -14
  297. package/src/server/self-test-redaction.ts +2 -2
  298. package/src/server/self-test.ts +1030 -180
  299. package/src/server/serve-implementation.ts +3062 -0
  300. package/src/server/serve.ts +1 -1781
  301. package/src/server/types.ts +12 -13
  302. package/src/stateful/README.md +146 -0
  303. package/src/stateful/errors.ts +35 -0
  304. package/src/stateful/http-provider-event-emitter.ts +314 -0
  305. package/src/stateful/http-session-owner-registry.ts +306 -0
  306. package/src/stateful/index.ts +18 -0
  307. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  308. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  309. package/src/stateful/provider-event-pipeline.ts +61 -0
  310. package/src/stateful/provider-events.ts +462 -0
  311. package/src/stateful/session-key.ts +111 -0
  312. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  313. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  314. package/src/stateful/stateful-provider-adapter.ts +562 -0
  315. package/src/stateful/stateful-provider-observability.ts +261 -0
  316. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  317. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  318. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  319. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  320. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  321. package/src/stateful-signing.ts +46 -0
  322. package/src/stealth/profiles.ts +27 -33
  323. package/src/stream-evidence.ts +988 -0
  324. package/src/stream.ts +16 -20
  325. package/src/testing/index.ts +11 -2
  326. package/src/testing/run.ts +668 -74
  327. package/src/types.ts +665 -35
  328. package/src/user-input.ts +118 -0
  329. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  330. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  331. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  332. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  333. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  334. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  335. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  336. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  337. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  338. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  339. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  340. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  341. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  342. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/src/define.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import ms from "ms";
2
2
 
3
- import { ProviderError, ValidationError } from "./errors";
4
- import { safeParseSchemaSync } from "./schema";
3
+ import { validateFailClosedDeclaration } from "./declaration-validation.js";
4
+ import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
5
+ import { ProviderError, ValidationError } from "./errors.js";
6
+ import {
7
+ NativeEgressPolicyValidationError,
8
+ validateNativeProviderConfig,
9
+ } from "./native-egress-policy.js";
10
+ import { safeParseSchemaSync } from "./schema.js";
11
+ import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
5
12
  import type {
6
13
  AuthConfig,
7
14
  BrowserEngine,
@@ -14,6 +21,7 @@ import type {
14
21
  HealthJourneySchedule,
15
22
  HealthScheduleRandomization,
16
23
  InferSchemaOutput,
24
+ NativeProviderConfig,
17
25
  OperationDefinition,
18
26
  OperationHandlerResult,
19
27
  OperationHttpStreamTransport,
@@ -21,10 +29,16 @@ import type {
21
29
  OperationTransport,
22
30
  OperationWebSocketTransport,
23
31
  ProviderAccessConfig,
32
+ ProviderChallengeKind,
24
33
  ProviderDefinition,
34
+ ProviderOcrConfig,
35
+ ProviderDeploymentOverrides,
25
36
  ProviderHealthMonitorConfig,
26
37
  ProviderProxyConfig,
38
+ ProviderProxyProvider,
27
39
  ProviderPublicProfile,
40
+ ProviderResolverConfig,
41
+ ProviderResolverVendor,
28
42
  ProviderReviewed,
29
43
  ProviderSecretDeclaration,
30
44
  ProviderStreamEvent,
@@ -32,7 +46,7 @@ import type {
32
46
  SchemaLike,
33
47
  SmsOtpMatcherDefinition,
34
48
  StealthPlatform,
35
- } from "./types";
49
+ } from "./types.js";
36
50
  import {
37
51
  HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
38
52
  HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
@@ -48,7 +62,8 @@ import {
48
62
  STREAM_IDLE_TIMEOUT_MS_MIN,
49
63
  STREAM_MAX_DURATION_MS_MAX,
50
64
  STREAM_MAX_DURATION_MS_MIN,
51
- } from "./types";
65
+ VALID_OPERATION_ERROR_STATUSES,
66
+ } from "./types.js";
52
67
 
53
68
  type ProviderImplementationSourceAccess =
54
69
  | "official_api"
@@ -79,45 +94,87 @@ const VALID_AUTH_MODES = [
79
94
  "platform-managed",
80
95
  "credentials",
81
96
  "oauth2",
97
+ "oauth2_proxied",
82
98
  ] 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",
99
+ const PROXIED_OAUTH_REQUIRED_FIELDS = [
100
+ "authorizeUrl",
101
+ "tokenUrl",
102
+ "customScheme",
103
+ "rewriteProfile",
104
+ "clientIdEnvKey",
93
105
  ] as const;
106
+ const PROXIED_OAUTH_ALLOWED_FIELDS = new Set([
107
+ ...PROXIED_OAUTH_REQUIRED_FIELDS,
108
+ "pkce",
109
+ "authorizeParams",
110
+ "tokenParams",
111
+ ]);
112
+ const PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS = new Set([
113
+ "client_id",
114
+ "response_type",
115
+ "state",
116
+ "code_challenge",
117
+ "code_challenge_method",
118
+ ]);
119
+ const PROXIED_OAUTH_PROFILE_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
120
+ const PROXIED_OAUTH_ENV_KEY_REGEX = /^[A-Z][A-Z0-9_]*__[A-Z0-9_]+$/;
121
+ const CUSTOM_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+$/;
122
+ const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
123
+ const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"] as const;
124
+ const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"] as const;
94
125
  const VALID_PROVIDER_PROXY_AFFINITIES = [
95
126
  "request",
96
127
  "operation",
97
128
  "auth-flow",
98
129
  "connection",
99
130
  ] as const;
131
+ const VALID_PROVIDER_OCR_MODES = ["optional", "required"] as const;
100
132
  const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
133
+ function exhaustiveLiteralArray<TUnion extends string>() {
134
+ return <const TValues extends readonly TUnion[]>(
135
+ values: TValues,
136
+ ..._missing: Exclude<TUnion, TValues[number]> extends never
137
+ ? []
138
+ : ["Missing runtime values", Exclude<TUnion, TValues[number]>]
139
+ ): TValues => values;
140
+ }
141
+
142
+ export const VALID_PROVIDER_RESOLVER_VENDORS = exhaustiveLiteralArray<ProviderResolverVendor>()([
143
+ "browser",
144
+ "capsolver",
145
+ "capmonster",
146
+ "2captcha",
147
+ "custom",
148
+ ] as const);
149
+ export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray<ProviderChallengeKind>()([
150
+ "turnstile",
151
+ "recaptcha_v2",
152
+ "recaptcha_v3",
153
+ "hcaptcha",
154
+ "cloudflare_interstitial",
155
+ "aws_waf",
156
+ "akamai_sec_cpt",
157
+ "akamai_sensor",
158
+ ] as const);
101
159
  const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
160
+ const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
161
+ const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
162
+ // Per-vendor provider-declared credential secrets. A required-mode chain must
163
+ // declare every secret of every credentialed vendor it names, so a missing
164
+ // credential fails at build/validation time rather than during a live outage: a
165
+ // declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
166
+ // exactly the failure class the multi-vendor chain exists to remove. Vendors
167
+ // absent from this map (the deprecated `custom`/`decodo` values have no managed
168
+ // adapter) impose no declaration requirement.
169
+ const VENDOR_REQUIRED_SECRETS: Partial<Record<ProviderProxyProvider, readonly string[]>> = {
170
+ smartproxy: [SMARTPROXY_APP_KEY_SECRET],
171
+ nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
172
+ };
102
173
  const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
103
174
  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;
175
+ const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
176
+ const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"] as const;
177
+ const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"] as const;
121
178
  const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
122
179
  const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
123
180
 
@@ -135,18 +192,16 @@ function msDurationMs(value: string): number {
135
192
  function parsePositiveMsDuration(value: string): number | undefined {
136
193
  const trimmed = value.trim();
137
194
  if (!MS_DURATION_PATTERN.test(trimmed)) return undefined;
138
- const parsed = ms(
139
- (trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue,
140
- );
195
+ const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue);
141
196
  if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
142
197
  return parsed;
143
198
  }
144
199
 
145
200
  type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
146
- type OperationConfig<
147
- TInput extends SchemaLike,
148
- TOutput extends SchemaLike,
149
- > = Omit<OperationDefinition<TInput, TOutput>, "handler"> & {
201
+ type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
202
+ OperationDefinition<TInput, TOutput>,
203
+ "handler"
204
+ > & {
150
205
  handler(
151
206
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
152
207
  input: InferSchemaOutput<TInput>,
@@ -154,59 +209,44 @@ type OperationConfig<
154
209
  | OperationHandlerResult<InferSchemaOutput<TOutput>>
155
210
  | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
156
211
  };
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
- > =
212
+ type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
213
+ [K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
214
+ ? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
215
+ : never;
216
+ };
217
+ type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
170
218
  | SseOperationConfig<TInput, TOutput>
171
219
  | HttpStreamOperationConfig<TInput, TOutput>
172
220
  | WebSocketOperationConfig<TInput, TOutput>;
173
- type SseOperationConfig<
174
- TInput extends SchemaLike,
175
- TOutput extends SchemaLike,
176
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
221
+ type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
222
+ OperationConfig<TInput, TOutput>,
223
+ "handler" | "transport"
224
+ > & {
177
225
  transport: OperationSseTransport;
178
226
  handler(
179
227
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
180
228
  input: InferSchemaOutput<TInput>,
181
- ):
182
- | AsyncIterable<ProviderStreamEvent>
183
- | Promise<AsyncIterable<ProviderStreamEvent>>;
229
+ ): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
184
230
  };
185
- type HttpStreamOperationConfig<
186
- TInput extends SchemaLike,
187
- TOutput extends SchemaLike,
188
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
231
+ type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
232
+ OperationConfig<TInput, TOutput>,
233
+ "handler" | "transport"
234
+ > & {
189
235
  transport: OperationHttpStreamTransport;
190
236
  handler(
191
237
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
192
238
  input: InferSchemaOutput<TInput>,
193
- ):
194
- | Response
195
- | ReadableStream<Uint8Array>
196
- | Promise<Response | ReadableStream<Uint8Array>>;
239
+ ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
197
240
  };
198
- type WebSocketOperationConfig<
199
- TInput extends SchemaLike,
200
- TOutput extends SchemaLike,
201
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
241
+ type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
242
+ OperationConfig<TInput, TOutput>,
243
+ "handler" | "transport"
244
+ > & {
202
245
  transport: OperationWebSocketTransport;
203
246
  handler(
204
247
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
205
248
  input: InferSchemaOutput<TInput>,
206
- ):
207
- | Response
208
- | ReadableStream<Uint8Array>
209
- | Promise<Response | ReadableStream<Uint8Array>>;
249
+ ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
210
250
  };
211
251
 
212
252
  type AuthStartNoInputGuard<TConfig> = TConfig extends {
@@ -221,19 +261,27 @@ type AuthStartNoInputGuard<TConfig> = TConfig extends {
221
261
  : unknown
222
262
  : unknown;
223
263
 
224
- export interface ProviderConfig<
225
- TOperations extends Record<string, ProviderOperation>,
226
- > {
264
+ export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
227
265
  id: string;
228
266
  version: string;
229
267
  runtime: "standard" | "shared" | "browser";
268
+ /**
269
+ * Optional deployment overrides, passed through verbatim onto the returned
270
+ * provider definition. The SDK types this field but does not deep-validate
271
+ * it — the APIFuse registry builder owns deployment validation and
272
+ * resolves omitted fields against the runtime deployment profiles.
273
+ */
274
+ deployment?: ProviderDeploymentOverrides;
230
275
  allowedHosts?: string[];
276
+ native?: NativeProviderConfig;
231
277
  stealth?: {
232
278
  profile: string;
233
279
  platform: StealthPlatform;
234
280
  };
235
281
  proxy?: ProviderProxyConfig;
282
+ ocr?: ProviderOcrConfig;
236
283
  stt?: ProviderSttConfig;
284
+ resolver?: ProviderResolverConfig;
237
285
  browser?: { engine: BrowserEngine };
238
286
  auth?: AuthConfig;
239
287
  reviewed?: ProviderReviewed;
@@ -270,35 +318,24 @@ export interface ProviderConfig<
270
318
  }
271
319
 
272
320
  /** Define one provider operation with schema-driven handler inference. */
273
- export function defineOperation<
274
- TInput extends SchemaLike,
275
- TOutput extends SchemaLike,
276
- >(
321
+ export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
277
322
  operation: OperationConfig<TInput, TOutput>,
278
323
  ): OperationDefinition<TInput, TOutput> {
279
324
  return operation;
280
325
  }
281
326
 
282
327
  /** Define a non-JSON provider operation with explicit transport metadata. */
283
- export function defineStreamOperation<
284
- TInput extends SchemaLike,
285
- TOutput extends SchemaLike,
286
- >(
328
+ export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
287
329
  operation: StreamOperationConfig<TInput, TOutput>,
288
330
  ): OperationDefinition<TInput, TOutput> {
289
331
  return operation;
290
332
  }
291
333
 
292
- function assertObjectConfig(
293
- value: unknown,
294
- ): asserts value is Record<string, unknown> {
334
+ function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
295
335
  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
- );
336
+ throw new ProviderError("defineProvider config must be an object. Offending field: config", {
337
+ fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
338
+ });
302
339
  }
303
340
  }
304
341
  function assertRequiredField(
@@ -328,6 +365,111 @@ function assertLiteralField<TValue extends string>(
328
365
  );
329
366
  }
330
367
  }
368
+
369
+ function validateProxiedOAuthParams(
370
+ value: unknown,
371
+ field: "authorizeParams" | "tokenParams",
372
+ providerId: string,
373
+ ): void {
374
+ if (value === undefined) return;
375
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
376
+ throw new ValidationError(
377
+ `Provider "${providerId}" auth.proxied.${field} must be an object of string values.`,
378
+ );
379
+ }
380
+ for (const [key, paramValue] of Object.entries(value)) {
381
+ if (!key.trim() || typeof paramValue !== "string") {
382
+ throw new ValidationError(
383
+ `Provider "${providerId}" auth.proxied.${field} must contain non-empty keys and string values.`,
384
+ );
385
+ }
386
+ if (
387
+ field === "authorizeParams" &&
388
+ PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())
389
+ ) {
390
+ throw new ValidationError(
391
+ `Provider "${providerId}" auth.proxied.authorizeParams cannot override reserved parameter "${key}".`,
392
+ );
393
+ }
394
+ }
395
+ }
396
+
397
+ function validateProxiedOAuthAuth(auth: Record<string, unknown>, providerId: string): void {
398
+ const proxied = auth.proxied;
399
+ if (auth.mode !== "oauth2_proxied") {
400
+ if (proxied !== undefined) {
401
+ throw new ValidationError(
402
+ `Provider "${providerId}" auth.proxied is only valid when auth.mode is "oauth2_proxied".`,
403
+ );
404
+ }
405
+ return;
406
+ }
407
+ if (!proxied || typeof proxied !== "object" || Array.isArray(proxied)) {
408
+ throw new ValidationError(
409
+ `Provider "${providerId}" with auth.mode "oauth2_proxied" must declare auth.proxied.`,
410
+ );
411
+ }
412
+ const config = Object.fromEntries(Object.entries(proxied));
413
+ for (const key of Object.keys(config)) {
414
+ if (!PROXIED_OAUTH_ALLOWED_FIELDS.has(key)) {
415
+ throw new ValidationError(
416
+ `Provider "${providerId}" has unknown auth.proxied field "${key}".`,
417
+ );
418
+ }
419
+ }
420
+ for (const field of PROXIED_OAUTH_REQUIRED_FIELDS) {
421
+ if (typeof config[field] !== "string" || !config[field].trim()) {
422
+ throw new ValidationError(
423
+ `Provider "${providerId}" auth.proxied.${field} must be a non-empty string.`,
424
+ );
425
+ }
426
+ }
427
+ for (const field of ["authorizeUrl", "tokenUrl"] as const) {
428
+ try {
429
+ const endpoint = new URL(String(config[field]));
430
+ if (
431
+ endpoint.protocol !== "https:" ||
432
+ endpoint.username ||
433
+ endpoint.password ||
434
+ endpoint.hash
435
+ ) {
436
+ throw new Error("invalid endpoint");
437
+ }
438
+ } catch {
439
+ throw new ValidationError(
440
+ `Provider "${providerId}" auth.proxied.${field} must be an absolute HTTPS URL without credentials or a fragment.`,
441
+ );
442
+ }
443
+ }
444
+ const customScheme = String(config.customScheme);
445
+ if (
446
+ !CUSTOM_SCHEME_REGEX.test(customScheme) ||
447
+ customScheme.toLowerCase().startsWith("http://") ||
448
+ customScheme.toLowerCase().startsWith("https://")
449
+ ) {
450
+ throw new ValidationError(
451
+ `Provider "${providerId}" auth.proxied.customScheme must be a non-HTTP custom-scheme URL prefix.`,
452
+ );
453
+ }
454
+ if (!PROXIED_OAUTH_PROFILE_REGEX.test(String(config.rewriteProfile))) {
455
+ throw new ValidationError(
456
+ `Provider "${providerId}" auth.proxied.rewriteProfile must be a kebab-case profile name.`,
457
+ );
458
+ }
459
+ if (!PROXIED_OAUTH_ENV_KEY_REGEX.test(String(config.clientIdEnvKey))) {
460
+ throw new ValidationError(
461
+ `Provider "${providerId}" auth.proxied.clientIdEnvKey must be an APIFuse-style uppercase environment key.`,
462
+ );
463
+ }
464
+ if (config.pkce !== undefined && config.pkce !== "S256" && config.pkce !== "none") {
465
+ throw new ValidationError(
466
+ `Provider "${providerId}" auth.proxied.pkce must be "S256" or "none".`,
467
+ );
468
+ }
469
+ validateProxiedOAuthParams(config.authorizeParams, "authorizeParams", providerId);
470
+ validateProxiedOAuthParams(config.tokenParams, "tokenParams", providerId);
471
+ }
472
+
331
473
  function validateProviderShape(config: unknown): void {
332
474
  assertObjectConfig(config);
333
475
  assertRequiredField(config, "id");
@@ -336,25 +478,13 @@ function validateProviderShape(config: unknown): void {
336
478
  assertRequiredField(config, "meta", String(config.id));
337
479
  assertRequiredField(config, "operations", String(config.id));
338
480
  if (typeof config.runtime === "string")
339
- assertLiteralField(
340
- config.runtime,
341
- "runtime",
342
- VALID_RUNTIMES,
343
- String(config.id),
344
- );
481
+ assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
345
482
  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
- );
483
+ if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
484
+ assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
485
+ if (auth && typeof auth === "object" && !Array.isArray(auth)) {
486
+ validateProxiedOAuthAuth(Object.fromEntries(Object.entries(auth)), String(config.id));
487
+ }
358
488
  if (auth && typeof auth === "object" && "exchange" in auth) {
359
489
  throw new ProviderError(
360
490
  `Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
@@ -390,9 +520,7 @@ function validateProviderShape(config: unknown): void {
390
520
  },
391
521
  );
392
522
  }
393
- const accessRecord: Record<string, unknown> = Object.fromEntries(
394
- Object.entries(access),
395
- );
523
+ const accessRecord: Record<string, unknown> = Object.fromEntries(Object.entries(access));
396
524
  for (const key of Object.keys(accessRecord)) {
397
525
  if (key !== "visibility") {
398
526
  throw new ValidationError(`Unknown field "${key}" on access.`, {
@@ -437,31 +565,26 @@ function validateProviderProxy(config: {
437
565
  },
438
566
  );
439
567
  }
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
- );
568
+ rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
569
+ assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
451
570
  if (proxy.provider !== undefined) {
452
- assertLiteralField(
453
- proxy.provider,
454
- "proxy.provider",
455
- VALID_PROVIDER_PROXY_PROVIDERS,
456
- config.id,
457
- );
571
+ assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
572
+ }
573
+ if (proxy.providers !== undefined) {
574
+ if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
575
+ throw new ValidationError(
576
+ `Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`,
577
+ {
578
+ fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
579
+ },
580
+ );
581
+ }
582
+ for (const vendor of proxy.providers) {
583
+ assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
584
+ }
458
585
  }
459
586
  if (proxy.geo !== undefined) {
460
- if (
461
- !proxy.geo ||
462
- typeof proxy.geo !== "object" ||
463
- Array.isArray(proxy.geo)
464
- ) {
587
+ if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
465
588
  throw new ValidationError(
466
589
  `Provider "${config.id}" has invalid proxy.geo: must be an object.`,
467
590
  {
@@ -469,11 +592,7 @@ function validateProviderProxy(config: {
469
592
  },
470
593
  );
471
594
  }
472
- rejectUnknownFields(
473
- proxy.geo,
474
- new Set(["country", "subdivision", "city"]),
475
- "proxy.geo",
476
- );
595
+ rejectUnknownFields(proxy.geo, new Set(["country", "subdivision", "city"]), "proxy.geo");
477
596
  if (proxy.geo.country !== undefined) {
478
597
  assertIsoCountry(proxy.geo.country, "proxy.geo.country");
479
598
  }
@@ -487,11 +606,7 @@ function validateProviderProxy(config: {
487
606
  }
488
607
  }
489
608
  if (proxy.session !== undefined) {
490
- if (
491
- !proxy.session ||
492
- typeof proxy.session !== "object" ||
493
- Array.isArray(proxy.session)
494
- ) {
609
+ if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
495
610
  throw new ValidationError(
496
611
  `Provider "${config.id}" has invalid proxy.session: must be an object.`,
497
612
  {
@@ -501,7 +616,7 @@ function validateProviderProxy(config: {
501
616
  }
502
617
  rejectUnknownFields(
503
618
  proxy.session,
504
- new Set(["affinity", "lifetimeMinutes", "poolSize"]),
619
+ new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]),
505
620
  "proxy.session",
506
621
  );
507
622
  if (proxy.session.affinity !== undefined) {
@@ -513,56 +628,195 @@ function validateProviderProxy(config: {
513
628
  );
514
629
  }
515
630
  const lifetime = proxy.session.lifetimeMinutes;
516
- if (
517
- lifetime !== undefined &&
518
- (!Number.isFinite(lifetime) || lifetime <= 0)
519
- ) {
631
+ if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
520
632
  throw new ValidationError(
521
633
  `Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`,
522
634
  );
523
635
  }
524
636
  const poolSize = proxy.session.poolSize;
637
+ if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
638
+ throw new ValidationError(
639
+ `Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
640
+ );
641
+ }
642
+ const drainLeadSeconds = proxy.session.drainLeadSeconds;
525
643
  if (
526
- poolSize !== undefined &&
527
- (!Number.isInteger(poolSize) || poolSize <= 0)
644
+ drainLeadSeconds !== undefined &&
645
+ (!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)
528
646
  ) {
529
647
  throw new ValidationError(
530
- `Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
648
+ `Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`,
649
+ {
650
+ fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
651
+ },
531
652
  );
532
653
  }
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) {
654
+ // A drain lead longer than the sticky lifetime would fire the expiring
655
+ // event before the session is even established, so the provider would
656
+ // never get a usable window. Reject the contradiction at build time.
657
+ if (
658
+ drainLeadSeconds !== undefined &&
659
+ lifetime !== undefined &&
660
+ Number.isFinite(lifetime) &&
661
+ drainLeadSeconds >= lifetime * 60
662
+ ) {
540
663
  throw new ValidationError(
541
- `Provider "${config.id}" requires Smartproxy egress but does not declare ${SMARTPROXY_APP_KEY_SECRET}.`,
664
+ `Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`,
542
665
  {
543
- fix: `Add secrets: [{ name: "${SMARTPROXY_APP_KEY_SECRET}", required: true }] to the provider.`,
666
+ fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
544
667
  },
545
668
  );
546
669
  }
547
670
  }
671
+ // Every credentialed vendor in a required-mode chain must declare its
672
+ // provider secret(s) so a missing credential fails at build/validation time,
673
+ // not during a live outage. This covers the fallback legs too (not just the
674
+ // first vendor): a declared-but-uncredentialed nodemaven fallback would leave
675
+ // the chain silently down to a single vendor, reintroducing the SPOF the chain
676
+ // removes.
677
+ const vendorChain =
678
+ proxy.providers && proxy.providers.length > 0
679
+ ? proxy.providers
680
+ : proxy.provider
681
+ ? [proxy.provider]
682
+ : [];
683
+ if (proxy.mode === "required") {
684
+ for (const vendor of vendorChain) {
685
+ const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
686
+ if (!requiredSecrets) continue;
687
+ for (const secretName of requiredSecrets) {
688
+ // Match the canonical runtime gate (assertRequiredSecretsPresent /
689
+ // listMissingRequiredSecrets), which enforces only `required === true`
690
+ // declarations. A declaration that omits `required` (defaulting to
691
+ // optional) is skipped at runtime, so accepting it here would pass
692
+ // validation while leaving the credential unenforced until proxy
693
+ // resolution during a live request — the fail-open gap this check exists
694
+ // to close.
695
+ const declared = config.secrets?.some(
696
+ (secret) => secret.name === secretName && secret.required === true,
697
+ );
698
+ if (!declared) {
699
+ throw new ValidationError(
700
+ `Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`,
701
+ {
702
+ fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
703
+ },
704
+ );
705
+ }
706
+ }
707
+ }
708
+ }
709
+ // `decodo`/`custom` are deprecated vendor values (string-union members, so the
710
+ // @deprecated symbol gate can't catch them — warn at validation time instead).
711
+ const deprecatedVendors = vendorChain.filter(
712
+ (vendor) => vendor === "decodo" || vendor === "custom",
713
+ );
714
+ if (
715
+ proxy.mode === "required" &&
716
+ vendorChain.length > 0 &&
717
+ deprecatedVendors.length === vendorChain.length
718
+ ) {
719
+ throw new ValidationError(
720
+ `Provider "${config.id}" requires proxy egress but declares only deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}.`,
721
+ {
722
+ fix: `Use proxy.provider or proxy.providers with "smartproxy" and/or "nodemaven".`,
723
+ },
724
+ );
725
+ }
726
+ if (deprecatedVendors.length > 0) {
727
+ console.warn(
728
+ `[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven".`,
729
+ );
730
+ }
548
731
  }
549
732
 
550
- function validateProviderStt(config: {
551
- id: string;
552
- stt?: ProviderSttConfig;
553
- }): void {
733
+ function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): void {
554
734
  const stt = config.stt;
555
735
  if (stt === undefined) return;
556
736
  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
- );
737
+ throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
738
+ fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
739
+ });
561
740
  }
562
741
  rejectUnknownFields(stt, new Set(["mode"]), "stt");
563
742
  assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
564
743
  }
565
744
 
745
+ function validateProviderOcr(config: { id: string; ocr?: ProviderOcrConfig }): void {
746
+ const ocr = config.ocr;
747
+ if (ocr === undefined) return;
748
+ if (!ocr || typeof ocr !== "object" || Array.isArray(ocr)) {
749
+ throw new ValidationError(`Provider "${config.id}" has invalid ocr: must be an object.`, {
750
+ fix: `Use ocr: { mode: "required" } or ocr: { mode: "optional" }.`,
751
+ });
752
+ }
753
+ rejectUnknownFields(ocr, new Set(["mode"]), "ocr");
754
+ assertLiteralField(ocr.mode, "ocr.mode", VALID_PROVIDER_OCR_MODES, config.id);
755
+ }
756
+
757
+ function validateProviderResolver(config: { id: string; resolver?: ProviderResolverConfig }): void {
758
+ const resolver = config.resolver;
759
+ if (resolver === undefined) return;
760
+ if (!resolver || typeof resolver !== "object" || Array.isArray(resolver)) {
761
+ throw new ValidationError(`Provider "${config.id}" has invalid resolver: must be an object.`, {
762
+ fix: `Set resolver for provider "${config.id}" to { vendors: ["2captcha"], kinds: ["turnstile"] }.`,
763
+ });
764
+ }
765
+ rejectUnknownFields(
766
+ resolver,
767
+ new Set(["vendors", "kinds", "clientProfile"]),
768
+ "resolver",
769
+ config.id,
770
+ );
771
+ validateResolverLiteralArray(
772
+ resolver.vendors,
773
+ "resolver.vendors",
774
+ VALID_PROVIDER_RESOLVER_VENDORS,
775
+ config.id,
776
+ );
777
+ validateResolverLiteralArray(
778
+ resolver.kinds,
779
+ "resolver.kinds",
780
+ VALID_PROVIDER_CHALLENGE_KINDS,
781
+ config.id,
782
+ );
783
+ if (
784
+ resolver.clientProfile !== undefined &&
785
+ (typeof resolver.clientProfile !== "string" || !resolver.clientProfile.trim())
786
+ ) {
787
+ throw new ValidationError(
788
+ `Provider "${config.id}" has invalid resolver.clientProfile: must be a non-empty string.`,
789
+ {
790
+ fix: `Set resolver.clientProfile for provider "${config.id}" to a transport-owned profile name.`,
791
+ },
792
+ );
793
+ }
794
+ }
795
+
796
+ function validateResolverLiteralArray<TValue extends string>(
797
+ value: readonly TValue[],
798
+ field: string,
799
+ validValues: readonly TValue[],
800
+ providerId: string,
801
+ ): void {
802
+ if (!Array.isArray(value)) {
803
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an array.`, {
804
+ fix: `Set ${field} for provider "${providerId}" to an array containing only: ${validValues.join(", ")}.`,
805
+ });
806
+ }
807
+ for (const [index, item] of value.entries()) {
808
+ if (typeof item === "string" && validValues.some((validValue) => validValue === item)) {
809
+ continue;
810
+ }
811
+ throw new ValidationError(
812
+ `Provider "${providerId}" has invalid ${field}[${index}]: ${JSON.stringify(item)}. Expected one of: ${validValues.join(", ")}`,
813
+ {
814
+ fix: `Set ${field}[${index}] for provider "${providerId}" to one of ${validValues.map((validValue) => `"${validValue}"`).join(", ")}.`,
815
+ },
816
+ );
817
+ }
818
+ }
819
+
566
820
  function validateOperationIds(
567
821
  providerId: string,
568
822
  operations: Record<string, ProviderOperation>,
@@ -584,16 +838,9 @@ function validateOperationIds(
584
838
  );
585
839
  }
586
840
  }
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;
841
+ const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
842
+ const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
843
+ const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"] as const;
597
844
 
598
845
  function assertNonEmptyString(
599
846
  value: unknown,
@@ -624,10 +871,7 @@ function validateToolRouterMetadata(
624
871
  },
625
872
  );
626
873
  }
627
- if (
628
- toolRouter.name !== undefined &&
629
- !MCP_TOOL_NAME_REGEX.test(toolRouter.name)
630
- ) {
874
+ if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
631
875
  throw new ValidationError(
632
876
  `Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`,
633
877
  {
@@ -699,10 +943,7 @@ function validateOperationContracts(
699
943
  providerId,
700
944
  );
701
945
  }
702
- if (
703
- contract.lifecycle === "deprecated" ||
704
- contract.lifecycle === "removed"
705
- ) {
946
+ if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
706
947
  if (!contract.deprecation || typeof contract.deprecation !== "object") {
707
948
  throw new ValidationError(
708
949
  `Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`,
@@ -750,10 +991,7 @@ function validateOperationAnnotations(
750
991
  fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
751
992
  },
752
993
  );
753
- if (
754
- timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
755
- timeoutMs > OPERATION_TIMEOUT_MS_MAX
756
- )
994
+ if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
757
995
  throw new ValidationError(
758
996
  `Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`,
759
997
  {
@@ -821,6 +1059,36 @@ function validateOperationObservability(
821
1059
  }
822
1060
  }
823
1061
 
1062
+ function validateOperationErrorCodes(
1063
+ providerId: string,
1064
+ operations: Record<string, ProviderOperation>,
1065
+ ): void {
1066
+ for (const [operationName, operation] of Object.entries(operations)) {
1067
+ for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
1068
+ if (
1069
+ errorCode.status !== undefined &&
1070
+ !VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)
1071
+ ) {
1072
+ const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
1073
+ throw new ValidationError(
1074
+ `Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`,
1075
+ {
1076
+ fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
1077
+ },
1078
+ );
1079
+ }
1080
+ if (
1081
+ errorCode.status !== undefined &&
1082
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)
1083
+ ) {
1084
+ console.warn(
1085
+ `[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.`,
1086
+ );
1087
+ }
1088
+ }
1089
+ }
1090
+ }
1091
+
824
1092
  const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
825
1093
  const SSE_TRANSPORT_FIELDS = new Set([
826
1094
  "kind",
@@ -951,12 +1219,7 @@ function validateOperationTransports(
951
1219
  },
952
1220
  );
953
1221
  }
954
- assertLiteralField(
955
- kind,
956
- `${fieldPath}.kind`,
957
- VALID_OPERATION_TRANSPORT_KINDS,
958
- providerId,
959
- );
1222
+ assertLiteralField(kind, `${fieldPath}.kind`, VALID_OPERATION_TRANSPORT_KINDS, providerId);
960
1223
 
961
1224
  switch (kind) {
962
1225
  case "json":
@@ -988,16 +1251,9 @@ function validateOperationTransports(
988
1251
  STREAM_MAX_DURATION_MS_MAX,
989
1252
  "max duration",
990
1253
  );
991
- assertPositiveBytes(
992
- Reflect.get(transport, "maxEventBytes"),
993
- `${fieldPath}.maxEventBytes`,
994
- );
1254
+ assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
995
1255
  const resumable = Reflect.get(transport, "resumable");
996
- if (
997
- resumable !== undefined &&
998
- resumable !== false &&
999
- resumable !== "last-event-id"
1000
- ) {
1256
+ if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
1001
1257
  throw new ValidationError(
1002
1258
  `Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`,
1003
1259
  {
@@ -1017,12 +1273,7 @@ function validateOperationTransports(
1017
1273
  rejectUnknownFields(transport, HTTP_STREAM_TRANSPORT_FIELDS, fieldPath);
1018
1274
  const contentType = Reflect.get(transport, "contentType");
1019
1275
  if (contentType !== undefined) {
1020
- assertNonEmptyString(
1021
- contentType,
1022
- `${fieldPath}.contentType`,
1023
- providerId,
1024
- operationName,
1025
- );
1276
+ assertNonEmptyString(contentType, `${fieldPath}.contentType`, providerId, operationName);
1026
1277
  }
1027
1278
  assertStreamMs(
1028
1279
  Reflect.get(transport, "idleTimeoutMs"),
@@ -1038,10 +1289,7 @@ function validateOperationTransports(
1038
1289
  STREAM_MAX_DURATION_MS_MAX,
1039
1290
  "max duration",
1040
1291
  );
1041
- assertPositiveBytes(
1042
- Reflect.get(transport, "maxChunkBytes"),
1043
- `${fieldPath}.maxChunkBytes`,
1044
- );
1292
+ assertPositiveBytes(Reflect.get(transport, "maxChunkBytes"), `${fieldPath}.maxChunkBytes`);
1045
1293
  break;
1046
1294
  }
1047
1295
  case "websocket": {
@@ -1066,10 +1314,7 @@ function validateOperationTransports(
1066
1314
  );
1067
1315
  }
1068
1316
  for (const subprotocol of subprotocols) {
1069
- if (
1070
- typeof subprotocol !== "string" ||
1071
- !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)
1072
- ) {
1317
+ if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
1073
1318
  throw new ValidationError(
1074
1319
  `Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`,
1075
1320
  {
@@ -1093,10 +1338,7 @@ function validateOperationTransports(
1093
1338
  STREAM_MAX_DURATION_MS_MAX,
1094
1339
  "max duration",
1095
1340
  );
1096
- assertPositiveBytes(
1097
- Reflect.get(transport, "maxFrameBytes"),
1098
- `${fieldPath}.maxFrameBytes`,
1099
- );
1341
+ assertPositiveBytes(Reflect.get(transport, "maxFrameBytes"), `${fieldPath}.maxFrameBytes`);
1100
1342
  break;
1101
1343
  }
1102
1344
  }
@@ -1159,10 +1401,7 @@ function levenshtein(a: string, b: string): number {
1159
1401
  return prev[n] ?? 0;
1160
1402
  }
1161
1403
 
1162
- function suggestField(
1163
- unknown: string,
1164
- candidates: ReadonlySet<string>,
1165
- ): string | undefined {
1404
+ function suggestField(unknown: string, candidates: ReadonlySet<string>): string | undefined {
1166
1405
  let best: string | undefined;
1167
1406
  let bestDist = 3;
1168
1407
  for (const candidate of candidates) {
@@ -1179,6 +1418,7 @@ function rejectUnknownFields(
1179
1418
  value: object,
1180
1419
  allowed: ReadonlySet<string>,
1181
1420
  fieldPath: string,
1421
+ providerId?: string,
1182
1422
  ): void {
1183
1423
  for (const key of Object.keys(value)) {
1184
1424
  if (allowed.has(key)) continue;
@@ -1187,7 +1427,11 @@ function rejectUnknownFields(
1187
1427
  hint
1188
1428
  ? `Unknown field "${key}" on ${fieldPath}. Did you mean "${hint}"?`
1189
1429
  : `Unknown field "${key}" on ${fieldPath}.`,
1190
- { fix: `Remove ${fieldPath}.${key} or rename it.` },
1430
+ {
1431
+ fix: providerId
1432
+ ? `Remove ${fieldPath}.${key} from provider "${providerId}" or rename it.`
1433
+ : `Remove ${fieldPath}.${key} or rename it.`,
1434
+ },
1191
1435
  );
1192
1436
  }
1193
1437
  }
@@ -1218,23 +1462,12 @@ function validateProviderHealthMonitor(
1218
1462
  field: "healthMonitor" | "healthProbe" = "healthMonitor",
1219
1463
  ): void {
1220
1464
  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
- );
1465
+ if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
1466
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
1467
+ fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
1468
+ });
1232
1469
  const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
1233
- rejectUnknownFields(
1234
- healthMonitorRecord,
1235
- PROVIDER_HEALTH_MONITOR_FIELDS,
1236
- field,
1237
- );
1470
+ rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
1238
1471
  if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
1239
1472
  assertBoundedIntegerMs(
1240
1473
  healthMonitorRecord.defaultProbeTimeoutMs,
@@ -1302,11 +1535,7 @@ function validateProviderHealthMonitor(
1302
1535
 
1303
1536
  const probeOverrides = healthMonitorRecord.probeOverrides;
1304
1537
  if (probeOverrides !== undefined) {
1305
- if (
1306
- !probeOverrides ||
1307
- typeof probeOverrides !== "object" ||
1308
- Array.isArray(probeOverrides)
1309
- )
1538
+ if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
1310
1539
  throw new ValidationError(
1311
1540
  `Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`,
1312
1541
  );
@@ -1372,14 +1601,8 @@ function validateHealthCheckCase(
1372
1601
  ): void {
1373
1602
  const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
1374
1603
  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
- );
1604
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1605
+ rejectUnknownFields(caseValue as Record<string, unknown>, HEALTH_CHECK_CASE_FIELDS, fieldPath);
1383
1606
  const c = caseValue as HealthCheckCase;
1384
1607
  if (typeof c.name !== "string" || c.name.length === 0)
1385
1608
  throw new ValidationError(
@@ -1407,15 +1630,11 @@ function validateHealthCheckCase(
1407
1630
  `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
1631
  );
1409
1632
  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
- );
1633
+ assertBoundedIntegerMs(c.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
1634
+ min: HEALTH_CHECK_TIMEOUT_MS_MIN,
1635
+ max: HEALTH_CHECK_TIMEOUT_MS_MAX,
1636
+ label: "timeout",
1637
+ });
1419
1638
  }
1420
1639
  if (
1421
1640
  c.expectedStatus !== undefined &&
@@ -1431,21 +1650,11 @@ function validateHealthCheckCase(
1431
1650
  );
1432
1651
  }
1433
1652
 
1434
- function validateHealthCheckSuite(
1435
- providerId: string,
1436
- operationName: string,
1437
- suite: unknown,
1438
- ): void {
1653
+ function validateHealthCheckSuite(providerId: string, operationName: string, suite: unknown): void {
1439
1654
  const fieldPath = `operations.${operationName}.healthCheck`;
1440
1655
  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
- );
1656
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1657
+ rejectUnknownFields(suite as Record<string, unknown>, HEALTH_CHECK_SUITE_FIELDS, fieldPath);
1449
1658
  const s = suite as HealthCheckSuite;
1450
1659
  if (!isPositiveMsDurationString(s.interval))
1451
1660
  throw new ValidationError(
@@ -1455,11 +1664,7 @@ function validateHealthCheckSuite(
1455
1664
  },
1456
1665
  );
1457
1666
  if (s.schedule !== undefined) {
1458
- if (
1459
- !s.schedule ||
1460
- typeof s.schedule !== "object" ||
1461
- Array.isArray(s.schedule)
1462
- ) {
1667
+ if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
1463
1668
  throw new ValidationError(
1464
1669
  `Provider "${providerId}" ${fieldPath}.schedule must be an object.`,
1465
1670
  );
@@ -1469,11 +1674,7 @@ function validateHealthCheckSuite(
1469
1674
  `Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`,
1470
1675
  );
1471
1676
  }
1472
- rejectUnknownFields(
1473
- s.schedule,
1474
- new Set(["randomize"]),
1475
- `${fieldPath}.schedule`,
1476
- );
1677
+ rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
1477
1678
  const randomize = Reflect.get(s.schedule, "randomize");
1478
1679
  if (randomize !== undefined) {
1479
1680
  validateScheduleRandomization(
@@ -1484,15 +1685,11 @@ function validateHealthCheckSuite(
1484
1685
  }
1485
1686
  }
1486
1687
  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
- );
1688
+ assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
1689
+ min: HEALTH_CHECK_TIMEOUT_MS_MIN,
1690
+ max: HEALTH_CHECK_TIMEOUT_MS_MAX,
1691
+ label: "timeout",
1692
+ });
1496
1693
  }
1497
1694
  if (s.degradedThresholdMs !== undefined) {
1498
1695
  assertBoundedIntegerMs(
@@ -1505,10 +1702,7 @@ function validateHealthCheckSuite(
1505
1702
  },
1506
1703
  );
1507
1704
  }
1508
- if (
1509
- s.requiresConnection !== undefined &&
1510
- typeof s.requiresConnection !== "boolean"
1511
- )
1705
+ if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
1512
1706
  throw new ValidationError(
1513
1707
  `Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`,
1514
1708
  );
@@ -1540,14 +1734,8 @@ function validateHealthCheckUnsupported(
1540
1734
  unsupported: unknown,
1541
1735
  ): void {
1542
1736
  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
- );
1737
+ if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
1738
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1551
1739
  rejectUnknownFields(
1552
1740
  unsupported as Record<string, unknown>,
1553
1741
  HEALTH_CHECK_UNSUPPORTED_FIELDS,
@@ -1581,12 +1769,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
1581
1769
  "steps",
1582
1770
  "run",
1583
1771
  ]);
1584
- const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
1585
- "kind",
1586
- "interval",
1587
- "jitter",
1588
- "randomize",
1589
- ]);
1772
+ const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
1590
1773
  const HEALTH_JOURNEY_STEP_FIELDS = new Set([
1591
1774
  "id",
1592
1775
  "description",
@@ -1605,10 +1788,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
1605
1788
  "minManualInterval",
1606
1789
  "publicRationale",
1607
1790
  ]);
1608
- const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
1609
- "enabled",
1610
- "reason",
1611
- ]);
1791
+ const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
1612
1792
  const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
1613
1793
  "enabled",
1614
1794
  "requiresAcknowledgement",
@@ -1628,32 +1808,18 @@ function validateHealthJourneyManualTrigger(
1628
1808
  manualTrigger: unknown,
1629
1809
  ): void {
1630
1810
  const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
1631
- if (
1632
- !manualTrigger ||
1633
- typeof manualTrigger !== "object" ||
1634
- Array.isArray(manualTrigger)
1635
- ) {
1811
+ if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
1636
1812
  throw new ValidationError(
1637
1813
  `Provider "${providerId}" ${fieldPath} must be an object when present.`,
1638
1814
  );
1639
1815
  }
1640
- rejectUnknownFields(
1641
- manualTrigger,
1642
- HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS,
1643
- fieldPath,
1644
- );
1816
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
1645
1817
  const enabled = Reflect.get(manualTrigger, "enabled");
1646
1818
  if (typeof enabled !== "boolean") {
1647
- throw new ValidationError(
1648
- `Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`,
1649
- );
1819
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`);
1650
1820
  }
1651
1821
  if (enabled === false) {
1652
- rejectUnknownFields(
1653
- manualTrigger,
1654
- HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS,
1655
- fieldPath,
1656
- );
1822
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS, fieldPath);
1657
1823
  if (
1658
1824
  Reflect.get(manualTrigger, "reason") !== undefined &&
1659
1825
  (typeof Reflect.get(manualTrigger, "reason") !== "string" ||
@@ -1665,25 +1831,15 @@ function validateHealthJourneyManualTrigger(
1665
1831
  }
1666
1832
  return;
1667
1833
  }
1668
- rejectUnknownFields(
1669
- manualTrigger,
1670
- HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS,
1671
- fieldPath,
1672
- );
1673
- const requiresAcknowledgement = Reflect.get(
1674
- manualTrigger,
1675
- "requiresAcknowledgement",
1676
- );
1834
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS, fieldPath);
1835
+ const requiresAcknowledgement = Reflect.get(manualTrigger, "requiresAcknowledgement");
1677
1836
  if (typeof requiresAcknowledgement !== "boolean") {
1678
1837
  throw new ValidationError(
1679
1838
  `Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`,
1680
1839
  );
1681
1840
  }
1682
1841
  const risk = Reflect.get(manualTrigger, "risk");
1683
- if (
1684
- typeof risk !== "string" ||
1685
- !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)
1686
- ) {
1842
+ if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
1687
1843
  throw new ValidationError(
1688
1844
  `Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`,
1689
1845
  );
@@ -1694,10 +1850,7 @@ function validateHealthJourneyManualTrigger(
1694
1850
  );
1695
1851
  }
1696
1852
  const minManualInterval = Reflect.get(manualTrigger, "minManualInterval");
1697
- assertIsoDuration(
1698
- minManualInterval,
1699
- `Provider "${providerId}" ${fieldPath}.minManualInterval`,
1700
- );
1853
+ assertIsoDuration(minManualInterval, `Provider "${providerId}" ${fieldPath}.minManualInterval`);
1701
1854
  if (isoDurationMs(minManualInterval) <= 0) {
1702
1855
  throw new ValidationError(
1703
1856
  `Provider "${providerId}" ${fieldPath}.minManualInterval must be a positive duration.`,
@@ -1728,18 +1881,14 @@ const SMS_ORIGIN_FIELDS_BY_KIND: Record<string, ReadonlySet<string>> = {
1728
1881
  e164: new Set(["kind", "value", "display"]),
1729
1882
  nationalServiceCode: new Set(["kind", "country", "value", "display"]),
1730
1883
  };
1731
- const DURATION_RE =
1732
- /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
1884
+ const DURATION_RE = /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
1733
1885
  const E164_RE = /^\+[1-9]\d{1,14}$/;
1734
1886
  const ISO_COUNTRY_RE = /^[A-Z]{2}$/;
1735
1887
  const NATIONAL_SERVICE_CODE_RE = /^[0-9]{2,15}$/;
1736
1888
  const BCP47_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
1737
1889
  const JOURNEY_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1738
1890
 
1739
- function assertIsoDuration(
1740
- value: unknown,
1741
- fieldPath: string,
1742
- ): asserts value is string {
1891
+ function assertIsoDuration(value: unknown, fieldPath: string): asserts value is string {
1743
1892
  if (typeof value !== "string" || !DURATION_RE.test(value)) {
1744
1893
  throw new ValidationError(
1745
1894
  `${fieldPath} must be an ISO 8601 duration for example PT8H or PT2M30S.`,
@@ -1754,15 +1903,10 @@ function isoDurationMs(value: string): number {
1754
1903
  const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
1755
1904
  const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
1756
1905
  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
- );
1906
+ return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
1760
1907
  }
1761
1908
 
1762
- function scheduleRandomizationMs(
1763
- randomize: unknown,
1764
- fieldPath: string,
1765
- ): number {
1909
+ function scheduleRandomizationMs(randomize: unknown, fieldPath: string): number {
1766
1910
  const mode = Reflect.get(randomize as object, "mode");
1767
1911
  switch (mode) {
1768
1912
  case "centered": {
@@ -1776,9 +1920,7 @@ function scheduleRandomizationMs(
1776
1920
  return isoDurationMs(maxDelay);
1777
1921
  }
1778
1922
  default:
1779
- throw new ValidationError(
1780
- `${fieldPath}.mode must be "centered" or "delayed".`,
1781
- );
1923
+ throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
1782
1924
  }
1783
1925
  }
1784
1926
 
@@ -1792,25 +1934,18 @@ function validateScheduleRandomization(
1792
1934
  }
1793
1935
  const mode = Reflect.get(randomize, "mode");
1794
1936
  const allowedFields =
1795
- mode === "centered"
1796
- ? new Set(["mode", "maxOffset"])
1797
- : new Set(["mode", "maxDelay"]);
1937
+ mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
1798
1938
  rejectUnknownFields(randomize, allowedFields, fieldPath);
1799
1939
  const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
1800
1940
  if (offsetMs <= 0) {
1801
1941
  throw new ValidationError(`${fieldPath} duration must be positive.`);
1802
1942
  }
1803
1943
  if (offsetMs >= intervalMs) {
1804
- throw new ValidationError(
1805
- `${fieldPath} duration must be shorter than schedule interval.`,
1806
- );
1944
+ throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
1807
1945
  }
1808
1946
  }
1809
1947
 
1810
- function assertIsoCountry(
1811
- value: unknown,
1812
- fieldPath: string,
1813
- ): asserts value is string {
1948
+ function assertIsoCountry(value: unknown, fieldPath: string): asserts value is string {
1814
1949
  if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
1815
1950
  throw new ValidationError(
1816
1951
  `${fieldPath} must be an ISO 3166-1 alpha-2 country code for example KR.`,
@@ -1833,9 +1968,7 @@ function normalizeIntervalDuration(input: string): string {
1833
1968
  ? durationMs / 3_600_000
1834
1969
  : durationMs / 86_400_000;
1835
1970
  if (!Number.isInteger(amount) || amount <= 0) {
1836
- throw new ValidationError(
1837
- `Journey schedule interval must be a positive duration.`,
1838
- );
1971
+ throw new ValidationError(`Journey schedule interval must be a positive duration.`);
1839
1972
  }
1840
1973
  if (unit === "s") return `PT${amount}S`;
1841
1974
  if (unit === "m") return `PT${amount}M`;
@@ -1894,11 +2027,7 @@ function countCapturingGroups(pattern: RegExp): number {
1894
2027
  if (inCharacterClass || char !== "(") continue;
1895
2028
  const next = source[i + 1];
1896
2029
  if (next === "?" && source[i + 2] !== "<") continue;
1897
- if (
1898
- next === "?" &&
1899
- source[i + 2] === "<" &&
1900
- (source[i + 3] === "=" || source[i + 3] === "!")
1901
- )
2030
+ if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
1902
2031
  continue;
1903
2032
  count += 1;
1904
2033
  }
@@ -1917,9 +2046,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
1917
2046
  }
1918
2047
  const kind = Reflect.get(origin, "kind");
1919
2048
  if (kind !== "e164" && kind !== "nationalServiceCode") {
1920
- throw new ValidationError(
1921
- `${fieldPath}.kind must be "e164" or "nationalServiceCode".`,
1922
- );
2049
+ throw new ValidationError(`${fieldPath}.kind must be "e164" or "nationalServiceCode".`);
1923
2050
  }
1924
2051
  rejectUnknownFields(origin, SMS_ORIGIN_FIELDS_BY_KIND[kind], fieldPath);
1925
2052
  if (kind === "e164") {
@@ -1946,9 +2073,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
1946
2073
  Reflect.get(origin, "display") !== undefined &&
1947
2074
  typeof Reflect.get(origin, "display") !== "string"
1948
2075
  ) {
1949
- throw new ValidationError(
1950
- `${fieldPath}.display must be a string when present.`,
1951
- );
2076
+ throw new ValidationError(`${fieldPath}.display must be a string when present.`);
1952
2077
  }
1953
2078
  }
1954
2079
 
@@ -1962,9 +2087,7 @@ function validateSmsOtpMatcher(
1962
2087
  rejectUnknownFields(matcher, SMS_OTP_MATCHER_FIELDS, fieldPath);
1963
2088
  const matcherId = Reflect.get(matcher, "id");
1964
2089
  if (typeof matcherId !== "string" || !JOURNEY_ID_RE.test(matcherId)) {
1965
- throw new ValidationError(
1966
- `${fieldPath}.id must be a kebab-case identifier.`,
1967
- );
2090
+ throw new ValidationError(`${fieldPath}.id must be a kebab-case identifier.`);
1968
2091
  }
1969
2092
  assertIsoCountry(Reflect.get(matcher, "country"), `${fieldPath}.country`);
1970
2093
  if (
@@ -1972,24 +2095,18 @@ function validateSmsOtpMatcher(
1972
2095
  (typeof Reflect.get(matcher, "locale") !== "string" ||
1973
2096
  !BCP47_RE.test(Reflect.get(matcher, "locale")))
1974
2097
  ) {
1975
- throw new ValidationError(
1976
- `${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`,
1977
- );
2098
+ throw new ValidationError(`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`);
1978
2099
  }
1979
2100
  if (
1980
2101
  Reflect.get(matcher, "phoneNumber") !== undefined &&
1981
2102
  (typeof Reflect.get(matcher, "phoneNumber") !== "string" ||
1982
2103
  !E164_RE.test(Reflect.get(matcher, "phoneNumber")))
1983
2104
  ) {
1984
- throw new ValidationError(
1985
- `${fieldPath}.phoneNumber must be an ITU-T E.164 number.`,
1986
- );
2105
+ throw new ValidationError(`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`);
1987
2106
  }
1988
2107
  const origins = Reflect.get(matcher, "origins");
1989
2108
  if (!Array.isArray(origins) || origins.length === 0) {
1990
- throw new ValidationError(
1991
- `${fieldPath}.origins must be a non-empty array.`,
1992
- );
2109
+ throw new ValidationError(`${fieldPath}.origins must be a non-empty array.`);
1993
2110
  }
1994
2111
  for (const [index, origin] of origins.entries()) {
1995
2112
  validateSmsOrigin(origin, `${fieldPath}.origins[${index}]`);
@@ -2010,10 +2127,7 @@ function validateSmsOtpMatcher(
2010
2127
  );
2011
2128
  }
2012
2129
  const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
2013
- if (
2014
- countCapturingGroups(regex) !== 1 &&
2015
- Reflect.get(code, "capture") === undefined
2016
- ) {
2130
+ if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
2017
2131
  throw new ValidationError(
2018
2132
  `${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`,
2019
2133
  );
@@ -2023,20 +2137,12 @@ function validateSmsOtpMatcher(
2023
2137
  typeof Reflect.get(code, "capture") !== "string" &&
2024
2138
  typeof Reflect.get(code, "capture") !== "number"
2025
2139
  ) {
2026
- throw new ValidationError(
2027
- `${fieldPath}.code.capture must be a string or number when present.`,
2028
- );
2140
+ throw new ValidationError(`${fieldPath}.code.capture must be a string or number when present.`);
2029
2141
  }
2030
2142
  assertIsoDuration(Reflect.get(matcher, "maxAge"), `${fieldPath}.maxAge`);
2031
- assertIsoDuration(
2032
- Reflect.get(matcher, "waitTimeout"),
2033
- `${fieldPath}.waitTimeout`,
2034
- );
2143
+ assertIsoDuration(Reflect.get(matcher, "waitTimeout"), `${fieldPath}.waitTimeout`);
2035
2144
  if (Reflect.get(matcher, "clockSkew") !== undefined)
2036
- assertIsoDuration(
2037
- Reflect.get(matcher, "clockSkew"),
2038
- `${fieldPath}.clockSkew`,
2039
- );
2145
+ assertIsoDuration(Reflect.get(matcher, "clockSkew"), `${fieldPath}.clockSkew`);
2040
2146
  }
2041
2147
 
2042
2148
  export function defineSmsOtpMatcher(
@@ -2068,9 +2174,7 @@ export function defineSmsOtpMatcher(
2068
2174
  return matcher;
2069
2175
  }
2070
2176
 
2071
- export function defineHealthJourney(
2072
- config: HealthJourneyDefinition,
2073
- ): HealthJourneyDefinition {
2177
+ export function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition {
2074
2178
  return config;
2075
2179
  }
2076
2180
 
@@ -2081,22 +2185,15 @@ function validateHealthJourneySchedule(
2081
2185
  ): void {
2082
2186
  const fieldPath = `healthJourneys.${journeyId}.schedule`;
2083
2187
  if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) {
2084
- throw new ValidationError(
2085
- `Provider "${providerId}" ${fieldPath} must be an object.`,
2086
- );
2188
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
2087
2189
  }
2088
2190
  rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
2089
2191
  if (Reflect.get(schedule, "kind") !== "interval")
2090
- throw new ValidationError(
2091
- `Provider "${providerId}" ${fieldPath}.kind must be "interval".`,
2092
- );
2192
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
2093
2193
  const interval = Reflect.get(schedule, "interval");
2094
2194
  assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
2095
2195
  const randomize = Reflect.get(schedule, "randomize");
2096
- if (
2097
- Reflect.get(schedule, "jitter") !== undefined &&
2098
- randomize !== undefined
2099
- ) {
2196
+ if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
2100
2197
  throw new ValidationError(
2101
2198
  `Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`,
2102
2199
  );
@@ -2123,17 +2220,13 @@ function validateHealthJourneys(
2123
2220
  const covered = new Set<string>();
2124
2221
  if (healthJourneys === undefined) return covered;
2125
2222
  if (!Array.isArray(healthJourneys)) {
2126
- throw new ValidationError(
2127
- `Provider "${providerId}" healthJourneys must be an array.`,
2128
- );
2223
+ throw new ValidationError(`Provider "${providerId}" healthJourneys must be an array.`);
2129
2224
  }
2130
2225
  const journeyIds = new Set<string>();
2131
2226
  for (const [index, journey] of healthJourneys.entries()) {
2132
2227
  const prefix = `healthJourneys[${index}]`;
2133
2228
  if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
2134
- throw new ValidationError(
2135
- `Provider "${providerId}" ${prefix} must be an object.`,
2136
- );
2229
+ throw new ValidationError(`Provider "${providerId}" ${prefix} must be an object.`);
2137
2230
  }
2138
2231
  rejectUnknownFields(journey, HEALTH_JOURNEY_FIELDS, prefix);
2139
2232
  if (typeof journey.id !== "string" || !JOURNEY_ID_RE.test(journey.id)) {
@@ -2147,10 +2240,7 @@ function validateHealthJourneys(
2147
2240
  );
2148
2241
  journeyIds.add(journey.id);
2149
2242
  validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
2150
- if (
2151
- !Array.isArray(journey.coversOperations) ||
2152
- journey.coversOperations.length === 0
2153
- ) {
2243
+ if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
2154
2244
  throw new ValidationError(
2155
2245
  `Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`,
2156
2246
  );
@@ -2185,10 +2275,7 @@ function validateHealthJourneys(
2185
2275
  `Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers must be an array.`,
2186
2276
  );
2187
2277
  for (const [matcherIndex, matcher] of journey.smsMatchers.entries()) {
2188
- validateSmsOtpMatcher(
2189
- matcher,
2190
- `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`,
2191
- );
2278
+ validateSmsOtpMatcher(matcher, `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`);
2192
2279
  if (matcherIds.has(matcher.id))
2193
2280
  throw new ValidationError(
2194
2281
  `Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers has duplicate matcher id "${matcher.id}".`,
@@ -2199,9 +2286,7 @@ function validateHealthJourneys(
2199
2286
  for (const [stepIndex, step] of journey.steps.entries()) {
2200
2287
  const stepPath = `healthJourneys.${journey.id}.steps[${stepIndex}]`;
2201
2288
  if (!step || typeof step !== "object" || Array.isArray(step))
2202
- throw new ValidationError(
2203
- `Provider "${providerId}" ${stepPath} must be an object.`,
2204
- );
2289
+ throw new ValidationError(`Provider "${providerId}" ${stepPath} must be an object.`);
2205
2290
  rejectUnknownFields(step, HEALTH_JOURNEY_STEP_FIELDS, stepPath);
2206
2291
  if (typeof step.id !== "string" || !JOURNEY_ID_RE.test(step.id))
2207
2292
  throw new ValidationError(
@@ -2211,20 +2296,13 @@ function validateHealthJourneys(
2211
2296
  throw new ValidationError(
2212
2297
  `Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`,
2213
2298
  );
2214
- if (
2215
- step.usesSmsMatcher !== undefined &&
2216
- !matcherIds.has(step.usesSmsMatcher)
2217
- )
2299
+ if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
2218
2300
  throw new ValidationError(
2219
2301
  `Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`,
2220
2302
  );
2221
2303
  }
2222
2304
  if (journey.manualTrigger !== undefined)
2223
- validateHealthJourneyManualTrigger(
2224
- providerId,
2225
- journey.id,
2226
- journey.manualTrigger,
2227
- );
2305
+ validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
2228
2306
  if (journey.timeout !== undefined)
2229
2307
  assertIsoDuration(
2230
2308
  journey.timeout,
@@ -2270,23 +2348,10 @@ function validateOperationHealthChecks(
2270
2348
  fix: `Remove either operations.${operationName}.healthCheck or operations.${operationName}.healthCheckUnsupported.`,
2271
2349
  },
2272
2350
  );
2273
- if (hasCheck)
2274
- validateHealthCheckSuite(
2275
- providerId,
2276
- operationName,
2277
- operation.healthCheck,
2278
- );
2351
+ if (hasCheck) validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
2279
2352
  if (hasUnsupported)
2280
- validateHealthCheckUnsupported(
2281
- providerId,
2282
- operationName,
2283
- operation.healthCheckUnsupported,
2284
- );
2285
- if (
2286
- !hasCheck &&
2287
- !hasUnsupported &&
2288
- !journeyCoveredOperations.has(operationName)
2289
- )
2353
+ validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
2354
+ if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
2290
2355
  throw new ValidationError(
2291
2356
  `Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`,
2292
2357
  {
@@ -2308,6 +2373,26 @@ function validateOperationFixtures(
2308
2373
  fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
2309
2374
  },
2310
2375
  );
2376
+ if (operation.fixtures?.recordedAt !== undefined) {
2377
+ const recordedAt = operation.fixtures.recordedAt;
2378
+ const parsed =
2379
+ typeof recordedAt === "string"
2380
+ ? new Date(`${recordedAt}T00:00:00.000Z`)
2381
+ : new Date(Number.NaN);
2382
+ const isCalendarDate =
2383
+ typeof recordedAt === "string" &&
2384
+ /^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
2385
+ !Number.isNaN(parsed.getTime()) &&
2386
+ parsed.toISOString().slice(0, 10) === recordedAt;
2387
+ const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
2388
+ if (!isCalendarDate || recordedAt > kstToday)
2389
+ throw new ValidationError(
2390
+ `Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`,
2391
+ {
2392
+ fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
2393
+ },
2394
+ );
2395
+ }
2311
2396
  if (operation.fixtures?.request !== undefined) {
2312
2397
  const result = safeParseSchemaSync(
2313
2398
  operation.input,
@@ -2341,6 +2426,44 @@ function validateOperationFixtures(
2341
2426
  }
2342
2427
  }
2343
2428
 
2429
+ function resolveOperationFixtureRequests<TOperations extends Record<string, ProviderOperation>>(
2430
+ operations: TOperations,
2431
+ ): TOperations {
2432
+ let changed = false;
2433
+ const resolvedOperations = Object.fromEntries(
2434
+ Object.entries(operations).map(([operationName, operation]) => {
2435
+ if (operation.fixtures?.request === undefined) return [operationName, operation];
2436
+ const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
2437
+ if (request === operation.fixtures.request) return [operationName, operation];
2438
+ changed = true;
2439
+ return [
2440
+ operationName,
2441
+ {
2442
+ ...operation,
2443
+ fixtures: {
2444
+ ...operation.fixtures,
2445
+ request,
2446
+ },
2447
+ },
2448
+ ];
2449
+ }),
2450
+ ) as TOperations;
2451
+ return changed ? resolvedOperations : operations;
2452
+ }
2453
+
2454
+ /**
2455
+ * Shallow shape guard only: the `deployment` object is passed through
2456
+ * verbatim and deliberately not deep-validated by the SDK — the APIFuse
2457
+ * registry builder owns deployment validation and profile resolution.
2458
+ */
2459
+ function validateProviderDeployment(providerId: string, deployment: unknown): void {
2460
+ if (deployment === undefined) return;
2461
+ if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
2462
+ throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
2463
+ fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
2464
+ });
2465
+ }
2466
+
2344
2467
  export function defineProvider<
2345
2468
  TOperations extends Record<string, ProviderOperation>,
2346
2469
  TConfig extends ProviderConfig<TOperations>,
@@ -2348,20 +2471,19 @@ export function defineProvider<
2348
2471
  config: TConfig & AuthStartNoInputGuard<TConfig>,
2349
2472
  ): ProviderDefinition & { operations: OperationMapConfig<TOperations> } {
2350
2473
  validateProviderShape(config);
2474
+ const operations = resolveOperationFixtureRequests(config.operations);
2351
2475
  if (!CONNECTOR_ID_REGEX.test(config.id))
2352
2476
  throw new ProviderError(`Invalid provider id: "${config.id}"`, {
2353
2477
  fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
2354
2478
  });
2355
2479
  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
- );
2480
+ throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
2481
+ fix: "Add at least one operation to the operations object",
2482
+ });
2362
2483
  validateOperationIds(config.id, config.operations);
2363
2484
  validateOperationAnnotations(config.id, config.operations);
2364
2485
  validateOperationObservability(config.id, config.operations);
2486
+ validateOperationErrorCodes(config.id, config.operations);
2365
2487
  validateOperationTransports(config.id, config.operations);
2366
2488
  validateOperationContracts(config.id, config.operations);
2367
2489
  validateToolRouterMetadata(config.id, config.operations);
@@ -2370,11 +2492,7 @@ export function defineProvider<
2370
2492
  config.operations,
2371
2493
  config.healthJourneys,
2372
2494
  );
2373
- validateOperationHealthChecks(
2374
- config.id,
2375
- config.operations,
2376
- journeyCoveredOperations,
2377
- );
2495
+ validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
2378
2496
  if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
2379
2497
  throw new ValidationError(
2380
2498
  `Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
@@ -2387,9 +2505,19 @@ export function defineProvider<
2387
2505
  config.healthProbe ?? config.healthMonitor,
2388
2506
  config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
2389
2507
  );
2390
- validateOperationFixtures(config.id, config.operations);
2508
+ validateOperationFixtures(config.id, operations);
2509
+ validateProviderDeployment(config.id, config.deployment);
2510
+ try {
2511
+ validateNativeProviderConfig(config.native);
2512
+ } catch (error) {
2513
+ if (error instanceof NativeEgressPolicyValidationError)
2514
+ throw new ValidationError(error.message);
2515
+ throw error;
2516
+ }
2391
2517
  validateProviderProxy(config);
2518
+ validateProviderOcr(config);
2392
2519
  validateProviderStt(config);
2520
+ validateProviderResolver(config);
2393
2521
  if (config.runtime === "browser" && !config.browser)
2394
2522
  throw new ProviderError(
2395
2523
  `Provider "${config.id}" must define browser.engine when runtime is "browser"`,
@@ -2402,14 +2530,20 @@ export function defineProvider<
2402
2530
  `Provider "${config.id}" cannot define browser config unless runtime is "browser"`,
2403
2531
  { fix: 'Set runtime: "browser" or remove the browser config' },
2404
2532
  );
2405
- return {
2533
+ const provider: ProviderDefinition & { operations: OperationMapConfig<TOperations> } = {
2406
2534
  id: config.id,
2407
2535
  version: config.version,
2408
2536
  runtime: config.runtime,
2537
+ // Verbatim passthrough: deployment validation and profile resolution
2538
+ // are owned by the APIFuse registry builder, not the SDK.
2539
+ deployment: config.deployment,
2409
2540
  allowedHosts: config.allowedHosts,
2541
+ native: config.native,
2410
2542
  stealth: config.stealth,
2411
2543
  proxy: config.proxy,
2544
+ ocr: config.ocr,
2412
2545
  stt: config.stt,
2546
+ resolver: config.resolver,
2413
2547
  browser: config.browser,
2414
2548
  auth: config.auth,
2415
2549
  reviewed: config.reviewed,
@@ -2418,11 +2552,13 @@ export function defineProvider<
2418
2552
  credential: config.credential,
2419
2553
  context: config.context,
2420
2554
  meta: config.meta,
2421
- operations: config.operations,
2555
+ operations,
2422
2556
  // Transitional healthMonitor → healthProbe alias: mirror whichever field
2423
2557
  // was declared onto both so old and new consumers keep working.
2424
2558
  healthMonitor: config.healthMonitor ?? config.healthProbe,
2425
2559
  healthProbe: config.healthProbe ?? config.healthMonitor,
2426
2560
  healthJourneys: config.healthJourneys,
2427
2561
  };
2562
+ validateFailClosedDeclaration(provider);
2563
+ return provider;
2428
2564
  }