@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/dist/define.js CHANGED
@@ -1,22 +1,89 @@
1
1
  import ms from "ms";
2
+ import { validateFailClosedOperationDeclaration, validateFailClosedProviderDeclaration, } from "./declaration-validation.js";
3
+ import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
2
4
  import { ProviderError, ValidationError } from "./errors.js";
5
+ import { NativeEgressPolicyValidationError, validateNativeProviderConfig, } from "./native-egress-policy.js";
3
6
  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";
7
+ import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
8
+ 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, VALID_OPERATION_ERROR_STATUSES, } from "./types.js";
5
9
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
6
10
  const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
7
11
  const VALID_RUNTIMES = ["standard", "shared", "browser"];
8
- const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"];
12
+ const VALID_AUTH_MODES = [
13
+ "none",
14
+ "platform-managed",
15
+ "credentials",
16
+ "oauth2",
17
+ "oauth2_proxied",
18
+ ];
19
+ const PROXIED_OAUTH_REQUIRED_FIELDS = [
20
+ "authorizeUrl",
21
+ "tokenUrl",
22
+ "customScheme",
23
+ "rewriteProfile",
24
+ "clientIdEnvKey",
25
+ ];
26
+ const PROXIED_OAUTH_ALLOWED_FIELDS = new Set([
27
+ ...PROXIED_OAUTH_REQUIRED_FIELDS,
28
+ "pkce",
29
+ "authorizeParams",
30
+ "tokenParams",
31
+ ]);
32
+ const PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS = new Set([
33
+ "client_id",
34
+ "response_type",
35
+ "state",
36
+ "code_challenge",
37
+ "code_challenge_method",
38
+ ]);
39
+ const PROXIED_OAUTH_PROFILE_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
40
+ const PROXIED_OAUTH_ENV_KEY_REGEX = /^[A-Z][A-Z0-9_]*__[A-Z0-9_]+$/;
41
+ const CUSTOM_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+$/;
9
42
  const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"];
10
43
  const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"];
11
- const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "decodo", "custom"];
44
+ const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"];
12
45
  const VALID_PROVIDER_PROXY_AFFINITIES = [
13
46
  "request",
14
47
  "operation",
15
48
  "auth-flow",
16
49
  "connection",
17
50
  ];
51
+ const VALID_PROVIDER_OCR_MODES = ["optional", "required"];
18
52
  const VALID_PROVIDER_STT_MODES = ["optional", "required"];
53
+ function exhaustiveLiteralArray() {
54
+ return (values, ..._missing) => values;
55
+ }
56
+ export const VALID_PROVIDER_RESOLVER_VENDORS = exhaustiveLiteralArray()([
57
+ "browser",
58
+ "capsolver",
59
+ "capmonster",
60
+ "2captcha",
61
+ "custom",
62
+ ]);
63
+ export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray()([
64
+ "turnstile",
65
+ "recaptcha_v2",
66
+ "recaptcha_v3",
67
+ "hcaptcha",
68
+ "cloudflare_interstitial",
69
+ "aws_waf",
70
+ "akamai_sec_cpt",
71
+ "akamai_sensor",
72
+ ]);
19
73
  const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
74
+ const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
75
+ const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
76
+ // Per-vendor provider-declared credential secrets. A required-mode chain must
77
+ // declare every secret of every credentialed vendor it names, so a missing
78
+ // credential fails at build/validation time rather than during a live outage: a
79
+ // declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
80
+ // exactly the failure class the multi-vendor chain exists to remove. Vendors
81
+ // absent from this map (the deprecated `custom`/`decodo` values have no managed
82
+ // adapter) impose no declaration requirement.
83
+ const VENDOR_REQUIRED_SECRETS = {
84
+ smartproxy: [SMARTPROXY_APP_KEY_SECRET],
85
+ nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
86
+ };
20
87
  const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
21
88
  const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
22
89
  const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"];
@@ -42,13 +109,357 @@ function parsePositiveMsDuration(value) {
42
109
  return undefined;
43
110
  return parsed;
44
111
  }
