@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.10

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 (263) hide show
  1. package/AUTHORING.md +240 -0
  2. package/CHANGELOG.md +93 -0
  3. package/README.md +26 -10
  4. package/SUBMISSION.md +11 -12
  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 +36 -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 +2177 -353
  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/auth-turn.v1.schema.json +89 -0
  20. package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  21. package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  22. package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  23. package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  24. package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  25. package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  26. package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  27. package/dist/auth-turn/fixtures/valid/abort.json +8 -0
  28. package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
  29. package/dist/auth-turn/fixtures/valid/complete.json +13 -0
  30. package/dist/auth-turn/fixtures/valid/form.json +14 -0
  31. package/dist/auth-turn/fixtures/valid/message.json +13 -0
  32. package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
  33. package/dist/auth-turn/fixtures/valid/pending.json +5 -0
  34. package/dist/auth-turn/fixtures/valid/poll.json +9 -0
  35. package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
  36. package/dist/auth-turn/fixtures/valid/retry.json +8 -0
  37. package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  38. package/dist/auth-turn/index.d.ts +195 -0
  39. package/dist/auth-turn/index.js +133 -0
  40. package/dist/auth.d.ts +76 -0
  41. package/dist/auth.js +427 -0
  42. package/dist/ceremonies/index.d.ts +1 -1
  43. package/dist/ceremonies/index.js +14 -48
  44. package/dist/cli/commands.d.ts +1 -1
  45. package/dist/cli/commands.js +8 -0
  46. package/dist/cli/create.d.ts +3 -0
  47. package/dist/cli/create.js +47 -33
  48. package/dist/cli/prompt-assets.d.ts +80 -0
  49. package/dist/cli/prompt-assets.js +743 -0
  50. package/dist/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  51. package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  52. package/dist/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
  53. package/dist/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  54. package/dist/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  55. package/dist/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
  56. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  57. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  58. package/dist/cli/templates/provider/AGENTS.md.tpl +96 -0
  59. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  60. package/dist/cli/templates/provider/README.md.tpl +163 -0
  61. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  62. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  63. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  64. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  65. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  66. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  67. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  68. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  69. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  70. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  71. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  72. package/dist/config/loader.d.ts +149 -8
  73. package/dist/config/loader.js +378 -89
  74. package/dist/contract-serialization.d.ts +2 -2
  75. package/dist/contract-serialization.js +3 -6
  76. package/dist/contract-types.d.ts +2 -2
  77. package/dist/contract.d.ts +3 -3
  78. package/dist/contract.js +5 -6
  79. package/dist/define.d.ts +13 -1
  80. package/dist/define.js +245 -178
  81. package/dist/dev.d.ts +1 -1
  82. package/dist/dev.js +1 -1
  83. package/dist/errors.d.ts +4 -1
  84. package/dist/errors.js +48 -0
  85. package/dist/i18n/catalog.d.ts +2 -2
  86. package/dist/i18n/catalog.js +4 -10
  87. package/dist/i18n/index.d.ts +2 -2
  88. package/dist/i18n/index.js +2 -2
  89. package/dist/i18n/keys.d.ts +2 -2
  90. package/dist/index.d.ts +44 -41
  91. package/dist/index.js +39 -36
  92. package/dist/lint.d.ts +1 -0
  93. package/dist/lint.js +35 -15
  94. package/dist/provider.d.ts +11 -9
  95. package/dist/provider.js +9 -8
  96. package/dist/public-schema-field-lint.d.ts +1 -1
  97. package/dist/recipes/gov-api.js +1 -1
  98. package/dist/runtime/auth-flow.d.ts +1 -1
  99. package/dist/runtime/auth-flow.js +4 -2
  100. package/dist/runtime/browser.d.ts +1 -1
  101. package/dist/runtime/browser.js +214 -25
  102. package/dist/runtime/cache.d.ts +1 -1
  103. package/dist/runtime/cache.js +4 -8
  104. package/dist/runtime/choice.d.ts +1 -1
  105. package/dist/runtime/choice.js +31 -35
  106. package/dist/runtime/credential.d.ts +1 -1
  107. package/dist/runtime/credential.js +1 -1
  108. package/dist/runtime/env.d.ts +1 -1
  109. package/dist/runtime/executor.d.ts +1 -1
  110. package/dist/runtime/executor.js +15 -3
  111. package/dist/runtime/http.d.ts +2 -2
  112. package/dist/runtime/http.js +160 -344
  113. package/dist/runtime/insights.d.ts +1 -1
  114. package/dist/runtime/insights.js +6 -13
  115. package/dist/runtime/instrumentation.d.ts +2 -2
  116. package/dist/runtime/instrumentation.js +56 -19
  117. package/dist/runtime/keyring.js +1 -1
  118. package/dist/runtime/namespace.js +1 -1
  119. package/dist/runtime/otlp.d.ts +1 -1
  120. package/dist/runtime/perf.d.ts +1 -1
  121. package/dist/runtime/provider.d.ts +1 -1
  122. package/dist/runtime/provider.js +1 -2
  123. package/dist/runtime/proxy-errors.d.ts +1 -1
  124. package/dist/runtime/proxy-errors.js +9 -7
  125. package/dist/runtime/proxy-nodemaven.d.ts +35 -0
  126. package/dist/runtime/proxy-nodemaven.js +128 -0
  127. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  128. package/dist/runtime/proxy-retry-policy.js +326 -0
  129. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  130. package/dist/runtime/proxy-telemetry.js +55 -52
  131. package/dist/runtime/redis.d.ts +1 -1
  132. package/dist/runtime/redis.js +2 -4
  133. package/dist/runtime/request-options.d.ts +1 -1
  134. package/dist/runtime/secrets.d.ts +27 -0
  135. package/dist/runtime/secrets.js +51 -0
  136. package/dist/runtime/state.d.ts +2 -2
  137. package/dist/runtime/state.js +15 -4
  138. package/dist/runtime/stealth.d.ts +7 -4
  139. package/dist/runtime/stealth.js +257 -215
  140. package/dist/runtime/stt.d.ts +1 -1
  141. package/dist/runtime/stt.js +11 -15
  142. package/dist/runtime/trace.d.ts +2 -2
  143. package/dist/runtime/trace.js +2 -4
  144. package/dist/runtime/waterfall.d.ts +1 -1
  145. package/dist/schema.d.ts +1 -1
  146. package/dist/schema.js +7 -15
  147. package/dist/serve.d.ts +1 -1
  148. package/dist/serve.js +1 -1
  149. package/dist/server/index.d.ts +7 -3
  150. package/dist/server/index.js +6 -2
  151. package/dist/server/self-test-input-tokens.d.ts +1 -0
  152. package/dist/server/self-test-input-tokens.js +37 -0
  153. package/dist/server/self-test-redaction.d.ts +20 -0
  154. package/dist/server/self-test-redaction.js +70 -0
  155. package/dist/server/self-test-token.d.ts +30 -0
  156. package/dist/server/self-test-token.js +50 -0
  157. package/dist/server/self-test.d.ts +199 -0
  158. package/dist/server/self-test.js +1113 -0
  159. package/dist/server/serve.d.ts +14 -3
  160. package/dist/server/serve.js +135 -64
  161. package/dist/server/types.d.ts +10 -9
  162. package/dist/server/types.js +3 -7
  163. package/dist/stealth/profiles.d.ts +1 -1
  164. package/dist/stealth/profiles.js +5 -14
  165. package/dist/stream.d.ts +1 -1
  166. package/dist/testing/index.d.ts +2 -2
  167. package/dist/testing/index.js +2 -2
  168. package/dist/testing/run.d.ts +1 -1
  169. package/dist/testing/run.js +12 -15
  170. package/dist/types.d.ts +237 -1
  171. package/dist/user-input.d.ts +30 -0
  172. package/dist/user-input.js +66 -0
  173. package/package.json +16 -5
  174. package/src/auth-turn/auth-turn.v1.schema.json +89 -0
  175. package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  176. package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  177. package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  178. package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  179. package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  180. package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  181. package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  182. package/src/auth-turn/fixtures/valid/abort.json +8 -0
  183. package/src/auth-turn/fixtures/valid/challenge.json +17 -0
  184. package/src/auth-turn/fixtures/valid/complete.json +13 -0
  185. package/src/auth-turn/fixtures/valid/form.json +14 -0
  186. package/src/auth-turn/fixtures/valid/message.json +13 -0
  187. package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
  188. package/src/auth-turn/fixtures/valid/pending.json +5 -0
  189. package/src/auth-turn/fixtures/valid/poll.json +9 -0
  190. package/src/auth-turn/fixtures/valid/redirect.json +16 -0
  191. package/src/auth-turn/fixtures/valid/retry.json +8 -0
  192. package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  193. package/src/auth-turn/index.ts +177 -0
  194. package/src/auth.ts +728 -0
  195. package/src/ceremonies/index.ts +33 -121
  196. package/src/cli/commands.ts +10 -0
  197. package/src/cli/create.ts +69 -99
  198. package/src/cli/prompt-assets.ts +865 -0
  199. package/src/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  200. package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  201. package/src/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
  202. package/src/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  203. package/src/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  204. package/src/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
  205. package/src/cli/templates/provider/AGENTS.md.tpl +96 -0
  206. package/src/cli/templates/provider/README.md.tpl +7 -4
  207. package/src/config/loader.ts +543 -208
  208. package/src/contract-serialization.ts +5 -11
  209. package/src/contract-types.ts +2 -2
  210. package/src/contract.ts +13 -28
  211. package/src/define.ts +397 -528
  212. package/src/dev.ts +4 -9
  213. package/src/errors.ts +58 -5
  214. package/src/i18n/catalog.ts +10 -32
  215. package/src/i18n/index.ts +2 -2
  216. package/src/i18n/keys.ts +5 -11
  217. package/src/index.ts +64 -41
  218. package/src/lint.ts +122 -159
  219. package/src/provider.ts +40 -9
  220. package/src/public-schema-field-lint.ts +7 -33
  221. package/src/recipes/gov-api.ts +2 -5
  222. package/src/runtime/auth-flow.ts +6 -6
  223. package/src/runtime/browser.ts +320 -151
  224. package/src/runtime/cache.ts +20 -67
  225. package/src/runtime/choice.ts +79 -132
  226. package/src/runtime/credential.ts +2 -2
  227. package/src/runtime/env.ts +1 -1
  228. package/src/runtime/executor.ts +23 -20
  229. package/src/runtime/http.ts +216 -539
  230. package/src/runtime/insights.ts +15 -53
  231. package/src/runtime/instrumentation.ts +78 -65
  232. package/src/runtime/keyring.ts +7 -19
  233. package/src/runtime/namespace.ts +2 -7
  234. package/src/runtime/otlp.ts +12 -23
  235. package/src/runtime/perf.ts +1 -1
  236. package/src/runtime/provider.ts +4 -9
  237. package/src/runtime/proxy-errors.ts +29 -42
  238. package/src/runtime/proxy-nodemaven.ts +178 -0
  239. package/src/runtime/proxy-retry-policy.ts +469 -0
  240. package/src/runtime/proxy-telemetry.ts +79 -77
  241. package/src/runtime/redis.ts +4 -12
  242. package/src/runtime/request-options.ts +4 -13
  243. package/src/runtime/secrets.ts +64 -0
  244. package/src/runtime/state.ts +41 -110
  245. package/src/runtime/stealth.ts +331 -369
  246. package/src/runtime/stt.ts +38 -94
  247. package/src/runtime/trace.ts +14 -44
  248. package/src/runtime/waterfall.ts +5 -18
  249. package/src/schema.ts +23 -84
  250. package/src/serve.ts +1 -1
  251. package/src/server/index.ts +44 -3
  252. package/src/server/self-test-input-tokens.ts +46 -0
  253. package/src/server/self-test-redaction.ts +97 -0
  254. package/src/server/self-test-token.ts +70 -0
  255. package/src/server/self-test.ts +1450 -0
  256. package/src/server/serve.ts +206 -216
  257. package/src/server/types.ts +7 -19
  258. package/src/stealth/profiles.ts +10 -26
  259. package/src/stream.ts +8 -19
  260. package/src/testing/index.ts +2 -2
  261. package/src/testing/run.ts +24 -64
  262. package/src/types.ts +274 -1
  263. package/src/user-input.ts +118 -0
