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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/AUTHORING.md +106 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +5 -1
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +14 -0
  9. package/bin/apifuse-pack-smoke.ts +22 -81
  10. package/bin/apifuse-pack-types.ts +266 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +53 -70
  13. package/bin/apifuse-submit-check.ts +510 -45
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth.d.ts +2 -2
  21. package/dist/auth.js +9 -18
  22. package/dist/ceremonies/index.d.ts +1 -1
  23. package/dist/ceremonies/index.js +8 -18
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/config/loader.d.ts +149 -8
  32. package/dist/config/loader.js +378 -89
  33. package/dist/contract-serialization.d.ts +2 -2
  34. package/dist/contract-serialization.js +3 -6
  35. package/dist/contract-types.d.ts +2 -2
  36. package/dist/contract.d.ts +3 -3
  37. package/dist/contract.js +4 -6
  38. package/dist/define.d.ts +8 -1
  39. package/dist/define.js +119 -122
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/errors.d.ts +4 -1
  43. package/dist/errors.js +48 -0
  44. package/dist/i18n/catalog.d.ts +2 -2
  45. package/dist/i18n/catalog.js +4 -10
  46. package/dist/i18n/index.d.ts +2 -2
  47. package/dist/i18n/index.js +2 -2
  48. package/dist/i18n/keys.d.ts +2 -2
  49. package/dist/index.d.ts +44 -42
  50. package/dist/index.js +39 -37
  51. package/dist/lint.js +8 -15
  52. package/dist/provider.d.ts +11 -11
  53. package/dist/provider.js +9 -9
  54. package/dist/public-schema-field-lint.d.ts +1 -1
  55. package/dist/recipes/gov-api.js +1 -1
  56. package/dist/runtime/auth-flow.d.ts +1 -1
  57. package/dist/runtime/auth-flow.js +3 -3
  58. package/dist/runtime/browser.d.ts +1 -1
  59. package/dist/runtime/browser.js +15 -29
  60. package/dist/runtime/cache.d.ts +1 -1
  61. package/dist/runtime/cache.js +4 -8
  62. package/dist/runtime/choice.d.ts +1 -1
  63. package/dist/runtime/choice.js +31 -35
  64. package/dist/runtime/credential.d.ts +1 -1
  65. package/dist/runtime/credential.js +1 -1
  66. package/dist/runtime/env.d.ts +1 -1
  67. package/dist/runtime/executor.d.ts +1 -1
  68. package/dist/runtime/executor.js +15 -3
  69. package/dist/runtime/http.d.ts +2 -2
  70. package/dist/runtime/http.js +144 -348
  71. package/dist/runtime/insights.d.ts +1 -1
  72. package/dist/runtime/insights.js +6 -13
  73. package/dist/runtime/instrumentation.d.ts +2 -2
  74. package/dist/runtime/instrumentation.js +56 -19
  75. package/dist/runtime/keyring.js +1 -1
  76. package/dist/runtime/namespace.js +1 -1
  77. package/dist/runtime/otlp.d.ts +1 -1
  78. package/dist/runtime/perf.d.ts +1 -1
  79. package/dist/runtime/provider.d.ts +1 -1
  80. package/dist/runtime/provider.js +1 -2
  81. package/dist/runtime/proxy-errors.d.ts +1 -1
  82. package/dist/runtime/proxy-errors.js +9 -7
  83. package/dist/runtime/proxy-nodemaven.d.ts +35 -0
  84. package/dist/runtime/proxy-nodemaven.js +128 -0
  85. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  86. package/dist/runtime/proxy-retry-policy.js +326 -0
  87. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  88. package/dist/runtime/proxy-telemetry.js +55 -52
  89. package/dist/runtime/redis.d.ts +1 -1
  90. package/dist/runtime/redis.js +2 -4
  91. package/dist/runtime/request-options.d.ts +1 -1
  92. package/dist/runtime/secrets.d.ts +27 -0
  93. package/dist/runtime/secrets.js +51 -0
  94. package/dist/runtime/state.d.ts +2 -2
  95. package/dist/runtime/state.js +15 -4
  96. package/dist/runtime/stealth.d.ts +2 -2
  97. package/dist/runtime/stealth.js +108 -212
  98. package/dist/runtime/stt.d.ts +1 -1
  99. package/dist/runtime/stt.js +11 -15
  100. package/dist/runtime/trace.d.ts +2 -2
  101. package/dist/runtime/trace.js +2 -4
  102. package/dist/runtime/waterfall.d.ts +1 -1
  103. package/dist/schema.d.ts +1 -1
  104. package/dist/schema.js +7 -15
  105. package/dist/serve.d.ts +1 -1
  106. package/dist/serve.js +1 -1
  107. package/dist/server/index.d.ts +7 -7
  108. package/dist/server/index.js +6 -6
  109. package/dist/server/self-test-redaction.d.ts +1 -1
  110. package/dist/server/self-test-redaction.js +1 -1
  111. package/dist/server/self-test.d.ts +104 -3
  112. package/dist/server/self-test.js +673 -115
  113. package/dist/server/serve.d.ts +8 -3
  114. package/dist/server/serve.js +105 -59
  115. package/dist/server/types.d.ts +10 -9
  116. package/dist/server/types.js +3 -7
  117. package/dist/stealth/profiles.d.ts +1 -1
  118. package/dist/stealth/profiles.js +5 -14
  119. package/dist/stream.d.ts +1 -1
  120. package/dist/testing/index.d.ts +2 -2
  121. package/dist/testing/index.js +2 -2
  122. package/dist/testing/run.d.ts +1 -1
  123. package/dist/testing/run.js +7 -14
  124. package/dist/types.d.ts +87 -1
  125. package/dist/user-input.d.ts +30 -0
  126. package/dist/user-input.js +66 -0
  127. package/package.json +9 -5
  128. package/src/auth-turn/index.ts +1 -1
  129. package/src/auth.ts +28 -86
  130. package/src/ceremonies/index.ts +24 -78
  131. package/src/cli/commands.ts +10 -0
  132. package/src/cli/create.ts +42 -35
  133. package/src/cli/prompt-assets.ts +865 -0
  134. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  135. package/src/config/loader.ts +543 -208
  136. package/src/contract-serialization.ts +5 -11
  137. package/src/contract-types.ts +2 -2
  138. package/src/contract.ts +12 -28
  139. package/src/define.ts +247 -505
  140. package/src/dev.ts +4 -9
  141. package/src/errors.ts +58 -5
  142. package/src/i18n/catalog.ts +10 -32
  143. package/src/i18n/index.ts +2 -2
  144. package/src/i18n/keys.ts +5 -11
  145. package/src/index.ts +49 -42
  146. package/src/lint.ts +89 -159
  147. package/src/provider.ts +15 -11
  148. package/src/public-schema-field-lint.ts +7 -33
  149. package/src/recipes/gov-api.ts +2 -5
  150. package/src/runtime/auth-flow.ts +5 -7
  151. package/src/runtime/browser.ts +64 -187
  152. package/src/runtime/cache.ts +20 -67
  153. package/src/runtime/choice.ts +79 -132
  154. package/src/runtime/credential.ts +2 -2
  155. package/src/runtime/env.ts +1 -1
  156. package/src/runtime/executor.ts +23 -20
  157. package/src/runtime/http.ts +195 -559
  158. package/src/runtime/insights.ts +15 -53
  159. package/src/runtime/instrumentation.ts +78 -65
  160. package/src/runtime/keyring.ts +7 -19
  161. package/src/runtime/namespace.ts +2 -7
  162. package/src/runtime/otlp.ts +12 -23
  163. package/src/runtime/perf.ts +1 -1
  164. package/src/runtime/provider.ts +4 -9
  165. package/src/runtime/proxy-errors.ts +29 -42
  166. package/src/runtime/proxy-nodemaven.ts +178 -0
  167. package/src/runtime/proxy-retry-policy.ts +469 -0
  168. package/src/runtime/proxy-telemetry.ts +79 -77
  169. package/src/runtime/redis.ts +4 -12
  170. package/src/runtime/request-options.ts +4 -13
  171. package/src/runtime/secrets.ts +64 -0
  172. package/src/runtime/state.ts +41 -110
  173. package/src/runtime/stealth.ts +162 -377
  174. package/src/runtime/stt.ts +38 -94
  175. package/src/runtime/trace.ts +14 -44
  176. package/src/runtime/waterfall.ts +5 -18
  177. package/src/schema.ts +23 -84
  178. package/src/serve.ts +1 -1
  179. package/src/server/index.ts +12 -7
  180. package/src/server/self-test-redaction.ts +2 -2
  181. package/src/server/self-test.ts +857 -132
  182. package/src/server/serve.ts +172 -245
  183. package/src/server/types.ts +7 -19
  184. package/src/stealth/profiles.ts +10 -26
  185. package/src/stream.ts +8 -19
  186. package/src/testing/index.ts +2 -2
  187. package/src/testing/run.ts +19 -67
  188. package/src/types.ts +88 -1
  189. package/src/user-input.ts +118 -0
  190. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  191. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  192. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  193. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  194. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  195. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  196. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  197. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  198. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  199. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  200. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  201. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  202. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  203. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,4 +1,4 @@