45
- /** Define one provider operation with schema-driven handler inference. */
46
- export function defineOperation(operation) {
47
- return operation;
112
+ function splitAuthStartParameters(parameters) {
113
+ const parts = [];
114
+ let start = 0;
115
+ let round = 0;
116
+ let square = 0;
117
+ let curly = 0;
118
+ let quote;
119
+ let escaped = false;
120
+ let lineComment = false;
121
+ let blockComment = false;
122
+ const templateDepths = [0];
123
+ templateDepths.length = 0;
124
+ for (let index = 0; index < parameters.length; index++) {
125
+ const character = parameters[index];
126
+ const nextCharacter = parameters[index + 1];
127
+ if (lineComment) {
128
+ if (character === "\n" || character === "\r")
129
+ lineComment = false;
130
+ else
131
+ continue;
132
+ }
133
+ if (blockComment) {
134
+ if (character === "*" && nextCharacter === "/") {
135
+ blockComment = false;
136
+ index++;
137
+ }
138
+ continue;
139
+ }
140
+ if (quote) {
141
+ if (escaped) {
142
+ escaped = false;
143
+ }
144
+ else if (character === "\\") {
145
+ escaped = true;
146
+ }
147
+ else if (quote === "`" && character === "$" && nextCharacter === "{") {
148
+ curly++;
149
+ templateDepths.push(curly);
150
+ quote = undefined;
151
+ index++;
152
+ }
153
+ else if (character === quote) {
154
+ quote = undefined;
155
+ }
156
+ continue;
157
+ }
158
+ if (character === "'" || character === '"' || character === "`") {
159
+ quote = character;
160
+ continue;
161
+ }
162
+ if (character === "/" && nextCharacter === "/") {
163
+ lineComment = true;
164
+ index++;
165
+ continue;
166
+ }
167
+ if (character === "/" && nextCharacter === "*") {
168
+ blockComment = true;
169
+ index++;
170
+ continue;
171
+ }
172
+ if (character === "/")
173
+ return undefined;
174
+ // Annex B HTML-like comments are not lexed here; give up rather than
175
+ // risk misreading the parameter list.
176
+ if (character === "<" && parameters.startsWith("!--", index + 1))
177
+ return undefined;
178
+ if (character === "-" && parameters.startsWith("->", index + 1))
179
+ return undefined;
180
+ if (character === "(")
181
+ round++;
182
+ else if (character === ")")
183
+ round--;
184
+ else if (character === "[")
185
+ square++;
186
+ else if (character === "]")
187
+ square--;
188
+ else if (character === "{")
189
+ curly++;
190
+ else if (character === "}") {
191
+ if (templateDepths.at(-1) === curly) {
192
+ templateDepths.pop();
193
+ curly--;
194
+ quote = "`";
195
+ }
196
+ else
197
+ curly--;
198
+ }
199
+ else if (character === "," && round === 0 && square === 0 && curly === 0) {
200
+ parts.push(parameters.slice(start, index));
201
+ start = index + 1;
202
+ }
203
+ if (round < 0 || square < 0 || curly < 0)
204
+ return undefined;
205
+ }
206
+ if (quote ||
207
+ blockComment ||
208
+ templateDepths.length > 0 ||
209
+ round !== 0 ||
210
+ square !== 0 ||
211
+ curly !== 0)
212
+ return undefined;
213
+ parts.push(parameters.slice(start));
214
+ return parts;
215
+ }
216
+ function authStartParameterList(source) {
217
+ let index = 0;
218
+ while (index < source.length && /\s/.test(source[index] ?? ""))
219
+ index++;
220
+ if (index >= source.length)
221
+ return undefined;
222
+ let openIndex = -1;
223
+ let parenthesizedArrow = false;
224
+ let asyncMethodOrArrow = false;
225
+ const skipTrivia = () => {
226
+ while (index < source.length) {
227
+ if (/\s/.test(source[index] ?? "")) {
228
+ index++;
229
+ continue;
230
+ }
231
+ if (source[index] === "/" && source[index + 1] === "/") {
232
+ index += 2;
233
+ while (index < source.length && source[index] !== "\n" && source[index] !== "\r")
234
+ index++;
235
+ continue;
236
+ }
237
+ if (source[index] === "/" && source[index + 1] === "*") {
238
+ const end = source.indexOf("*/", index + 2);
239
+ if (end < 0) {
240
+ index = source.length;
241
+ return;
242
+ }
243
+ index = end + 2;
244
+ continue;
245
+ }
246
+ return;
247
+ }
248
+ };
249
+ const initial = source.slice(index);
250
+ const isAsync = initial.startsWith("async") && !/[\w$]/.test(initial[5] ?? "");
251
+ if (isAsync) {
252
+ index += 5;
253
+ skipTrivia();
254
+ }
255
+ const afterAsync = source.slice(index);
256
+ const isFunction = afterAsync.startsWith("function") && !/[\w$]/.test(afterAsync[8] ?? "");
257
+ if (isFunction) {
258
+ index += 8;
259
+ skipTrivia();
260
+ if (source[index] === "*") {
261
+ index++;
262
+ skipTrivia();
263
+ }
264
+ }
265
+ else if (source[index] === "*") {
266
+ index++;
267
+ skipTrivia();
268
+ }
269
+ if (source[index] === "(") {
270
+ openIndex = index;
271
+ parenthesizedArrow = !isFunction;
272
+ asyncMethodOrArrow = isAsync && !isFunction;
273
+ }
274
+ else if (isFunction) {
275
+ if (!/[A-Za-z_$]/.test(source[index] ?? ""))
276
+ return undefined;
277
+ index++;
278
+ while (index < source.length && /[A-Za-z0-9_$]/.test(source[index] ?? ""))
279
+ index++;
280
+ skipTrivia();
281
+ if (source[index] !== "(")
282
+ return undefined;
283
+ openIndex = index;
284
+ }
285
+ else {
286
+ const identifierStart = index;
287
+ if (!/[A-Za-z_$]/.test(source[index] ?? ""))
288
+ return undefined;
289
+ index++;
290
+ while (index < source.length && /[A-Za-z0-9_$]/.test(source[index] ?? ""))
291
+ index++;
292
+ const firstIdentifier = source.slice(identifierStart, index);
293
+ skipTrivia();
294
+ if (source[index] === "=" && source[index + 1] === ">")
295
+ return undefined;
296
+ if (source[index] !== "(") {
297
+ if (firstIdentifier !== "get" && firstIdentifier !== "set")
298
+ return undefined;
299
+ if (!/[A-Za-z_$]/.test(source[index] ?? ""))
300
+ return undefined;
301
+ index++;
302
+ while (index < source.length && /[A-Za-z0-9_$]/.test(source[index] ?? ""))
303
+ index++;
304
+ skipTrivia();
305
+ }
306
+ if (source[index] !== "(")
307
+ return undefined;
308
+ openIndex = index;
309
+ }
310
+ if (openIndex < 0)
311
+ return undefined;
312
+ let depth = 1;
313
+ let square = 0;
314
+ let curly = 0;
315
+ let quote;
316
+ let escaped = false;
317
+ let lineComment = false;
318
+ let blockComment = false;
319
+ const templateDepths = [0];
320
+ templateDepths.length = 0;
321
+ for (index = openIndex + 1; index < source.length; index++) {
322
+ const character = source[index];
323
+ const nextCharacter = source[index + 1];
324
+ if (lineComment) {
325
+ if (character === "\n" || character === "\r")
326
+ lineComment = false;
327
+ else
328
+ continue;
329
+ }
330
+ if (blockComment) {
331
+ if (character === "*" && nextCharacter === "/") {
332
+ blockComment = false;
333
+ index++;
334
+ }
335
+ continue;
336
+ }
337
+ if (quote) {
338
+ if (escaped)
339
+ escaped = false;
340
+ else if (character === "\\")
341
+ escaped = true;
342
+ else if (quote === "`" && character === "$" && nextCharacter === "{") {
343
+ curly++;
344
+ templateDepths.push(curly);
345
+ quote = undefined;
346
+ index++;
347
+ }
348
+ else if (character === quote)
349
+ quote = undefined;
350
+ continue;
351
+ }
352
+ if (character === "'" || character === '"' || character === "`") {
353
+ quote = character;
354
+ continue;
355
+ }
356
+ if (character === "/" && nextCharacter === "/") {
357
+ lineComment = true;
358
+ index++;
359
+ continue;
360
+ }
361
+ if (character === "/" && nextCharacter === "*") {
362
+ blockComment = true;
363
+ index++;
364
+ continue;
365
+ }
366
+ if (character === "/")
367
+ return undefined;
368
+ // Annex B HTML-like comments are not lexed here; give up rather than
369
+ // risk misreading the parameter list.
370
+ if (character === "<" && source.startsWith("!--", index + 1))
371
+ return undefined;
372
+ if (character === "-" && source.startsWith("->", index + 1))
373
+ return undefined;
374
+ if (character === "(")
375
+ depth++;
376
+ else if (character === ")") {
377
+ depth--;
378
+ if (depth === 0 && square === 0 && curly === 0) {
379
+ const closeIndex = index;
380
+ if (parenthesizedArrow) {
381
+ index++;
382
+ skipTrivia();
383
+ const hasArrow = source[index] === "=" && source[index + 1] === ">";
384
+ if (!hasArrow && (!asyncMethodOrArrow || source[index] !== "{"))
385
+ return undefined;
386
+ }
387
+ return source.slice(openIndex + 1, closeIndex);
388
+ }
389
+ if (depth < 0)
390
+ return undefined;
391
+ }
392
+ else if (character === "[")
393
+ square++;
394
+ else if (character === "]") {
395
+ square--;
396
+ if (square < 0)
397
+ return undefined;
398
+ }
399
+ else if (character === "{")
400
+ curly++;
401
+ else if (character === "}") {
402
+ if (templateDepths.at(-1) === curly) {
403
+ templateDepths.pop();
404
+ curly--;
405
+ quote = "`";
406
+ }
407
+ else
408
+ curly--;
409
+ if (curly < 0)
410
+ return undefined;
411
+ }
412
+ }
413
+ return undefined;
414
+ }
415
+ /**
416
+ * Conservative defense in depth for defaulted second parameters, which
417
+ * JavaScript intentionally omits from Function.length. Ambiguous source is
418
+ * ignored so this check can never reject a valid provider on weak evidence.
419
+ */
420
+ function authStartHasHiddenInput(start) {
421
+ let source;
422
+ try {
423
+ source = Function.prototype.toString.call(start);
424
+ }
425
+ catch {
426
+ return false;
427
+ }
428
+ if (!source ||
429
+ source.includes("[native code]") ||
430
+ /^\s*(?:async\s+)?function\s+bound\b/.test(source))
431
+ return false;
432
+ const parameters = authStartParameterList(source);
433
+ if (!parameters)
434
+ return false;
435
+ const parts = splitAuthStartParameters(parameters);
436
+ if (!parts || parts.length < 2)
437
+ return false;
438
+ // Require ordinary, readable source formatting. This intentionally fails
439
+ // open for minified output and for transpilers that rewrite defaults.
440
+ const commaIndex = parameters.indexOf(",");
441
+ if (commaIndex < 0 || !/\s/.test(parameters[commaIndex + 1] ?? ""))
442
+ return false;
443
+ const first = parts[0].trim();
444
+ const second = parts[1].trim();
445
+ const identifier = /^[_$A-Za-z][_$A-Za-z0-9]*/;
446
+ const firstName = first.match(identifier)?.[0];
447
+ const secondName = second.match(identifier)?.[0];
448
+ if (!firstName || !secondName || firstName.length < 3 || secondName.length < 3)
449
+ return false;
450
+ return /\s=\s/.test(second);
451
+ }
452
+ /** Define one factored provider operation with schema-driven handler inference. */
453
+ export function defineOperation() {
454
+ return function operation(config) {
455
+ return config;
456
+ };
48
457
  }
49
- /** Define a non-JSON provider operation with explicit transport metadata. */
50
- export function defineStreamOperation(operation) {
51
- return operation;
458
+ /** Define a factored non-JSON operation with explicit transport metadata. */
459
+ export function defineStreamOperation() {
460
+ return function streamOperation(config) {
461
+ return config;
462
+ };
52
463
  }
53
464
  function assertObjectConfig(value) {
54
465
  if (!value || typeof value !== "object") {
@@ -72,18 +483,95 @@ function assertLiteralField(value, field, validValues, providerId) {
72
483
  });
73
484
  }
74
485
  }
75
- function validateProviderShape(config) {
486
+ function validateProxiedOAuthParams(value, field, providerId) {
487
+ if (value === undefined)
488
+ return;
489
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
490
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be an object of string values.`);
491
+ }
492
+ for (const [key, paramValue] of Object.entries(value)) {
493
+ if (!key.trim() || typeof paramValue !== "string") {
494
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must contain non-empty keys and string values.`);
495
+ }
496
+ if (field === "authorizeParams" &&
497
+ PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())) {
498
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.authorizeParams cannot override reserved parameter "${key}".`);
499
+ }
500
+ }
501
+ }
502
+ function validateProxiedOAuthAuth(auth, providerId) {
503
+ const proxied = auth.proxied;
504
+ if (auth.mode !== "oauth2_proxied") {
505
+ if (proxied !== undefined) {
506
+ throw new ValidationError(`Provider "${providerId}" auth.proxied is only valid when auth.mode is "oauth2_proxied".`);
507
+ }
508
+ return;
509
+ }
510
+ if (!proxied || typeof proxied !== "object" || Array.isArray(proxied)) {
511
+ throw new ValidationError(`Provider "${providerId}" with auth.mode "oauth2_proxied" must declare auth.proxied.`);
512
+ }
513
+ const config = Object.fromEntries(Object.entries(proxied));
514
+ for (const key of Object.keys(config)) {
515
+ if (!PROXIED_OAUTH_ALLOWED_FIELDS.has(key)) {
516
+ throw new ValidationError(`Provider "${providerId}" has unknown auth.proxied field "${key}".`);
517
+ }
518
+ }
519
+ for (const field of PROXIED_OAUTH_REQUIRED_FIELDS) {
520
+ if (typeof config[field] !== "string" || !config[field].trim()) {
521
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be a non-empty string.`);
522
+ }
523
+ }
524
+ for (const field of ["authorizeUrl", "tokenUrl"]) {
525
+ try {
526
+ const endpoint = new URL(String(config[field]));
527
+ if (endpoint.protocol !== "https:" ||
528
+ endpoint.username ||
529
+ endpoint.password ||
530
+ endpoint.hash) {
531
+ throw new Error("invalid endpoint");
532
+ }
533
+ }
534
+ catch {
535
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be an absolute HTTPS URL without credentials or a fragment.`);
536
+ }
537
+ }
538
+ const customScheme = String(config.customScheme);
539
+ if (!CUSTOM_SCHEME_REGEX.test(customScheme) ||
540
+ customScheme.toLowerCase().startsWith("http://") ||
541
+ customScheme.toLowerCase().startsWith("https://")) {
542
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.customScheme must be a non-HTTP custom-scheme URL prefix.`);
543
+ }
544
+ if (!PROXIED_OAUTH_PROFILE_REGEX.test(String(config.rewriteProfile))) {
545
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.rewriteProfile must be a kebab-case profile name.`);
546
+ }
547
+ if (!PROXIED_OAUTH_ENV_KEY_REGEX.test(String(config.clientIdEnvKey))) {
548
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.clientIdEnvKey must be an APIFuse-style uppercase environment key.`);
549
+ }
550
+ if (config.pkce !== undefined && config.pkce !== "S256" && config.pkce !== "none") {
551
+ throw new ValidationError(`Provider "${providerId}" auth.proxied.pkce must be "S256" or "none".`);
552
+ }
553
+ validateProxiedOAuthParams(config.authorizeParams, "authorizeParams", providerId);
554
+ validateProxiedOAuthParams(config.tokenParams, "tokenParams", providerId);
555
+ }
556
+ function validateProviderDeclarationShape(config) {
76
557
  assertObjectConfig(config);
77
558
  assertRequiredField(config, "id");
78
559
  assertRequiredField(config, "version", String(config.id));
79
560
  assertRequiredField(config, "runtime", String(config.id));
80
561
  assertRequiredField(config, "meta", String(config.id));
81
- assertRequiredField(config, "operations", String(config.id));
82
562
  if (typeof config.runtime === "string")
83
563
  assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
564
+ if (config.native !== undefined && config.runtime === "browser") {
565
+ throw new ValidationError(`Provider "${String(config.id)}" cannot declare capability "native" with runtime "browser"`, {
566
+ fix: 'Use runtime: "standard" or runtime: "shared", or remove the native declaration.',
567
+ });
568
+ }
84
569
  const auth = config.auth;
85
570
  if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
86
571
  assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
572
+ if (auth && typeof auth === "object" && !Array.isArray(auth)) {
573
+ validateProxiedOAuthAuth(Object.fromEntries(Object.entries(auth)), String(config.id));
574
+ }
87
575
  if (auth && typeof auth === "object" && "exchange" in auth) {
88
576
  throw new ProviderError(`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`, {
89
577
  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.",
@@ -101,6 +589,19 @@ function validateProviderShape(config) {
101
589
  fix: "Return a form turn from start(ctx), then receive user input in continue(ctx, input).",
102
590
  });
103
591
  }
592
+ if (auth &&
593
+ typeof auth === "object" &&
594
+ "flow" in auth &&
595
+ auth.flow &&
596
+ typeof auth.flow === "object" &&
597
+ "start" in auth.flow &&
598
+ typeof auth.flow.start === "function" &&
599
+ auth.flow.start.length <= 1 &&
600
+ authStartHasHiddenInput(auth.flow.start)) {
601
+ throw new ProviderError(`Provider "${String(config.id)}" auth.flow.start must not declare an input parameter`, {
602
+ fix: "Return a form turn from start(ctx), then receive user input in continue(ctx, input).",
603
+ });
604
+ }
104
605
  const access = config.access;
105
606
  if (access !== undefined) {
106
607
  if (!access || typeof access !== "object" || Array.isArray(access)) {
@@ -127,6 +628,10 @@ function validateProviderShape(config) {
127
628
  }
128
629
  }
129
630
  }
631
+ function validateProviderImplementationShape(config) {
632
+ const configRecord = config;
633
+ assertRequiredField(configRecord, "operations", String(config.id));
634
+ }
130
635
  function validateProviderProxy(config) {
131
636
  const proxy = config.proxy;
132
637
  if (proxy === undefined || typeof proxy === "boolean") {
@@ -137,11 +642,21 @@ function validateProviderProxy(config) {
137
642
  fix: `Use proxy: { mode: "required", provider: "smartproxy", geo: { country: "KR" }, session: { affinity: "connection", lifetimeMinutes: 30 } }`,
138
643
  });
139
644
  }
140
- rejectUnknownFields(proxy, new Set(["mode", "provider", "geo", "session"]), "proxy");
645
+ rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
141
646
  assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
142
647
  if (proxy.provider !== undefined) {
143
648
  assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
144
649
  }
650
+ if (proxy.providers !== undefined) {
651
+ if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
652
+ throw new ValidationError(`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`, {
653
+ fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
654
+ });
655
+ }
656
+ for (const vendor of proxy.providers) {
657
+ assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
658
+ }
659
+ }
145
660
  if (proxy.geo !== undefined) {
146
661
  if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
147
662
  throw new ValidationError(`Provider "${config.id}" has invalid proxy.geo: must be an object.`, {
@@ -165,7 +680,7 @@ function validateProviderProxy(config) {
165
680
  fix: `Use proxy.session: { affinity: "connection", lifetimeMinutes: 30 }.`,
166
681
  });
167
682
  }
168
- rejectUnknownFields(proxy.session, new Set(["affinity", "lifetimeMinutes", "poolSize"]), "proxy.session");
683
+ rejectUnknownFields(proxy.session, new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]), "proxy.session");
169
684
  if (proxy.session.affinity !== undefined) {
170
685
  assertLiteralField(proxy.session.affinity, "proxy.session.affinity", VALID_PROVIDER_PROXY_AFFINITIES, config.id);
171
686
  }