package/dist/define.js CHANGED
@@ -1,26 +1,14 @@
1
- import { ProviderError, ValidationError } from "./errors";
2
- import { safeParseSchemaSync } from "./schema";
3
- import { HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX, HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN, HEALTH_CHECK_TIMEOUT_MS_MAX, HEALTH_CHECK_TIMEOUT_MS_MIN, OPERATION_TIMEOUT_MS_MAX, OPERATION_TIMEOUT_MS_MIN, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, } from "./types";
1
+ import ms from "ms";
2
+ import { ProviderError, ValidationError } from "./errors.js";
3
+ import { safeParseSchemaSync } from "./schema.js";
4
+ import { HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX, HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN, HEALTH_CHECK_TIMEOUT_MS_MAX, HEALTH_CHECK_TIMEOUT_MS_MIN, OPERATION_TIMEOUT_MS_MAX, OPERATION_TIMEOUT_MS_MIN, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, } from "./types.js";
4
5
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
5
6
  const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
6
7
  const VALID_RUNTIMES = ["standard", "shared", "browser"];
7
- const VALID_AUTH_MODES = [
8
- "none",
9
- "platform-managed",
10
- "credentials",
11
- "oauth2",
12
- ];
8
+ const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"];
13
9
  const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"];
14
- const VALID_PROVIDER_PROXY_MODES = [
15
- "disabled",
16
- "optional",
17
- "required",
18
- ];
19
- const VALID_PROVIDER_PROXY_PROVIDERS = [
20
- "smartproxy",
21
- "decodo",
22
- "custom",
23
- ];
10
+ const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"];
11
+ const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"];
24
12
  const VALID_PROVIDER_PROXY_AFFINITIES = [
25
13
  "request",
26
14
  "operation",
@@ -29,72 +17,44 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
29
17
  ];
