@apifuse/provider-sdk 2.2.0-beta.37 → 2.2.0-beta.39

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.
@@ -1,5 +1,6 @@
1
1
  import { describeSchema } from "./contract-serialization.js";
2
2
  import { ProviderError } from "./errors.js";
3
+ import { HealthScenarioSchema } from "./health-scenario.js";
3
4
  import type {
4
5
  HealthJourneyDefinition,
5
6
  ProviderDefinition,
@@ -12,6 +13,8 @@ export const DECLARATION_INVALID_CODE = "DECLARATION_INVALID";
12
13
  export const DECLARATION_RULE_IDS = {
13
14
  challengeShape: "credentials-challenge-shape",
14
15
  journeyExecutable: "health-journey-executable",
16
+ journeyRunScenarioExclusive: "health-journey-run-scenario-exclusive",
17
+ journeyScenarioValid: "health-journey-scenario-valid",
15
18
  schemaSerializable: "operation-schema-serializable",
16
19
  proxyExplicitPolicy: "proxy-explicit-policy",
17
20
  proxyVendorExclusive: "proxy-vendor-fields-exclusive",
@@ -56,21 +59,82 @@ export function validateFailClosedDeclaration(provider: ProviderDefinition): voi
56
59
  if (violations.length > 0) throw declarationInvalidError(violations);
57
60
  }
58
61
 
62
+ type ProviderDeclarationRulesInput = Pick<ProviderDefinition, "healthJourneys" | "proxy">;
63
+ type OperationDeclarationRulesInput = Pick<ProviderDefinition, "operations">;
64
+
65
+ /** Enforces fail-closed rules that only depend on the provider declaration. */
66
+ export function validateFailClosedProviderDeclaration(
67
+ provider: ProviderDeclarationRulesInput,
68
+ ): void {
69
+ const violations: DeclarationViolation[] = [];
70
+ collectProviderDeclarationViolations(provider, violations);
71
+ if (violations.length > 0) throw declarationInvalidError(violations);
72
+ }
73
+
74
+ /** Enforces fail-closed rules that depend on the operation implementation. */
75
+ export function validateFailClosedOperationDeclaration(
76
+ provider: OperationDeclarationRulesInput,
77
+ ): void {
78
+ const violations: DeclarationViolation[] = [];
79
+ collectOperationDeclarationViolations(provider, violations);
80
+ if (violations.length > 0) throw declarationInvalidError(violations);
81
+ }
82
+
83
+ function collectProviderDeclarationViolations(
84
+ provider: ProviderDeclarationRulesInput,
85
+ violations: DeclarationViolation[],
86
+ ): void {
87
+ validateHealthDeclaration(provider, violations);
88
+ validateProxyDeclaration(provider, violations);
89
+ }
90
+
91
+ function collectOperationDeclarationViolations(
92
+ provider: OperationDeclarationRulesInput,
93
+ violations: DeclarationViolation[],
94
+ ): void {
95
+ validateSchemaDeclaration(provider, violations);
96
+ validateOperationDeclaration(provider, violations);
97
+ }
98
+
59
99
  function validateHealthDeclaration(
60
- provider: ProviderDefinition,
100
+ provider: ProviderDeclarationRulesInput,
61
101
  violations: DeclarationViolation[],
62
102
  ): void {
63
103
  for (const [index, journey] of (provider.healthJourneys ?? []).entries()) {
64
104
  if (!journey || typeof journey !== "object") continue;
65
- if (typeof journey.run !== "function") {
105
+ const hasRun = journey.run !== undefined;
106
+ const hasScenario = journey.scenario !== undefined;
107
+ if (hasRun && hasScenario) {
108
+ const journeyPath = healthJourneyPath(journey, index);
109
+ violations.push({
110
+ ruleId: DECLARATION_RULE_IDS.journeyRunScenarioExclusive,
111
+ path: journeyPath,
112
+ message: "A health journey must declare exactly one of run or scenario, not both.",
113
+ fix: `Remove either ${journeyPath}.run or ${journeyPath}.scenario.`,
114
+ });
115
+ }
116
+ if (!hasRun && !hasScenario) {
66
117
  const journeyPath = healthJourneyPath(journey, index);
67
118
  violations.push({
68
119
  ruleId: DECLARATION_RULE_IDS.journeyExecutable,
69
120
  path: `${journeyPath}.run`,
70
- message: "coversOperations cannot provide health coverage without executable run logic.",
71
- fix: `Add an async run(ctx) implementation to ${journeyPath}.`,
121
+ message:
122
+ "coversOperations cannot provide health coverage without run or a declarative scenario.",
123
+ fix: `Add an async run(ctx) implementation or a valid scenario to ${journeyPath}.`,
72
124
  });
73
125
  }
126
+ if (hasScenario) {
127
+ const parsed = HealthScenarioSchema.safeParse(journey.scenario);
128
+ if (!parsed.success) {
129
+ const journeyPath = healthJourneyPath(journey, index);
130
+ violations.push({
131
+ ruleId: DECLARATION_RULE_IDS.journeyScenarioValid,
132
+ path: `${journeyPath}.scenario`,
133
+ message: "scenario must conform to HealthScenario.",
134
+ fix: "Build the scenario with defineHealthScenario().",
135
+ });
136
+ }
137
+ }
74
138
  }
75
139
 
76
140
  // NOTE: healthCheck.cases[].enabled is intentionally NOT validated here.
@@ -86,7 +150,7 @@ function healthJourneyPath(journey: HealthJourneyDefinition, index: number): str
86
150
  }
87
151
 
88
152
  function validateSchemaDeclaration(
89
- provider: ProviderDefinition,
153
+ provider: OperationDeclarationRulesInput,
90
154
  violations: DeclarationViolation[],
91
155
  ): void {
92
156
  for (const [operationId, operation] of Object.entries(provider.operations ?? {})) {
@@ -126,7 +190,7 @@ const MANAGED_PROXY_VENDORS = new Set<ProviderProxyProvider>(["smartproxy", "nod
126
190
  const STATIC_PROXY_VENDORS = new Set<ProviderProxyProvider>(["custom", "decodo"]);
127
191
 
128
192
  function validateProxyDeclaration(
129
- provider: ProviderDefinition,
193
+ provider: ProviderDeclarationRulesInput,
130
194
  violations: DeclarationViolation[],
131
195
  ): void {
132
196
  if (provider.proxy === true) {
@@ -186,7 +250,7 @@ function declaredProxyVendors(policy: ProviderProxyPolicy): ProviderProxyProvide
186
250
  }
187
251
 
188
252
  function validateOperationDeclaration(
189
- provider: ProviderDefinition,
253
+ provider: OperationDeclarationRulesInput,
190
254
  violations: DeclarationViolation[],
191
255
  ): void {
192
256
  for (const [operationId, operation] of Object.entries(provider.operations ?? {})) {
package/src/define.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import ms from "ms";
2
2
 
3
- import { validateFailClosedDeclaration } from "./declaration-validation.js";
3
+ import {
4
+ validateFailClosedOperationDeclaration,
5
+ validateFailClosedProviderDeclaration,
6
+ } from "./declaration-validation.js";
4
7
  import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
5
8
  import { ProviderError, ValidationError } from "./errors.js";
6
9
  import {
@@ -30,12 +33,12 @@ import type {
30
33
  OperationWebSocketTransport,
31
34
  ProviderAccessConfig,
32
35
  ProviderChallengeKind,
33
- ProviderDefinition,
34
36
  ProviderContext,
35
37
  ProviderContextFor,
36
- ProviderOcrConfig,
38
+ ProviderDefinition,
37
39
  ProviderDeploymentOverrides,
38
40
  ProviderHealthMonitorConfig,
41
+ ProviderOcrConfig,
39
42
  ProviderProxyConfig,
40
43
  ProviderProxyProvider,
41
44
  ProviderPublicProfile,
@@ -806,13 +809,12 @@ function validateProxiedOAuthAuth(auth: Record<string, unknown>, providerId: str
806
809
  validateProxiedOAuthParams(config.tokenParams, "tokenParams", providerId);
807
810
  }
808
811
 
809
- function validateProviderShape(config: unknown): void {
812
+ function validateProviderDeclarationShape(config: unknown): void {
810
813
  assertObjectConfig(config);
811
814
  assertRequiredField(config, "id");
812
815
  assertRequiredField(config, "version", String(config.id));
813
816
  assertRequiredField(config, "runtime", String(config.id));
814
817
  assertRequiredField(config, "meta", String(config.id));
815
- assertRequiredField(config, "operations", String(config.id));
816
818
  if (typeof config.runtime === "string")
817
819
  assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
818
820
  if (config.native !== undefined && config.runtime === "browser") {
@@ -910,6 +912,11 @@ function validateProviderShape(config: unknown): void {
910
912
  }
911
913
  }
912
914
 
915
+ function validateProviderImplementationShape(config: { id: string }): void {
916
+ const configRecord = config as unknown as Record<string, unknown>;
917
+ assertRequiredField(configRecord, "operations", String(config.id));
918
+ }
919
+
913
920
  function validateProviderProxy(config: {
914
921
  id: string;
915
922
  proxy?: ProviderProxyConfig;
@@ -2132,6 +2139,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
2132
2139
  "manualTrigger",
2133
2140
  "steps",
2134
2141
  "run",
2142
+ "scenario",
2135
2143
  ]);
2136
2144
  const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
2137
2145
  const HEALTH_JOURNEY_STEP_FIELDS = new Set([
@@ -2667,6 +2675,14 @@ function validateHealthJourneys(
2667
2675
  }
2668
2676
  if (journey.manualTrigger !== undefined)
2669
2677
  validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
2678
+ if (journey.scenario !== undefined && journey.smsMatchers !== undefined)
2679
+ throw new ValidationError(
2680
+ `Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers is not allowed on declarative scenarios.`,
2681
+ );
2682
+ if (journey.scenario !== undefined && journey.requiredSecrets !== undefined)
2683
+ throw new ValidationError(
2684
+ `Provider "${providerId}" healthJourneys.${journey.id}.requiredSecrets is not allowed on declarative scenarios.`,
2685
+ );
2670
2686
  if (journey.timeout !== undefined)
2671
2687
  assertIsoDuration(
2672
2688
  journey.timeout,
@@ -2835,7 +2851,7 @@ export type ProviderBuilder<TDeclaration extends ProviderDeclaration> = <
2835
2851
  implementation: {
2836
2852
  operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
2837
2853
  },
2838
- ) => ProviderDefinition & {
2854
+ ) => Omit<ProviderDefinition, "operations"> & {
2839
2855
  operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
2840
2856
  };
2841
2857
 
@@ -2850,6 +2866,7 @@ export function defineProvider<const TDeclaration extends ProviderDeclaration>(
2850
2866
  Record<Exclude<keyof TDeclaration, keyof ProviderDeclaration>, never> &
2851
2867
  AuthStartNoInputGuard<TDeclaration>,
2852
2868
  ): ProviderBuilder<TDeclaration> {
2869
+ validateProviderDeclaration(declaration);
2853
2870
  const buildProvider = <TOperations extends Record<string, ProviderOperation>>(
2854
2871
  implementation: {
2855
2872
  operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
@@ -2862,37 +2879,12 @@ export function defineProvider<const TDeclaration extends ProviderDeclaration>(
2862
2879
  return buildProvider as ProviderBuilder<TDeclaration>;
2863
2880
  }
2864
2881
 
2865
- function finalizeProvider<
2866
- TOperations extends Record<string, ProviderOperation>,
2867
- TContext,
2868
- >(
2869
- config: ProviderConfig<TOperations, TContext>,
2870
- ): ProviderDefinition & {
2871
- operations: OperationMapConfig<TOperations, TContext>;
2872
- } {
2873
- validateProviderShape(config);
2874
- const operations = resolveOperationFixtureRequests(config.operations);
2882
+ function validateProviderDeclaration(config: ProviderDeclaration): void {
2883
+ validateProviderDeclarationShape(config);
2875
2884
  if (!CONNECTOR_ID_REGEX.test(config.id))
2876
2885
  throw new ProviderError(`Invalid provider id: "${config.id}"`, {
2877
2886
  fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
2878
2887
  });
2879
- if (Object.keys(config.operations).length === 0)
2880
- throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
2881
- fix: "Add at least one operation to the operations object",
2882
- });
2883
- validateOperationIds(config.id, config.operations);
2884
- validateOperationAnnotations(config.id, config.operations);
2885
- validateOperationObservability(config.id, config.operations);
2886
- validateOperationErrorCodes(config.id, config.operations);
2887
- validateOperationTransports(config.id, config.operations);
2888
- validateOperationContracts(config.id, config.operations);
2889
- validateToolRouterMetadata(config.id, config.operations);
2890
- const journeyCoveredOperations = validateHealthJourneys(
2891
- config.id,
2892
- config.operations,
2893
- config.healthJourneys,
2894
- );
2895
- validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
2896
2888
  if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
2897
2889
  throw new ValidationError(
2898
2890
  `Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
@@ -2905,7 +2897,6 @@ function finalizeProvider<
2905
2897
  config.healthProbe ?? config.healthMonitor,
2906
2898
  config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
2907
2899
  );
2908
- validateOperationFixtures(config.id, operations);
2909
2900
  validateProviderDeployment(config.id, config.deployment);
2910
2901
  try {
2911
2902
  validateNativeProviderConfig(config.native);
@@ -2930,7 +2921,38 @@ function finalizeProvider<
2930
2921
  `Provider "${config.id}" cannot define browser config unless runtime is "browser"`,
2931
2922
  { fix: 'Set runtime: "browser" or remove the browser config' },
2932
2923
  );
2933
- const provider: ProviderDefinition & {
2924
+ validateFailClosedProviderDeclaration(config);
2925
+ }
2926
+
2927
+ function finalizeProvider<
2928
+ TOperations extends Record<string, ProviderOperation>,
2929
+ TContext,
2930
+ >(
2931
+ config: ProviderConfig<TOperations, TContext>,
2932
+ ): Omit<ProviderDefinition, "operations"> & {
2933
+ operations: OperationMapConfig<TOperations, TContext>;
2934
+ } {
2935
+ validateProviderImplementationShape(config);
2936
+ const operations = resolveOperationFixtureRequests(config.operations);
2937
+ if (Object.keys(config.operations).length === 0)
2938
+ throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
2939
+ fix: "Add at least one operation to the operations object",
2940
+ });
2941
+ validateOperationIds(config.id, config.operations);
2942
+ validateOperationAnnotations(config.id, config.operations);
2943
+ validateOperationObservability(config.id, config.operations);
2944
+ validateOperationErrorCodes(config.id, config.operations);
2945
+ validateOperationTransports(config.id, config.operations);
2946
+ validateOperationContracts(config.id, config.operations);
2947
+ validateToolRouterMetadata(config.id, config.operations);
2948
+ const journeyCoveredOperations = validateHealthJourneys(
2949
+ config.id,
2950
+ config.operations,
2951
+ config.healthJourneys,
2952
+ );
2953
+ validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
2954
+ validateOperationFixtures(config.id, operations);
2955
+ const provider: Omit<ProviderDefinition, "operations"> & {
2934
2956
  operations: OperationMapConfig<TOperations, TContext>;
2935
2957
  } = {
2936
2958
  id: config.id,
@@ -2954,14 +2976,15 @@ function finalizeProvider<
2954
2976
  credential: config.credential,
2955
2977
  context: config.context,
2956
2978
  meta: config.meta,
2957
- operations: operations as ProviderDefinition["operations"] &
2958
- OperationMapConfig<TOperations, TContext>,
2979
+ operations,
2959
2980
  // Transitional healthMonitor → healthProbe alias: mirror whichever field
2960
2981
  // was declared onto both so old and new consumers keep working.
2961
2982
  healthMonitor: config.healthMonitor ?? config.healthProbe,
2962
2983
  healthProbe: config.healthProbe ?? config.healthMonitor,
2963
2984
  healthJourneys: config.healthJourneys,
2964
2985
  };
2965
- validateFailClosedDeclaration(provider);
2986
+ // Declaration validation never invokes handlers, so their declaration-bound
2987
+ // context parameter is irrelevant to the runtime ProviderDefinition shape.
2988
+ validateFailClosedOperationDeclaration(provider as unknown as ProviderDefinition);
2966
2989
  return provider;
2967
2990
  }