@apifuse/provider-sdk 2.2.0-beta.1 → 2.2.0-beta.11

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 (261) hide show
  1. package/AUTHORING.md +106 -0
  2. package/CHANGELOG.md +41 -0
  3. package/README.md +5 -1
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +14 -0
  9. package/bin/apifuse-pack-smoke.ts +22 -81
  10. package/bin/apifuse-pack-types.ts +266 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +53 -70
  13. package/bin/apifuse-submit-check.ts +510 -45
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth.d.ts +2 -2
  21. package/dist/auth.js +9 -18
  22. package/dist/ceremonies/index.d.ts +1 -1
  23. package/dist/ceremonies/index.js +8 -18
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/config/loader.d.ts +149 -8
  32. package/dist/config/loader.js +378 -89
  33. package/dist/contract-serialization.d.ts +2 -2
  34. package/dist/contract-serialization.js +3 -6
  35. package/dist/contract-types.d.ts +2 -2
  36. package/dist/contract.d.ts +3 -3
  37. package/dist/contract.js +4 -6
  38. package/dist/define.d.ts +8 -1
  39. package/dist/define.js +119 -122
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/errors.d.ts +4 -1
  43. package/dist/errors.js +48 -0
  44. package/dist/i18n/catalog.d.ts +2 -2
  45. package/dist/i18n/catalog.js +4 -10
  46. package/dist/i18n/index.d.ts +2 -2
  47. package/dist/i18n/index.js +2 -2
  48. package/dist/i18n/keys.d.ts +2 -2
  49. package/dist/index.d.ts +44 -42
  50. package/dist/index.js +39 -37
  51. package/dist/lint.js +8 -15
  52. package/dist/provider.d.ts +11 -11
  53. package/dist/provider.js +9 -9
  54. package/dist/public-schema-field-lint.d.ts +1 -1
  55. package/dist/recipes/gov-api.js +1 -1
  56. package/dist/runtime/auth-flow.d.ts +1 -1
  57. package/dist/runtime/auth-flow.js +3 -3
  58. package/dist/runtime/browser.d.ts +1 -1
  59. package/dist/runtime/browser.js +15 -29
  60. package/dist/runtime/cache.d.ts +1 -1
  61. package/dist/runtime/cache.js +4 -8
  62. package/dist/runtime/choice.d.ts +1 -1
  63. package/dist/runtime/choice.js +31 -35
  64. package/dist/runtime/credential.d.ts +1 -1
  65. package/dist/runtime/credential.js +1 -1
  66. package/dist/runtime/env.d.ts +1 -1
  67. package/dist/runtime/executor.d.ts +1 -1
  68. package/dist/runtime/executor.js +15 -3
  69. package/dist/runtime/http.d.ts +2 -2
  70. package/dist/runtime/http.js +144 -348
  71. package/dist/runtime/insights.d.ts +1 -1
  72. package/dist/runtime/insights.js +6 -13
  73. package/dist/runtime/instrumentation.d.ts +2 -2
  74. package/dist/runtime/instrumentation.js +56 -19
  75. package/dist/runtime/keyring.js +1 -1
  76. package/dist/runtime/namespace.js +1 -1
  77. package/dist/runtime/otlp.d.ts +1 -1
  78. package/dist/runtime/perf.d.ts +1 -1
  79. package/dist/runtime/provider.d.ts +1 -1
  80. package/dist/runtime/provider.js +1 -2
  81. package/dist/runtime/proxy-errors.d.ts +1 -1
  82. package/dist/runtime/proxy-errors.js +9 -7
  83. package/dist/runtime/proxy-nodemaven.d.ts +35 -0
  84. package/dist/runtime/proxy-nodemaven.js +128 -0
  85. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  86. package/dist/runtime/proxy-retry-policy.js +326 -0
  87. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  88. package/dist/runtime/proxy-telemetry.js +55 -52
  89. package/dist/runtime/redis.d.ts +1 -1
  90. package/dist/runtime/redis.js +2 -4
  91. package/dist/runtime/request-options.d.ts +1 -1
  92. package/dist/runtime/secrets.d.ts +27 -0
  93. package/dist/runtime/secrets.js +51 -0
  94. package/dist/runtime/state.d.ts +2 -2
  95. package/dist/runtime/state.js +15 -4
  96. package/dist/runtime/stealth.d.ts +2 -2
  97. package/dist/runtime/stealth.js +108 -212
  98. package/dist/runtime/stt.d.ts +1 -1
  99. package/dist/runtime/stt.js +11 -15
  100. package/dist/runtime/trace.d.ts +2 -2
  101. package/dist/runtime/trace.js +2 -4
  102. package/dist/runtime/waterfall.d.ts +1 -1
  103. package/dist/schema.d.ts +1 -1
  104. package/dist/schema.js +7 -15
  105. package/dist/serve.d.ts +1 -1
  106. package/dist/serve.js +1 -1
  107. package/dist/server/index.d.ts +7 -7
  108. package/dist/server/index.js +6 -6
  109. package/dist/server/self-test-redaction.d.ts +1 -1
  110. package/dist/server/self-test-redaction.js +1 -1
  111. package/dist/server/self-test.d.ts +104 -3
  112. package/dist/server/self-test.js +673 -115
  113. package/dist/server/serve.d.ts +105 -4
  114. package/dist/server/serve.js +588 -80
  115. package/dist/server/types.d.ts +10 -9
  116. package/dist/server/types.js +3 -7
  117. package/dist/stateful/errors.d.ts +14 -0
  118. package/dist/stateful/errors.js +14 -0
  119. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  120. package/dist/stateful/http-provider-event-emitter.js +237 -0
  121. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  122. package/dist/stateful/http-session-owner-registry.js +210 -0
  123. package/dist/stateful/index.d.ts +18 -0
  124. package/dist/stateful/index.js +18 -0
  125. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  126. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  127. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  128. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  129. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  130. package/dist/stateful/provider-event-pipeline.js +1 -0
  131. package/dist/stateful/provider-events.d.ts +101 -0
  132. package/dist/stateful/provider-events.js +289 -0
  133. package/dist/stateful/session-key.d.ts +15 -0
  134. package/dist/stateful/session-key.js +86 -0
  135. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  136. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  137. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  138. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  139. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  140. package/dist/stateful/stateful-provider-adapter.js +287 -0
  141. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  142. package/dist/stateful/stateful-provider-observability.js +161 -0
  143. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  144. package/dist/stateful/stateful-provider-owner-forwarder.js +207 -0
  145. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  146. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  147. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  148. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  149. package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
  150. package/dist/stateful/stateful-provider-session-routing.js +353 -0
  151. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  152. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  153. package/dist/stateful-signing.d.ts +18 -0
  154. package/dist/stateful-signing.js +27 -0
  155. package/dist/stealth/profiles.d.ts +1 -1
  156. package/dist/stealth/profiles.js +5 -14
  157. package/dist/stream.d.ts +1 -1
  158. package/dist/testing/index.d.ts +2 -2
  159. package/dist/testing/index.js +2 -2
  160. package/dist/testing/run.d.ts +1 -1
  161. package/dist/testing/run.js +7 -14
  162. package/dist/types.d.ts +87 -1
  163. package/dist/user-input.d.ts +30 -0
  164. package/dist/user-input.js +66 -0
  165. package/package.json +14 -5
  166. package/src/auth-turn/index.ts +1 -1
  167. package/src/auth.ts +28 -86
  168. package/src/ceremonies/index.ts +24 -78
  169. package/src/cli/commands.ts +10 -0
  170. package/src/cli/create.ts +42 -35
  171. package/src/cli/prompt-assets.ts +865 -0
  172. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  173. package/src/config/loader.ts +543 -208
  174. package/src/contract-serialization.ts +5 -11
  175. package/src/contract-types.ts +2 -2
  176. package/src/contract.ts +12 -28
  177. package/src/define.ts +247 -505
  178. package/src/dev.ts +4 -9
  179. package/src/errors.ts +58 -5
  180. package/src/i18n/catalog.ts +10 -32
  181. package/src/i18n/index.ts +2 -2
  182. package/src/i18n/keys.ts +5 -11
  183. package/src/index.ts +49 -42
  184. package/src/lint.ts +89 -159
  185. package/src/provider.ts +15 -11
  186. package/src/public-schema-field-lint.ts +7 -33
  187. package/src/recipes/gov-api.ts +2 -5
  188. package/src/runtime/auth-flow.ts +5 -7
  189. package/src/runtime/browser.ts +64 -187
  190. package/src/runtime/cache.ts +20 -67
  191. package/src/runtime/choice.ts +79 -132
  192. package/src/runtime/credential.ts +2 -2
  193. package/src/runtime/env.ts +1 -1
  194. package/src/runtime/executor.ts +23 -20
  195. package/src/runtime/http.ts +195 -559
  196. package/src/runtime/insights.ts +15 -53
  197. package/src/runtime/instrumentation.ts +78 -65
  198. package/src/runtime/keyring.ts +7 -19
  199. package/src/runtime/namespace.ts +2 -7
  200. package/src/runtime/otlp.ts +12 -23
  201. package/src/runtime/perf.ts +1 -1
  202. package/src/runtime/provider.ts +4 -9
  203. package/src/runtime/proxy-errors.ts +29 -42
  204. package/src/runtime/proxy-nodemaven.ts +178 -0
  205. package/src/runtime/proxy-retry-policy.ts +469 -0
  206. package/src/runtime/proxy-telemetry.ts +79 -77
  207. package/src/runtime/redis.ts +4 -12
  208. package/src/runtime/request-options.ts +4 -13
  209. package/src/runtime/secrets.ts +64 -0
  210. package/src/runtime/state.ts +41 -110
  211. package/src/runtime/stealth.ts +162 -377
  212. package/src/runtime/stt.ts +38 -94
  213. package/src/runtime/trace.ts +14 -44
  214. package/src/runtime/waterfall.ts +5 -18
  215. package/src/schema.ts +23 -84
  216. package/src/serve.ts +1 -1
  217. package/src/server/index.ts +24 -7
  218. package/src/server/self-test-redaction.ts +2 -2
  219. package/src/server/self-test.ts +857 -132
  220. package/src/server/serve.ts +859 -266
  221. package/src/server/types.ts +7 -19
  222. package/src/stateful/README.md +146 -0
  223. package/src/stateful/errors.ts +23 -0
  224. package/src/stateful/http-provider-event-emitter.ts +314 -0
  225. package/src/stateful/http-session-owner-registry.ts +306 -0
  226. package/src/stateful/index.ts +18 -0
  227. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  228. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  229. package/src/stateful/provider-event-pipeline.ts +61 -0
  230. package/src/stateful/provider-events.ts +462 -0
  231. package/src/stateful/session-key.ts +111 -0
  232. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  233. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  234. package/src/stateful/stateful-provider-adapter.ts +562 -0
  235. package/src/stateful/stateful-provider-observability.ts +261 -0
  236. package/src/stateful/stateful-provider-owner-forwarder.ts +279 -0
  237. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  238. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  239. package/src/stateful/stateful-provider-session-routing.ts +555 -0
  240. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  241. package/src/stateful-signing.ts +46 -0
  242. package/src/stealth/profiles.ts +10 -26
  243. package/src/stream.ts +8 -19
  244. package/src/testing/index.ts +2 -2
  245. package/src/testing/run.ts +19 -67
  246. package/src/types.ts +88 -1
  247. package/src/user-input.ts +118 -0
  248. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  249. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  250. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  251. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  252. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  253. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  254. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  255. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  256. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  257. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  258. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  259. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  260. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  261. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/src/define.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import ms from "ms";