30
18
  const VALID_PROVIDER_STT_MODES = ["optional", "required"];
31
19
  const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
20
+ const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
21
+ const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
22
+ // Per-vendor provider-declared credential secrets. A required-mode chain must
23
+ // declare every secret of every credentialed vendor it names, so a missing
24
+ // credential fails at build/validation time rather than during a live outage: a
25
+ // declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
26
+ // exactly the failure class the multi-vendor chain exists to remove. Vendors
27
+ // absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
28
+ // `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
29
+ // no declaration requirement.
30
+ const VENDOR_REQUIRED_SECRETS = {
31
+ smartproxy: [SMARTPROXY_APP_KEY_SECRET],
32
+ nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
33
+ };
32
34
  const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
33
35
  const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
34
- const VALID_OPERATION_RISK_CLASSES = [
35
- "read",
36
- "write",
37
- "destructive",
38
- "external-send",
39
- ];
40
- const VALID_OPERATION_APPROVAL_POLICIES = [
41
- "never",
42
- "risk-based",
43
- "always",
44
- ];
45
- const VALID_OPERATION_TRANSPORT_KINDS = [
46
- "json",
47
- "sse",
48
- "http-stream",
49
- "websocket",
50
- ];
36
+ const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"];
37
+ const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"];
38
+ const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"];
51
39
  const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
52
40
  const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
53
- const MS_DURATION_UNITS = new Set([
54
- "years",
55
- "year",
56
- "yrs",
57
- "yr",
58
- "y",
59
- "weeks",
60
- "week",
61
- "w",
62
- "days",
63
- "day",
64
- "d",
65
- "hours",
66
- "hour",
67
- "hrs",
68
- "hr",
69
- "h",
70
- "minutes",
71
- "minute",
72
- "mins",
73
- "min",
74
- "m",
75
- "seconds",
76
- "second",
77
- "secs",
78
- "sec",
79
- "s",
80
- "milliseconds",
81
- "millisecond",
82
- "msecs",
83
- "msec",
84
- "ms",
85
- ]);
86
41
  const MS_DURATION_PATTERN = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*([a-zA-Z]+)?$/;
87
42
  function isPositiveMsDurationString(value) {
88
43
  if (typeof value !== "string")
89
44
  return false;
90
- const match = value.trim().match(MS_DURATION_PATTERN);
91
- if (!match)
92
- return false;
93
- const amount = Number(match[1]);
94
- if (!Number.isFinite(amount) || amount <= 0)
95
- return false;
96
- const unit = match[2]?.toLowerCase();
97
- return unit === undefined || MS_DURATION_UNITS.has(unit);
45
+ return parsePositiveMsDuration(value) !== undefined;
46
+ }
47
+ function msDurationMs(value) {
48
+ return parsePositiveMsDuration(value) ?? 0;
49
+ }
50
+ function parsePositiveMsDuration(value) {
51
+ const trimmed = value.trim();
52
+ if (!MS_DURATION_PATTERN.test(trimmed))
53
+ return undefined;
54
+ const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed));
55
+ if (!Number.isFinite(parsed) || parsed <= 0)
56
+ return undefined;
57
+ return parsed;
98
58
  }
99
59
  /** Define one provider operation with schema-driven handler inference. */
