@apifuse/provider-sdk 2.2.0-beta.4 → 2.2.0-beta.40

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 (303) hide show
  1. package/AUTHORING.md +501 -13
  2. package/CHANGELOG.md +165 -1
  3. package/README.md +72 -14
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +88 -4
  6. package/bin/apifuse-dev.ts +38 -5
  7. package/bin/apifuse-pack-check.ts +22 -2
  8. package/bin/apifuse-pack-smoke.ts +57 -2
  9. package/bin/apifuse-pack-types.ts +356 -38
  10. package/bin/apifuse-perf.ts +14 -13
  11. package/bin/apifuse-record.ts +691 -68
  12. package/bin/apifuse-submit-check.ts +518 -37
  13. package/bin/apifuse-sync-assets.ts +117 -0
  14. package/bin/submit-check-delimited-text.ts +50 -0
  15. package/dist/auth-turn/index.d.ts +3 -3
  16. package/dist/auth-turn/index.js +1 -1
  17. package/dist/auth.d.ts +14 -0
  18. package/dist/auth.js +67 -0
  19. package/dist/ceremonies/index.d.ts +16 -0
  20. package/dist/ceremonies/index.js +141 -36
  21. package/dist/cli/commands.d.ts +1 -1
  22. package/dist/cli/commands.js +8 -0
  23. package/dist/cli/create.d.ts +3 -0
  24. package/dist/cli/create.js +34 -35
  25. package/dist/cli/prompt-assets.d.ts +80 -0
  26. package/dist/cli/prompt-assets.js +743 -0
  27. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  28. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  29. package/dist/cli/templates/provider/README.md.tpl +4 -4
  30. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  31. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  32. package/dist/config/loader.d.ts +177 -16
  33. package/dist/config/loader.js +424 -127
  34. package/dist/contract-serialization.js +4 -8
  35. package/dist/contract-types.d.ts +1 -0
  36. package/dist/contract.js +2 -0
  37. package/dist/declaration-validation.d.ts +32 -0
  38. package/dist/declaration-validation.js +207 -0
  39. package/dist/define.d.ts +51 -25
  40. package/dist/define.js +752 -38
  41. package/dist/error-resolution.d.ts +4 -0
  42. package/dist/error-resolution.js +122 -0
  43. package/dist/errors.d.ts +18 -0
  44. package/dist/errors.js +40 -0
  45. package/dist/fixture-sanitization.d.ts +28 -0
  46. package/dist/fixture-sanitization.js +217 -0
  47. package/dist/health-scenario.d.ts +1842 -0
  48. package/dist/health-scenario.js +624 -0
  49. package/dist/index.d.ts +19 -9
  50. package/dist/index.js +10 -6
  51. package/dist/lint.d.ts +6 -1
  52. package/dist/lint.js +362 -3
  53. package/dist/native-address.d.ts +43 -0
  54. package/dist/native-address.js +281 -0
  55. package/dist/native-egress-policy.d.ts +31 -0
  56. package/dist/native-egress-policy.js +288 -0
  57. package/dist/observability.d.ts +5 -2
  58. package/dist/observability.js +48 -1
  59. package/dist/provider.d.ts +8 -2
  60. package/dist/provider.js +3 -1
  61. package/dist/runtime/auth-flow.d.ts +5 -1
  62. package/dist/runtime/auth-flow.js +6 -0
  63. package/dist/runtime/browser.d.ts +1 -0
  64. package/dist/runtime/browser.js +492 -49
  65. package/dist/runtime/cache.d.ts +1 -0
  66. package/dist/runtime/cache.js +169 -15
  67. package/dist/runtime/choice-wordlist.d.ts +9 -0
  68. package/dist/runtime/choice-wordlist.js +138 -0
  69. package/dist/runtime/choice.d.ts +13 -1
  70. package/dist/runtime/choice.js +490 -102
  71. package/dist/runtime/executor.d.ts +2 -2
  72. package/dist/runtime/executor.js +26 -2
  73. package/dist/runtime/http.d.ts +1 -0
  74. package/dist/runtime/http.js +515 -53
  75. package/dist/runtime/instrumentation.d.ts +2 -2
  76. package/dist/runtime/instrumentation.js +366 -8
  77. package/dist/runtime/native-network-errors.d.ts +33 -0
  78. package/dist/runtime/native-network-errors.js +69 -0
  79. package/dist/runtime/native-network.d.ts +96 -0
  80. package/dist/runtime/native-network.js +1232 -0
  81. package/dist/runtime/ocr.d.ts +29 -0
  82. package/dist/runtime/ocr.js +440 -0
  83. package/dist/runtime/proxy-errors.js +6 -2
  84. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  85. package/dist/runtime/proxy-nodemaven.js +146 -0
  86. package/dist/runtime/proxy-telemetry.d.ts +80 -1
  87. package/dist/runtime/proxy-telemetry.js +154 -47
  88. package/dist/runtime/redirects.d.ts +29 -0
  89. package/dist/runtime/redirects.js +36 -0
  90. package/dist/runtime/redis.d.ts +1 -1
  91. package/dist/runtime/redis.js +4 -2
  92. package/dist/runtime/request-options.d.ts +68 -1
  93. package/dist/runtime/request-options.js +548 -0
  94. package/dist/runtime/resolver-config.d.ts +6 -0
  95. package/dist/runtime/resolver-config.js +6 -0
  96. package/dist/runtime/resolver-public.d.ts +1 -0
  97. package/dist/runtime/resolver-public.js +1 -0
  98. package/dist/runtime/resolver-shared.d.ts +3 -0
  99. package/dist/runtime/resolver-shared.js +12 -0
  100. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  101. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  102. package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
  103. package/dist/runtime/resolver-vendors/browser.js +377 -0
  104. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  105. package/dist/runtime/resolver-vendors/capsolver.js +526 -0
  106. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  107. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  108. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  109. package/dist/runtime/resolver-vendors/twocaptcha.js +407 -0
  110. package/dist/runtime/resolver-vendors/types.d.ts +94 -0
  111. package/dist/runtime/resolver-vendors/types.js +96 -0
  112. package/dist/runtime/resolver.d.ts +60 -0
  113. package/dist/runtime/resolver.js +737 -0
  114. package/dist/runtime/secrets.d.ts +27 -0
  115. package/dist/runtime/secrets.js +51 -0
  116. package/dist/runtime/state.d.ts +3 -0
  117. package/dist/runtime/state.js +277 -71
  118. package/dist/runtime/stealth-cookies.d.ts +20 -0
  119. package/dist/runtime/stealth-cookies.js +111 -0
  120. package/dist/runtime/stealth.d.ts +28 -3
  121. package/dist/runtime/stealth.js +519 -255
  122. package/dist/runtime/stt.js +1 -12
  123. package/dist/runtime/timeout.d.ts +5 -0
  124. package/dist/runtime/timeout.js +12 -0
  125. package/dist/runtime/trace-config.d.ts +12 -0
  126. package/dist/runtime/trace-config.js +61 -0
  127. package/dist/serve.d.ts +1 -1
  128. package/dist/serve.js +1 -1
  129. package/dist/server/index.d.ts +5 -3
  130. package/dist/server/index.js +3 -3
  131. package/dist/server/self-test-input-tokens.d.ts +2 -1
  132. package/dist/server/self-test-input-tokens.js +18 -14
  133. package/dist/server/self-test.d.ts +114 -0
  134. package/dist/server/self-test.js +784 -148
  135. package/dist/server/serve-implementation.d.ts +213 -0
  136. package/dist/server/serve-implementation.js +2173 -0
  137. package/dist/server/serve.d.ts +1 -70
  138. package/dist/server/serve.js +1 -1130
  139. package/dist/server/trace-output.d.ts +4 -0
  140. package/dist/server/trace-output.js +20 -0
  141. package/dist/server/types.d.ts +30 -5
  142. package/dist/server/types.js +13 -1
  143. package/dist/stateful/errors.d.ts +19 -0
  144. package/dist/stateful/errors.js +24 -0
  145. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  146. package/dist/stateful/http-provider-event-emitter.js +237 -0
  147. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  148. package/dist/stateful/http-session-owner-registry.js +210 -0
  149. package/dist/stateful/index.d.ts +18 -0
  150. package/dist/stateful/index.js +18 -0
  151. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  152. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  153. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  154. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  155. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  156. package/dist/stateful/provider-event-pipeline.js +1 -0
  157. package/dist/stateful/provider-events.d.ts +101 -0
  158. package/dist/stateful/provider-events.js +289 -0
  159. package/dist/stateful/session-key.d.ts +15 -0
  160. package/dist/stateful/session-key.js +86 -0
  161. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  162. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  163. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  164. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  165. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  166. package/dist/stateful/stateful-provider-adapter.js +287 -0
  167. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  168. package/dist/stateful/stateful-provider-observability.js +161 -0
  169. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  170. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  171. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  172. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  173. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  174. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  175. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  176. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  177. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  178. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  179. package/dist/stateful-signing.d.ts +18 -0
  180. package/dist/stateful-signing.js +27 -0
  181. package/dist/stealth/profiles.js +16 -7
  182. package/dist/stream-evidence.d.ts +74 -0
  183. package/dist/stream-evidence.js +785 -0
  184. package/dist/stream.js +7 -1
  185. package/dist/testing/index.d.ts +2 -1
  186. package/dist/testing/index.js +2 -1
  187. package/dist/testing/run.d.ts +32 -2
  188. package/dist/testing/run.js +489 -21
  189. package/dist/trace-sanitization.d.ts +5 -0
  190. package/dist/trace-sanitization.js +45 -0
  191. package/dist/types.d.ts +545 -23
  192. package/dist/types.js +1 -0
  193. package/package.json +44 -5
  194. package/src/auth-turn/index.ts +1 -1
  195. package/src/auth.ts +118 -0
  196. package/src/ceremonies/index.ts +189 -46
  197. package/src/cli/commands.ts +10 -0
  198. package/src/cli/create.ts +42 -35
  199. package/src/cli/prompt-assets.ts +865 -0
  200. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  201. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  202. package/src/cli/templates/provider/README.md.tpl +4 -4
  203. package/src/cli/templates/provider/index.ts.tpl +6 -3
  204. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  205. package/src/config/loader.ts +665 -163
  206. package/src/contract-serialization.ts +5 -7
  207. package/src/contract-types.ts +1 -0
  208. package/src/contract.ts +2 -0
  209. package/src/declaration-validation.ts +266 -0
  210. package/src/define.ts +970 -87
  211. package/src/error-resolution.ts +127 -0
  212. package/src/errors.ts +52 -0
  213. package/src/fixture-sanitization.ts +248 -0
  214. package/src/health-scenario.ts +875 -0
  215. package/src/index.ts +204 -8
  216. package/src/lint.ts +408 -4
  217. package/src/native-address.ts +340 -0
  218. package/src/native-egress-policy.ts +358 -0
  219. package/src/observability.ts +51 -1
  220. package/src/provider.ts +133 -0
  221. package/src/runtime/auth-flow.ts +12 -0
  222. package/src/runtime/browser.ts +661 -63
  223. package/src/runtime/cache.ts +189 -14
  224. package/src/runtime/choice-wordlist.ts +145 -0
  225. package/src/runtime/choice.ts +631 -120
  226. package/src/runtime/executor.ts +40 -7
  227. package/src/runtime/http.ts +641 -61
  228. package/src/runtime/instrumentation.ts +520 -15
  229. package/src/runtime/native-network-errors.ts +99 -0
  230. package/src/runtime/native-network.ts +1605 -0
  231. package/src/runtime/ocr.ts +523 -0
  232. package/src/runtime/proxy-errors.ts +12 -4
  233. package/src/runtime/proxy-nodemaven.ts +221 -0
  234. package/src/runtime/proxy-telemetry.ts +244 -75
  235. package/src/runtime/redirects.ts +66 -0
  236. package/src/runtime/redis.ts +7 -2
  237. package/src/runtime/request-options.ts +680 -1
  238. package/src/runtime/resolver-config.ts +6 -0
  239. package/src/runtime/resolver-public.ts +20 -0
  240. package/src/runtime/resolver-shared.ts +17 -0
  241. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  242. package/src/runtime/resolver-vendors/browser.ts +533 -0
  243. package/src/runtime/resolver-vendors/capsolver.ts +700 -0
  244. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  245. package/src/runtime/resolver-vendors/twocaptcha.ts +539 -0
  246. package/src/runtime/resolver-vendors/types.ts +212 -0
  247. package/src/runtime/resolver.ts +1103 -0
  248. package/src/runtime/secrets.ts +64 -0
  249. package/src/runtime/state.ts +394 -77
  250. package/src/runtime/stealth-cookies.ts +132 -0
  251. package/src/runtime/stealth.ts +675 -289
  252. package/src/runtime/stt.ts +1 -19
  253. package/src/runtime/timeout.ts +18 -0
  254. package/src/runtime/trace-config.ts +77 -0
  255. package/src/serve.ts +6 -1
  256. package/src/server/index.ts +37 -2
  257. package/src/server/self-test-input-tokens.ts +29 -14
  258. package/src/server/self-test.ts +1025 -175
  259. package/src/server/serve-implementation.ts +3250 -0
  260. package/src/server/serve.ts +1 -1626
  261. package/src/server/trace-output.ts +32 -0
  262. package/src/server/types.ts +13 -1
  263. package/src/stateful/README.md +146 -0
  264. package/src/stateful/errors.ts +35 -0
  265. package/src/stateful/http-provider-event-emitter.ts +314 -0
  266. package/src/stateful/http-session-owner-registry.ts +306 -0
  267. package/src/stateful/index.ts +18 -0
  268. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  269. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  270. package/src/stateful/provider-event-pipeline.ts +61 -0
  271. package/src/stateful/provider-events.ts +462 -0
  272. package/src/stateful/session-key.ts +111 -0
  273. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  274. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  275. package/src/stateful/stateful-provider-adapter.ts +562 -0
  276. package/src/stateful/stateful-provider-observability.ts +261 -0
  277. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  278. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  279. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  280. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  281. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  282. package/src/stateful-signing.ts +46 -0
  283. package/src/stealth/profiles.ts +17 -7
  284. package/src/stream-evidence.ts +988 -0
  285. package/src/stream.ts +8 -1
  286. package/src/testing/index.ts +10 -1
  287. package/src/testing/run.ts +658 -15
  288. package/src/trace-sanitization.ts +63 -0
  289. package/src/types.ts +660 -38
  290. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  291. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  292. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  293. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  294. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  295. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  296. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  297. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  298. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  299. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  300. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  301. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  302. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  303. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/src/define.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  import ms from "ms";