1
- import { type JsonValue } from "./contract-json";
2
- import type { SchemaLike } from "./types";
1
+ import { type JsonValue } from "./contract-json.js";
2
+ import type { SchemaLike } from "./types.js";
3
3
  export declare function describeSchema(schema: SchemaLike): JsonValue;
4
4
  export declare function serializeSmsMatcher(value: Record<string, unknown>): Record<string, unknown>;
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { z } from "zod";
3
- import { canonicalJson, compactObject, isRecord, toJsonValue, } from "./contract-json";
3
+ import { canonicalJson, compactObject, isRecord, toJsonValue, } from "./contract-json.js";
4
4
  export function describeSchema(schema) {
5
5
  if (isZodSchema(schema)) {
6
6
  const jsonSchema = zodJsonSchema(schema);
@@ -9,9 +9,7 @@ export function describeSchema(schema) {
9
9
  vendor: "zod",
10
10
  typeName: getSchemaTypeName(schema),
11
11
  jsonSchema,
12
- jsonSchemaHash: jsonSchema === undefined
13
- ? undefined
14
- : digest(canonicalJson(jsonSchema)),
12
+ jsonSchemaHash: jsonSchema === undefined ? undefined : digest(canonicalJson(jsonSchema)),
15
13
  });
16
14
  }