100
60
  export function defineOperation(operation) {
@@ -136,11 +96,13 @@ function validateProviderShape(config) {
136
96
  if (typeof config.runtime === "string")
137
97
  assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
138
98
  const auth = config.auth;
139
- if (auth &&
140
- typeof auth === "object" &&
141
- "mode" in auth &&
142
- typeof auth.mode === "string")
99
+ if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
143
100
  assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
101
+ if (auth && typeof auth === "object" && "exchange" in auth) {
102
+ throw new ProviderError(`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`, {
103
+ fix: "Use the single canonical auth interface: auth.flow. Gateway calls auth.flow.start/continue/poll/abort/refresh only and persists complete turn data.credential as-is, so put login/token/session exchange inside auth.flow.continue.",
104
+ });
105
+ }
144
106
  if (auth &&
145
107
  typeof auth === "object" &&
146
108
  "flow" in auth &&
@@ -189,15 +151,23 @@ function validateProviderProxy(config) {
189
151
  fix: `Use proxy: { mode: "required", provider: "smartproxy", geo: { country: "KR" }, session: { affinity: "connection", lifetimeMinutes: 30 } }`,
190
152
  });
191
153
  }
192
- rejectUnknownFields(proxy, new Set(["mode", "provider", "geo", "session"]), "proxy");
154
+ rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
193
155
  assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
194
156
  if (proxy.provider !== undefined) {
195
157
  assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
196
158
  }
159
+ if (proxy.providers !== undefined) {
160
+ if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
161
+ throw new ValidationError(`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`, {
162
+ fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
163
+ });
164
+ }
165
+ for (const vendor of proxy.providers) {
166
+ assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
167
+ }
168
+ }
197
169
  if (proxy.geo !== undefined) {
198
- if (!proxy.geo ||
199
- typeof proxy.geo !== "object" ||
200
- Array.isArray(proxy.geo)) {
170
+ if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
201
171
  throw new ValidationError(`Provider "${config.id}" has invalid proxy.geo: must be an object.`, {
202
172
  fix: `Use proxy.geo: { country: "KR" } with ISO alpha-2 country codes.`,
203
173
  });
@@ -214,9 +184,7 @@ function validateProviderProxy(config) {
214
184
  }
215
185
  }
216
186
  if (proxy.session !== undefined) {
217
- if (!proxy.session ||
218
- typeof proxy.session !== "object" ||
219
- Array.isArray(proxy.session)) {
187
+ if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
220
188
  throw new ValidationError(`Provider "${config.id}" has invalid proxy.session: must be an object.`, {
221
189
  fix: `Use proxy.session: { affinity: "connection", lifetimeMinutes: 30 }.`,
222
190
  });
@@ -226,31 +194,62 @@ function validateProviderProxy(config) {
226
194
  assertLiteralField(proxy.session.affinity, "proxy.session.affinity", VALID_PROVIDER_PROXY_AFFINITIES, config.id);
227
195
  }
228
196
  const lifetime = proxy.session.lifetimeMinutes;
229
- if (lifetime !== undefined &&
230
- (!Number.isFinite(lifetime) || lifetime <= 0)) {
197
+ if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
231
198
  throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`);
232
199
  }
233
200
  const poolSize = proxy.session.poolSize;
234
- if (poolSize !== undefined &&
235
- (!Number.isInteger(poolSize) || poolSize <= 0)) {
201
+ if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
236
202
  throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`);
237
203
  }
238
204
  }
239
- if (proxy.mode === "required" && proxy.provider === "smartproxy") {
240
- const hasSmartproxySecret = config.secrets?.some((secret) => secret.name === SMARTPROXY_APP_KEY_SECRET && secret.required !== false);
241
- if (!hasSmartproxySecret) {
242
- throw new ValidationError(`Provider "${config.id}" requires Smartproxy egress but does not declare ${SMARTPROXY_APP_KEY_SECRET}.`, {
243
- fix: `Add secrets: [{ name: "${SMARTPROXY_APP_KEY_SECRET}", required: true }] to the provider.`,
244
- });
205
+ // Every credentialed vendor in a required-mode chain must declare its
206
+ // provider secret(s) so a missing credential fails at build/validation time,
207
+ // not during a live outage. This covers the fallback legs too (not just the
208
+ // first vendor): a declared-but-uncredentialed nodemaven fallback would leave
209
+ // the chain silently down to a single vendor, reintroducing the SPOF the chain
210
+ // removes.
211
+ const vendorChain = proxy.providers && proxy.providers.length > 0
212
+ ? proxy.providers
213
+ : proxy.provider
214
+ ? [proxy.provider]
215
+ : [];
216
+ if (proxy.mode === "required") {
217
+ for (const vendor of vendorChain) {
218
+ const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
219
+ if (!requiredSecrets)
220
+ continue;
221
+ for (const secretName of requiredSecrets) {
222
+ // Match the canonical runtime gate (assertRequiredSecretsPresent /
223
+ // listMissingRequiredSecrets), which enforces only `required === true`
224
+ // declarations. A declaration that omits `required` (defaulting to
225
+ // optional) is skipped at runtime, so accepting it here would pass
226
+ // validation while leaving the credential unenforced until proxy
227
+ // resolution during a live request — the fail-open gap this check exists
228
+ // to close.
229
+ const declared = config.secrets?.some((secret) => secret.name === secretName && secret.required === true);
230
+ if (!declared) {
231
+ throw new ValidationError(`Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`, {
232
+ fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
233
+ });
234
+ }
235
+ }
245
236
  }
246
237
  }
238
+ // `decodo`/`custom` are deprecated vendor values (string-union members, so the
239
+ // @deprecated symbol gate can't catch them — warn at validation time instead).
240
+ const deprecatedVendors = vendorChain.filter((vendor) => vendor === "decodo" || vendor === "custom");
241
+ if (deprecatedVendors.length > 0) {
242
+ console.warn(`[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.`);
243
+ }
247
244
  }
248
245
  function validateProviderStt(config) {
249
246
  const stt = config.stt;
250
247
  if (stt === undefined)
251
248
  return;
252
249
  if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
253
- throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, { fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.` });
250
+ throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
251
+ fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
252
+ });
254
253
  }
255
254
  rejectUnknownFields(stt, new Set(["mode"]), "stt");
256
255
  assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
@@ -269,12 +268,7 @@ function validateOperationIds(providerId, operations) {
269
268
  }
270
269
  const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
271
270
  const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
272
- const VALID_OPERATION_LIFECYCLES = [
273
- "stable",
274
- "beta",
275
- "deprecated",
276
- "removed",
277
- ];
271
+ const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"];
278
272
  function assertNonEmptyString(value, field, providerId, operationName) {
279
273
  if (typeof value !== "string" || value.trim().length === 0) {
280
274
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${field}: must be a non-empty string.`, { fix: `Set ${field} to a non-empty customer-facing value.` });
@@ -290,8 +284,7 @@ function validateToolRouterMetadata(providerId, operations) {
290
284
  fix: `Remove operations.${operationName}.toolRouter or provide MCP-safe metadata.`,
291
285
  });
292
286
  }
293
- if (toolRouter.name !== undefined &&
294
- !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
287
+ if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
295
288
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`, {
296
289
  fix: `Use letters, numbers, and underscores only, starting with a letter, for example "${providerId.replace(/[^A-Za-z0-9]+/g, "_")}__${operationName.replace(/[^A-Za-z0-9]+/g, "_")}".`,
297
290
  });
@@ -329,8 +322,7 @@ function validateOperationContracts(providerId, operations) {
329
322
  if (contract.lifecycle !== undefined) {
330
323
  assertLiteralField(contract.lifecycle, `operations.${operationName}.contract.lifecycle`, VALID_OPERATION_LIFECYCLES, providerId);
331
324
  }
332
- if (contract.lifecycle === "deprecated" ||
333
- contract.lifecycle === "removed") {
325
+ if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
334
326
  if (!contract.deprecation || typeof contract.deprecation !== "object") {
335
327
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`, {
336
328
  fix: `Add announcedAt, removalAfter, and migrationGuide to operations.${operationName}.contract.deprecation.`,
@@ -355,8 +347,7 @@ function validateOperationAnnotations(providerId, operations) {
355
347
  throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an integer number of milliseconds.`, {
356
348
  fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
357
349
  });
358
- if (timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
359
- timeoutMs > OPERATION_TIMEOUT_MS_MAX)
350
+ if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
360
351
  throw new ValidationError(`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`, {
361
352
  fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms (the upper bound stays below the gateway/ALB ceiling).`,
362
353
  });
@@ -500,9 +491,7 @@ function validateOperationTransports(providerId, operations) {
500
491
  assertStreamMs(maxDurationMs, `${fieldPath}.maxDurationMs`, STREAM_MAX_DURATION_MS_MIN, STREAM_MAX_DURATION_MS_MAX, "max duration");
501
492
  assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
502
493
  const resumable = Reflect.get(transport, "resumable");
503
- if (resumable !== undefined &&
504
- resumable !== false &&
505
- resumable !== "last-event-id") {
494
+ if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
506
495
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`, {
507
496
  fix: `Use ${fieldPath}.resumable: "last-event-id" for SSE Last-Event-ID resume support, or false to disable resume.`,
508
497
  });
@@ -537,8 +526,7 @@ function validateOperationTransports(providerId, operations) {
537
526
  });
538
527
  }
539
528
  for (const subprotocol of subprotocols) {
540
- if (typeof subprotocol !== "string" ||
541
- !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
529
+ if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
542
530
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`, {
543
531
  fix: `Use values such as "apifuse.v1" without spaces or separators that are invalid for Sec-WebSocket-Protocol.`,
544
532
  });
@@ -555,6 +543,7 @@ function validateOperationTransports(providerId, operations) {
555
543
  }
556
544
  const HEALTH_CHECK_SUITE_FIELDS = new Set([
557
545
  "interval",
546
+ "schedule",
558
547
  "timeoutMs",
559
548
  "degradedThresholdMs",
560
549
  "cases",
@@ -642,26 +631,24 @@ function assertBoundedIntegerMs(value, fieldPath, options) {
642
631
  });
643
632
  }
644
633
  }
645
- function validateProviderHealthMonitor(providerId, healthMonitor) {
634
+ function validateProviderHealthMonitor(providerId, healthMonitor, field = "healthMonitor") {
646
635
  if (healthMonitor === undefined)
647
636
  return;
648
- if (!healthMonitor ||
649
- typeof healthMonitor !== "object" ||
650
- Array.isArray(healthMonitor))
651
- throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor: must be an object.`, {
652
- fix: `Set healthMonitor to { requiredSecrets?: string[]; serviceAccount?: string }`,
637
+ if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
638
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
639
+ fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
653
640
  });
654
641
  const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
655
- rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, "healthMonitor");
642
+ rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
656
643
  if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
657
- assertBoundedIntegerMs(healthMonitorRecord.defaultProbeTimeoutMs, `Provider "${providerId}" healthMonitor.defaultProbeTimeoutMs`, {
644
+ assertBoundedIntegerMs(healthMonitorRecord.defaultProbeTimeoutMs, `Provider "${providerId}" ${field}.defaultProbeTimeoutMs`, {
658
645
  min: HEALTH_CHECK_TIMEOUT_MS_MIN,
659
646
  max: HEALTH_CHECK_TIMEOUT_MS_MAX,
660
647
  label: "timeout",
661
648
  });
662
649
  }
663
650
  if (healthMonitorRecord.defaultDegradedThresholdMs !== undefined) {
664
- assertBoundedIntegerMs(healthMonitorRecord.defaultDegradedThresholdMs, `Provider "${providerId}" healthMonitor.defaultDegradedThresholdMs`, {
651
+ assertBoundedIntegerMs(healthMonitorRecord.defaultDegradedThresholdMs, `Provider "${providerId}" ${field}.defaultDegradedThresholdMs`, {
665
652
  min: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
666
653
  max: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
667
654
  label: "degraded threshold",
@@ -670,10 +657,10 @@ function validateProviderHealthMonitor(providerId, healthMonitor) {
670
657
  const requiredSecrets = healthMonitorRecord.requiredSecrets;
671
658
  if (requiredSecrets !== undefined) {
672
659
  if (!Array.isArray(requiredSecrets))
673
- throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor.requiredSecrets: must be string[].`);
660
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}.requiredSecrets: must be string[].`);
674
661
  for (const [index, secret] of requiredSecrets.entries()) {
675
662
  if (typeof secret !== "string" || secret.length === 0)
676
- throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor.requiredSecrets[${index}]: must be a non-empty string.`);
663
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}.requiredSecrets[${index}]: must be a non-empty string.`);
677
664
  }
678
665
  }
679
666
  const credentialInputs = healthMonitorRecord.credentialInputs;
@@ -681,45 +668,43 @@ function validateProviderHealthMonitor(providerId, healthMonitor) {
681
668
  if (!credentialInputs ||
682
669
  typeof credentialInputs !== "object" ||
683
670
  Array.isArray(credentialInputs)) {
684
- throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor.credentialInputs: must be an object mapping auth input fields to env var names.`);
671
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}.credentialInputs: must be an object mapping auth input fields to env var names.`);
685
672
  }
686
- for (const [field, envVar] of Object.entries(credentialInputs)) {
687
- if (field.trim().length === 0) {
688
- throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor.credentialInputs key: must be a non-empty auth input field.`);
673
+ for (const [inputField, envVar] of Object.entries(credentialInputs)) {
674
+ if (inputField.trim().length === 0) {
675
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}.credentialInputs key: must be a non-empty auth input field.`);
689
676
  }
690
677
  if (typeof envVar !== "string" || envVar.trim().length === 0) {
691
- throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor.credentialInputs.${field}: must be a non-empty env var name.`);
678
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}.credentialInputs.${inputField}: must be a non-empty env var name.`);
692
679
  }
693
680
  if (Array.isArray(requiredSecrets) && !requiredSecrets.includes(envVar)) {
694
- throw new ValidationError(`Provider "${providerId}" healthMonitor.credentialInputs.${field} references ${envVar}, which must also be listed in healthMonitor.requiredSecrets.`);
681
+ throw new ValidationError(`Provider "${providerId}" ${field}.credentialInputs.${inputField} references ${envVar}, which must also be listed in ${field}.requiredSecrets.`);
695
682
  }
696
683
  }
697
684
  }
698
685
  const probeOverrides = healthMonitorRecord.probeOverrides;
699
686
  if (probeOverrides !== undefined) {
700
- if (!probeOverrides ||
701
- typeof probeOverrides !== "object" ||
702
- Array.isArray(probeOverrides))
703
- throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor.probeOverrides: must be an object keyed by probe id.`);
687
+ if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
688
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`);
704
689
  for (const [probeId, override] of Object.entries(probeOverrides)) {
705
690
  if (probeId.length === 0)
706
- throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor.probeOverrides key: must be a non-empty probe id.`);
691
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides key: must be a non-empty probe id.`);
707
692
  if (!override || typeof override !== "object" || Array.isArray(override))
708
- throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor.probeOverrides["${probeId}"]: must be an object.`);
693
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides["${probeId}"]: must be an object.`);
709
694
  const overrideRecord = Object.fromEntries(Object.entries(override));
710
- rejectUnknownFields(overrideRecord, PROVIDER_HEALTH_MONITOR_PROBE_OVERRIDE_FIELDS, `healthMonitor.probeOverrides["${probeId}"]`);
695
+ rejectUnknownFields(overrideRecord, PROVIDER_HEALTH_MONITOR_PROBE_OVERRIDE_FIELDS, `${field}.probeOverrides["${probeId}"]`);
711
696
  const interval = overrideRecord.interval;
712
697
  if (interval !== undefined && !isPositiveMsDurationString(interval))
713
- throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor.probeOverrides["${probeId}"].interval: must be a positive ms-style duration string such as 30s, 5m, 8h, or 1 day.`);
698
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides["${probeId}"].interval: must be a positive ms-style duration string such as 30s, 5m, 8h, or 1 day.`);
714
699
  if (overrideRecord.timeoutMs !== undefined) {
715
- assertBoundedIntegerMs(overrideRecord.timeoutMs, `Provider "${providerId}" healthMonitor.probeOverrides["${probeId}"].timeoutMs`, {
700
+ assertBoundedIntegerMs(overrideRecord.timeoutMs, `Provider "${providerId}" ${field}.probeOverrides["${probeId}"].timeoutMs`, {
716
701
  min: HEALTH_CHECK_TIMEOUT_MS_MIN,
717
702
  max: HEALTH_CHECK_TIMEOUT_MS_MAX,
718
703
  label: "timeout",
719
704
  });
720
705
  }
721
706
  if (overrideRecord.degradedThresholdMs !== undefined) {
722
- assertBoundedIntegerMs(overrideRecord.degradedThresholdMs, `Provider "${providerId}" healthMonitor.probeOverrides["${probeId}"].degradedThresholdMs`, {
707
+ assertBoundedIntegerMs(overrideRecord.degradedThresholdMs, `Provider "${providerId}" ${field}.probeOverrides["${probeId}"].degradedThresholdMs`, {
723
708
  min: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
724
709
  max: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
725
710
  label: "degraded threshold",
@@ -730,7 +715,7 @@ function validateProviderHealthMonitor(providerId, healthMonitor) {
730
715
  const serviceAccount = healthMonitorRecord.serviceAccount;
731
716
  if (serviceAccount !== undefined &&
732
717
  (typeof serviceAccount !== "string" || serviceAccount.length === 0))
733
- throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor.serviceAccount: must be a non-empty string.`);
718
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}.serviceAccount: must be a non-empty string.`);
734
719
  }
735
720
  function validateHealthCheckCase(providerId, operationName, caseValue, caseIndex) {
736
721
  const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
@@ -776,6 +761,19 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
776
761
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.interval must be a positive ms-style duration string such as 30s, 5m, 8h, or 1 day.`, {
777
762
  fix: `Set ${fieldPath}.interval to a positive ms-style duration string.`,
778
763
  });