2
2
 
3
- import { ProviderError, ValidationError } from "./errors";
4
- import { safeParseSchemaSync } from "./schema";
3
+ import { ProviderError, ValidationError } from "./errors.js";
4
+ import { safeParseSchemaSync } from "./schema.js";
5
5
  import type {
6
6
  AuthConfig,
7
7
  BrowserEngine,
@@ -22,8 +22,10 @@ import type {
22
22
  OperationWebSocketTransport,
23
23
  ProviderAccessConfig,
24
24
  ProviderDefinition,
25
+ ProviderDeploymentOverrides,
25
26
  ProviderHealthMonitorConfig,
26
27
  ProviderProxyConfig,
28
+ ProviderProxyProvider,
27
29
  ProviderPublicProfile,
28
30
  ProviderReviewed,
29
31
  ProviderSecretDeclaration,
@@ -32,7 +34,7 @@ import type {
32
34
  SchemaLike,
33
35
  SmsOtpMatcherDefinition,
34
36
  StealthPlatform,
35
- } from "./types";
37
+ } from "./types.js";
36
38
  import {
37
39
  HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
38
40
  HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
@@ -48,7 +50,7 @@ import {
48
50
  STREAM_IDLE_TIMEOUT_MS_MIN,
49
51
  STREAM_MAX_DURATION_MS_MAX,
50
52
  STREAM_MAX_DURATION_MS_MIN,
51
- } from "./types";
53
+ } from "./types.js";
52
54
 
53
55
  type ProviderImplementationSourceAccess =
54
56
  | "official_api"
@@ -74,23 +76,10 @@ interface ProviderImplementationProfile {
74
76
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
75
77
  const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
76
78
  const VALID_RUNTIMES = ["standard", "shared", "browser"] as const;
77
- const VALID_AUTH_MODES = [
78
- "none",
79
- "platform-managed",
80
- "credentials",
81
- "oauth2",
82
- ] as const;
79
+ const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"] as const;
83
80
  const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
84
- const VALID_PROVIDER_PROXY_MODES = [
85
- "disabled",
86
- "optional",
87
- "required",
88
- ] as const;
89
- const VALID_PROVIDER_PROXY_PROVIDERS = [
90
- "smartproxy",
91
- "decodo",
92
- "custom",
93
- ] as const;
81
+ const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"] as const;
82
+ const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"] as const;
94
83
  const VALID_PROVIDER_PROXY_AFFINITIES = [
95
84
  "request",
96
85
  "operation",
@@ -99,25 +88,25 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
99
88
  ] as const;
100
89
  const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
101
90
  const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
91
+ const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
92
+ const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
93
+ // Per-vendor provider-declared credential secrets. A required-mode chain must
94
+ // declare every secret of every credentialed vendor it names, so a missing
95
+ // credential fails at build/validation time rather than during a live outage: a
96
+ // declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
97
+ // exactly the failure class the multi-vendor chain exists to remove. Vendors
98
+ // absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
99
+ // `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
100
+ // no declaration requirement.
101
+ const VENDOR_REQUIRED_SECRETS: Partial<Record<ProviderProxyProvider, readonly string[]>> = {
102
+ smartproxy: [SMARTPROXY_APP_KEY_SECRET],
103
+ nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
104
+ };
102
105
  const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
103
106
  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;
107
+ const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
108
+ const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"] as const;
109
+ const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"] as const;
121
110
  const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
122
111
  const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
123
112
 
@@ -135,18 +124,16 @@ function msDurationMs(value: string): number {
135
124
  function parsePositiveMsDuration(value: string): number | undefined {
136
125
  const trimmed = value.trim();
137
126
  if (!MS_DURATION_PATTERN.test(trimmed)) return undefined;
138
- const parsed = ms(
139
- (trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue,
140
- );
127
+ const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue);
141
128
  if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
142
129
  return parsed;
143
130
  }
144
131
 
145
132
  type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
146
- type OperationConfig<
147
- TInput extends SchemaLike,
148
- TOutput extends SchemaLike,
149
- > = Omit<OperationDefinition<TInput, TOutput>, "handler"> & {
133
+ type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
134
+ OperationDefinition<TInput, TOutput>,
135
+ "handler"
136
+ > & {
150
137
  handler(
151
138
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
152
139
  input: InferSchemaOutput<TInput>,
@@ -154,59 +141,44 @@ type OperationConfig<
154
141
  | OperationHandlerResult<InferSchemaOutput<TOutput>>
155
142
  | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
156
143
  };
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
- > =
144
+ type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
145
+ [K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
146
+ ? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
147
+ : never;
148
+ };
149
+ type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
170
150
  | SseOperationConfig<TInput, TOutput>
171
151
  | HttpStreamOperationConfig<TInput, TOutput>
172
152
  | WebSocketOperationConfig<TInput, TOutput>;
173
- type SseOperationConfig<
174
- TInput extends SchemaLike,
175
- TOutput extends SchemaLike,
176
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
153
+ type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
154
+ OperationConfig<TInput, TOutput>,
155
+ "handler" | "transport"
156
+ > & {
177
157
  transport: OperationSseTransport;
178
158
  handler(
179
159
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
180
160
  input: InferSchemaOutput<TInput>,
181
- ):
182
- | AsyncIterable<ProviderStreamEvent>
183
- | Promise<AsyncIterable<ProviderStreamEvent>>;
161
+ ): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
184
162
  };
185
- type HttpStreamOperationConfig<
186
- TInput extends SchemaLike,
187
- TOutput extends SchemaLike,
188
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
163
+ type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
164
+ OperationConfig<TInput, TOutput>,
165
+ "handler" | "transport"
166
+ > & {
189
167
  transport: OperationHttpStreamTransport;
190
168
  handler(
191
169
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
192
170
  input: InferSchemaOutput<TInput>,
193
- ):
194
- | Response
195
- | ReadableStream<Uint8Array>
196
- | Promise<Response | ReadableStream<Uint8Array>>;
171
+ ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
197
172
  };
198
- type WebSocketOperationConfig<
199
- TInput extends SchemaLike,
200
- TOutput extends SchemaLike,
201
- > = Omit<OperationConfig<TInput, TOutput>, "handler" | "transport"> & {
173
+ type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
174
+ OperationConfig<TInput, TOutput>,
175
+ "handler" | "transport"
176
+ > & {
202
177
  transport: OperationWebSocketTransport;
203
178
  handler(
204
179
  ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
205
180
  input: InferSchemaOutput<TInput>,
206
- ):
207
- | Response
208
- | ReadableStream<Uint8Array>
209
- | Promise<Response | ReadableStream<Uint8Array>>;
181
+ ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
210
182
  };
211
183
 
212
184
  type AuthStartNoInputGuard<TConfig> = TConfig extends {
@@ -221,12 +193,17 @@ type AuthStartNoInputGuard<TConfig> = TConfig extends {
221
193
  : unknown
222
194
  : unknown;
223
195
 
224
- export interface ProviderConfig<
225
- TOperations extends Record<string, ProviderOperation>,
226
- > {
196
+ export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
227
197
  id: string;
228
198
  version: string;
229
199
  runtime: "standard" | "shared" | "browser";
200
+ /**
201
+ * Optional deployment overrides, passed through verbatim onto the returned
202
+ * provider definition. The SDK types this field but does not deep-validate
203
+ * it — the APIFuse registry builder owns deployment validation and
204
+ * resolves omitted fields against the runtime deployment profiles.
205
+ */
206
+ deployment?: ProviderDeploymentOverrides;
230
207
  allowedHosts?: string[];
231
208
  stealth?: {
232
209
  profile: string;
@@ -270,35 +247,24 @@ export interface ProviderConfig<
270
247
  }
271
248
 
272
249
  /** Define one provider operation with schema-driven handler inference. */
273
- export function defineOperation<
274
- TInput extends SchemaLike,
275
- TOutput extends SchemaLike,
276
- >(
250
+ export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
277
251
  operation: OperationConfig<TInput, TOutput>,
278
252
  ): OperationDefinition<TInput, TOutput> {
279
253
  return operation;
280
254
  }
281
255
 
282
256
  /** Define a non-JSON provider operation with explicit transport metadata. */
283
- export function defineStreamOperation<
284
- TInput extends SchemaLike,
285
- TOutput extends SchemaLike,
286
- >(
257
+ export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
287
258
  operation: StreamOperationConfig<TInput, TOutput>,
288
259
  ): OperationDefinition<TInput, TOutput> {
289
260
  return operation;
290
261
  }
291
262
 
292
- function assertObjectConfig(
293
- value: unknown,
294
- ): asserts value is Record<string, unknown> {
263
+ function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
295
264
  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
- );
265
+ throw new ProviderError("defineProvider config must be an object. Offending field: config", {
266
+ fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
267
+ });
302
268
  }
303
269
  }
304
270
  function assertRequiredField(
@@ -336,25 +302,10 @@ function validateProviderShape(config: unknown): void {
336
302
  assertRequiredField(config, "meta", String(config.id));
337
303
  assertRequiredField(config, "operations", String(config.id));
338
304
  if (typeof config.runtime === "string")
339
- assertLiteralField(
340
- config.runtime,
341
- "runtime",
342
- VALID_RUNTIMES,
343
- String(config.id),
344
- );
305
+ assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
345
306
  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
- );
307
+ if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
308
+ assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
358
309
  if (auth && typeof auth === "object" && "exchange" in auth) {
359
310
  throw new ProviderError(
360
311
  `Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
@@ -390,9 +341,7 @@ function validateProviderShape(config: unknown): void {
390
341
  },
391
342
  );
392
343
  }
393
- const accessRecord: Record<string, unknown> = Object.fromEntries(
394
- Object.entries(access),
395
- );
344
+ const accessRecord: Record<string, unknown> = Object.fromEntries(Object.entries(access));
396
345
  for (const key of Object.keys(accessRecord)) {
397
346
  if (key !== "visibility") {
398
347
  throw new ValidationError(`Unknown field "${key}" on access.`, {
@@ -437,31 +386,26 @@ function validateProviderProxy(config: {
437
386
  },
438
387
  );
439
388
  }
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
- );
389
+ rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
390
+ assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
451
391
  if (proxy.provider !== undefined) {
452
- assertLiteralField(
453
- proxy.provider,
454
- "proxy.provider",
455
- VALID_PROVIDER_PROXY_PROVIDERS,
456
- config.id,
457
- );
392
+ assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
393
+ }
394
+ if (proxy.providers !== undefined) {
395
+ if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
396
+ throw new ValidationError(
397
+ `Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`,
398
+ {
399
+ fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
400
+ },
401
+ );
402
+ }
403
+ for (const vendor of proxy.providers) {
404
+ assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
405
+ }
458
406
  }
459
407
  if (proxy.geo !== undefined) {
460
- if (
461
- !proxy.geo ||
462
- typeof proxy.geo !== "object" ||
463
- Array.isArray(proxy.geo)
464
- ) {
408
+ if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
465
409
  throw new ValidationError(
466
410
  `Provider "${config.id}" has invalid proxy.geo: must be an object.`,
467
411
  {
@@ -469,11 +413,7 @@ function validateProviderProxy(config: {
469
413
  },
470
414
  );
471
415
  }
472
- rejectUnknownFields(
473
- proxy.geo,
474
- new Set(["country", "subdivision", "city"]),
475
- "proxy.geo",
476
- );
416
+ rejectUnknownFields(proxy.geo, new Set(["country", "subdivision", "city"]), "proxy.geo");
477
417
  if (proxy.geo.country !== undefined) {
478
418
  assertIsoCountry(proxy.geo.country, "proxy.geo.country");
479
419
  }
@@ -487,11 +427,7 @@ function validateProviderProxy(config: {
487
427
  }
488
428
  }
489
429
  if (proxy.session !== undefined) {
490
- if (
491
- !proxy.session ||
492
- typeof proxy.session !== "object" ||
493
- Array.isArray(proxy.session)
494
- ) {
430
+ if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
495
431
  throw new ValidationError(
496
432
  `Provider "${config.id}" has invalid proxy.session: must be an object.`,
497
433
  {
@@ -513,51 +449,75 @@ function validateProviderProxy(config: {
513
449
  );
514
450
  }
515
451
  const lifetime = proxy.session.lifetimeMinutes;
516
- if (
517
- lifetime !== undefined &&
518
- (!Number.isFinite(lifetime) || lifetime <= 0)
519
- ) {
452
+ if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
520
453
  throw new ValidationError(
521
454
  `Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`,
522
455
  );
523
456
  }
524
457
  const poolSize = proxy.session.poolSize;
525
- if (
526
- poolSize !== undefined &&
527
- (!Number.isInteger(poolSize) || poolSize <= 0)
528
- ) {
458
+ if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
529
459
  throw new ValidationError(
530
460
  `Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
531
461
  );
532
462
  }
533
463
  }
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) {
540
- throw new ValidationError(
541
- `Provider "${config.id}" requires Smartproxy egress but does not declare ${SMARTPROXY_APP_KEY_SECRET}.`,
542
- {
543
- fix: `Add secrets: [{ name: "${SMARTPROXY_APP_KEY_SECRET}", required: true }] to the provider.`,
544
- },
545
- );
464
+ // Every credentialed vendor in a required-mode chain must declare its
465
+ // provider secret(s) so a missing credential fails at build/validation time,
466
+ // not during a live outage. This covers the fallback legs too (not just the
467
+ // first vendor): a declared-but-uncredentialed nodemaven fallback would leave
468
+ // the chain silently down to a single vendor, reintroducing the SPOF the chain
469
+ // removes.
470
+ const vendorChain =
471
+ proxy.providers && proxy.providers.length > 0
472
+ ? proxy.providers
473
+ : proxy.provider
474
+ ? [proxy.provider]
475
+ : [];
476
+ if (proxy.mode === "required") {
477
+ for (const vendor of vendorChain) {
478
+ const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
479
+ if (!requiredSecrets) continue;
480
+ for (const secretName of requiredSecrets) {
481
+ // Match the canonical runtime gate (assertRequiredSecretsPresent /
482
+ // listMissingRequiredSecrets), which enforces only `required === true`
483
+ // declarations. A declaration that omits `required` (defaulting to
484
+ // optional) is skipped at runtime, so accepting it here would pass
485
+ // validation while leaving the credential unenforced until proxy
486
+ // resolution during a live request — the fail-open gap this check exists
487
+ // to close.
488
+ const declared = config.secrets?.some(
489
+ (secret) => secret.name === secretName && secret.required === true,
490
+ );
491
+ if (!declared) {
492
+ throw new ValidationError(
493
+ `Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`,
494
+ {
495
+ fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
496
+ },
497
+ );
498
+ }
499
+ }
546
500
  }
547
501
  }
502
+ // `decodo`/`custom` are deprecated vendor values (string-union members, so the
503
+ // @deprecated symbol gate can't catch them — warn at validation time instead).
504
+ const deprecatedVendors = vendorChain.filter(
505
+ (vendor) => vendor === "decodo" || vendor === "custom",
506
+ );
507
+ if (deprecatedVendors.length > 0) {
508
+ console.warn(
509
+ `[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven", or the APIFUSE__PROXY__URL bring-your-own escape hatch.`,
510
+ );
511
+ }
548
512
  }
549
513
 
550
- function validateProviderStt(config: {
551
- id: string;
552
- stt?: ProviderSttConfig;
553
- }): void {
514
+ function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): void {
554
515
  const stt = config.stt;
555
516
  if (stt === undefined) return;
556
517
  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
- );
518
+ throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
519
+ fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
520
+ });
561
521
  }
562
522
  rejectUnknownFields(stt, new Set(["mode"]), "stt");
563
523
  assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
@@ -584,16 +544,9 @@ function validateOperationIds(
584
544
  );
585
545
  }
586
546
  }
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;
547
+ const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
548
+ const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
549
+ const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"] as const;
597
550
 