@@ -177,15 +692,71 @@ function validateProviderProxy(config) {
177
692
  if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
178
693
  throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`);
179
694
  }
180
- }
181
- if (proxy.mode === "required" && proxy.provider === "smartproxy") {
182
- const hasSmartproxySecret = config.secrets?.some((secret) => secret.name === SMARTPROXY_APP_KEY_SECRET && secret.required !== false);
183
- if (!hasSmartproxySecret) {
184
- throw new ValidationError(`Provider "${config.id}" requires Smartproxy egress but does not declare ${SMARTPROXY_APP_KEY_SECRET}.`, {
185
- fix: `Add secrets: [{ name: "${SMARTPROXY_APP_KEY_SECRET}", required: true }] to the provider.`,
695
+ const drainLeadSeconds = proxy.session.drainLeadSeconds;
696
+ if (drainLeadSeconds !== undefined &&
697
+ (!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)) {
698
+ throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`, {
699
+ fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
700
+ });
701
+ }
702
+ // A drain lead longer than the sticky lifetime would fire the expiring
703
+ // event before the session is even established, so the provider would
704
+ // never get a usable window. Reject the contradiction at build time.
705
+ if (drainLeadSeconds !== undefined &&
706
+ lifetime !== undefined &&
707
+ Number.isFinite(lifetime) &&
708
+ drainLeadSeconds >= lifetime * 60) {
709
+ throw new ValidationError(`Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`, {
710
+ fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
186
711
  });
187
712
  }