17
15
  const standard = isRecord(schema) ? schema["~standard"] : undefined;
@@ -20,8 +18,7 @@ export function describeSchema(schema) {
20
18
  kind: "schema",
21
19
  standard: "standard-schema-v1",
22
20
  vendor: typeof standard.vendor === "string" ? standard.vendor : "unknown",
23
- version: typeof standard.version === "number" ||
24
- typeof standard.version === "string"
21
+ version: typeof standard.version === "number" || typeof standard.version === "string"
25
22
  ? standard.version
26
23
  : undefined,
27
24
  });
@@ -1,5 +1,5 @@
1
- import type { JsonValue } from "./contract-json";
2
- import type { ProviderDefinition } from "./types";
1
+ import type { JsonValue } from "./contract-json.js";
2
+ import type { ProviderDefinition } from "./types.js";
3
3
  export declare const PROVIDER_CONTRACT_SCHEMA_VERSION = "2026-06-23";
4
4
  export interface ProviderContractSnapshot {
5
5
  readonly schemaVersion: typeof PROVIDER_CONTRACT_SCHEMA_VERSION;
@@ -1,6 +1,6 @@
1
- import { canonicalJson, type JsonPrimitive, type JsonValue } from "./contract-json";
2
- import { PROVIDER_CONTRACT_SCHEMA_VERSION, type ProviderContractOperation, type ProviderContractSnapshot } from "./contract-types";
3
- import type { ProviderDefinition } from "./types";
1
+ import { canonicalJson, type JsonPrimitive, type JsonValue } from "./contract-json.js";
2
+ import { PROVIDER_CONTRACT_SCHEMA_VERSION, type ProviderContractOperation, type ProviderContractSnapshot } from "./contract-types.js";
3
+ import type { ProviderDefinition } from "./types.js";
4
4
  export { canonicalJson, type JsonPrimitive, type JsonValue, PROVIDER_CONTRACT_SCHEMA_VERSION, type ProviderContractOperation, type ProviderContractSnapshot, };
5
5
  export declare function extractProviderContract(provider: ProviderDefinition): ProviderContractSnapshot;
6
6
  export declare function digestProviderContract(snapshot: ProviderContractSnapshot): string;
package/dist/contract.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { canonicalJson, compactObject, copyRecordWithout, toJsonValue, } from "./contract-json";
3
- import { describeSchema, serializeSmsMatcher } from "./contract-serialization";
4
- import { PROVIDER_CONTRACT_SCHEMA_VERSION, } from "./contract-types";
2
+ import { canonicalJson, compactObject, copyRecordWithout, toJsonValue, } from "./contract-json.js";
3
+ import { describeSchema, serializeSmsMatcher } from "./contract-serialization.js";
4
+ import { PROVIDER_CONTRACT_SCHEMA_VERSION, } from "./contract-types.js";
5
5
  export { canonicalJson, PROVIDER_CONTRACT_SCHEMA_VERSION, };
6
6
  export function extractProviderContract(provider) {
7
7
  const auth = extractAuth(provider.auth);
@@ -27,9 +27,7 @@ export function extractProviderContract(provider) {
27
27
  operations: Object.entries(provider.operations)
28
28
  .sort(([leftId], [rightId]) => leftId.localeCompare(rightId))
29
29
  .map(([operationId, operation]) => extractOperation(operationId, operation)),
30
- ...(provider.allowedHosts
31
- ? { allowedHosts: [...provider.allowedHosts].sort() }
32
- : {}),
30
+ ...(provider.allowedHosts ? { allowedHosts: [...provider.allowedHosts].sort() } : {}),
33
31
  ...(stealth === undefined ? {} : { stealth }),
34
32
  ...(proxy === undefined ? {} : { proxy }),
35
33
  ...(stt === undefined ? {} : { stt }),
package/dist/define.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AuthConfig, BrowserEngine, ContextDeclaration, CredentialDeclaration, HealthJourneyDefinition, HealthJourneySchedule, HealthScheduleRandomization, InferSchemaOutput, OperationDefinition, OperationHandlerResult, OperationHttpStreamTransport, OperationSseTransport, OperationWebSocketTransport, ProviderAccessConfig, ProviderDefinition, ProviderHealthMonitorConfig, ProviderProxyConfig, ProviderPublicProfile, ProviderReviewed, ProviderSecretDeclaration, ProviderStreamEvent, ProviderSttConfig, SchemaLike, SmsOtpMatcherDefinition, StealthPlatform } from "./types";
1
+ import type { AuthConfig, BrowserEngine, ContextDeclaration, CredentialDeclaration, HealthJourneyDefinition, HealthJourneySchedule, HealthScheduleRandomization, InferSchemaOutput, OperationDefinition, OperationHandlerResult, OperationHttpStreamTransport, OperationSseTransport, OperationWebSocketTransport, ProviderAccessConfig, ProviderDefinition, ProviderDeploymentOverrides, ProviderHealthMonitorConfig, ProviderProxyConfig, ProviderPublicProfile, ProviderReviewed, ProviderSecretDeclaration, ProviderStreamEvent, ProviderSttConfig, SchemaLike, SmsOtpMatcherDefinition, StealthPlatform } from "./types.js";
2
2
  type ProviderImplementationSourceAccess = "official_api" | "private_api" | "browser_flow" | "hybrid";
3
3
  type ProviderImplementationCredentialStrategy = "apifuse_managed" | "workspace_secret" | "user_oauth" | "user_session" | "none";
4
4
  interface ProviderImplementationProfile {
@@ -41,6 +41,13 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
41
41
  id: string;
42
42
  version: string;
43
43
  runtime: "standard" | "shared" | "browser";
44
+ /**
45
+ * Optional deployment overrides, passed through verbatim onto the returned
46
+ * provider definition. The SDK types this field but does not deep-validate
47
+ * it — the APIFuse registry builder owns deployment validation and
48
+ * resolves omitted fields against the runtime deployment profiles.
49
+ */
50
+ deployment?: ProviderDeploymentOverrides;
44
51
  allowedHosts?: string[];
45
52
  stealth?: {
46
53
  profile: string;
package/dist/define.js CHANGED
@@ -1,27 +1,14 @@
1
1
  import ms from "ms";
2
- import { ProviderError, ValidationError } from "./errors";
3
- import { safeParseSchemaSync } from "./schema";
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";
2
+ import { ProviderError, ValidationError } from "./errors.js";
3
+ import { safeParseSchemaSync } from "./schema.js";
4
+ import { HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX, HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN, HEALTH_CHECK_TIMEOUT_MS_MAX, HEALTH_CHECK_TIMEOUT_MS_MIN, OPERATION_TIMEOUT_MS_MAX, OPERATION_TIMEOUT_MS_MIN, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, } from "./types.js";
5
5
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
6
6
  const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
7
7
  const VALID_RUNTIMES = ["standard", "shared", "browser"];
8
- const VALID_AUTH_MODES = [
9
- "none",
10
- "platform-managed",
11
- "credentials",
12
- "oauth2",
13
- ];
8
+ const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"];
14
9
  const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"];
15
- const VALID_PROVIDER_PROXY_MODES = [
16
- "disabled",
17
- "optional",
18
- "required",
19
- ];
20
- const VALID_PROVIDER_PROXY_PROVIDERS = [
21
- "smartproxy",
22
- "decodo",
23
- "custom",
24
- ];
10
+ const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"];
11
+ const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"];
25
12
  const VALID_PROVIDER_PROXY_AFFINITIES = [
26
13
  "request",
27
14
  "operation",
@@ -30,25 +17,25 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
30
17
  ];
31
18
  const VALID_PROVIDER_STT_MODES = ["optional", "required"];
32
19
  const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
20
+ const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
21
+ const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
22
+ // Per-vendor provider-declared credential secrets. A required-mode chain must
23
+ // declare every secret of every credentialed vendor it names, so a missing
24
+ // credential fails at build/validation time rather than during a live outage: a
25
+ // declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
26
+ // exactly the failure class the multi-vendor chain exists to remove. Vendors
27
+ // absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
28
+ // `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
29
+ // no declaration requirement.
30
+ const VENDOR_REQUIRED_SECRETS = {
31
+ smartproxy: [SMARTPROXY_APP_KEY_SECRET],
32
+ nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
33
+ };
33
34
  const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
34
35
  const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
35
- const VALID_OPERATION_RISK_CLASSES = [
36
- "read",
37
- "write",
38
- "destructive",
39
- "external-send",
40
- ];
41
- const VALID_OPERATION_APPROVAL_POLICIES = [
42
- "never",
43
- "risk-based",
44
- "always",
45
- ];
46
- const VALID_OPERATION_TRANSPORT_KINDS = [
47
- "json",
48
- "sse",
49
- "http-stream",
50
- "websocket",
51
- ];
36
+ const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"];
37
+ const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"];
38
+ const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"];
52
39
  const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
53
40
  const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
54
41
  const MS_DURATION_PATTERN = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*([a-zA-Z]+)?$/;
@@ -109,10 +96,7 @@ function validateProviderShape(config) {
109
96
  if (typeof config.runtime === "string")
110
97
  assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
111
98
  const auth = config.auth;
112
- if (auth &&
113
- typeof auth === "object" &&
114
- "mode" in auth &&
115
- typeof auth.mode === "string")
99
+ if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
116
100
  assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
117
101
  if (auth && typeof auth === "object" && "exchange" in auth) {
118
102
  throw new ProviderError(`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`, {
@@ -167,15 +151,23 @@ function validateProviderProxy(config) {
167
151
  fix: `Use proxy: { mode: "required", provider: "smartproxy", geo: { country: "KR" }, session: { affinity: "connection", lifetimeMinutes: 30 } }`,
168
152
  });
169
153
  }
170
- rejectUnknownFields(proxy, new Set(["mode", "provider", "geo", "session"]), "proxy");
154
+ rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
171
155
  assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
172
156
  if (proxy.provider !== undefined) {
173
157
  assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
174
158
  }
159
+ if (proxy.providers !== undefined) {
160
+ if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
161
+ throw new ValidationError(`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`, {
162
+ fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
163
+ });
164
+ }
165
+ for (const vendor of proxy.providers) {
166
+ assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
167
+ }
168
+ }
175
169
  if (proxy.geo !== undefined) {
176
- if (!proxy.geo ||
177
- typeof proxy.geo !== "object" ||
178
- Array.isArray(proxy.geo)) {
170
+ if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
179
171
  throw new ValidationError(`Provider "${config.id}" has invalid proxy.geo: must be an object.`, {
180
172
  fix: `Use proxy.geo: { country: "KR" } with ISO alpha-2 country codes.`,
181
173
  });
@@ -192,9 +184,7 @@ function validateProviderProxy(config) {
192
184
  }
193
185
  }
194
186
  if (proxy.session !== undefined) {
195
- if (!proxy.session ||
196
- typeof proxy.session !== "object" ||
197
- Array.isArray(proxy.session)) {
187
+ if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
198
188
  throw new ValidationError(`Provider "${config.id}" has invalid proxy.session: must be an object.`, {
199
189
  fix: `Use proxy.session: { affinity: "connection", lifetimeMinutes: 30 }.`,
200
190
  });
@@ -204,31 +194,62 @@ function validateProviderProxy(config) {
204
194
  assertLiteralField(proxy.session.affinity, "proxy.session.affinity", VALID_PROVIDER_PROXY_AFFINITIES, config.id);
205
195
  }
206
196
  const lifetime = proxy.session.lifetimeMinutes;
207
- if (lifetime !== undefined &&
208
- (!Number.isFinite(lifetime) || lifetime <= 0)) {
197
+ if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
209
198
  throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`);
210
199
  }
211
200
  const poolSize = proxy.session.poolSize;
212
- if (poolSize !== undefined &&
213
- (!Number.isInteger(poolSize) || poolSize <= 0)) {
201
+ if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
214
202
  throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`);
215
203
  }
216
204
  }
217
- if (proxy.mode === "required" && proxy.provider === "smartproxy") {
218
- const hasSmartproxySecret = config.secrets?.some((secret) => secret.name === SMARTPROXY_APP_KEY_SECRET && secret.required !== false);
219
- if (!hasSmartproxySecret) {
220
- throw new ValidationError(`Provider "${config.id}" requires Smartproxy egress but does not declare ${SMARTPROXY_APP_KEY_SECRET}.`, {
221
- fix: `Add secrets: [{ name: "${SMARTPROXY_APP_KEY_SECRET}", required: true }] to the provider.`,
222
- });
205
+ // Every credentialed vendor in a required-mode chain must declare its
206
+ // provider secret(s) so a missing credential fails at build/validation time,
207
+ // not during a live outage. This covers the fallback legs too (not just the
208
+ // first vendor): a declared-but-uncredentialed nodemaven fallback would leave
209
+ // the chain silently down to a single vendor, reintroducing the SPOF the chain
210
+ // removes.
211
+ const vendorChain = proxy.providers && proxy.providers.length > 0
212
+ ? proxy.providers
213
+ : proxy.provider
214
+ ? [proxy.provider]
215
+ : [];
216
+ if (proxy.mode === "required") {
217
+ for (const vendor of vendorChain) {
218
+ const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
219
+ if (!requiredSecrets)
220
+ continue;
221
+ for (const secretName of requiredSecrets) {
222
+ // Match the canonical runtime gate (assertRequiredSecretsPresent /
223
+ // listMissingRequiredSecrets), which enforces only `required === true`
224
+ // declarations. A declaration that omits `required` (defaulting to
225
+ // optional) is skipped at runtime, so accepting it here would pass
226
+ // validation while leaving the credential unenforced until proxy
227
+ // resolution during a live request — the fail-open gap this check exists
228
+ // to close.
229
+ const declared = config.secrets?.some((secret) => secret.name === secretName && secret.required === true);
230
+ if (!declared) {
231
+ throw new ValidationError(`Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`, {
232
+ fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
233
+ });
234
+ }
235
+ }
223
236
  }
224
237
  }
238
+ // `decodo`/`custom` are deprecated vendor values (string-union members, so the
239
+ // @deprecated symbol gate can't catch them — warn at validation time instead).
240
+ const deprecatedVendors = vendorChain.filter((vendor) => vendor === "decodo" || vendor === "custom");
241
+ if (deprecatedVendors.length > 0) {
242
+ console.warn(`[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven", or the APIFUSE__PROXY__URL bring-your-own escape hatch.`);
243
+ }
225
244
  }
226
245
  function validateProviderStt(config) {
227
246
  const stt = config.stt;
228
247
  if (stt === undefined)
229
248
  return;
230
249
  if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
231
- throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, { fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.` });
250
+ throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
251
+ fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
252
+ });
232
253
  }
233
254
  rejectUnknownFields(stt, new Set(["mode"]), "stt");
234
255
  assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
@@ -247,12 +268,7 @@ function validateOperationIds(providerId, operations) {
247
268
  }
248
269
  const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
249
270
  const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
250
- const VALID_OPERATION_LIFECYCLES = [
251
- "stable",
252
- "beta",
253
- "deprecated",
254
- "removed",
255
- ];
271
+ const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"];
256
272
  function assertNonEmptyString(value, field, providerId, operationName) {
257
273
  if (typeof value !== "string" || value.trim().length === 0) {
258
274
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${field}: must be a non-empty string.`, { fix: `Set ${field} to a non-empty customer-facing value.` });
@@ -268,8 +284,7 @@ function validateToolRouterMetadata(providerId, operations) {
268
284
  fix: `Remove operations.${operationName}.toolRouter or provide MCP-safe metadata.`,
269
285
  });
270
286
  }
271
- if (toolRouter.name !== undefined &&
272
- !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
287
+ if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
273
288
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`, {
274
289
  fix: `Use letters, numbers, and underscores only, starting with a letter, for example "${providerId.replace(/[^A-Za-z0-9]+/g, "_")}__${operationName.replace(/[^A-Za-z0-9]+/g, "_")}".`,
275
290
  });
@@ -307,8 +322,7 @@ function validateOperationContracts(providerId, operations) {
307
322
  if (contract.lifecycle !== undefined) {
308
323
  assertLiteralField(contract.lifecycle, `operations.${operationName}.contract.lifecycle`, VALID_OPERATION_LIFECYCLES, providerId);
309
324
  }
310
- if (contract.lifecycle === "deprecated" ||
311
- contract.lifecycle === "removed") {
325
+ if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
312
326
  if (!contract.deprecation || typeof contract.deprecation !== "object") {
313
327
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`, {
314
328
  fix: `Add announcedAt, removalAfter, and migrationGuide to operations.${operationName}.contract.deprecation.`,
@@ -333,8 +347,7 @@ function validateOperationAnnotations(providerId, operations) {
333
347
  throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an integer number of milliseconds.`, {
334
348
  fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
335
349
  });
336
- if (timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
337
- timeoutMs > OPERATION_TIMEOUT_MS_MAX)
350
+ if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
338
351
  throw new ValidationError(`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`, {
339
352
  fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms (the upper bound stays below the gateway/ALB ceiling).`,
340
353
  });
@@ -478,9 +491,7 @@ function validateOperationTransports(providerId, operations) {
478
491
  assertStreamMs(maxDurationMs, `${fieldPath}.maxDurationMs`, STREAM_MAX_DURATION_MS_MIN, STREAM_MAX_DURATION_MS_MAX, "max duration");
479
492
  assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
480
493
  const resumable = Reflect.get(transport, "resumable");
481
- if (resumable !== undefined &&
482
- resumable !== false &&
483
- resumable !== "last-event-id") {
494
+ if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
484
495
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`, {
485
496
  fix: `Use ${fieldPath}.resumable: "last-event-id" for SSE Last-Event-ID resume support, or false to disable resume.`,
486
497
  });
@@ -515,8 +526,7 @@ function validateOperationTransports(providerId, operations) {
515
526
  });
516
527
  }
517
528
  for (const subprotocol of subprotocols) {
518
- if (typeof subprotocol !== "string" ||
519
- !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
529
+ if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
520
530
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`, {
521
531
  fix: `Use values such as "apifuse.v1" without spaces or separators that are invalid for Sec-WebSocket-Protocol.`,
522
532
  });
@@ -624,9 +634,7 @@ function assertBoundedIntegerMs(value, fieldPath, options) {
624
634
  function validateProviderHealthMonitor(providerId, healthMonitor, field = "healthMonitor") {
625
635
  if (healthMonitor === undefined)
626
636
  return;
627
- if (!healthMonitor ||
628
- typeof healthMonitor !== "object" ||
629
- Array.isArray(healthMonitor))
637
+ if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
630
638
  throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
631
639
  fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
632
640
  });
@@ -676,9 +684,7 @@ function validateProviderHealthMonitor(providerId, healthMonitor, field = "healt
676
684
  }
677
685
  const probeOverrides = healthMonitorRecord.probeOverrides;
678
686
  if (probeOverrides !== undefined) {
679
- if (!probeOverrides ||
680
- typeof probeOverrides !== "object" ||
681
- Array.isArray(probeOverrides))
687
+ if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
682
688
  throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`);
683
689
  for (const [probeId, override] of Object.entries(probeOverrides)) {
684
690
  if (probeId.length === 0)
@@ -756,9 +762,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
756
762
  fix: `Set ${fieldPath}.interval to a positive ms-style duration string.`,
757
763
  });
758
764
  if (s.schedule !== undefined) {
759
- if (!s.schedule ||
760
- typeof s.schedule !== "object" ||
761
- Array.isArray(s.schedule)) {
765
+ if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
762
766
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.schedule must be an object.`);
763
767
  }
764
768
  if (Reflect.get(s.schedule, "jitter") !== undefined) {
@@ -784,8 +788,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
784
788
  label: "degraded threshold",
785
789
  });
786
790
  }
787
- if (s.requiresConnection !== undefined &&
788
- typeof s.requiresConnection !== "boolean")
791
+ if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
789
792
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`);
790
793
  if (!Array.isArray(s.cases) || s.cases.length === 0)
791
794
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.cases must be a non-empty array.`, {
@@ -804,9 +807,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
804
807
  }
805
808
  function validateHealthCheckUnsupported(providerId, operationName, unsupported) {
806
809
  const fieldPath = `operations.${operationName}.healthCheckUnsupported`;
807
- if (!unsupported ||
808
- typeof unsupported !== "object" ||
809
- Array.isArray(unsupported))
810
+ if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
810
811
  throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
811
812
  rejectUnknownFields(unsupported, HEALTH_CHECK_UNSUPPORTED_FIELDS, fieldPath);
812
813
  const u = unsupported;
@@ -831,12 +832,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
831
832
  "steps",
832
833
  "run",
833
834
  ]);
834
- const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
835
- "kind",
836
- "interval",
837
- "jitter",
838
- "randomize",
839
- ]);
835
+ const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
840
836
  const HEALTH_JOURNEY_STEP_FIELDS = new Set([
841
837
  "id",
842
838
  "description",
@@ -854,10 +850,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
854
850
  "minManualInterval",
855
851
  "publicRationale",
856
852
  ]);
857
- const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
858
- "enabled",
859
- "reason",
860
- ]);
853
+ const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
861
854
  const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