598
551
  function assertNonEmptyString(
599
552
  value: unknown,
@@ -624,10 +577,7 @@ function validateToolRouterMetadata(
624
577
  },
625
578
  );
626
579
  }
627
- if (
628
- toolRouter.name !== undefined &&
629
- !MCP_TOOL_NAME_REGEX.test(toolRouter.name)
630
- ) {
580
+ if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
631
581
  throw new ValidationError(
632
582
  `Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`,
633
583
  {
@@ -699,10 +649,7 @@ function validateOperationContracts(
699
649
  providerId,
700
650
  );
701
651
  }
702
- if (
703
- contract.lifecycle === "deprecated" ||
704
- contract.lifecycle === "removed"
705
- ) {
652
+ if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
706
653
  if (!contract.deprecation || typeof contract.deprecation !== "object") {
707
654
  throw new ValidationError(
708
655
  `Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`,
@@ -750,10 +697,7 @@ function validateOperationAnnotations(
750
697
  fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
751
698
  },
752
699
  );
753
- if (
754
- timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
755
- timeoutMs > OPERATION_TIMEOUT_MS_MAX
756
- )
700
+ if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
757
701
  throw new ValidationError(
758
702
  `Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`,
759
703
  {
@@ -951,12 +895,7 @@ function validateOperationTransports(
951
895
  },
952
896
  );
953
897
  }
954
- assertLiteralField(
955
- kind,
956
- `${fieldPath}.kind`,
957
- VALID_OPERATION_TRANSPORT_KINDS,
958
- providerId,
959
- );
898
+ assertLiteralField(kind, `${fieldPath}.kind`, VALID_OPERATION_TRANSPORT_KINDS, providerId);
960
899
 
961
900
  switch (kind) {
962
901
  case "json":
@@ -988,16 +927,9 @@ function validateOperationTransports(
988
927
  STREAM_MAX_DURATION_MS_MAX,
989
928
  "max duration",
990
929
  );
991
- assertPositiveBytes(
992
- Reflect.get(transport, "maxEventBytes"),
993
- `${fieldPath}.maxEventBytes`,
994
- );
930
+ assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
995
931
  const resumable = Reflect.get(transport, "resumable");
996
- if (
997
- resumable !== undefined &&
998
- resumable !== false &&
999
- resumable !== "last-event-id"
1000
- ) {
932
+ if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
1001
933
  throw new ValidationError(
1002
934
  `Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`,
1003
935
  {
@@ -1017,12 +949,7 @@ function validateOperationTransports(
1017
949
  rejectUnknownFields(transport, HTTP_STREAM_TRANSPORT_FIELDS, fieldPath);
1018
950
  const contentType = Reflect.get(transport, "contentType");
1019
951
  if (contentType !== undefined) {
1020
- assertNonEmptyString(
1021
- contentType,
1022
- `${fieldPath}.contentType`,
1023
- providerId,
1024
- operationName,
1025
- );
952
+ assertNonEmptyString(contentType, `${fieldPath}.contentType`, providerId, operationName);
1026
953
  }
1027
954
  assertStreamMs(
1028
955
  Reflect.get(transport, "idleTimeoutMs"),
@@ -1038,10 +965,7 @@ function validateOperationTransports(
1038
965
  STREAM_MAX_DURATION_MS_MAX,
1039
966
  "max duration",
1040
967
  );
1041
- assertPositiveBytes(
1042
- Reflect.get(transport, "maxChunkBytes"),
1043
- `${fieldPath}.maxChunkBytes`,
1044
- );
968
+ assertPositiveBytes(Reflect.get(transport, "maxChunkBytes"), `${fieldPath}.maxChunkBytes`);
1045
969
  break;
1046
970
  }
1047
971
  case "websocket": {
@@ -1066,10 +990,7 @@ function validateOperationTransports(
1066
990
  );
1067
991
  }
1068
992
  for (const subprotocol of subprotocols) {
1069
- if (
1070
- typeof subprotocol !== "string" ||
1071
- !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)
1072
- ) {
993
+ if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
1073
994
  throw new ValidationError(
1074
995
  `Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`,
1075
996
  {
@@ -1093,10 +1014,7 @@ function validateOperationTransports(
1093
1014
  STREAM_MAX_DURATION_MS_MAX,
1094
1015
  "max duration",
1095
1016
  );
1096
- assertPositiveBytes(
1097
- Reflect.get(transport, "maxFrameBytes"),
1098
- `${fieldPath}.maxFrameBytes`,
1099
- );
1017
+ assertPositiveBytes(Reflect.get(transport, "maxFrameBytes"), `${fieldPath}.maxFrameBytes`);
1100
1018
  break;
1101
1019
  }
1102
1020
  }
@@ -1159,10 +1077,7 @@ function levenshtein(a: string, b: string): number {
1159
1077
  return prev[n] ?? 0;
1160
1078
  }
1161
1079
 
1162
- function suggestField(
1163
- unknown: string,
1164
- candidates: ReadonlySet<string>,
1165
- ): string | undefined {
1080
+ function suggestField(unknown: string, candidates: ReadonlySet<string>): string | undefined {
1166
1081
  let best: string | undefined;
1167
1082
  let bestDist = 3;
1168
1083
  for (const candidate of candidates) {
@@ -1175,11 +1090,7 @@ function suggestField(
1175
1090
  return best;
1176
1091
  }
1177
1092
 
1178
- function rejectUnknownFields(
1179
- value: object,
1180
- allowed: ReadonlySet<string>,
1181
- fieldPath: string,
1182
- ): void {
1093
+ function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldPath: string): void {
1183
1094
  for (const key of Object.keys(value)) {
1184
1095
  if (allowed.has(key)) continue;
1185
1096
  const hint = suggestField(key, allowed);
@@ -1218,23 +1129,12 @@ function validateProviderHealthMonitor(
1218
1129
  field: "healthMonitor" | "healthProbe" = "healthMonitor",
1219
1130
  ): void {
1220
1131
  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
- );
1132
+ if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
1133
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
1134
+ fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
1135
+ });
1232
1136
  const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
1233
- rejectUnknownFields(
1234
- healthMonitorRecord,
1235
- PROVIDER_HEALTH_MONITOR_FIELDS,
1236
- field,
1237
- );
1137
+ rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
1238
1138
  if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
1239
1139
  assertBoundedIntegerMs(
1240
1140
  healthMonitorRecord.defaultProbeTimeoutMs,
@@ -1302,11 +1202,7 @@ function validateProviderHealthMonitor(
1302
1202
 
1303
1203
  const probeOverrides = healthMonitorRecord.probeOverrides;
1304
1204
  if (probeOverrides !== undefined) {
1305
- if (
1306
- !probeOverrides ||
1307
- typeof probeOverrides !== "object" ||
1308
- Array.isArray(probeOverrides)
1309
- )
1205
+ if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
1310
1206
  throw new ValidationError(
1311
1207
  `Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`,
1312
1208
  );
@@ -1372,14 +1268,8 @@ function validateHealthCheckCase(
1372
1268
  ): void {
1373
1269
  const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
1374
1270
  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
- );
1271
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1272
+ rejectUnknownFields(caseValue as Record<string, unknown>, HEALTH_CHECK_CASE_FIELDS, fieldPath);
1383
1273
  const c = caseValue as HealthCheckCase;
1384
1274
  if (typeof c.name !== "string" || c.name.length === 0)
1385
1275
  throw new ValidationError(
@@ -1407,15 +1297,11 @@ function validateHealthCheckCase(
1407
1297
  `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
1298
  );
1409
1299
  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
- );
1300
+ assertBoundedIntegerMs(c.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
1301
+ min: HEALTH_CHECK_TIMEOUT_MS_MIN,
1302
+ max: HEALTH_CHECK_TIMEOUT_MS_MAX,
1303
+ label: "timeout",
1304
+ });
1419
1305
  }
1420
1306
  if (
1421
1307
  c.expectedStatus !== undefined &&
@@ -1431,21 +1317,11 @@ function validateHealthCheckCase(
1431
1317
  );
1432
1318
  }
1433
1319
 
1434
- function validateHealthCheckSuite(
1435
- providerId: string,
1436
- operationName: string,
1437
- suite: unknown,
1438
- ): void {
1320
+ function validateHealthCheckSuite(providerId: string, operationName: string, suite: unknown): void {
1439
1321
  const fieldPath = `operations.${operationName}.healthCheck`;
1440
1322
  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
- );
1323
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1324
+ rejectUnknownFields(suite as Record<string, unknown>, HEALTH_CHECK_SUITE_FIELDS, fieldPath);
1449
1325
  const s = suite as HealthCheckSuite;
1450
1326
  if (!isPositiveMsDurationString(s.interval))
1451
1327
  throw new ValidationError(
@@ -1455,11 +1331,7 @@ function validateHealthCheckSuite(
1455
1331
  },
1456
1332
  );
1457
1333
  if (s.schedule !== undefined) {
1458
- if (
1459
- !s.schedule ||
1460
- typeof s.schedule !== "object" ||
1461
- Array.isArray(s.schedule)
1462
- ) {
1334
+ if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
1463
1335
  throw new ValidationError(
1464
1336
  `Provider "${providerId}" ${fieldPath}.schedule must be an object.`,
1465
1337
  );
@@ -1469,11 +1341,7 @@ function validateHealthCheckSuite(
1469
1341
  `Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`,
1470
1342
  );
1471
1343
  }
1472
- rejectUnknownFields(
1473
- s.schedule,
1474
- new Set(["randomize"]),
1475
- `${fieldPath}.schedule`,
1476
- );
1344
+ rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
1477
1345
  const randomize = Reflect.get(s.schedule, "randomize");
1478
1346
  if (randomize !== undefined) {
1479
1347
  validateScheduleRandomization(
@@ -1484,15 +1352,11 @@ function validateHealthCheckSuite(
1484
1352
  }
1485
1353
  }
1486
1354
  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
- );
1355
+ assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
1356
+ min: HEALTH_CHECK_TIMEOUT_MS_MIN,
1357
+ max: HEALTH_CHECK_TIMEOUT_MS_MAX,
1358
+ label: "timeout",
1359
+ });
1496
1360
  }
1497
1361
  if (s.degradedThresholdMs !== undefined) {
1498
1362
  assertBoundedIntegerMs(
@@ -1505,10 +1369,7 @@ function validateHealthCheckSuite(
1505
1369
  },
1506
1370
  );
1507
1371
  }
1508
- if (
1509
- s.requiresConnection !== undefined &&
1510
- typeof s.requiresConnection !== "boolean"
1511
- )
1372
+ if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
1512
1373
  throw new ValidationError(
1513
1374
  `Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`,
1514
1375
  );
@@ -1540,14 +1401,8 @@ function validateHealthCheckUnsupported(
1540
1401
  unsupported: unknown,
1541
1402
  ): void {
1542
1403
  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
- );
1404
+ if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
1405
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
1551
1406
  rejectUnknownFields(
1552
1407
  unsupported as Record<string, unknown>,
1553
1408
  HEALTH_CHECK_UNSUPPORTED_FIELDS,
@@ -1581,12 +1436,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
1581
1436
  "steps",
1582
1437
  "run",
1583
1438
  ]);
1584
- const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
1585
- "kind",
1586
- "interval",
1587
- "jitter",
1588
- "randomize",
1589
- ]);
1439
+ const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
1590
1440
  const HEALTH_JOURNEY_STEP_FIELDS = new Set([
1591
1441
  "id",
1592
1442
  "description",
@@ -1605,10 +1455,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
1605
1455
  "minManualInterval",
1606
1456
  "publicRationale",
1607
1457
  ]);
1608
- const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
1609
- "enabled",
1610
- "reason",
1611
- ]);
1458
+ const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
1612
1459
  const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
1613
1460
  "enabled",
1614
1461
  "requiresAcknowledgement",
@@ -1628,32 +1475,18 @@ function validateHealthJourneyManualTrigger(
1628
1475
  manualTrigger: unknown,
1629
1476
  ): void {
1630
1477
  const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
1631
- if (
1632
- !manualTrigger ||
1633
- typeof manualTrigger !== "object" ||
1634
- Array.isArray(manualTrigger)
1635
- ) {
1478
+ if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
1636
1479
  throw new ValidationError(
1637
1480
  `Provider "${providerId}" ${fieldPath} must be an object when present.`,
1638
1481
  );
1639
1482
  }
1640
- rejectUnknownFields(
1641
- manualTrigger,
1642
- HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS,
1643
- fieldPath,
1644
- );
1483
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
1645
1484
  const enabled = Reflect.get(manualTrigger, "enabled");
1646
1485
  if (typeof enabled !== "boolean") {
1647
- throw new ValidationError(
1648
- `Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`,
1649
- );
1486
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`);
1650
1487
  }
1651
1488
  if (enabled === false) {
1652
- rejectUnknownFields(
1653
- manualTrigger,
1654
- HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS,
1655
- fieldPath,
1656
- );
1489
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS, fieldPath);
1657
1490
  if (
1658
1491
  Reflect.get(manualTrigger, "reason") !== undefined &&
1659
1492
  (typeof Reflect.get(manualTrigger, "reason") !== "string" ||
@@ -1665,25 +1498,15 @@ function validateHealthJourneyManualTrigger(
1665
1498
  }
1666
1499
  return;
1667
1500
  }
1668
- rejectUnknownFields(
1669
- manualTrigger,
1670
- HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS,
1671
- fieldPath,
1672
- );
1673
- const requiresAcknowledgement = Reflect.get(
1674
- manualTrigger,
1675
- "requiresAcknowledgement",
1676
- );
1501
+ rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS, fieldPath);
1502
+ const requiresAcknowledgement = Reflect.get(manualTrigger, "requiresAcknowledgement");
1677
1503
  if (typeof requiresAcknowledgement !== "boolean") {
1678
1504
  throw new ValidationError(
1679
1505
  `Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`,
1680
1506
  );
1681
1507
  }
1682
1508
  const risk = Reflect.get(manualTrigger, "risk");
1683
- if (
1684
- typeof risk !== "string" ||
1685
- !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)
1686
- ) {
1509
+ if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
1687
1510
  throw new ValidationError(
1688
1511
  `Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`,
1689
1512
  );
@@ -1694,10 +1517,7 @@ function validateHealthJourneyManualTrigger(
1694
1517
  );
1695
1518
  }
1696
1519
  const minManualInterval = Reflect.get(manualTrigger, "minManualInterval");
1697
- assertIsoDuration(
1698
- minManualInterval,
1699
- `Provider "${providerId}" ${fieldPath}.minManualInterval`,
1700
- );
1520
+ assertIsoDuration(minManualInterval, `Provider "${providerId}" ${fieldPath}.minManualInterval`);
1701
1521
  if (isoDurationMs(minManualInterval) <= 0) {
1702
1522
  throw new ValidationError(
1703
1523
  `Provider "${providerId}" ${fieldPath}.minManualInterval must be a positive duration.`,
@@ -1728,18 +1548,14 @@ const SMS_ORIGIN_FIELDS_BY_KIND: Record<string, ReadonlySet<string>> = {
1728
1548
  e164: new Set(["kind", "value", "display"]),
1729
1549
  nationalServiceCode: new Set(["kind", "country", "value", "display"]),
1730
1550
  };
1731
- const DURATION_RE =
1732
- /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
1551
+ const DURATION_RE = /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
1733
1552
  const E164_RE = /^\+[1-9]\d{1,14}$/;
1734
1553
  const ISO_COUNTRY_RE = /^[A-Z]{2}$/;
1735
1554
  const NATIONAL_SERVICE_CODE_RE = /^[0-9]{2,15}$/;
1736
1555
  const BCP47_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
1737
1556
  const JOURNEY_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
1738
1557
 
1739
- function assertIsoDuration(
1740
- value: unknown,
1741
- fieldPath: string,
1742
- ): asserts value is string {
1558
+ function assertIsoDuration(value: unknown, fieldPath: string): asserts value is string {
1743
1559
  if (typeof value !== "string" || !DURATION_RE.test(value)) {
1744
1560
  throw new ValidationError(
1745
1561
  `${fieldPath} must be an ISO 8601 duration for example PT8H or PT2M30S.`,
@@ -1754,15 +1570,10 @@ function isoDurationMs(value: string): number {
1754
1570
  const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
1755
1571
  const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
1756
1572
  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
- );
1573
+ return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
1760
1574
  }
1761
1575
 
1762
- function scheduleRandomizationMs(
1763
- randomize: unknown,
1764
- fieldPath: string,
1765
- ): number {
1576
+ function scheduleRandomizationMs(randomize: unknown, fieldPath: string): number {
1766
1577
  const mode = Reflect.get(randomize as object, "mode");
1767
1578
  switch (mode) {
1768
1579
  case "centered": {
@@ -1776,9 +1587,7 @@ function scheduleRandomizationMs(
1776
1587
  return isoDurationMs(maxDelay);
1777
1588
  }
1778
1589
  default:
1779
- throw new ValidationError(
1780
- `${fieldPath}.mode must be "centered" or "delayed".`,
1781
- );
1590
+ throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
1782
1591
  }
1783
1592
  }
1784
1593
 
@@ -1792,25 +1601,18 @@ function validateScheduleRandomization(
1792
1601
  }
1793
1602
  const mode = Reflect.get(randomize, "mode");
1794
1603
  const allowedFields =
1795
- mode === "centered"
1796
- ? new Set(["mode", "maxOffset"])
1797
- : new Set(["mode", "maxDelay"]);
1604
+ mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
1798
1605
  rejectUnknownFields(randomize, allowedFields, fieldPath);
1799
1606
  const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
1800
1607
  if (offsetMs <= 0) {
1801
1608
  throw new ValidationError(`${fieldPath} duration must be positive.`);
1802
1609
  }
1803
1610
  if (offsetMs >= intervalMs) {
1804
- throw new ValidationError(
1805
- `${fieldPath} duration must be shorter than schedule interval.`,
1806
- );
1611
+ throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
1807
1612
  }
1808
1613
  }
1809
1614
 
1810
- function assertIsoCountry(
1811
- value: unknown,
1812
- fieldPath: string,
1813
- ): asserts value is string {
1615
+ function assertIsoCountry(value: unknown, fieldPath: string): asserts value is string {
1814
1616
  if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
1815
1617
  throw new ValidationError(
1816
1618
  `${fieldPath} must be an ISO 3166-1 alpha-2 country code for example KR.`,
@@ -1833,9 +1635,7 @@ function normalizeIntervalDuration(input: string): string {
1833
1635
  ? durationMs / 3_600_000
1834
1636
  : durationMs / 86_400_000;
1835
1637
  if (!Number.isInteger(amount) || amount <= 0) {
1836
- throw new ValidationError(
1837
- `Journey schedule interval must be a positive duration.`,
1838
- );
1638
+ throw new ValidationError(`Journey schedule interval must be a positive duration.`);
1839
1639
  }
1840
1640
  if (unit === "s") return `PT${amount}S`;
1841
1641
  if (unit === "m") return `PT${amount}M`;
@@ -1894,11 +1694,7 @@ function countCapturingGroups(pattern: RegExp): number {
1894
1694
  if (inCharacterClass || char !== "(") continue;
1895
1695
  const next = source[i + 1];
1896
1696
  if (next === "?" && source[i + 2] !== "<") continue;
1897
- if (
1898
- next === "?" &&
1899
- source[i + 2] === "<" &&
1900
- (source[i + 3] === "=" || source[i + 3] === "!")
1901
- )
1697
+ if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
1902
1698
  continue;
1903
1699
  count += 1;
1904
1700
  }
@@ -1917,9 +1713,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
1917
1713
  }
1918
1714
  const kind = Reflect.get(origin, "kind");
1919
1715
  if (kind !== "e164" && kind !== "nationalServiceCode") {
1920
- throw new ValidationError(
1921
- `${fieldPath}.kind must be "e164" or "nationalServiceCode".`,
1922
- );
1716
+ throw new ValidationError(`${fieldPath}.kind must be "e164" or "nationalServiceCode".`);
1923
1717
  }
1924
1718
  rejectUnknownFields(origin, SMS_ORIGIN_FIELDS_BY_KIND[kind], fieldPath);
1925
1719
  if (kind === "e164") {
@@ -1946,9 +1740,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
1946
1740
  Reflect.get(origin, "display") !== undefined &&
1947
1741
  typeof Reflect.get(origin, "display") !== "string"
1948
1742
  ) {
1949
- throw new ValidationError(
1950
- `${fieldPath}.display must be a string when present.`,
1951
- );
1743
+ throw new ValidationError(`${fieldPath}.display must be a string when present.`);
1952
1744
  }
1953
1745
  }
1954
1746
 
@@ -1962,9 +1754,7 @@ function validateSmsOtpMatcher(
1962
1754
  rejectUnknownFields(matcher, SMS_OTP_MATCHER_FIELDS, fieldPath);
1963
1755
  const matcherId = Reflect.get(matcher, "id");
1964
1756
  if (typeof matcherId !== "string" || !JOURNEY_ID_RE.test(matcherId)) {
1965
- throw new ValidationError(
1966
- `${fieldPath}.id must be a kebab-case identifier.`,
1967
- );
1757
+ throw new ValidationError(`${fieldPath}.id must be a kebab-case identifier.`);
1968
1758
  }
1969
1759
  assertIsoCountry(Reflect.get(matcher, "country"), `${fieldPath}.country`);
1970
1760
  if (
@@ -1972,24 +1762,18 @@ function validateSmsOtpMatcher(
1972
1762
  (typeof Reflect.get(matcher, "locale") !== "string" ||
1973
1763
  !BCP47_RE.test(Reflect.get(matcher, "locale")))
1974
1764
  ) {
1975
- throw new ValidationError(
1976
- `${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`,
1977
- );
1765
+ throw new ValidationError(`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`);
1978
1766
  }
1979
1767
  if (
1980
1768
  Reflect.get(matcher, "phoneNumber") !== undefined &&
1981
1769
  (typeof Reflect.get(matcher, "phoneNumber") !== "string" ||
1982
1770
  !E164_RE.test(Reflect.get(matcher, "phoneNumber")))
1983
1771
  ) {
1984
- throw new ValidationError(
1985
- `${fieldPath}.phoneNumber must be an ITU-T E.164 number.`,
1986
- );
1772
+ throw new ValidationError(`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`);
1987
1773
  }
1988
1774
  const origins = Reflect.get(matcher, "origins");
1989
1775
  if (!Array.isArray(origins) || origins.length === 0) {
1990
- throw new ValidationError(
1991
- `${fieldPath}.origins must be a non-empty array.`,
1992
- );
1776
+ throw new ValidationError(`${fieldPath}.origins must be a non-empty array.`);
1993
1777
  }
1994
1778
  for (const [index, origin] of origins.entries()) {
1995
1779
  validateSmsOrigin(origin, `${fieldPath}.origins[${index}]`);
@@ -2010,10 +1794,7 @@ function validateSmsOtpMatcher(
2010
1794
  );
2011
1795
  }
2012
1796
  const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
2013
- if (
2014
- countCapturingGroups(regex) !== 1 &&
2015
- Reflect.get(code, "capture") === undefined
2016
- ) {
1797
+ if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
2017
1798
  throw new ValidationError(
2018
1799
  `${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`,
2019
1800
  );
@@ -2023,20 +1804,12 @@ function validateSmsOtpMatcher(
2023
1804
  typeof Reflect.get(code, "capture") !== "string" &&
2024
1805
  typeof Reflect.get(code, "capture") !== "number"
2025
1806
  ) {
2026
- throw new ValidationError(
2027
- `${fieldPath}.code.capture must be a string or number when present.`,
2028
- );
1807
+ throw new ValidationError(`${fieldPath}.code.capture must be a string or number when present.`);
2029
1808
  }
2030
1809
  assertIsoDuration(Reflect.get(matcher, "maxAge"), `${fieldPath}.maxAge`);
2031
- assertIsoDuration(
2032
- Reflect.get(matcher, "waitTimeout"),
2033
- `${fieldPath}.waitTimeout`,
2034
- );
1810
+ assertIsoDuration(Reflect.get(matcher, "waitTimeout"), `${fieldPath}.waitTimeout`);
2035
1811
  if (Reflect.get(matcher, "clockSkew") !== undefined)
2036
- assertIsoDuration(
2037
- Reflect.get(matcher, "clockSkew"),
2038
- `${fieldPath}.clockSkew`,
2039
- );
1812
+ assertIsoDuration(Reflect.get(matcher, "clockSkew"), `${fieldPath}.clockSkew`);
2040
1813
  }
2041
1814
 
2042
1815
  export function defineSmsOtpMatcher(
@@ -2068,9 +1841,7 @@ export function defineSmsOtpMatcher(
2068
1841
  return matcher;
2069
1842
  }
2070
1843
 
2071
- export function defineHealthJourney(
2072
- config: HealthJourneyDefinition,
2073
- ): HealthJourneyDefinition {
1844
+ export function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition {
2074
1845
  return config;
2075
1846
  }
2076
1847
 
@@ -2081,22 +1852,15 @@ function validateHealthJourneySchedule(
2081
1852
  ): void {
2082
1853
  const fieldPath = `healthJourneys.${journeyId}.schedule`;
2083
1854
  if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) {
2084
- throw new ValidationError(
2085
- `Provider "${providerId}" ${fieldPath} must be an object.`,
2086
- );
1855
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
2087
1856
  }
2088
1857
  rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
2089
1858
  if (Reflect.get(schedule, "kind") !== "interval")
2090
- throw new ValidationError(
2091
- `Provider "${providerId}" ${fieldPath}.kind must be "interval".`,
2092
- );
1859
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
2093
1860
  const interval = Reflect.get(schedule, "interval");
2094
1861
  assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
2095
1862
  const randomize = Reflect.get(schedule, "randomize");
2096
- if (
2097
- Reflect.get(schedule, "jitter") !== undefined &&
2098
- randomize !== undefined
2099
- ) {
1863
+ if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
2100
1864
  throw new ValidationError(
2101
1865
  `Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`,
2102
1866
  );
@@ -2123,17 +1887,13 @@ function validateHealthJourneys(
2123
1887
  const covered = new Set<string>();
2124
1888
  if (healthJourneys === undefined) return covered;
2125
1889
  if (!Array.isArray(healthJourneys)) {
2126
- throw new ValidationError(
2127
- `Provider "${providerId}" healthJourneys must be an array.`,
2128
- );
1890
+ throw new ValidationError(`Provider "${providerId}" healthJourneys must be an array.`);
2129
1891
  }
2130
1892
  const journeyIds = new Set<string>();
2131
1893
  for (const [index, journey] of healthJourneys.entries()) {
2132
1894
  const prefix = `healthJourneys[${index}]`;
2133
1895
  if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
2134
- throw new ValidationError(
2135
- `Provider "${providerId}" ${prefix} must be an object.`,
2136
- );
1896
+ throw new ValidationError(`Provider "${providerId}" ${prefix} must be an object.`);
2137
1897
  }
2138
1898
  rejectUnknownFields(journey, HEALTH_JOURNEY_FIELDS, prefix);
2139
1899
  if (typeof journey.id !== "string" || !JOURNEY_ID_RE.test(journey.id)) {
@@ -2147,10 +1907,7 @@ function validateHealthJourneys(
2147
1907
  );
2148
1908
  journeyIds.add(journey.id);
2149
1909
  validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
2150
- if (
2151
- !Array.isArray(journey.coversOperations) ||
2152
- journey.coversOperations.length === 0
2153
- ) {
1910
+ if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
2154
1911
  throw new ValidationError(
2155
1912
  `Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`,
2156
1913
  );
@@ -2185,10 +1942,7 @@ function validateHealthJourneys(
2185
1942
  `Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers must be an array.`,
2186
1943
  );
2187
1944
  for (const [matcherIndex, matcher] of journey.smsMatchers.entries()) {
2188
- validateSmsOtpMatcher(
2189
- matcher,
2190
- `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`,
2191
- );
1945
+ validateSmsOtpMatcher(matcher, `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`);
2192
1946
  if (matcherIds.has(matcher.id))
2193
1947
  throw new ValidationError(
2194
1948
  `Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers has duplicate matcher id "${matcher.id}".`,
@@ -2199,9 +1953,7 @@ function validateHealthJourneys(
2199
1953
  for (const [stepIndex, step] of journey.steps.entries()) {
2200
1954
  const stepPath = `healthJourneys.${journey.id}.steps[${stepIndex}]`;
2201
1955
  if (!step || typeof step !== "object" || Array.isArray(step))
2202
- throw new ValidationError(
2203
- `Provider "${providerId}" ${stepPath} must be an object.`,
2204
- );
1956
+ throw new ValidationError(`Provider "${providerId}" ${stepPath} must be an object.`);
2205
1957
  rejectUnknownFields(step, HEALTH_JOURNEY_STEP_FIELDS, stepPath);
2206
1958
  if (typeof step.id !== "string" || !JOURNEY_ID_RE.test(step.id))
2207
1959
  throw new ValidationError(
@@ -2211,20 +1963,13 @@ function validateHealthJourneys(
2211
1963
  throw new ValidationError(
2212
1964
  `Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`,
2213
1965
  );
2214
- if (
2215
- step.usesSmsMatcher !== undefined &&
2216
- !matcherIds.has(step.usesSmsMatcher)
2217
- )
1966
+ if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
2218
1967
  throw new ValidationError(
2219
1968
  `Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`,
2220
1969
  );
2221
1970
  }
2222
1971
  if (journey.manualTrigger !== undefined)
2223
- validateHealthJourneyManualTrigger(
2224
- providerId,
2225
- journey.id,
2226
- journey.manualTrigger,
2227
- );
1972
+ validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
2228
1973
  if (journey.timeout !== undefined)
2229
1974
  assertIsoDuration(
2230
1975
  journey.timeout,
@@ -2270,23 +2015,10 @@ function validateOperationHealthChecks(
2270
2015
  fix: `Remove either operations.${operationName}.healthCheck or operations.${operationName}.healthCheckUnsupported.`,
2271
2016
  },
2272
2017
  );
2273
- if (hasCheck)
2274
- validateHealthCheckSuite(
2275
- providerId,
2276
- operationName,
2277
- operation.healthCheck,
2278
- );
2018
+ if (hasCheck) validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
2279
2019
  if (hasUnsupported)
2280
- validateHealthCheckUnsupported(
2281
- providerId,
2282
- operationName,
2283
- operation.healthCheckUnsupported,
2284
- );
2285
- if (
2286
- !hasCheck &&
2287
- !hasUnsupported &&
2288
- !journeyCoveredOperations.has(operationName)
2289
- )
2020
+ validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
2021
+ if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
2290
2022
  throw new ValidationError(
2291
2023
  `Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`,
2292
2024
  {
@@ -2341,6 +2073,19 @@ function validateOperationFixtures(
2341
2073
  }
2342
2074
  }
2343
2075
 
2076
+ /**
2077
+ * Shallow shape guard only: the `deployment` object is passed through
2078
+ * verbatim and deliberately not deep-validated by the SDK — the APIFuse
2079
+ * registry builder owns deployment validation and profile resolution.
2080
+ */
2081
+ function validateProviderDeployment(providerId: string, deployment: unknown): void {
2082
+ if (deployment === undefined) return;
2083
+ if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
2084
+ throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
2085
+ fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
2086
+ });
2087
+ }
2088
+
2344
2089
  export function defineProvider<
2345
2090
  TOperations extends Record<string, ProviderOperation>,
2346
2091
  TConfig extends ProviderConfig<TOperations>,
@@ -2353,12 +2098,9 @@ export function defineProvider<
2353
2098
  fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
2354
2099
  });
2355
2100
  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
- );
2101
+ throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
2102
+ fix: "Add at least one operation to the operations object",
2103
+ });
2362
2104
  validateOperationIds(config.id, config.operations);
2363
2105
  validateOperationAnnotations(config.id, config.operations);
2364
2106
  validateOperationObservability(config.id, config.operations);
@@ -2370,11 +2112,7 @@ export function defineProvider<
2370
2112
  config.operations,
2371
2113
  config.healthJourneys,
2372
2114
  );
2373
- validateOperationHealthChecks(
2374
- config.id,
2375
- config.operations,
2376
- journeyCoveredOperations,
2377
- );
2115
+ validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
2378
2116
  if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
2379
2117
  throw new ValidationError(
2380
2118
  `Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
@@ -2388,6 +2126,7 @@ export function defineProvider<
2388
2126
  config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
2389
2127
  );
2390
2128
  validateOperationFixtures(config.id, config.operations);
2129
+ validateProviderDeployment(config.id, config.deployment);
2391
2130
  validateProviderProxy(config);
2392
2131
  validateProviderStt(config);
2393
2132
  if (config.runtime === "browser" && !config.browser)
@@ -2406,6 +2145,9 @@ export function defineProvider<
2406
2145
  id: config.id,
2407
2146
  version: config.version,
2408
2147
  runtime: config.runtime,
2148
+ // Verbatim passthrough: deployment validation and profile resolution
2149
+ // are owned by the APIFuse registry builder, not the SDK.
2150
+ deployment: config.deployment,
2409
2151
  allowedHosts: config.allowedHosts,
2410
2152
  stealth: config.stealth,
2411
2153
  proxy: config.proxy,