188
713
  }
714
+ // Every credentialed vendor in a required-mode chain must declare its
715
+ // provider secret(s) so a missing credential fails at build/validation time,
716
+ // not during a live outage. This covers the fallback legs too (not just the
717
+ // first vendor): a declared-but-uncredentialed nodemaven fallback would leave
718
+ // the chain silently down to a single vendor, reintroducing the SPOF the chain
719
+ // removes.
720
+ const vendorChain = proxy.providers && proxy.providers.length > 0
721
+ ? proxy.providers
722
+ : proxy.provider
723
+ ? [proxy.provider]
724
+ : [];
725
+ if (proxy.mode === "required") {
726
+ for (const vendor of vendorChain) {
727
+ const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
728
+ if (!requiredSecrets)
729
+ continue;
730
+ for (const secretName of requiredSecrets) {
731
+ // Match the canonical runtime gate (assertRequiredSecretsPresent /
732
+ // listMissingRequiredSecrets), which enforces only `required === true`
733
+ // declarations. A declaration that omits `required` (defaulting to
734
+ // optional) is skipped at runtime, so accepting it here would pass
735
+ // validation while leaving the credential unenforced until proxy
736
+ // resolution during a live request — the fail-open gap this check exists
737
+ // to close.
738
+ const declared = config.secrets?.some((secret) => secret.name === secretName && secret.required === true);
739
+ if (!declared) {
740
+ throw new ValidationError(`Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`, {
741
+ fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
742
+ });
743
+ }
744
+ }
745
+ }
746
+ }
747
+ // `decodo`/`custom` are deprecated vendor values (string-union members, so the
748
+ // @deprecated symbol gate can't catch them — warn at validation time instead).
749
+ const deprecatedVendors = vendorChain.filter((vendor) => vendor === "decodo" || vendor === "custom");
750
+ if (proxy.mode === "required" &&
751
+ vendorChain.length > 0 &&
752
+ deprecatedVendors.length === vendorChain.length) {
753
+ throw new ValidationError(`Provider "${config.id}" requires proxy egress but declares only deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}.`, {
754
+ fix: `Use proxy.provider or proxy.providers with "smartproxy" and/or "nodemaven".`,
755
+ });
756
+ }
757
+ if (deprecatedVendors.length > 0) {
758
+ console.warn(`[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven".`);
759
+ }
189
760
  }