2
2
 
3
+ import {
4
+ validateFailClosedOperationDeclaration,
5
+ validateFailClosedProviderDeclaration,
6
+ } from "./declaration-validation.js";
7
+ import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
3
8
  import { ProviderError, ValidationError } from "./errors.js";
9
+ import {
10
+ NativeEgressPolicyValidationError,
11
+ validateNativeProviderConfig,
12
+ } from "./native-egress-policy.js";
4
13
  import { safeParseSchemaSync } from "./schema.js";
14
+ import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
5
15
  import type {
6
16
  AuthConfig,
7
17
  BrowserEngine,
@@ -14,6 +24,7 @@ import type {
14
24
  HealthJourneySchedule,
15
25
  HealthScheduleRandomization,
16
26
  InferSchemaOutput,
27
+ NativeProviderConfig,
17
28
  OperationDefinition,
18
29
  OperationHandlerResult,
19
30
  OperationHttpStreamTransport,
@@ -21,11 +32,18 @@ import type {
21
32
  OperationTransport,
22
33
  OperationWebSocketTransport,
23
34
  ProviderAccessConfig,
35
+ ProviderChallengeKind,
36
+ ProviderContext,
37
+ ProviderContextFor,
24
38
  ProviderDefinition,
25
39
  ProviderDeploymentOverrides,
26
40
  ProviderHealthMonitorConfig,
41
+ ProviderOcrConfig,
27
42
  ProviderProxyConfig,
43
+ ProviderProxyProvider,
28
44
  ProviderPublicProfile,
45
+ ProviderResolverConfig,
46
+ ProviderResolverVendor,
29
47
  ProviderReviewed,
30
48
  ProviderSecretDeclaration,
31
49
  ProviderStreamEvent,
@@ -49,6 +67,7 @@ import {
49
67
  STREAM_IDLE_TIMEOUT_MS_MIN,
50
68
  STREAM_MAX_DURATION_MS_MAX,
51
69
  STREAM_MAX_DURATION_MS_MIN,
70
+ VALID_OPERATION_ERROR_STATUSES,
52
71
  } from "./types.js";
53
72
 
54
73
  type ProviderImplementationSourceAccess =
@@ -75,18 +94,87 @@ interface ProviderImplementationProfile {
75
94
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
76
95
  const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
77
96
  const VALID_RUNTIMES = ["standard", "shared", "browser"] as const;
78
- const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"] as const;
97
+ const VALID_AUTH_MODES = [
98
+ "none",
99
+ "platform-managed",
100
+ "credentials",
101
+ "oauth2",
102
+ "oauth2_proxied",
103
+ ] as const;
104
+ const PROXIED_OAUTH_REQUIRED_FIELDS = [
105
+ "authorizeUrl",
106
+ "tokenUrl",
107
+ "customScheme",
108
+ "rewriteProfile",
109
+ "clientIdEnvKey",
110
+ ] as const;
111
+ const PROXIED_OAUTH_ALLOWED_FIELDS = new Set([
112
+ ...PROXIED_OAUTH_REQUIRED_FIELDS,
113
+ "pkce",
114
+ "authorizeParams",
115
+ "tokenParams",
116
+ ]);
117
+ const PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS = new Set([
118
+ "client_id",
119
+ "response_type",
120
+ "state",
121
+ "code_challenge",
122
+ "code_challenge_method",
123
+ ]);
124
+ const PROXIED_OAUTH_PROFILE_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
125
+ const PROXIED_OAUTH_ENV_KEY_REGEX = /^[A-Z][A-Z0-9_]*__[A-Z0-9_]+$/;
126
+ const CUSTOM_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+$/;
79
127
  const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
80
128
  const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"] as const;
81
- const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "decodo", "custom"] as const;
129
+ const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"] as const;
82
130
  const VALID_PROVIDER_PROXY_AFFINITIES = [
83
131
  "request",
84
132
  "operation",
85
133
  "auth-flow",
86
134
  "connection",
87
135
  ] as const;
136
+ const VALID_PROVIDER_OCR_MODES = ["optional", "required"] as const;
88
137
  const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
138
+ function exhaustiveLiteralArray<TUnion extends string>() {
139
+ return <const TValues extends readonly TUnion[]>(
140
+ values: TValues,
141
+ ..._missing: Exclude<TUnion, TValues[number]> extends never
142
+ ? []
143
+ : ["Missing runtime values", Exclude<TUnion, TValues[number]>]
144
+ ): TValues => values;
145
+ }
146
+
147
+ export const VALID_PROVIDER_RESOLVER_VENDORS = exhaustiveLiteralArray<ProviderResolverVendor>()([
148
+ "browser",
149
+ "capsolver",
150
+ "capmonster",
151
+ "2captcha",
152
+ "custom",
153
+ ] as const);
154
+ export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray<ProviderChallengeKind>()([
155
+ "turnstile",
156
+ "recaptcha_v2",
157
+ "recaptcha_v3",
158
+ "hcaptcha",
159
+ "cloudflare_interstitial",
160
+ "aws_waf",
161
+ "akamai_sec_cpt",
162
+ "akamai_sensor",
163
+ ] as const);
89
164
  const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
165
+ const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
166
+ const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
167
+ // Per-vendor provider-declared credential secrets. A required-mode chain must
168
+ // declare every secret of every credentialed vendor it names, so a missing
169
+ // credential fails at build/validation time rather than during a live outage: a
170
+ // declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
171
+ // exactly the failure class the multi-vendor chain exists to remove. Vendors
172
+ // absent from this map (the deprecated `custom`/`decodo` values have no managed
173
+ // adapter) impose no declaration requirement.
174
+ const VENDOR_REQUIRED_SECRETS: Partial<Record<ProviderProxyProvider, readonly string[]>> = {
175
+ smartproxy: [SMARTPROXY_APP_KEY_SECRET],
176
+ nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
177
+ };
90
178
  const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
91
179
  const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
92
180
  const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
@@ -114,71 +202,383 @@ function parsePositiveMsDuration(value: string): number | undefined {
114
202
  return parsed;
115
203
  }
116
204
 
117
- type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
118
- type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
119
- OperationDefinition<TInput, TOutput>,
120
- "handler"
121
- > & {
205
+ type ProviderOperation = OperationDefinition<any, any, any>;
206
+ type OperationConfig<
207
+ TInput extends SchemaLike,
208
+ TOutput extends SchemaLike,
209
+ TContext = ProviderContext,
210
+ > = Omit<OperationDefinition<TInput, TOutput, TContext>, "handler"> & {
122
211
  handler(
123
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
212
+ ctx: TContext,
124
213
  input: InferSchemaOutput<TInput>,
125
214
  ):
126
215
  | OperationHandlerResult<InferSchemaOutput<TOutput>>
127
216
  | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
128
217
  };
129
- type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
218
+ type OperationMapConfig<
219
+ TOperations extends Record<string, ProviderOperation>,
220
+ TContext = ProviderContext,
221
+ > = {
130
222
  [K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
131
- ? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
223
+ ? OperationConfig<TInput, TOutput, TContext> | OperationDefinition<TInput, TOutput, TContext>
132
224
  : never;
133
225
  };
134
- type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
135
- | SseOperationConfig<TInput, TOutput>
136
- | HttpStreamOperationConfig<TInput, TOutput>
137
- | WebSocketOperationConfig<TInput, TOutput>;
138
- type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
139
- OperationConfig<TInput, TOutput>,
140
- "handler" | "transport"
141
- > & {
226
+ type StreamOperationConfig<
227
+ TInput extends SchemaLike,
228
+ TOutput extends SchemaLike,
229
+ TContext = ProviderContext,
230
+ > =
231
+ | SseOperationConfig<TInput, TOutput, TContext>
232
+ | HttpStreamOperationConfig<TInput, TOutput, TContext>
233
+ | WebSocketOperationConfig<TInput, TOutput, TContext>;
234
+ type SseOperationConfig<
235
+ TInput extends SchemaLike,
236
+ TOutput extends SchemaLike,
237
+ TContext = ProviderContext,
238
+ > = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
142
239
  transport: OperationSseTransport;
143
240
  handler(
144
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
241
+ ctx: TContext,
145
242
  input: InferSchemaOutput<TInput>,
146
243
  ): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
147
244
  };
148
- type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
149
- OperationConfig<TInput, TOutput>,
150
- "handler" | "transport"
151
- > & {
245
+ type HttpStreamOperationConfig<
246
+ TInput extends SchemaLike,
247
+ TOutput extends SchemaLike,
248
+ TContext = ProviderContext,
249
+ > = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
152
250
  transport: OperationHttpStreamTransport;
153
251
  handler(
154
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
252
+ ctx: TContext,
155
253
  input: InferSchemaOutput<TInput>,
156
254
  ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
157
255
  };
158
- type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
159
- OperationConfig<TInput, TOutput>,
160
- "handler" | "transport"
161
- > & {
256
+ type WebSocketOperationConfig<
257
+ TInput extends SchemaLike,
258
+ TOutput extends SchemaLike,
259
+ TContext = ProviderContext,
260
+ > = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
162
261
  transport: OperationWebSocketTransport;
163
262
  handler(
164
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
263
+ ctx: TContext,
165
264
  input: InferSchemaOutput<TInput>,
166
265
  ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
167
266
  };
168
267
 
169
- type AuthStartNoInputGuard<TConfig> = TConfig extends {
268
+ type AuthStartHandlerNoInputGuard<TStart> = TStart extends (...args: infer TArgs) => unknown
269
+ ? TArgs["length"] extends 0 | 1
270
+ ? unknown
271
+ : {
272
+ "auth start handlers must not declare input parameters; return a form turn from start and receive user input in continue": never;
273
+ }
274
+ : unknown;
275
+
276
+ export type AuthStartNoInputGuard<TConfig> = TConfig extends {
170
277
  auth?: { flow?: { start: infer TStart } };
171
278
  }
172
- ? TStart extends (...args: infer TArgs) => unknown
173
- ? TArgs extends [unknown]
174
- ? unknown
175
- : {
176
- "auth start handlers must not declare input parameters; return a form turn from start and receive user input in continue": never;
279
+ ? AuthStartHandlerNoInputGuard<TStart>
280
+ : TConfig extends { start: infer TStart }
281
+ ? AuthStartHandlerNoInputGuard<TStart>
282
+ : unknown;
283
+
284
+ function splitAuthStartParameters(parameters: string): string[] | undefined {
285
+ const parts: string[] = [];
286
+ let start = 0;
287
+ let round = 0;
288
+ let square = 0;
289
+ let curly = 0;
290
+ let quote: "'" | '"' | "`" | undefined;
291
+ let escaped = false;
292
+ let lineComment = false;
293
+ let blockComment = false;
294
+ const templateDepths = [0];
295
+ templateDepths.length = 0;
296
+
297
+ for (let index = 0; index < parameters.length; index++) {
298
+ const character = parameters[index];
299
+ const nextCharacter = parameters[index + 1];
300
+ if (lineComment) {
301
+ if (character === "\n" || character === "\r") lineComment = false;
302
+ else continue;
303
+ }
304
+ if (blockComment) {
305
+ if (character === "*" && nextCharacter === "/") {
306
+ blockComment = false;
307
+ index++;
308
+ }
309
+ continue;
310
+ }
311
+ if (quote) {
312
+ if (escaped) {
313
+ escaped = false;
314
+ } else if (character === "\\") {
315
+ escaped = true;
316
+ } else if (quote === "`" && character === "$" && nextCharacter === "{") {
317
+ curly++;
318
+ templateDepths.push(curly);
319
+ quote = undefined;
320
+ index++;
321
+ } else if (character === quote) {
322
+ quote = undefined;
323
+ }
324
+ continue;
325
+ }
326
+ if (character === "'" || character === '"' || character === "`") {
327
+ quote = character;
328
+ continue;
329
+ }
330
+ if (character === "/" && nextCharacter === "/") {
331
+ lineComment = true;
332
+ index++;
333
+ continue;
334
+ }
335
+ if (character === "/" && nextCharacter === "*") {
336
+ blockComment = true;
337
+ index++;
338
+ continue;
339
+ }
340
+ if (character === "/") return undefined;
341
+ // Annex B HTML-like comments are not lexed here; give up rather than
342
+ // risk misreading the parameter list.
343
+ if (character === "<" && parameters.startsWith("!--", index + 1)) return undefined;
344
+ if (character === "-" && parameters.startsWith("->", index + 1)) return undefined;
345
+ if (character === "(") round++;
346
+ else if (character === ")") round--;
347
+ else if (character === "[") square++;
348
+ else if (character === "]") square--;
349
+ else if (character === "{") curly++;
350
+ else if (character === "}") {
351
+ if (templateDepths.at(-1) === curly) {
352
+ templateDepths.pop();
353
+ curly--;
354
+ quote = "`";
355
+ } else curly--;
356
+ } else if (character === "," && round === 0 && square === 0 && curly === 0) {
357
+ parts.push(parameters.slice(start, index));
358
+ start = index + 1;
359
+ }
360
+ if (round < 0 || square < 0 || curly < 0) return undefined;
361
+ }
362
+
363
+ if (
364
+ quote ||
365
+ blockComment ||
366
+ templateDepths.length > 0 ||
367
+ round !== 0 ||
368
+ square !== 0 ||
369
+ curly !== 0
370
+ )
371
+ return undefined;
372
+ parts.push(parameters.slice(start));
373
+ return parts;
374
+ }
375
+
376
+ function authStartParameterList(source: string): string | undefined {
377
+ let index = 0;
378
+ while (index < source.length && /\s/.test(source[index] ?? "")) index++;
379
+ if (index >= source.length) return undefined;
380
+
381
+ let openIndex = -1;
382
+ let parenthesizedArrow = false;
383
+ let asyncMethodOrArrow = false;
384
+ const skipTrivia = () => {
385
+ while (index < source.length) {
386
+ if (/\s/.test(source[index] ?? "")) {
387
+ index++;
388
+ continue;
389
+ }
390
+ if (source[index] === "/" && source[index + 1] === "/") {
391
+ index += 2;
392
+ while (index < source.length && source[index] !== "\n" && source[index] !== "\r") index++;
393
+ continue;
394
+ }
395
+ if (source[index] === "/" && source[index + 1] === "*") {
396
+ const end = source.indexOf("*/", index + 2);
397
+ if (end < 0) {
398
+ index = source.length;
399
+ return;
177
400
  }
178
- : unknown
179
- : unknown;
401
+ index = end + 2;
402
+ continue;
403
+ }
404
+ return;
405
+ }
406
+ };
407
+
408
+ const initial = source.slice(index);
409
+ const isAsync = initial.startsWith("async") && !/[\w$]/.test(initial[5] ?? "");
410
+ if (isAsync) {
411
+ index += 5;
412
+ skipTrivia();
413
+ }
414
+
415
+ const afterAsync = source.slice(index);
416
+ const isFunction = afterAsync.startsWith("function") && !/[\w$]/.test(afterAsync[8] ?? "");
417
+ if (isFunction) {
418
+ index += 8;
419
+ skipTrivia();
420
+ if (source[index] === "*") {
421
+ index++;
422
+ skipTrivia();
423
+ }
424
+ } else if (source[index] === "*") {
425
+ index++;
426
+ skipTrivia();
427
+ }
428
+
429
+ if (source[index] === "(") {
430
+ openIndex = index;
431
+ parenthesizedArrow = !isFunction;
432
+ asyncMethodOrArrow = isAsync && !isFunction;
433
+ } else if (isFunction) {
434
+ if (!/[A-Za-z_$]/.test(source[index] ?? "")) return undefined;
435
+ index++;
436
+ while (index < source.length && /[A-Za-z0-9_$]/.test(source[index] ?? "")) index++;
437
+ skipTrivia();
438
+ if (source[index] !== "(") return undefined;
439
+ openIndex = index;
440
+ } else {
441
+ const identifierStart = index;
442
+ if (!/[A-Za-z_$]/.test(source[index] ?? "")) return undefined;
443
+ index++;
444
+ while (index < source.length && /[A-Za-z0-9_$]/.test(source[index] ?? "")) index++;
445
+ const firstIdentifier = source.slice(identifierStart, index);
446
+ skipTrivia();
447
+ if (source[index] === "=" && source[index + 1] === ">") return undefined;
448
+ if (source[index] !== "(") {
449
+ if (firstIdentifier !== "get" && firstIdentifier !== "set") return undefined;
450
+ if (!/[A-Za-z_$]/.test(source[index] ?? "")) return undefined;
451
+ index++;
452
+ while (index < source.length && /[A-Za-z0-9_$]/.test(source[index] ?? "")) index++;
453
+ skipTrivia();
454
+ }
455
+ if (source[index] !== "(") return undefined;
456
+ openIndex = index;
457
+ }
458
+ if (openIndex < 0) return undefined;
459
+
460
+ let depth = 1;
461
+ let square = 0;
462
+ let curly = 0;
463
+ let quote: "'" | '"' | "`" | undefined;
464
+ let escaped = false;
465
+ let lineComment = false;
466
+ let blockComment = false;
467
+ const templateDepths = [0];
468
+ templateDepths.length = 0;
469
+ for (index = openIndex + 1; index < source.length; index++) {
470
+ const character = source[index];
471
+ const nextCharacter = source[index + 1];
472
+ if (lineComment) {
473
+ if (character === "\n" || character === "\r") lineComment = false;
474
+ else continue;
475
+ }
476
+ if (blockComment) {
477
+ if (character === "*" && nextCharacter === "/") {
478
+ blockComment = false;
479
+ index++;
480
+ }
481
+ continue;
482
+ }
483
+ if (quote) {
484
+ if (escaped) escaped = false;
485
+ else if (character === "\\") escaped = true;
486
+ else if (quote === "`" && character === "$" && nextCharacter === "{") {
487
+ curly++;
488
+ templateDepths.push(curly);
489
+ quote = undefined;
490
+ index++;
491
+ } else if (character === quote) quote = undefined;
492
+ continue;
493
+ }
494
+ if (character === "'" || character === '"' || character === "`") {
495
+ quote = character;
496
+ continue;
497
+ }
498
+ if (character === "/" && nextCharacter === "/") {
499
+ lineComment = true;
500
+ index++;
501
+ continue;
502
+ }
503
+ if (character === "/" && nextCharacter === "*") {
504
+ blockComment = true;
505
+ index++;
506
+ continue;
507
+ }
508
+ if (character === "/") return undefined;
509
+ // Annex B HTML-like comments are not lexed here; give up rather than
510
+ // risk misreading the parameter list.
511
+ if (character === "<" && source.startsWith("!--", index + 1)) return undefined;
512
+ if (character === "-" && source.startsWith("->", index + 1)) return undefined;
513
+ if (character === "(") depth++;
514
+ else if (character === ")") {
515
+ depth--;
516
+ if (depth === 0 && square === 0 && curly === 0) {
517
+ const closeIndex = index;
518
+ if (parenthesizedArrow) {
519
+ index++;
520
+ skipTrivia();
521
+ const hasArrow = source[index] === "=" && source[index + 1] === ">";
522
+ if (!hasArrow && (!asyncMethodOrArrow || source[index] !== "{")) return undefined;
523
+ }
524
+ return source.slice(openIndex + 1, closeIndex);
525
+ }
526
+ if (depth < 0) return undefined;
527
+ } else if (character === "[") square++;
528
+ else if (character === "]") {
529
+ square--;
530
+ if (square < 0) return undefined;
531
+ } else if (character === "{") curly++;
532
+ else if (character === "}") {
533
+ if (templateDepths.at(-1) === curly) {
534
+ templateDepths.pop();
535
+ curly--;
536
+ quote = "`";
537
+ } else curly--;
538
+ if (curly < 0) return undefined;
539
+ }
540
+ }
541
+ return undefined;
542
+ }
543
+
544
+ /**
545
+ * Conservative defense in depth for defaulted second parameters, which
546
+ * JavaScript intentionally omits from Function.length. Ambiguous source is
547
+ * ignored so this check can never reject a valid provider on weak evidence.
548
+ */
549
+ function authStartHasHiddenInput(start: unknown): boolean {
550
+ let source: string;
551
+ try {
552
+ source = Function.prototype.toString.call(start);
553
+ } catch {
554
+ return false;
555
+ }
556
+ if (
557
+ !source ||
558
+ source.includes("[native code]") ||
559
+ /^\s*(?:async\s+)?function\s+bound\b/.test(source)
560
+ )
561
+ return false;
562
+
563
+ const parameters = authStartParameterList(source);
564
+ if (!parameters) return false;
565
+ const parts = splitAuthStartParameters(parameters);
566
+ if (!parts || parts.length < 2) return false;
567
+
568
+ // Require ordinary, readable source formatting. This intentionally fails
569
+ // open for minified output and for transpilers that rewrite defaults.
570
+ const commaIndex = parameters.indexOf(",");
571
+ if (commaIndex < 0 || !/\s/.test(parameters[commaIndex + 1] ?? "")) return false;
572
+ const first = parts[0].trim();
573
+ const second = parts[1].trim();
574
+ const identifier = /^[_$A-Za-z][_$A-Za-z0-9]*/;
575
+ const firstName = first.match(identifier)?.[0];
576
+ const secondName = second.match(identifier)?.[0];
577
+ if (!firstName || !secondName || firstName.length < 3 || secondName.length < 3) return false;
578
+ return /\s=\s/.test(second);
579
+ }
180
580
 
181
- export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
581
+ export interface ProviderDeclaration {
182
582
  id: string;
183
583
  version: string;
184
584
  runtime: "standard" | "shared" | "browser";
@@ -189,20 +589,35 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
189
589
  * resolves omitted fields against the runtime deployment profiles.
190
590
  */
191
591
  deployment?: ProviderDeploymentOverrides;
592
+ /** Declares that provider operations use the SDK HTTP client. */
593
+ http?: true;
192
594
  allowedHosts?: string[];
595
+ native?: NativeProviderConfig;
193
596
  stealth?: {
194
597
  profile: string;
195
598
  platform: StealthPlatform;
196
599
  };
197
600
  proxy?: ProviderProxyConfig;
601
+ ocr?: ProviderOcrConfig;
198
602
  stt?: ProviderSttConfig;
603
+ resolver?: ProviderResolverConfig;
199
604
  browser?: { engine: BrowserEngine };
200
605
  auth?: AuthConfig;
606
+ /** Declares that provider operations issue and consume SDK choice tokens. */
607
+ choice?: true;
201
608
  reviewed?: ProviderReviewed;
202
609
  access?: ProviderAccessConfig;
203
610
  secrets?: ProviderSecretDeclaration[];
611
+ /** Declares that provider operations read SDK-managed environment values. */
612
+ env?: true;
204
613
  credential?: CredentialDeclaration;
205
614
  context?: ContextDeclaration;
615
+ /** Declares that provider operations use SDK-managed persistent state. */
616
+ state?: true;
617
+ /** Declares that provider operations use the SDK provider cache. */
618
+ cache?: true;
619
+ /** Declares that provider operations access runtime-resolvable files. */
620
+ files?: true;
206
621
  meta: {
207
622
  displayName: string;
208
623
  displayNameKey?: string;
@@ -224,25 +639,35 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
224
639
  publicSchemaFieldNames?: "normalized";
225
640
  };
226
641
  };
227
- operations: OperationMapConfig<TOperations>;
228
642
  healthMonitor?: ProviderHealthMonitorConfig;
229
643
  /** New name for `healthMonitor` (transitional alias); declaring both is a ValidationError. */
230
644
  healthProbe?: ProviderHealthMonitorConfig;
231
645
  healthJourneys?: readonly HealthJourneyDefinition[];
232
646
  }
233
647
 
234
- /** Define one provider operation with schema-driven handler inference. */
235
- export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
236
- operation: OperationConfig<TInput, TOutput>,
237
- ): OperationDefinition<TInput, TOutput> {
238
- return operation;
648
+ interface ProviderConfig<
649
+ TOperations extends Record<string, ProviderOperation>,
650
+ TContext = ProviderContext,
651
+ > extends ProviderDeclaration {
652
+ operations: OperationMapConfig<TOperations, TContext>;
239
653
  }
240
654
 
241
- /** Define a non-JSON provider operation with explicit transport metadata. */
242
- export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
243
- operation: StreamOperationConfig<TInput, TOutput>,
244
- ): OperationDefinition<TInput, TOutput> {
245
- return operation;
655
+ /** Define one factored provider operation with schema-driven handler inference. */
656
+ export function defineOperation<TContext>() {
657
+ return function operation<TInput extends SchemaLike, TOutput extends SchemaLike>(
658
+ config: OperationConfig<TInput, TOutput, TContext>,
659
+ ): OperationDefinition<TInput, TOutput, TContext> {
660
+ return config;
661
+ };
662
+ }
663
+
664
+ /** Define a factored non-JSON operation with explicit transport metadata. */
665
+ export function defineStreamOperation<TContext>() {
666
+ return function streamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
667
+ config: StreamOperationConfig<TInput, TOutput, TContext>,
668
+ ): OperationDefinition<TInput, TOutput, TContext> {
669
+ return config;
670
+ };
246
671
  }
247
672
 
248
673
  function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
@@ -279,18 +704,133 @@ function assertLiteralField<TValue extends string>(
279
704
  );
280
705
  }
281
706
  }
282
- function validateProviderShape(config: unknown): void {
707
+
708
+ function validateProxiedOAuthParams(
709
+ value: unknown,
710
+ field: "authorizeParams" | "tokenParams",
711
+ providerId: string,
712
+ ): void {
713
+ if (value === undefined) return;
714
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
715
+ throw new ValidationError(
716
+ `Provider "${providerId}" auth.proxied.${field} must be an object of string values.`,
717
+ );
718
+ }
719
+ for (const [key, paramValue] of Object.entries(value)) {
720
+ if (!key.trim() || typeof paramValue !== "string") {
721
+ throw new ValidationError(
722
+ `Provider "${providerId}" auth.proxied.${field} must contain non-empty keys and string values.`,
723
+ );
724
+ }
725
+ if (
726
+ field === "authorizeParams" &&
727
+ PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())
728
+ ) {
729
+ throw new ValidationError(
730
+ `Provider "${providerId}" auth.proxied.authorizeParams cannot override reserved parameter "${key}".`,
731
+ );
732
+ }
733
+ }
734
+ }
735
+
736
+ function validateProxiedOAuthAuth(auth: Record<string, unknown>, providerId: string): void {
737
+ const proxied = auth.proxied;
738
+ if (auth.mode !== "oauth2_proxied") {
739
+ if (proxied !== undefined) {
740
+ throw new ValidationError(
741
+ `Provider "${providerId}" auth.proxied is only valid when auth.mode is "oauth2_proxied".`,
742
+ );
743
+ }
744
+ return;
745
+ }
746
+ if (!proxied || typeof proxied !== "object" || Array.isArray(proxied)) {
747
+ throw new ValidationError(
748
+ `Provider "${providerId}" with auth.mode "oauth2_proxied" must declare auth.proxied.`,
749
+ );
750
+ }
751
+ const config = Object.fromEntries(Object.entries(proxied));
752
+ for (const key of Object.keys(config)) {
753
+ if (!PROXIED_OAUTH_ALLOWED_FIELDS.has(key)) {
754
+ throw new ValidationError(
755
+ `Provider "${providerId}" has unknown auth.proxied field "${key}".`,
756
+ );
757
+ }
758
+ }
759
+ for (const field of PROXIED_OAUTH_REQUIRED_FIELDS) {
760
+ if (typeof config[field] !== "string" || !config[field].trim()) {
761
+ throw new ValidationError(
762
+ `Provider "${providerId}" auth.proxied.${field} must be a non-empty string.`,
763
+ );
764
+ }
765
+ }
766
+ for (const field of ["authorizeUrl", "tokenUrl"] as const) {
767
+ try {
768
+ const endpoint = new URL(String(config[field]));
769
+ if (
770
+ endpoint.protocol !== "https:" ||
771
+ endpoint.username ||
772
+ endpoint.password ||
773
+ endpoint.hash
774
+ ) {
775
+ throw new Error("invalid endpoint");
776
+ }
777
+ } catch {
778
+ throw new ValidationError(
779
+ `Provider "${providerId}" auth.proxied.${field} must be an absolute HTTPS URL without credentials or a fragment.`,
780
+ );
781
+ }
782
+ }
783
+ const customScheme = String(config.customScheme);
784
+ if (
785
+ !CUSTOM_SCHEME_REGEX.test(customScheme) ||
786
+ customScheme.toLowerCase().startsWith("http://") ||
787
+ customScheme.toLowerCase().startsWith("https://")
788
+ ) {
789
+ throw new ValidationError(
790
+ `Provider "${providerId}" auth.proxied.customScheme must be a non-HTTP custom-scheme URL prefix.`,
791
+ );
792
+ }
793
+ if (!PROXIED_OAUTH_PROFILE_REGEX.test(String(config.rewriteProfile))) {
794
+ throw new ValidationError(
795
+ `Provider "${providerId}" auth.proxied.rewriteProfile must be a kebab-case profile name.`,
796
+ );
797
+ }
798
+ if (!PROXIED_OAUTH_ENV_KEY_REGEX.test(String(config.clientIdEnvKey))) {
799
+ throw new ValidationError(
800
+ `Provider "${providerId}" auth.proxied.clientIdEnvKey must be an APIFuse-style uppercase environment key.`,
801
+ );
802
+ }
803
+ if (config.pkce !== undefined && config.pkce !== "S256" && config.pkce !== "none") {
804
+ throw new ValidationError(
805
+ `Provider "${providerId}" auth.proxied.pkce must be "S256" or "none".`,
806
+ );
807
+ }
808
+ validateProxiedOAuthParams(config.authorizeParams, "authorizeParams", providerId);
809
+ validateProxiedOAuthParams(config.tokenParams, "tokenParams", providerId);
810
+ }
811
+
812
+ function validateProviderDeclarationShape(config: unknown): void {
283
813
  assertObjectConfig(config);
284
814
  assertRequiredField(config, "id");
285
815
  assertRequiredField(config, "version", String(config.id));
286
816
  assertRequiredField(config, "runtime", String(config.id));
287
817
  assertRequiredField(config, "meta", String(config.id));
288
- assertRequiredField(config, "operations", String(config.id));
289
818
  if (typeof config.runtime === "string")
290
819
  assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
820
+ if (config.native !== undefined && config.runtime === "browser") {
821
+ throw new ValidationError(
822
+ `Provider "${String(config.id)}" cannot declare capability "native" with runtime "browser"`,
823
+ {
824
+ fix: 'Use runtime: "standard" or runtime: "shared", or remove the native declaration.',
825
+ },
826
+ );
827
+ }
291
828
  const auth = config.auth;
292
829
  if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
293
830
  assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
831
+ if (auth && typeof auth === "object" && !Array.isArray(auth)) {
832
+ validateProxiedOAuthAuth(Object.fromEntries(Object.entries(auth)), String(config.id));
833
+ }
294
834
  if (auth && typeof auth === "object" && "exchange" in auth) {
295
835
  throw new ProviderError(
296
836
  `Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
@@ -316,6 +856,24 @@ function validateProviderShape(config: unknown): void {
316
856
  },
317
857
  );
318
858
  }
859
+ if (
860
+ auth &&
861
+ typeof auth === "object" &&
862
+ "flow" in auth &&
863
+ auth.flow &&
864
+ typeof auth.flow === "object" &&
865
+ "start" in auth.flow &&
866
+ typeof auth.flow.start === "function" &&
867
+ auth.flow.start.length <= 1 &&
868
+ authStartHasHiddenInput(auth.flow.start)
869
+ ) {
870
+ throw new ProviderError(
871
+ `Provider "${String(config.id)}" auth.flow.start must not declare an input parameter`,
872
+ {
873
+ fix: "Return a form turn from start(ctx), then receive user input in continue(ctx, input).",
874
+ },
875
+ );
876
+ }
319
877
  const access = config.access;
320
878
  if (access !== undefined) {
321
879
  if (!access || typeof access !== "object" || Array.isArray(access)) {
@@ -354,6 +912,11 @@ function validateProviderShape(config: unknown): void {
354
912
  }
355
913
  }
356
914
 
915
+ function validateProviderImplementationShape(config: { id: string }): void {
916
+ const configRecord = config as unknown as Record<string, unknown>;
917
+ assertRequiredField(configRecord, "operations", String(config.id));
918
+ }
919
+
357
920
  function validateProviderProxy(config: {
358
921
  id: string;
359
922
  proxy?: ProviderProxyConfig;
@@ -371,11 +934,24 @@ function validateProviderProxy(config: {
371
934
  },
372
935
  );
373
936
  }
374
- rejectUnknownFields(proxy, new Set(["mode", "provider", "geo", "session"]), "proxy");
937
+ rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
375
938
  assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
376
939
  if (proxy.provider !== undefined) {
377
940
  assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
378
941
  }
942
+ if (proxy.providers !== undefined) {
943
+ if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
944
+ throw new ValidationError(
945
+ `Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`,
946
+ {
947
+ fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
948
+ },
949
+ );
950
+ }
951
+ for (const vendor of proxy.providers) {
952
+ assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
953
+ }
954
+ }
379
955
  if (proxy.geo !== undefined) {
380
956
  if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
381
957
  throw new ValidationError(
@@ -409,7 +985,7 @@ function validateProviderProxy(config: {
409
985
  }
410
986
  rejectUnknownFields(
411
987
  proxy.session,
412
- new Set(["affinity", "lifetimeMinutes", "poolSize"]),
988
+ new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]),
413
989
  "proxy.session",
414
990
  );
415
991
  if (proxy.session.affinity !== undefined) {
@@ -432,20 +1008,95 @@ function validateProviderProxy(config: {
432
1008
  `Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
433
1009
  );
434
1010
  }
435
- }
436
- if (proxy.mode === "required" && proxy.provider === "smartproxy") {
437
- const hasSmartproxySecret = config.secrets?.some(
438
- (secret) => secret.name === SMARTPROXY_APP_KEY_SECRET && secret.required !== false,
439
- );
440
- if (!hasSmartproxySecret) {
1011
+ const drainLeadSeconds = proxy.session.drainLeadSeconds;
1012
+ if (
1013
+ drainLeadSeconds !== undefined &&
1014
+ (!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)
1015
+ ) {
1016
+ throw new ValidationError(
1017
+ `Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`,
1018
+ {
1019
+ fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
1020
+ },
1021
+ );
1022
+ }
1023
+ // A drain lead longer than the sticky lifetime would fire the expiring
1024
+ // event before the session is even established, so the provider would
1025
+ // never get a usable window. Reject the contradiction at build time.
1026
+ if (
1027
+ drainLeadSeconds !== undefined &&
1028
+ lifetime !== undefined &&
1029
+ Number.isFinite(lifetime) &&
1030
+ drainLeadSeconds >= lifetime * 60
1031
+ ) {
441
1032
  throw new ValidationError(
442
- `Provider "${config.id}" requires Smartproxy egress but does not declare ${SMARTPROXY_APP_KEY_SECRET}.`,
1033
+ `Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`,
443
1034
  {
444
- fix: `Add secrets: [{ name: "${SMARTPROXY_APP_KEY_SECRET}", required: true }] to the provider.`,
1035
+ fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
445
1036
  },
446
1037
  );
447
1038
  }
448
1039
  }
1040
+ // Every credentialed vendor in a required-mode chain must declare its
1041
+ // provider secret(s) so a missing credential fails at build/validation time,
1042
+ // not during a live outage. This covers the fallback legs too (not just the
1043
+ // first vendor): a declared-but-uncredentialed nodemaven fallback would leave
1044
+ // the chain silently down to a single vendor, reintroducing the SPOF the chain
1045
+ // removes.
1046
+ const vendorChain =
1047
+ proxy.providers && proxy.providers.length > 0
1048
+ ? proxy.providers
1049
+ : proxy.provider
1050
+ ? [proxy.provider]
1051
+ : [];
1052
+ if (proxy.mode === "required") {
1053
+ for (const vendor of vendorChain) {
1054
+ const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
1055
+ if (!requiredSecrets) continue;
1056
+ for (const secretName of requiredSecrets) {
1057
+ // Match the canonical runtime gate (assertRequiredSecretsPresent /
1058
+ // listMissingRequiredSecrets), which enforces only `required === true`
1059
+ // declarations. A declaration that omits `required` (defaulting to
1060
+ // optional) is skipped at runtime, so accepting it here would pass
1061
+ // validation while leaving the credential unenforced until proxy
1062
+ // resolution during a live request — the fail-open gap this check exists
1063
+ // to close.
1064
+ const declared = config.secrets?.some(
1065
+ (secret) => secret.name === secretName && secret.required === true,
1066
+ );
1067
+ if (!declared) {
1068
+ throw new ValidationError(
1069
+ `Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`,
1070
+ {
1071
+ fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
1072
+ },
1073
+ );
1074
+ }
1075
+ }
1076
+ }
1077
+ }
1078
+ // `decodo`/`custom` are deprecated vendor values (string-union members, so the
1079
+ // @deprecated symbol gate can't catch them — warn at validation time instead).
1080
+ const deprecatedVendors = vendorChain.filter(
1081
+ (vendor) => vendor === "decodo" || vendor === "custom",
1082
+ );
1083
+ if (
1084
+ proxy.mode === "required" &&
1085
+ vendorChain.length > 0 &&
1086
+ deprecatedVendors.length === vendorChain.length
1087
+ ) {
1088
+ throw new ValidationError(
1089
+ `Provider "${config.id}" requires proxy egress but declares only deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}.`,
1090
+ {
1091
+ fix: `Use proxy.provider or proxy.providers with "smartproxy" and/or "nodemaven".`,
1092
+ },
1093
+ );
1094
+ }
1095
+ if (deprecatedVendors.length > 0) {
1096
+ console.warn(
1097
+ `[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven".`,
1098
+ );
1099
+ }
449
1100
  }
450
1101
 
451
1102
  function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): void {
@@ -460,6 +1111,83 @@ function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): v
460
1111
  assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
461
1112
  }
462
1113
 
1114
+ function validateProviderOcr(config: { id: string; ocr?: ProviderOcrConfig }): void {
1115
+ const ocr = config.ocr;
1116
+ if (ocr === undefined) return;
1117
+ if (!ocr || typeof ocr !== "object" || Array.isArray(ocr)) {
1118
+ throw new ValidationError(`Provider "${config.id}" has invalid ocr: must be an object.`, {
1119
+ fix: `Use ocr: { mode: "required" } or ocr: { mode: "optional" }.`,
1120
+ });
1121
+ }
1122
+ rejectUnknownFields(ocr, new Set(["mode"]), "ocr");
1123
+ assertLiteralField(ocr.mode, "ocr.mode", VALID_PROVIDER_OCR_MODES, config.id);
1124
+ }
1125
+
1126
+ function validateProviderResolver(config: { id: string; resolver?: ProviderResolverConfig }): void {
1127
+ const resolver = config.resolver;
1128
+ if (resolver === undefined) return;
1129
+ if (!resolver || typeof resolver !== "object" || Array.isArray(resolver)) {
1130
+ throw new ValidationError(`Provider "${config.id}" has invalid resolver: must be an object.`, {
1131
+ fix: `Set resolver for provider "${config.id}" to { vendors: ["2captcha"], kinds: ["turnstile"] }.`,
1132
+ });
1133
+ }
1134
+ rejectUnknownFields(
1135
+ resolver,
1136
+ new Set(["vendors", "kinds", "clientProfile"]),
1137
+ "resolver",
1138
+ config.id,
1139
+ );
1140
+ if (resolver.vendors !== undefined) {
1141
+ validateResolverLiteralArray(
1142
+ resolver.vendors,
1143
+ "resolver.vendors",
1144
+ VALID_PROVIDER_RESOLVER_VENDORS,
1145
+ config.id,
1146
+ );
1147
+ }
1148
+ validateResolverLiteralArray(
1149
+ resolver.kinds,
1150
+ "resolver.kinds",
1151
+ VALID_PROVIDER_CHALLENGE_KINDS,
1152
+ config.id,
1153
+ );
1154
+ if (
1155
+ resolver.clientProfile !== undefined &&
1156
+ (typeof resolver.clientProfile !== "string" || !resolver.clientProfile.trim())
1157
+ ) {
1158
+ throw new ValidationError(
1159
+ `Provider "${config.id}" has invalid resolver.clientProfile: must be a non-empty string.`,
1160
+ {
1161
+ fix: `Set resolver.clientProfile for provider "${config.id}" to a transport-owned profile name.`,
1162
+ },
1163
+ );
1164
+ }
1165
+ }
1166
+
1167
+ function validateResolverLiteralArray<TValue extends string>(
1168
+ value: readonly TValue[],
1169
+ field: string,
1170
+ validValues: readonly TValue[],
1171
+ providerId: string,
1172
+ ): void {
1173
+ if (!Array.isArray(value)) {
1174
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an array.`, {
1175
+ fix: `Set ${field} for provider "${providerId}" to an array containing only: ${validValues.join(", ")}.`,
1176
+ });
1177
+ }
1178
+ for (const [index, item] of value.entries()) {
1179
+ if (typeof item === "string" && validValues.some((validValue) => validValue === item)) {
1180
+ continue;
1181
+ }
1182
+ throw new ValidationError(
1183
+ `Provider "${providerId}" has invalid ${field}[${index}]: ${JSON.stringify(item)}. Expected one of: ${validValues.join(", ")}`,
1184
+ {
1185
+ fix: `Set ${field}[${index}] for provider "${providerId}" to one of ${validValues.map((validValue) => `"${validValue}"`).join(", ")}.`,
1186
+ },
1187
+ );
1188
+ }
1189
+ }
1190
+
463
1191
  function validateOperationIds(
464
1192
  providerId: string,
465
1193
  operations: Record<string, ProviderOperation>,
@@ -702,6 +1430,36 @@ function validateOperationObservability(
702
1430
  }
703
1431
  }
704
1432
 
1433
+ function validateOperationErrorCodes(
1434
+ providerId: string,
1435
+ operations: Record<string, ProviderOperation>,
1436
+ ): void {
1437
+ for (const [operationName, operation] of Object.entries(operations)) {
1438
+ for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
1439
+ if (
1440
+ errorCode.status !== undefined &&
1441
+ !VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)
1442
+ ) {
1443
+ const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
1444
+ throw new ValidationError(
1445
+ `Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`,
1446
+ {
1447
+ fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
1448
+ },
1449
+ );
1450
+ }
1451
+ if (
1452
+ errorCode.status !== undefined &&
1453
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)
1454
+ ) {
1455
+ console.warn(
1456
+ `[provider-sdk] Provider "${providerId}" operation "${operationName}" declares status ${errorCode.status} for SDK-owned error code "${errorCode.code}"; the declared status is documentation-only and will be ignored at runtime.`,
1457
+ );
1458
+ }
1459
+ }
1460
+ }
1461
+ }
1462
+
705
1463
  const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
706
1464
  const SSE_TRANSPORT_FIELDS = new Set([
707
1465
  "kind",
@@ -1027,7 +1785,12 @@ function suggestField(unknown: string, candidates: ReadonlySet<string>): string
1027
1785
  return best;
1028
1786
  }
1029
1787
 
1030
- function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldPath: string): void {
1788
+ function rejectUnknownFields(
1789
+ value: object,
1790
+ allowed: ReadonlySet<string>,
1791
+ fieldPath: string,
1792
+ providerId?: string,
1793
+ ): void {
1031
1794
  for (const key of Object.keys(value)) {
1032
1795
  if (allowed.has(key)) continue;
1033
1796
  const hint = suggestField(key, allowed);
@@ -1035,7 +1798,11 @@ function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldP
1035
1798
  hint
1036
1799
  ? `Unknown field "${key}" on ${fieldPath}. Did you mean "${hint}"?`
1037
1800
  : `Unknown field "${key}" on ${fieldPath}.`,
1038
- { fix: `Remove ${fieldPath}.${key} or rename it.` },
1801
+ {
1802
+ fix: providerId
1803
+ ? `Remove ${fieldPath}.${key} from provider "${providerId}" or rename it.`
1804
+ : `Remove ${fieldPath}.${key} or rename it.`,
1805
+ },
1039
1806
  );
1040
1807
  }
1041
1808
  }
@@ -1372,6 +2139,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
1372
2139
  "manualTrigger",
1373
2140
  "steps",
1374
2141
  "run",
2142
+ "scenario",
1375
2143
  ]);
1376
2144
  const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
1377
2145
  const HEALTH_JOURNEY_STEP_FIELDS = new Set([
@@ -1907,6 +2675,14 @@ function validateHealthJourneys(
1907
2675
  }
1908
2676
  if (journey.manualTrigger !== undefined)
1909
2677
  validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
2678
+ if (journey.scenario !== undefined && journey.smsMatchers !== undefined)
2679
+ throw new ValidationError(
2680
+ `Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers is not allowed on declarative scenarios.`,
2681
+ );
2682
+ if (journey.scenario !== undefined && journey.requiredSecrets !== undefined)
2683
+ throw new ValidationError(
2684
+ `Provider "${providerId}" healthJourneys.${journey.id}.requiredSecrets is not allowed on declarative scenarios.`,
2685
+ );
1910
2686
  if (journey.timeout !== undefined)
1911
2687
  assertIsoDuration(
1912
2688
  journey.timeout,
@@ -1977,6 +2753,26 @@ function validateOperationFixtures(
1977
2753
  fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
1978
2754
  },
1979
2755
  );
2756
+ if (operation.fixtures?.recordedAt !== undefined) {
2757
+ const recordedAt = operation.fixtures.recordedAt;
2758
+ const parsed =
2759
+ typeof recordedAt === "string"
2760
+ ? new Date(`${recordedAt}T00:00:00.000Z`)
2761
+ : new Date(Number.NaN);
2762
+ const isCalendarDate =
2763
+ typeof recordedAt === "string" &&
2764
+ /^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
2765
+ !Number.isNaN(parsed.getTime()) &&
2766
+ parsed.toISOString().slice(0, 10) === recordedAt;
2767
+ const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
2768
+ if (!isCalendarDate || recordedAt > kstToday)
2769
+ throw new ValidationError(
2770
+ `Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`,
2771
+ {
2772
+ fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
2773
+ },
2774
+ );
2775
+ }
1980
2776
  if (operation.fixtures?.request !== undefined) {
1981
2777
  const result = safeParseSchemaSync(
1982
2778
  operation.input,
@@ -2010,6 +2806,31 @@ function validateOperationFixtures(
2010
2806
  }
2011
2807
  }
2012
2808
 
2809
+ function resolveOperationFixtureRequests<TOperations extends Record<string, ProviderOperation>>(
2810
+ operations: TOperations,
2811
+ ): TOperations {
2812
+ let changed = false;
2813
+ const resolvedOperations = Object.fromEntries(
2814
+ Object.entries(operations).map(([operationName, operation]) => {
2815
+ if (operation.fixtures?.request === undefined) return [operationName, operation];
2816
+ const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
2817
+ if (request === operation.fixtures.request) return [operationName, operation];
2818
+ changed = true;
2819
+ return [
2820
+ operationName,
2821
+ {
2822
+ ...operation,
2823
+ fixtures: {
2824
+ ...operation.fixtures,
2825
+ request,
2826
+ },
2827
+ },
2828
+ ];
2829
+ }),
2830
+ ) as TOperations;
2831
+ return changed ? resolvedOperations : operations;
2832
+ }
2833
+
2013
2834
  /**
2014
2835
  * Shallow shape guard only: the `deployment` object is passed through
2015
2836
  * verbatim and deliberately not deep-validated by the SDK — the APIFuse
@@ -2023,33 +2844,47 @@ function validateProviderDeployment(providerId: string, deployment: unknown): vo
2023
2844
  });
2024
2845
  }
2025
2846
 
2026
- export function defineProvider<
2847
+ /** The second authoring phase for a declaration established by defineProvider. */
2848
+ export type ProviderBuilder<TDeclaration extends ProviderDeclaration> = <
2027
2849
  TOperations extends Record<string, ProviderOperation>,
2028
- TConfig extends ProviderConfig<TOperations>,
2029
2850
  >(
2030
- config: TConfig & AuthStartNoInputGuard<TConfig>,
2031
- ): ProviderDefinition & { operations: OperationMapConfig<TOperations> } {
2032
- validateProviderShape(config);
2851
+ implementation: {
2852
+ operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
2853
+ },
2854
+ ) => Omit<ProviderDefinition, "operations"> & {
2855
+ operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
2856
+ };
2857
+
2858
+ /** Extract the declaration-derived operation context from a provider builder. */
2859
+ export type ProviderContextOf<TBuilder> = TBuilder extends ProviderBuilder<infer TDeclaration>
2860
+ ? ProviderContextFor<TDeclaration>
2861
+ : never;
2862
+
2863
+ /** Establish a provider declaration before its operations are contextually typed. */
2864
+ export function defineProvider<const TDeclaration extends ProviderDeclaration>(
2865
+ declaration: TDeclaration &
2866
+ Record<Exclude<keyof TDeclaration, keyof ProviderDeclaration>, never> &
2867
+ AuthStartNoInputGuard<TDeclaration>,
2868
+ ): ProviderBuilder<TDeclaration> {
2869
+ validateProviderDeclaration(declaration);
2870
+ const buildProvider = <TOperations extends Record<string, ProviderOperation>>(
2871
+ implementation: {
2872
+ operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
2873
+ },
2874
+ ) =>
2875
+ finalizeProvider({
2876
+ ...declaration,
2877
+ ...implementation,
2878
+ } as ProviderConfig<TOperations, ProviderContextFor<TDeclaration>>);
2879
+ return buildProvider as ProviderBuilder<TDeclaration>;
2880
+ }
2881
+
2882
+ function validateProviderDeclaration(config: ProviderDeclaration): void {
2883
+ validateProviderDeclarationShape(config);
2033
2884
  if (!CONNECTOR_ID_REGEX.test(config.id))
2034
2885
  throw new ProviderError(`Invalid provider id: "${config.id}"`, {
2035
2886
  fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
2036
2887
  });
2037
- if (Object.keys(config.operations).length === 0)
2038
- throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
2039
- fix: "Add at least one operation to the operations object",
2040
- });
2041
- validateOperationIds(config.id, config.operations);
2042
- validateOperationAnnotations(config.id, config.operations);
2043
- validateOperationObservability(config.id, config.operations);
2044
- validateOperationTransports(config.id, config.operations);
2045
- validateOperationContracts(config.id, config.operations);
2046
- validateToolRouterMetadata(config.id, config.operations);
2047
- const journeyCoveredOperations = validateHealthJourneys(
2048
- config.id,
2049
- config.operations,
2050
- config.healthJourneys,
2051
- );
2052
- validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
2053
2888
  if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
2054
2889
  throw new ValidationError(
2055
2890
  `Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
@@ -2062,10 +2897,18 @@ export function defineProvider<
2062
2897
  config.healthProbe ?? config.healthMonitor,
2063
2898
  config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
2064
2899
  );
2065
- validateOperationFixtures(config.id, config.operations);
2066
2900
  validateProviderDeployment(config.id, config.deployment);
2901
+ try {
2902
+ validateNativeProviderConfig(config.native);
2903
+ } catch (error) {
2904
+ if (error instanceof NativeEgressPolicyValidationError)
2905
+ throw new ValidationError(error.message);
2906
+ throw error;
2907
+ }
2067
2908
  validateProviderProxy(config);
2909
+ validateProviderOcr(config);
2068
2910
  validateProviderStt(config);
2911
+ validateProviderResolver(config);
2069
2912
  if (config.runtime === "browser" && !config.browser)
2070
2913
  throw new ProviderError(
2071
2914
  `Provider "${config.id}" must define browser.engine when runtime is "browser"`,
@@ -2078,7 +2921,40 @@ export function defineProvider<
2078
2921
  `Provider "${config.id}" cannot define browser config unless runtime is "browser"`,
2079
2922
  { fix: 'Set runtime: "browser" or remove the browser config' },
2080
2923
  );
2081
- return {
2924
+ validateFailClosedProviderDeclaration(config);
2925
+ }
2926
+
2927
+ function finalizeProvider<
2928
+ TOperations extends Record<string, ProviderOperation>,
2929
+ TContext,
2930
+ >(
2931
+ config: ProviderConfig<TOperations, TContext>,
2932
+ ): Omit<ProviderDefinition, "operations"> & {
2933
+ operations: OperationMapConfig<TOperations, TContext>;
2934
+ } {
2935
+ validateProviderImplementationShape(config);
2936
+ const operations = resolveOperationFixtureRequests(config.operations);
2937
+ if (Object.keys(config.operations).length === 0)
2938
+ throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
2939
+ fix: "Add at least one operation to the operations object",
2940
+ });
2941
+ validateOperationIds(config.id, config.operations);
2942
+ validateOperationAnnotations(config.id, config.operations);
2943
+ validateOperationObservability(config.id, config.operations);
2944
+ validateOperationErrorCodes(config.id, config.operations);
2945
+ validateOperationTransports(config.id, config.operations);
2946
+ validateOperationContracts(config.id, config.operations);
2947
+ validateToolRouterMetadata(config.id, config.operations);
2948
+ const journeyCoveredOperations = validateHealthJourneys(
2949
+ config.id,
2950
+ config.operations,
2951
+ config.healthJourneys,
2952
+ );
2953
+ validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
2954
+ validateOperationFixtures(config.id, operations);
2955
+ const provider: Omit<ProviderDefinition, "operations"> & {
2956
+ operations: OperationMapConfig<TOperations, TContext>;
2957
+ } = {
2082
2958
  id: config.id,
2083
2959
  version: config.version,
2084
2960
  runtime: config.runtime,
@@ -2086,9 +2962,12 @@ export function defineProvider<
2086
2962
  // are owned by the APIFuse registry builder, not the SDK.
2087
2963
  deployment: config.deployment,
2088
2964
  allowedHosts: config.allowedHosts,
2965
+ native: config.native,
2089
2966
  stealth: config.stealth,
2090
2967
  proxy: config.proxy,
2968
+ ocr: config.ocr,
2091
2969
  stt: config.stt,
2970
+ resolver: config.resolver,
2092
2971
  browser: config.browser,
2093
2972
  auth: config.auth,
2094
2973
  reviewed: config.reviewed,
@@ -2097,11 +2976,15 @@ export function defineProvider<
2097
2976
  credential: config.credential,
2098
2977
  context: config.context,
2099
2978
  meta: config.meta,
2100
- operations: config.operations,
2979
+ operations,
2101
2980
  // Transitional healthMonitor → healthProbe alias: mirror whichever field
2102
2981
  // was declared onto both so old and new consumers keep working.
2103
2982
  healthMonitor: config.healthMonitor ?? config.healthProbe,
2104
2983
  healthProbe: config.healthProbe ?? config.healthMonitor,
2105
2984
  healthJourneys: config.healthJourneys,
2106
2985
  };
2986
+ // Declaration validation never invokes handlers, so their declaration-bound
2987
+ // context parameter is irrelevant to the runtime ProviderDefinition shape.
2988
+ validateFailClosedOperationDeclaration(provider as unknown as ProviderDefinition);
2989
+ return provider;
2107
2990
  }