764
+ if (s.schedule !== undefined) {
765
+ if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
766
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.schedule must be an object.`);
767
+ }
768
+ if (Reflect.get(s.schedule, "jitter") !== undefined) {
769
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`);
770
+ }
771
+ rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
772
+ const randomize = Reflect.get(s.schedule, "randomize");
773
+ if (randomize !== undefined) {
774
+ validateScheduleRandomization(randomize, `Provider "${providerId}" ${fieldPath}.schedule.randomize`, msDurationMs(s.interval));
775
+ }
776
+ }
779
777
  if (s.timeoutMs !== undefined) {
780
778
  assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
781
779
  min: HEALTH_CHECK_TIMEOUT_MS_MIN,
@@ -790,8 +788,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
790
788
  label: "degraded threshold",
791
789
  });
792
790
  }
793
- if (s.requiresConnection !== undefined &&
794
- typeof s.requiresConnection !== "boolean")
791
+ if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
795
792
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`);
796
793
  if (!Array.isArray(s.cases) || s.cases.length === 0)
797
794
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.cases must be a non-empty array.`, {
@@ -810,9 +807,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
810
807
  }
811
808
  function validateHealthCheckUnsupported(providerId, operationName, unsupported) {
812
809
  const fieldPath = `operations.${operationName}.healthCheckUnsupported`;
813
- if (!unsupported ||
814
- typeof unsupported !== "object" ||
815
- Array.isArray(unsupported))
810
+ if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
816
811
  throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
817
812
  rejectUnknownFields(unsupported, HEALTH_CHECK_UNSUPPORTED_FIELDS, fieldPath);
818
813
  const u = unsupported;
@@ -837,7 +832,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
837
832
  "steps",
838
833
  "run",
839
834
  ]);