190
761
  function validateProviderStt(config) {
191
762
  const stt = config.stt;
@@ -199,6 +770,54 @@ function validateProviderStt(config) {
199
770
  rejectUnknownFields(stt, new Set(["mode"]), "stt");
200
771
  assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
201
772
  }
773
+ function validateProviderOcr(config) {
774
+ const ocr = config.ocr;
775
+ if (ocr === undefined)
776
+ return;
777
+ if (!ocr || typeof ocr !== "object" || Array.isArray(ocr)) {
778
+ throw new ValidationError(`Provider "${config.id}" has invalid ocr: must be an object.`, {
779
+ fix: `Use ocr: { mode: "required" } or ocr: { mode: "optional" }.`,
780
+ });
781
+ }
782
+ rejectUnknownFields(ocr, new Set(["mode"]), "ocr");
783
+ assertLiteralField(ocr.mode, "ocr.mode", VALID_PROVIDER_OCR_MODES, config.id);
784
+ }
785
+ function validateProviderResolver(config) {
786
+ const resolver = config.resolver;
787
+ if (resolver === undefined)
788
+ return;
789
+ if (!resolver || typeof resolver !== "object" || Array.isArray(resolver)) {
790
+ throw new ValidationError(`Provider "${config.id}" has invalid resolver: must be an object.`, {
791
+ fix: `Set resolver for provider "${config.id}" to { vendors: ["2captcha"], kinds: ["turnstile"] }.`,
792
+ });
793
+ }
794
+ rejectUnknownFields(resolver, new Set(["vendors", "kinds", "clientProfile"]), "resolver", config.id);
795
+ if (resolver.vendors !== undefined) {
796
+ validateResolverLiteralArray(resolver.vendors, "resolver.vendors", VALID_PROVIDER_RESOLVER_VENDORS, config.id);
797
+ }
798
+ validateResolverLiteralArray(resolver.kinds, "resolver.kinds", VALID_PROVIDER_CHALLENGE_KINDS, config.id);
799
+ if (resolver.clientProfile !== undefined &&
800
+ (typeof resolver.clientProfile !== "string" || !resolver.clientProfile.trim())) {
801
+ throw new ValidationError(`Provider "${config.id}" has invalid resolver.clientProfile: must be a non-empty string.`, {
802
+ fix: `Set resolver.clientProfile for provider "${config.id}" to a transport-owned profile name.`,
803
+ });
804
+ }
805
+ }
806
+ function validateResolverLiteralArray(value, field, validValues, providerId) {
807
+ if (!Array.isArray(value)) {
808
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an array.`, {
809
+ fix: `Set ${field} for provider "${providerId}" to an array containing only: ${validValues.join(", ")}.`,
810
+ });
811
+ }
812
+ for (const [index, item] of value.entries()) {
813
+ if (typeof item === "string" && validValues.some((validValue) => validValue === item)) {
814
+ continue;
815
+ }
816
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}[${index}]: ${JSON.stringify(item)}. Expected one of: ${validValues.join(", ")}`, {
817
+ fix: `Set ${field}[${index}] for provider "${providerId}" to one of ${validValues.map((validValue) => `"${validValue}"`).join(", ")}.`,
818
+ });
819
+ }
820
+ }
202
821
  function validateOperationIds(providerId, operations) {
203
822
  for (const operationName of Object.keys(operations)) {
204
823
  if (!OPERATION_ID_REGEX.test(operationName))
@@ -335,6 +954,23 @@ function validateOperationObservability(providerId, operations) {
335
954
  }
336
955
  }
337
956
  }