862
855
  "enabled",
863
856
  "requiresAcknowledgement",
@@ -872,9 +865,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS = new Set([
872
865
  ]);
873
866
  function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger) {
874
867
  const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
875
- if (!manualTrigger ||
876
- typeof manualTrigger !== "object" ||
877
- Array.isArray(manualTrigger)) {
868
+ if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
878
869
  throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object when present.`);
879
870
  }
880
871
  rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
@@ -897,8 +888,7 @@ function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger
897
888
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`);
898
889
  }
899
890
  const risk = Reflect.get(manualTrigger, "risk");
900
- if (typeof risk !== "string" ||
901
- !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
891
+ if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
902
892
  throw new ValidationError(`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`);
903
893
  }
904
894
  if (risk !== "read_only" && requiresAcknowledgement !== true) {
@@ -950,7 +940,7 @@ function isoDurationMs(value) {
950
940
  const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
951
941
  const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
952
942
  const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
953
- return (days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000);
943
+ return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
954
944
  }
955
945
  function scheduleRandomizationMs(randomize, fieldPath) {
956
946
  const mode = Reflect.get(randomize, "mode");
@@ -974,9 +964,7 @@ function validateScheduleRandomization(randomize, fieldPath, intervalMs) {
974
964
  throw new ValidationError(`${fieldPath} must be an object.`);
975
965
  }
976
966
  const mode = Reflect.get(randomize, "mode");
977
- const allowedFields = mode === "centered"
978
- ? new Set(["mode", "maxOffset"])
979
- : new Set(["mode", "maxDelay"]);
967
+ const allowedFields = mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
980
968
  rejectUnknownFields(randomize, allowedFields, fieldPath);
981
969
  const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
982
970
  if (offsetMs <= 0) {
@@ -1062,9 +1050,7 @@ function countCapturingGroups(pattern) {
1062
1050
  const next = source[i + 1];
1063
1051
  if (next === "?" && source[i + 2] !== "<")
1064
1052
  continue;
1065
- if (next === "?" &&
1066
- source[i + 2] === "<" &&
1067
- (source[i + 3] === "=" || source[i + 3] === "!"))
1053
+ if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
1068
1054
  continue;
1069
1055
  count += 1;
1070
1056
  }
@@ -1142,8 +1128,7 @@ function validateSmsOtpMatcher(matcher, fieldPath) {
1142
1128
  throw new ValidationError(`${fieldPath}.code.pattern must be a RegExp or pattern source string.`);
1143
1129
  }
1144
1130
  const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
1145
- if (countCapturingGroups(regex) !== 1 &&
1146
- Reflect.get(code, "capture") === undefined) {
1131
+ if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
1147
1132
  throw new ValidationError(`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`);
1148
1133
  }
1149
1134
  if (Reflect.get(code, "capture") !== undefined &&
@@ -1195,8 +1180,7 @@ function validateHealthJourneySchedule(providerId, journeyId, schedule) {
1195
1180
  const interval = Reflect.get(schedule, "interval");
1196
1181
  assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
1197
1182
  const randomize = Reflect.get(schedule, "randomize");
1198
- if (Reflect.get(schedule, "jitter") !== undefined &&
1199
- randomize !== undefined) {
1183
+ if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
1200
1184
  throw new ValidationError(`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`);
1201
1185
  }
1202
1186
  if (Reflect.get(schedule, "jitter") !== undefined)
@@ -1226,8 +1210,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
1226
1210
  throw new ValidationError(`Provider "${providerId}" has duplicate health journey id "${journey.id}".`);
1227
1211
  journeyIds.add(journey.id);
1228
1212
  validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
1229
- if (!Array.isArray(journey.coversOperations) ||
1230
- journey.coversOperations.length === 0) {
1213
+ if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
1231
1214
  throw new ValidationError(`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`);
1232
1215
  }
1233
1216
  for (const operationId of journey.coversOperations) {
@@ -1265,8 +1248,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
1265
1248
  throw new ValidationError(`Provider "${providerId}" ${stepPath}.id must be a kebab-case identifier.`);
1266
1249
  if (step.operationId !== undefined && !operations[step.operationId])
1267
1250
  throw new ValidationError(`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`);
1268
- if (step.usesSmsMatcher !== undefined &&
1269
- !matcherIds.has(step.usesSmsMatcher))
1251
+ if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
1270
1252
  throw new ValidationError(`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`);
1271
1253
  }
1272
1254
  if (journey.manualTrigger !== undefined)
@@ -1300,9 +1282,7 @@ function validateOperationHealthChecks(providerId, operations, journeyCoveredOpe
1300
1282
  validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
1301
1283
  if (hasUnsupported)
1302
1284
  validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
1303
- if (!hasCheck &&
1304
- !hasUnsupported &&
1305
- !journeyCoveredOperations.has(operationName))
1285
+ if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
1306
1286
  throw new ValidationError(`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`, {
1307
1287
  fix: `Add \`healthCheck: { interval, cases: [...] }\` or \`healthCheckUnsupported: { reason: "..." }\` to operations.${operationName}.`,
1308
1288
  });
@@ -1332,6 +1312,19 @@ function validateOperationFixtures(providerId, operations) {
1332
1312
  }
1333
1313
  }
1334
1314
  }
1315
+ /**
1316
+ * Shallow shape guard only: the `deployment` object is passed through
1317
+ * verbatim and deliberately not deep-validated by the SDK — the APIFuse
1318
+ * registry builder owns deployment validation and profile resolution.
1319
+ */
1320
+ function validateProviderDeployment(providerId, deployment) {
1321
+ if (deployment === undefined)
1322
+ return;
1323
+ if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
1324
+ throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
1325
+ fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
1326
+ });
1327
+ }
1335
1328
  export function defineProvider(config) {
1336
1329
  validateProviderShape(config);
1337
1330
  if (!CONNECTOR_ID_REGEX.test(config.id))
@@ -1356,6 +1349,7 @@ export function defineProvider(config) {
1356
1349
  });
1357
1350
  validateProviderHealthMonitor(config.id, config.healthProbe ?? config.healthMonitor, config.healthProbe !== undefined ? "healthProbe" : "healthMonitor");
1358
1351
  validateOperationFixtures(config.id, config.operations);
1352
+ validateProviderDeployment(config.id, config.deployment);
1359
1353
  validateProviderProxy(config);
1360
1354
  validateProviderStt(config);
1361
1355
  if (config.runtime === "browser" && !config.browser)
@@ -1368,6 +1362,9 @@ export function defineProvider(config) {
1368
1362
  id: config.id,
1369
1363
  version: config.version,
1370
1364
  runtime: config.runtime,
1365
+ // Verbatim passthrough: deployment validation and profile resolution
1366
+ // are owned by the APIFuse registry builder, not the SDK.
1367
+ deployment: config.deployment,
1371
1368
  allowedHosts: config.allowedHosts,
1372
1369
  stealth: config.stealth,
1373
1370
  proxy: config.proxy,
package/dist/dev.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ProviderDefinition } from "./types";
1
+ import type { ProviderDefinition } from "./types.js";
2
2
  export interface DevServerOptions {
3
3
  port?: number;
4
4
  sessionDbPath?: string;
package/dist/dev.js CHANGED
@@ -1,4 +1,4 @@
1
- import { serve } from "./server/serve";
1
+ import { serve } from "./server/serve.js";
2
2
  export function createDevServer(provider, options) {
3
3
  const port = options?.port ?? 3900;
4
4
  return {
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ProviderErrorCategory } from "./observability";
1
+ import type { ProviderErrorCategory } from "./observability.js";
2
2
  export type ProviderErrorOptions = {
3
3
  fix?: string;
4
4
  code?: string;
@@ -39,6 +39,9 @@ export declare class TransportError extends ProviderError {
39
39
  readonly upstreamStatus?: number;
40
40
  constructor(message: string, options?: TransportErrorOptions);
41
41
  }
42
+ export declare function isProviderError(value: unknown): value is ProviderError;
43
+ export declare function isSessionExpiredError(value: unknown): value is SessionExpiredError;
44
+ export declare function isTransportError(value: unknown): value is TransportError;
42
45
  export declare class ProviderSecretError extends ProviderError {
43
46
  constructor(message: string, options?: ProviderErrorOptions);
44
47
  }