840
- const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter"]);
835
+ const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
841
836
  const HEALTH_JOURNEY_STEP_FIELDS = new Set([
842
837
  "id",
843
838
  "description",
@@ -855,10 +850,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
855
850
  "minManualInterval",
856
851
  "publicRationale",
857
852
  ]);
858
- const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
859
- "enabled",
860
- "reason",
861
- ]);
853
+ const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
862
854
  const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
863
855
  "enabled",
864
856
  "requiresAcknowledgement",
@@ -873,9 +865,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS = new Set([
873
865
  ]);
874
866
  function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger) {
875
867
  const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
876
- if (!manualTrigger ||
877
- typeof manualTrigger !== "object" ||
878
- Array.isArray(manualTrigger)) {
868
+ if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
879
869
  throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object when present.`);
880
870
  }
881
871
  rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
@@ -898,8 +888,7 @@ function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger
898
888
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`);
899
889
  }
900
890
  const risk = Reflect.get(manualTrigger, "risk");
901
- if (typeof risk !== "string" ||
902
- !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
891
+ if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
903
892
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`);
904
893
  }
905
894
  if (risk !== "read_only" && requiresAcknowledgement !== true) {
@@ -951,7 +940,39 @@ function isoDurationMs(value) {
951
940
  const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
952
941
  const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
953
942
  const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
954
- return (days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000);
943
+ return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
944
+ }
945
+ function scheduleRandomizationMs(randomize, fieldPath) {
946
+ const mode = Reflect.get(randomize, "mode");
947
+ switch (mode) {
948
+ case "centered": {
949
+ const maxOffset = Reflect.get(randomize, "maxOffset");
950
+ assertIsoDuration(maxOffset, `${fieldPath}.maxOffset`);
951
+ return isoDurationMs(maxOffset);
952
+ }
953
+ case "delayed": {
954
+ const maxDelay = Reflect.get(randomize, "maxDelay");
955
+ assertIsoDuration(maxDelay, `${fieldPath}.maxDelay`);
956
+ return isoDurationMs(maxDelay);
957
+ }
958
+ default:
959
+ throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
960
+ }
961
+ }
962
+ function validateScheduleRandomization(randomize, fieldPath, intervalMs) {
963
+ if (!randomize || typeof randomize !== "object" || Array.isArray(randomize)) {
964
+ throw new ValidationError(`${fieldPath} must be an object.`);
965
+ }
966
+ const mode = Reflect.get(randomize, "mode");
967
+ const allowedFields = mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
968
+ rejectUnknownFields(randomize, allowedFields, fieldPath);
969
+ const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
970
+ if (offsetMs <= 0) {
971
+ throw new ValidationError(`${fieldPath} duration must be positive.`);
972
+ }
973
+ if (offsetMs >= intervalMs) {
974
+ throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
975
+ }
955
976
  }
956
977
  function assertIsoCountry(value, fieldPath) {
957
978
  if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
@@ -962,11 +983,18 @@ function normalizeIntervalDuration(input) {
962
983
  const trimmed = input.trim();
963
984
  const shorthand = /^(\d+)(s|m|h|d)$/i.exec(trimmed);
964
985
  if (shorthand) {
965
- const amount = Number(shorthand[1]);
986
+ const durationMs = msDurationMs(trimmed);
987
+ const unit = shorthand[2]?.toLowerCase();
988
+ const amount = unit === "s"
989
+ ? durationMs / 1_000
990
+ : unit === "m"
991
+ ? durationMs / 60_000
992
+ : unit === "h"
993
+ ? durationMs / 3_600_000
994
+ : durationMs / 86_400_000;
966
995
  if (!Number.isInteger(amount) || amount <= 0) {
967
996
  throw new ValidationError(`Journey schedule interval must be a positive duration.`);
968
997
  }
969
- const unit = shorthand[2]?.toLowerCase();
970
998
  if (unit === "s")
971
999
  return `PT${amount}S`;
972
1000
  if (unit === "m")
@@ -980,15 +1008,27 @@ function normalizeIntervalDuration(input) {
980
1008
  return trimmed;
981
1009
  }
982
1010
  export function every(interval, options = {}) {
1011
+ if (options.jitter !== undefined && options.randomize !== undefined) {
1012
+ throw new ValidationError(`Schedule cannot define both jitter and randomize. Use randomize instead.`);
1013
+ }
983
1014
  const schedule = {
984
1015
  kind: "interval",
985
1016
  interval: normalizeIntervalDuration(interval),
986
1017
  };
1018
+ if (options.randomize !== undefined) {
1019
+ schedule.randomize = options.randomize;
1020
+ }
987
1021
  if (options.jitter !== undefined) {
988
1022
  schedule.jitter = normalizeIntervalDuration(options.jitter);
989
1023
  }
990
1024
  return schedule;
991
1025
  }
1026
+ export function centered(maxOffset) {
1027
+ return { mode: "centered", maxOffset: normalizeIntervalDuration(maxOffset) };
1028
+ }
1029
+ export function delayed(maxDelay) {
1030
+ return { mode: "delayed", maxDelay: normalizeIntervalDuration(maxDelay) };
1031
+ }
992
1032
  function countCapturingGroups(pattern) {
993
1033
  let count = 0;
994
1034
  const source = pattern.source;
@@ -1010,9 +1050,7 @@ function countCapturingGroups(pattern) {
1010
1050
  const next = source[i + 1];
1011
1051
  if (next === "?" && source[i + 2] !== "<")
1012
1052
  continue;
1013
- if (next === "?" &&
1014
- source[i + 2] === "<" &&
1015
- (source[i + 3] === "=" || source[i + 3] === "!"))
1053
+ if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
1016
1054
  continue;
1017
1055
  count += 1;
1018
1056
  }
@@ -1090,8 +1128,7 @@ function validateSmsOtpMatcher(matcher, fieldPath) {
1090
1128
  throw new ValidationError(`${fieldPath}.code.pattern must be a RegExp or pattern source string.`);
1091
1129
  }
1092
1130
  const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
1093
- if (countCapturingGroups(regex) !== 1 &&
1094
- Reflect.get(code, "capture") === undefined) {
1131
+ if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
1095
1132
  throw new ValidationError(`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`);
1096
1133
  }
1097
1134
  if (Reflect.get(code, "capture") !== undefined &&
@@ -1140,9 +1177,17 @@ function validateHealthJourneySchedule(providerId, journeyId, schedule) {
1140
1177
  rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
1141
1178
  if (Reflect.get(schedule, "kind") !== "interval")
1142
1179
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
1143
- assertIsoDuration(Reflect.get(schedule, "interval"), `Provider "${providerId}" ${fieldPath}.interval`);
1180
+ const interval = Reflect.get(schedule, "interval");
1181
+ assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
1182
+ const randomize = Reflect.get(schedule, "randomize");
1183
+ if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
1184
+ throw new ValidationError(`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`);
1185
+ }
1144
1186
  if (Reflect.get(schedule, "jitter") !== undefined)
1145
1187
  assertIsoDuration(Reflect.get(schedule, "jitter"), `Provider "${providerId}" ${fieldPath}.jitter`);
1188
+ if (randomize !== undefined) {
1189
+ validateScheduleRandomization(randomize, `Provider "${providerId}" ${fieldPath}.randomize`, isoDurationMs(interval));
1190
+ }
1146
1191
  }
1147
1192
  function validateHealthJourneys(providerId, operations, healthJourneys) {
1148
1193
  const covered = new Set();
@@ -1165,8 +1210,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
1165
1210
  throw new ValidationError(`Provider "${providerId}" has duplicate health journey id "${journey.id}".`);
1166
1211
  journeyIds.add(journey.id);
1167
1212
  validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
1168
- if (!Array.isArray(journey.coversOperations) ||
1169
- journey.coversOperations.length === 0) {
1213
+ if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
1170
1214
  throw new ValidationError(`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`);
1171
1215
  }
1172
1216
  for (const operationId of journey.coversOperations) {
@@ -1204,8 +1248,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
1204
1248
  throw new ValidationError(`Provider "${providerId}" ${stepPath}.id must be a kebab-case identifier.`);
1205
1249
  if (step.operationId !== undefined && !operations[step.operationId])
1206
1250
  throw new ValidationError(`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`);
1207
- if (step.usesSmsMatcher !== undefined &&
1208
- !matcherIds.has(step.usesSmsMatcher))
1251
+ if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
1209
1252
  throw new ValidationError(`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`);
1210
1253
  }
1211
1254
  if (journey.manualTrigger !== undefined)
@@ -1239,9 +1282,7 @@ function validateOperationHealthChecks(providerId, operations, journeyCoveredOpe
1239
1282
  validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
1240
1283
  if (hasUnsupported)
1241
1284
  validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
1242
- if (!hasCheck &&
1243
- !hasUnsupported &&
1244
- !journeyCoveredOperations.has(operationName))
1285
+ if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
1245
1286
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`, {
1246
1287
  fix: `Add \`healthCheck: { interval, cases: [...] }\` or \`healthCheckUnsupported: { reason: "..." }\` to operations.${operationName}.`,
1247
1288
  });