957
+ function validateOperationErrorCodes(providerId, operations) {
958
+ for (const [operationName, operation] of Object.entries(operations)) {
959
+ for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
960
+ if (errorCode.status !== undefined &&
961
+ !VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)) {
962
+ const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
963
+ throw new ValidationError(`Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`, {
964
+ fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
965
+ });
966
+ }
967
+ if (errorCode.status !== undefined &&
968
+ SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)) {
969
+ console.warn(`[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.`);
970
+ }
971
+ }
972
+ }
973
+ }
338
974
  const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
339
975
  const SSE_TRANSPORT_FIELDS = new Set([
340
976
  "kind",
@@ -556,14 +1192,18 @@ function suggestField(unknown, candidates) {
556
1192
  }
557
1193
  return best;
558
1194
  }
559
- function rejectUnknownFields(value, allowed, fieldPath) {
1195
+ function rejectUnknownFields(value, allowed, fieldPath, providerId) {
560
1196
  for (const key of Object.keys(value)) {
561
1197
  if (allowed.has(key))
562
1198
  continue;
563
1199
  const hint = suggestField(key, allowed);
564
1200
  throw new ValidationError(hint
565
1201
  ? `Unknown field "${key}" on ${fieldPath}. Did you mean "${hint}"?`
566
- : `Unknown field "${key}" on ${fieldPath}.`, { fix: `Remove ${fieldPath}.${key} or rename it.` });
1202
+ : `Unknown field "${key}" on ${fieldPath}.`, {
1203
+ fix: providerId
1204
+ ? `Remove ${fieldPath}.${key} from provider "${providerId}" or rename it.`
1205
+ : `Remove ${fieldPath}.${key} or rename it.`,
1206
+ });
567
1207
  }
568
1208
  }
569
1209
  function assertBoundedIntegerMs(value, fieldPath, options) {
@@ -776,6 +1416,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
776
1416
  "manualTrigger",
777
1417
  "steps",
778
1418
  "run",
1419
+ "scenario",
779
1420
  ]);
780
1421
  const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
781
1422
  const HEALTH_JOURNEY_STEP_FIELDS = new Set([
@@ -1198,6 +1839,10 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
1198
1839
  }
1199
1840
  if (journey.manualTrigger !== undefined)
1200
1841
  validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
1842
+ if (journey.scenario !== undefined && journey.smsMatchers !== undefined)
1843
+ throw new ValidationError(`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers is not allowed on declarative scenarios.`);
1844
+ if (journey.scenario !== undefined && journey.requiredSecrets !== undefined)
1845
+ throw new ValidationError(`Provider "${providerId}" healthJourneys.${journey.id}.requiredSecrets is not allowed on declarative scenarios.`);
1201
1846
  if (journey.timeout !== undefined)
1202
1847
  assertIsoDuration(journey.timeout, `Provider "${providerId}" healthJourneys.${journey.id}.timeout`);
1203
1848
  if (journey.cooldown !== undefined)
@@ -1239,6 +1884,21 @@ function validateOperationFixtures(providerId, operations) {
1239
1884
  throw new ValidationError(`Operation handler must be defined for provider "${providerId}" operation "${operationName}"`, {
1240
1885
  fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
1241
1886
  });
1887
+ if (operation.fixtures?.recordedAt !== undefined) {
1888
+ const recordedAt = operation.fixtures.recordedAt;
1889
+ const parsed = typeof recordedAt === "string"
1890
+ ? new Date(`${recordedAt}T00:00:00.000Z`)
1891
+ : new Date(Number.NaN);
1892
+ const isCalendarDate = typeof recordedAt === "string" &&
1893
+ /^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
1894
+ !Number.isNaN(parsed.getTime()) &&
1895
+ parsed.toISOString().slice(0, 10) === recordedAt;
1896
+ const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
1897
+ if (!isCalendarDate || recordedAt > kstToday)
1898
+ throw new ValidationError(`Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`, {
1899
+ fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
1900
+ });
1901
+ }
1242
1902
  if (operation.fixtures?.request !== undefined) {
1243
1903
  const result = safeParseSchemaSync(operation.input, operation.fixtures.request, `operations.${operationName}.fixtures.request`);
1244
1904
  if (!result.success)
@@ -1257,6 +1917,28 @@ function validateOperationFixtures(providerId, operations) {
1257
1917
  }
1258
1918
  }
1259
1919
  }
1920
+ function resolveOperationFixtureRequests(operations) {
1921
+ let changed = false;
1922
+ const resolvedOperations = Object.fromEntries(Object.entries(operations).map(([operationName, operation]) => {
1923
+ if (operation.fixtures?.request === undefined)
1924
+ return [operationName, operation];
1925
+ const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
1926
+ if (request === operation.fixtures.request)
1927
+ return [operationName, operation];
1928
+ changed = true;
1929
+ return [
1930
+ operationName,
1931
+ {
1932
+ ...operation,
1933
+ fixtures: {
1934
+ ...operation.fixtures,
1935
+ request,
1936
+ },
1937
+ },
1938
+ ];
1939
+ }));
1940
+ return changed ? resolvedOperations : operations;
1941
+ }
1260
1942
  /**
1261
1943
  * Shallow shape guard only: the `deployment` object is passed through
1262
1944
  * verbatim and deliberately not deep-validated by the SDK — the APIFuse
@@ -1270,40 +1952,65 @@ function validateProviderDeployment(providerId, deployment) {
1270
1952
  fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
1271
1953
  });
1272
1954
  }
1273
- export function defineProvider(config) {
1274
- validateProviderShape(config);
1955
+ /** Establish a provider declaration before its operations are contextually typed. */
1956
+ export function defineProvider(declaration) {
1957
+ validateProviderDeclaration(declaration);
1958
+ const buildProvider = (implementation) => finalizeProvider({
1959
+ ...declaration,
1960
+ ...implementation,
1961
+ });
1962
+ return buildProvider;
1963
+ }
1964
+ function validateProviderDeclaration(config) {
1965
+ validateProviderDeclarationShape(config);
1275
1966
  if (!CONNECTOR_ID_REGEX.test(config.id))
1276
1967
  throw new ProviderError(`Invalid provider id: "${config.id}"`, {
1277
1968
  fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
1278
1969
  });
1279
- if (Object.keys(config.operations).length === 0)
1280
- throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
1281
- fix: "Add at least one operation to the operations object",
1282
- });
1283
- validateOperationIds(config.id, config.operations);
1284
- validateOperationAnnotations(config.id, config.operations);
1285
- validateOperationObservability(config.id, config.operations);
1286
- validateOperationTransports(config.id, config.operations);
1287
- validateOperationContracts(config.id, config.operations);
1288
- validateToolRouterMetadata(config.id, config.operations);
1289
- const journeyCoveredOperations = validateHealthJourneys(config.id, config.operations, config.healthJourneys);
1290
- validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
1291
1970
  if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