@@ -1271,6 +1312,19 @@ function validateOperationFixtures(providerId, operations) {
1271
1312
  }
1272
1313
  }
1273
1314
  }
1315
+ /**
1316
+ * Shallow shape guard only: the `deployment` object is passed through
1317
+ * verbatim and deliberately not deep-validated by the SDK — the APIFuse
1318
+ * registry builder owns deployment validation and profile resolution.
1319
+ */
1320
+ function validateProviderDeployment(providerId, deployment) {
1321
+ if (deployment === undefined)
1322
+ return;
1323
+ if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
1324
+ throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
1325
+ fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
1326
+ });
1327
+ }
1274
1328
  export function defineProvider(config) {
1275
1329
  validateProviderShape(config);
1276
1330
  if (!CONNECTOR_ID_REGEX.test(config.id))
@@ -1278,7 +1332,9 @@ export function defineProvider(config) {
1278
1332
  fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
1279
1333
  });
1280
1334
  if (Object.keys(config.operations).length === 0)
1281
- throw new ProviderError(`Provider "${config.id}" must define at least one operation`, { fix: "Add at least one operation to the operations object" });
1335
+ throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
1336
+ fix: "Add at least one operation to the operations object",
1337
+ });
1282
1338
  validateOperationIds(config.id, config.operations);