1292
1971
  throw new ValidationError(`Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`, {
1293
1972
  fix: "Keep healthProbe (the new name) and delete the healthMonitor block.",
1294
1973
  });
1295
1974
  validateProviderHealthMonitor(config.id, config.healthProbe ?? config.healthMonitor, config.healthProbe !== undefined ? "healthProbe" : "healthMonitor");
1296
- validateOperationFixtures(config.id, config.operations);
1297
1975
  validateProviderDeployment(config.id, config.deployment);
1976
+ try {
1977
+ validateNativeProviderConfig(config.native);
1978
+ }
1979
+ catch (error) {
1980
+ if (error instanceof NativeEgressPolicyValidationError)
1981
+ throw new ValidationError(error.message);
1982
+ throw error;
1983
+ }
1298
1984
  validateProviderProxy(config);
1985
+ validateProviderOcr(config);
1299
1986
  validateProviderStt(config);
1987
+ validateProviderResolver(config);
1300
1988
  if (config.runtime === "browser" && !config.browser)
1301
1989
  throw new ProviderError(`Provider "${config.id}" must define browser.engine when runtime is "browser"`, {
1302
1990
  fix: 'Add browser: { engine: "playwright-stealth" } for TypeScript providers, or another supported engine for your runtime',
1303
1991
  });
1304
1992
  if (config.browser && config.runtime !== "browser")
1305
1993
  throw new ProviderError(`Provider "${config.id}" cannot define browser config unless runtime is "browser"`, { fix: 'Set runtime: "browser" or remove the browser config' });
1306
- return {
1994
+ validateFailClosedProviderDeclaration(config);
1995
+ }
1996
+ function finalizeProvider(config) {
1997
+ validateProviderImplementationShape(config);
1998
+ const operations = resolveOperationFixtureRequests(config.operations);
1999
+ if (Object.keys(config.operations).length === 0)
2000
+ throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
2001
+ fix: "Add at least one operation to the operations object",
2002
+ });
2003
+ validateOperationIds(config.id, config.operations);
2004
+ validateOperationAnnotations(config.id, config.operations);
2005
+ validateOperationObservability(config.id, config.operations);
2006
+ validateOperationErrorCodes(config.id, config.operations);
2007
+ validateOperationTransports(config.id, config.operations);
2008
+ validateOperationContracts(config.id, config.operations);
2009
+ validateToolRouterMetadata(config.id, config.operations);
2010
+ const journeyCoveredOperations = validateHealthJourneys(config.id, config.operations, config.healthJourneys);
2011
+ validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
2012
+ validateOperationFixtures(config.id, operations);
2013
+ const provider = {
1307
2014
  id: config.id,
1308
2015
  version: config.version,
1309
2016
  runtime: config.runtime,
@@ -1311,9 +2018,12 @@ export function defineProvider(config) {
1311
2018
  // are owned by the APIFuse registry builder, not the SDK.
1312
2019
  deployment: config.deployment,
1313
2020
  allowedHosts: config.allowedHosts,
2021
+ native: config.native,
1314
2022
  stealth: config.stealth,
1315
2023
  proxy: config.proxy,
2024
+ ocr: config.ocr,
1316
2025
  stt: config.stt,
2026
+ resolver: config.resolver,
1317
2027
  browser: config.browser,
1318
2028
  auth: config.auth,
1319
2029
  reviewed: config.reviewed,
@@ -1322,11 +2032,15 @@ export function defineProvider(config) {
1322
2032
  credential: config.credential,
1323
2033
  context: config.context,
1324
2034
  meta: config.meta,
1325
- operations: config.operations,
2035
+ operations,
1326
2036
  // Transitional healthMonitor → healthProbe alias: mirror whichever field
1327
2037
  // was declared onto both so old and new consumers keep working.
1328
2038
  healthMonitor: config.healthMonitor ?? config.healthProbe,
1329
2039
  healthProbe: config.healthProbe ?? config.healthMonitor,
1330
2040
  healthJourneys: config.healthJourneys,
1331
2041
  };
2042
+ // Declaration validation never invokes handlers, so their declaration-bound
2043
+ // context parameter is irrelevant to the runtime ProviderDefinition shape.
2044
+ validateFailClosedOperationDeclaration(provider);
2045
+ return provider;
1332
2046
  }