1283
1339
  validateOperationAnnotations(config.id, config.operations);
1284
1340
  validateOperationObservability(config.id, config.operations);
@@ -1287,8 +1343,13 @@ export function defineProvider(config) {
1287
1343
  validateToolRouterMetadata(config.id, config.operations);
1288
1344
  const journeyCoveredOperations = validateHealthJourneys(config.id, config.operations, config.healthJourneys);
1289
1345
  validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
1290
- validateProviderHealthMonitor(config.id, config.healthMonitor);
1346
+ if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
1347
+ throw new ValidationError(`Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`, {
1348
+ fix: "Keep healthProbe (the new name) and delete the healthMonitor block.",
1349
+ });
1350
+ validateProviderHealthMonitor(config.id, config.healthProbe ?? config.healthMonitor, config.healthProbe !== undefined ? "healthProbe" : "healthMonitor");
1291
1351
  validateOperationFixtures(config.id, config.operations);
1352
+ validateProviderDeployment(config.id, config.deployment);
1292
1353
  validateProviderProxy(config);
1293
1354
  validateProviderStt(config);
1294
1355
  if (config.runtime === "browser" && !config.browser)
@@ -1301,6 +1362,9 @@ export function defineProvider(config) {
1301
1362
  id: config.id,
1302
1363
  version: config.version,
1303
1364
  runtime: config.runtime,
1365
+ // Verbatim passthrough: deployment validation and profile resolution
1366
+ // are owned by the APIFuse registry builder, not the SDK.
1367
+ deployment: config.deployment,
1304
1368
  allowedHosts: config.allowedHosts,
1305
1369
  stealth: config.stealth,
1306
1370
  proxy: config.proxy,
@@ -1314,7 +1378,10 @@ export function defineProvider(config) {
1314
1378
  context: config.context,
1315
1379
  meta: config.meta,
1316
1380
  operations: config.operations,
1317
- healthMonitor: config.healthMonitor,
1381
+ // Transitional healthMonitor → healthProbe alias: mirror whichever field
1382
+ // was declared onto both so old and new consumers keep working.
1383
+ healthMonitor: config.healthMonitor ?? config.healthProbe,
1384
+ healthProbe: config.healthProbe ?? config.healthMonitor,
1318
1385
  healthJourneys: config.healthJourneys,
1319
1386
  };
1320
1387
  }