@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.21
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.
- package/AUTHORING.md +487 -0
- package/CHANGELOG.md +90 -0
- package/README.md +49 -3
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-pack-smoke.ts +36 -81
- package/bin/apifuse-pack-types.ts +305 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +659 -111
- package/bin/apifuse-submit-check.ts +546 -48
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +65 -18
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/config/loader.d.ts +176 -8
- package/dist/config/loader.js +424 -95
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +9 -1
- package/dist/define.js +304 -119
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/errors.d.ts +22 -1
- package/dist/errors.js +88 -0
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +46 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +1 -1
- package/dist/lint.js +8 -15
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +12 -11
- package/dist/provider.js +10 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +2 -1
- package/dist/runtime/auth-flow.js +4 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +31 -4
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +387 -47
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +345 -22
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network.d.ts +127 -0
- package/dist/runtime/native-network.js +1298 -0
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +238 -26
- package/dist/runtime/stealth.d.ts +5 -3
- package/dist/runtime/stealth.js +423 -143
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +104 -3
- package/dist/server/self-test.js +673 -115
- package/dist/server/serve.d.ts +116 -4
- package/dist/server/serve.js +799 -128
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -1
- package/dist/stateful/errors.d.ts +14 -0
- package/dist/stateful/errors.js +14 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
- package/dist/stateful/stateful-provider-session-routing.js +353 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +478 -28
- package/dist/types.d.ts +342 -14
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +103 -78
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +652 -204
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +499 -498
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +91 -0
- package/src/errors.ts +110 -5
- package/src/fixture-sanitization.ts +247 -0
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +112 -42
- package/src/lint.ts +88 -152
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +66 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +7 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +43 -20
- package/src/runtime/http.ts +494 -57
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +495 -66
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network.ts +1686 -0
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +353 -133
- package/src/runtime/stealth.ts +505 -154
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +29 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +1151 -328
- package/src/server/types.ts +12 -13
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +23 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +555 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +10 -26
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +10 -2
- package/src/testing/run.ts +653 -74
- package/src/types.ts +408 -28
- package/src/user-input.ts +118 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/src/define.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
4
|
+
import { ProviderError, ValidationError } from "./errors.js";
|
|
5
|
+
import {
|
|
6
|
+
NativeEgressPolicyValidationError,
|
|
7
|
+
validateNativeProviderConfig,
|
|
8
|
+
} from "./native-egress-policy.js";
|
|
9
|
+
import { safeParseSchemaSync } from "./schema.js";
|
|
10
|
+
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
5
11
|
import type {
|
|
6
12
|
AuthConfig,
|
|
7
13
|
BrowserEngine,
|
|
@@ -20,10 +26,13 @@ import type {
|
|
|
20
26
|
OperationSseTransport,
|
|
21
27
|
OperationTransport,
|
|
22
28
|
OperationWebSocketTransport,
|
|
29
|
+
NativeProviderConfig,
|
|
23
30
|
ProviderAccessConfig,
|
|
24
31
|
ProviderDefinition,
|
|
32
|
+
ProviderDeploymentOverrides,
|
|
25
33
|
ProviderHealthMonitorConfig,
|
|
26
34
|
ProviderProxyConfig,
|
|
35
|
+
ProviderProxyProvider,
|
|
27
36
|
ProviderPublicProfile,
|
|
28
37
|
ProviderReviewed,
|
|
29
38
|
ProviderSecretDeclaration,
|
|
@@ -32,7 +41,7 @@ import type {
|
|
|
32
41
|
SchemaLike,
|
|
33
42
|
SmsOtpMatcherDefinition,
|
|
34
43
|
StealthPlatform,
|
|
35
|
-
} from "./types";
|
|
44
|
+
} from "./types.js";
|
|
36
45
|
import {
|
|
37
46
|
HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
38
47
|
HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
|
|
@@ -48,7 +57,8 @@ import {
|
|
|
48
57
|
STREAM_IDLE_TIMEOUT_MS_MIN,
|
|
49
58
|
STREAM_MAX_DURATION_MS_MAX,
|
|
50
59
|
STREAM_MAX_DURATION_MS_MIN,
|
|
51
|
-
|
|
60
|
+
VALID_OPERATION_ERROR_STATUSES,
|
|
61
|
+
} from "./types.js";
|
|
52
62
|
|
|
53
63
|
type ProviderImplementationSourceAccess =
|
|
54
64
|
| "official_api"
|
|
@@ -79,18 +89,34 @@ const VALID_AUTH_MODES = [
|
|
|
79
89
|
"platform-managed",
|
|
80
90
|
"credentials",
|
|
81
91
|
"oauth2",
|
|
92
|
+
"oauth2_proxied",
|
|
82
93
|
] as const;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
const VALID_PROVIDER_PROXY_PROVIDERS = [
|
|
90
|
-
"smartproxy",
|
|
91
|
-
"decodo",
|
|
92
|
-
"custom",
|
|
94
|
+
const PROXIED_OAUTH_REQUIRED_FIELDS = [
|
|
95
|
+
"authorizeUrl",
|
|
96
|
+
"tokenUrl",
|
|
97
|
+
"customScheme",
|
|
98
|
+
"rewriteProfile",
|
|
99
|
+
"clientIdEnvKey",
|
|
93
100
|
] as const;
|
|
101
|
+
const PROXIED_OAUTH_ALLOWED_FIELDS = new Set([
|
|
102
|
+
...PROXIED_OAUTH_REQUIRED_FIELDS,
|
|
103
|
+
"pkce",
|
|
104
|
+
"authorizeParams",
|
|
105
|
+
"tokenParams",
|
|
106
|
+
]);
|
|
107
|
+
const PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS = new Set([
|
|
108
|
+
"client_id",
|
|
109
|
+
"response_type",
|
|
110
|
+
"state",
|
|
111
|
+
"code_challenge",
|
|
112
|
+
"code_challenge_method",
|
|
113
|
+
]);
|
|
114
|
+
const PROXIED_OAUTH_PROFILE_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
115
|
+
const PROXIED_OAUTH_ENV_KEY_REGEX = /^[A-Z][A-Z0-9_]*__[A-Z0-9_]+$/;
|
|
116
|
+
const CUSTOM_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+$/;
|
|
117
|
+
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
|
|
118
|
+
const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"] as const;
|
|
119
|
+
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"] as const;
|
|
94
120
|
const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
95
121
|
"request",
|
|
96
122
|
"operation",
|
|
@@ -99,25 +125,25 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
|
99
125
|
] as const;
|
|
100
126
|
const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
|
|
101
127
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
128
|
+
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
129
|
+
const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
130
|
+
// Per-vendor provider-declared credential secrets. A required-mode chain must
|
|
131
|
+
// declare every secret of every credentialed vendor it names, so a missing
|
|
132
|
+
// credential fails at build/validation time rather than during a live outage: a
|
|
133
|
+
// declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
|
|
134
|
+
// exactly the failure class the multi-vendor chain exists to remove. Vendors
|
|
135
|
+
// absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
|
|
136
|
+
// `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
|
|
137
|
+
// no declaration requirement.
|
|
138
|
+
const VENDOR_REQUIRED_SECRETS: Partial<Record<ProviderProxyProvider, readonly string[]>> = {
|
|
139
|
+
smartproxy: [SMARTPROXY_APP_KEY_SECRET],
|
|
140
|
+
nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
|
|
141
|
+
};
|
|
102
142
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
103
143
|
const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
|
|
104
|
-
const VALID_OPERATION_RISK_CLASSES = [
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"destructive",
|
|
108
|
-
"external-send",
|
|
109
|
-
] as const;
|
|
110
|
-
const VALID_OPERATION_APPROVAL_POLICIES = [
|
|
111
|
-
"never",
|
|
112
|
-
"risk-based",
|
|
113
|
-
"always",
|
|
114
|
-
] as const;
|
|
115
|
-
const VALID_OPERATION_TRANSPORT_KINDS = [
|
|
116
|
-
"json",
|
|
117
|
-
"sse",
|
|
118
|
-
"http-stream",
|
|
119
|
-
"websocket",
|
|
120
|
-
] as const;
|
|
144
|
+
const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
|
|
145
|
+
const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"] as const;
|
|
146
|
+
const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"] as const;
|
|
121
147
|
const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
122
148
|
const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
123
149
|
|
|
@@ -135,18 +161,16 @@ function msDurationMs(value: string): number {
|
|
|
135
161
|
function parsePositiveMsDuration(value: string): number | undefined {
|
|
136
162
|
const trimmed = value.trim();
|
|
137
163
|
if (!MS_DURATION_PATTERN.test(trimmed)) return undefined;
|
|
138
|
-
const parsed = ms(
|
|
139
|
-
(trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue,
|
|
140
|
-
);
|
|
164
|
+
const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue);
|
|
141
165
|
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
142
166
|
return parsed;
|
|
143
167
|
}
|
|
144
168
|
|
|
145
169
|
type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
|
|
146
|
-
type OperationConfig<
|
|
147
|
-
TInput
|
|
148
|
-
|
|
149
|
-
>
|
|
170
|
+
type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
171
|
+
OperationDefinition<TInput, TOutput>,
|
|
172
|
+
"handler"
|
|
173
|
+
> & {
|
|
150
174
|
handler(
|
|
151
175
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
152
176
|
input: InferSchemaOutput<TInput>,
|
|
@@ -154,59 +178,44 @@ type OperationConfig<
|
|
|
154
178
|
| OperationHandlerResult<InferSchemaOutput<TOutput>>
|
|
155
179
|
| Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
|
|
156
180
|
};
|
|
157
|
-
type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
|
|
164
|
-
: never;
|
|
165
|
-
};
|
|
166
|
-
type StreamOperationConfig<
|
|
167
|
-
TInput extends SchemaLike,
|
|
168
|
-
TOutput extends SchemaLike,
|
|
169
|
-
> =
|
|
181
|
+
type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
|
|
182
|
+
[K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
|
|
183
|
+
? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
|
|
184
|
+
: never;
|
|
185
|
+
};
|
|
186
|
+
type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
|
|
170
187
|
| SseOperationConfig<TInput, TOutput>
|
|
171
188
|
| HttpStreamOperationConfig<TInput, TOutput>
|
|
172
189
|
| WebSocketOperationConfig<TInput, TOutput>;
|
|
173
|
-
type SseOperationConfig<
|
|
174
|
-
TInput
|
|
175
|
-
|
|
176
|
-
>
|
|
190
|
+
type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
191
|
+
OperationConfig<TInput, TOutput>,
|
|
192
|
+
"handler" | "transport"
|
|
193
|
+
> & {
|
|
177
194
|
transport: OperationSseTransport;
|
|
178
195
|
handler(
|
|
179
196
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
180
197
|
input: InferSchemaOutput<TInput>,
|
|
181
|
-
):
|
|
182
|
-
| AsyncIterable<ProviderStreamEvent>
|
|
183
|
-
| Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
198
|
+
): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
184
199
|
};
|
|
185
|
-
type HttpStreamOperationConfig<
|
|
186
|
-
TInput
|
|
187
|
-
|
|
188
|
-
>
|
|
200
|
+
type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
201
|
+
OperationConfig<TInput, TOutput>,
|
|
202
|
+
"handler" | "transport"
|
|
203
|
+
> & {
|
|
189
204
|
transport: OperationHttpStreamTransport;
|
|
190
205
|
handler(
|
|
191
206
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
192
207
|
input: InferSchemaOutput<TInput>,
|
|
193
|
-
):
|
|
194
|
-
| Response
|
|
195
|
-
| ReadableStream<Uint8Array>
|
|
196
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
208
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
197
209
|
};
|
|
198
|
-
type WebSocketOperationConfig<
|
|
199
|
-
TInput
|
|
200
|
-
|
|
201
|
-
>
|
|
210
|
+
type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
211
|
+
OperationConfig<TInput, TOutput>,
|
|
212
|
+
"handler" | "transport"
|
|
213
|
+
> & {
|
|
202
214
|
transport: OperationWebSocketTransport;
|
|
203
215
|
handler(
|
|
204
216
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
205
217
|
input: InferSchemaOutput<TInput>,
|
|
206
|
-
):
|
|
207
|
-
| Response
|
|
208
|
-
| ReadableStream<Uint8Array>
|
|
209
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
218
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
210
219
|
};
|
|
211
220
|
|
|
212
221
|
type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
@@ -221,13 +230,19 @@ type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
|
221
230
|
: unknown
|
|
222
231
|
: unknown;
|
|
223
232
|
|
|
224
|
-
export interface ProviderConfig<
|
|
225
|
-
TOperations extends Record<string, ProviderOperation>,
|
|
226
|
-
> {
|
|
233
|
+
export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
|
|
227
234
|
id: string;
|
|
228
235
|
version: string;
|
|
229
236
|
runtime: "standard" | "shared" | "browser";
|
|
237
|
+
/**
|
|
238
|
+
* Optional deployment overrides, passed through verbatim onto the returned
|
|
239
|
+
* provider definition. The SDK types this field but does not deep-validate
|
|
240
|
+
* it — the APIFuse registry builder owns deployment validation and
|
|
241
|
+
* resolves omitted fields against the runtime deployment profiles.
|
|
242
|
+
*/
|
|
243
|
+
deployment?: ProviderDeploymentOverrides;
|
|
230
244
|
allowedHosts?: string[];
|
|
245
|
+
native?: NativeProviderConfig;
|
|
231
246
|
stealth?: {
|
|
232
247
|
profile: string;
|
|
233
248
|
platform: StealthPlatform;
|
|
@@ -270,35 +285,24 @@ export interface ProviderConfig<
|
|
|
270
285
|
}
|
|
271
286
|
|
|
272
287
|
/** Define one provider operation with schema-driven handler inference. */
|
|
273
|
-
export function defineOperation<
|
|
274
|
-
TInput extends SchemaLike,
|
|
275
|
-
TOutput extends SchemaLike,
|
|
276
|
-
>(
|
|
288
|
+
export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
277
289
|
operation: OperationConfig<TInput, TOutput>,
|
|
278
290
|
): OperationDefinition<TInput, TOutput> {
|
|
279
291
|
return operation;
|
|
280
292
|
}
|
|
281
293
|
|
|
282
294
|
/** Define a non-JSON provider operation with explicit transport metadata. */
|
|
283
|
-
export function defineStreamOperation<
|
|
284
|
-
TInput extends SchemaLike,
|
|
285
|
-
TOutput extends SchemaLike,
|
|
286
|
-
>(
|
|
295
|
+
export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
287
296
|
operation: StreamOperationConfig<TInput, TOutput>,
|
|
288
297
|
): OperationDefinition<TInput, TOutput> {
|
|
289
298
|
return operation;
|
|
290
299
|
}
|
|
291
300
|
|
|
292
|
-
function assertObjectConfig(
|
|
293
|
-
value: unknown,
|
|
294
|
-
): asserts value is Record<string, unknown> {
|
|
301
|
+
function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
|
|
295
302
|
if (!value || typeof value !== "object") {
|
|
296
|
-
throw new ProviderError(
|
|
297
|
-
"defineProvider
|
|
298
|
-
|
|
299
|
-
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
300
|
-
},
|
|
301
|
-
);
|
|
303
|
+
throw new ProviderError("defineProvider config must be an object. Offending field: config", {
|
|
304
|
+
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
305
|
+
});
|
|
302
306
|
}
|
|
303
307
|
}
|
|
304
308
|
function assertRequiredField(
|
|
@@ -328,6 +332,111 @@ function assertLiteralField<TValue extends string>(
|
|
|
328
332
|
);
|
|
329
333
|
}
|
|
330
334
|
}
|
|
335
|
+
|
|
336
|
+
function validateProxiedOAuthParams(
|
|
337
|
+
value: unknown,
|
|
338
|
+
field: "authorizeParams" | "tokenParams",
|
|
339
|
+
providerId: string,
|
|
340
|
+
): void {
|
|
341
|
+
if (value === undefined) return;
|
|
342
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
343
|
+
throw new ValidationError(
|
|
344
|
+
`Provider "${providerId}" auth.proxied.${field} must be an object of string values.`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
for (const [key, paramValue] of Object.entries(value)) {
|
|
348
|
+
if (!key.trim() || typeof paramValue !== "string") {
|
|
349
|
+
throw new ValidationError(
|
|
350
|
+
`Provider "${providerId}" auth.proxied.${field} must contain non-empty keys and string values.`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (
|
|
354
|
+
field === "authorizeParams" &&
|
|
355
|
+
PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())
|
|
356
|
+
) {
|
|
357
|
+
throw new ValidationError(
|
|
358
|
+
`Provider "${providerId}" auth.proxied.authorizeParams cannot override reserved parameter "${key}".`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function validateProxiedOAuthAuth(auth: Record<string, unknown>, providerId: string): void {
|
|
365
|
+
const proxied = auth.proxied;
|
|
366
|
+
if (auth.mode !== "oauth2_proxied") {
|
|
367
|
+
if (proxied !== undefined) {
|
|
368
|
+
throw new ValidationError(
|
|
369
|
+
`Provider "${providerId}" auth.proxied is only valid when auth.mode is "oauth2_proxied".`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!proxied || typeof proxied !== "object" || Array.isArray(proxied)) {
|
|
375
|
+
throw new ValidationError(
|
|
376
|
+
`Provider "${providerId}" with auth.mode "oauth2_proxied" must declare auth.proxied.`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const config = Object.fromEntries(Object.entries(proxied));
|
|
380
|
+
for (const key of Object.keys(config)) {
|
|
381
|
+
if (!PROXIED_OAUTH_ALLOWED_FIELDS.has(key)) {
|
|
382
|
+
throw new ValidationError(
|
|
383
|
+
`Provider "${providerId}" has unknown auth.proxied field "${key}".`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const field of PROXIED_OAUTH_REQUIRED_FIELDS) {
|
|
388
|
+
if (typeof config[field] !== "string" || !config[field].trim()) {
|
|
389
|
+
throw new ValidationError(
|
|
390
|
+
`Provider "${providerId}" auth.proxied.${field} must be a non-empty string.`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
for (const field of ["authorizeUrl", "tokenUrl"] as const) {
|
|
395
|
+
try {
|
|
396
|
+
const endpoint = new URL(String(config[field]));
|
|
397
|
+
if (
|
|
398
|
+
endpoint.protocol !== "https:" ||
|
|
399
|
+
endpoint.username ||
|
|
400
|
+
endpoint.password ||
|
|
401
|
+
endpoint.hash
|
|
402
|
+
) {
|
|
403
|
+
throw new Error("invalid endpoint");
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
throw new ValidationError(
|
|
407
|
+
`Provider "${providerId}" auth.proxied.${field} must be an absolute HTTPS URL without credentials or a fragment.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const customScheme = String(config.customScheme);
|
|
412
|
+
if (
|
|
413
|
+
!CUSTOM_SCHEME_REGEX.test(customScheme) ||
|
|
414
|
+
customScheme.toLowerCase().startsWith("http://") ||
|
|
415
|
+
customScheme.toLowerCase().startsWith("https://")
|
|
416
|
+
) {
|
|
417
|
+
throw new ValidationError(
|
|
418
|
+
`Provider "${providerId}" auth.proxied.customScheme must be a non-HTTP custom-scheme URL prefix.`,
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
if (!PROXIED_OAUTH_PROFILE_REGEX.test(String(config.rewriteProfile))) {
|
|
422
|
+
throw new ValidationError(
|
|
423
|
+
`Provider "${providerId}" auth.proxied.rewriteProfile must be a kebab-case profile name.`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
if (!PROXIED_OAUTH_ENV_KEY_REGEX.test(String(config.clientIdEnvKey))) {
|
|
427
|
+
throw new ValidationError(
|
|
428
|
+
`Provider "${providerId}" auth.proxied.clientIdEnvKey must be an APIFuse-style uppercase environment key.`,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
if (config.pkce !== undefined && config.pkce !== "S256" && config.pkce !== "none") {
|
|
432
|
+
throw new ValidationError(
|
|
433
|
+
`Provider "${providerId}" auth.proxied.pkce must be "S256" or "none".`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
validateProxiedOAuthParams(config.authorizeParams, "authorizeParams", providerId);
|
|
437
|
+
validateProxiedOAuthParams(config.tokenParams, "tokenParams", providerId);
|
|
438
|
+
}
|
|
439
|
+
|
|
331
440
|
function validateProviderShape(config: unknown): void {
|
|
332
441
|
assertObjectConfig(config);
|
|
333
442
|
assertRequiredField(config, "id");
|
|
@@ -336,25 +445,13 @@ function validateProviderShape(config: unknown): void {
|
|
|
336
445
|
assertRequiredField(config, "meta", String(config.id));
|
|
337
446
|
assertRequiredField(config, "operations", String(config.id));
|
|
338
447
|
if (typeof config.runtime === "string")
|
|
339
|
-
assertLiteralField(
|
|
340
|
-
config.runtime,
|
|
341
|
-
"runtime",
|
|
342
|
-
VALID_RUNTIMES,
|
|
343
|
-
String(config.id),
|
|
344
|
-
);
|
|
448
|
+
assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
|
|
345
449
|
const auth = config.auth;
|
|
346
|
-
if (
|
|
347
|
-
auth
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
)
|
|
352
|
-
assertLiteralField(
|
|
353
|
-
auth.mode,
|
|
354
|
-
"auth.mode",
|
|
355
|
-
VALID_AUTH_MODES,
|
|
356
|
-
String(config.id),
|
|
357
|
-
);
|
|
450
|
+
if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
|
|
451
|
+
assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
|
|
452
|
+
if (auth && typeof auth === "object" && !Array.isArray(auth)) {
|
|
453
|
+
validateProxiedOAuthAuth(Object.fromEntries(Object.entries(auth)), String(config.id));
|
|
454
|
+
}
|
|
358
455
|
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
359
456
|
throw new ProviderError(
|
|
360
457
|
`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
|
|
@@ -390,9 +487,7 @@ function validateProviderShape(config: unknown): void {
|
|
|
390
487
|
},
|
|
391
488
|
);
|
|
392
489
|
}
|
|
393
|
-
const accessRecord: Record<string, unknown> = Object.fromEntries(
|
|
394
|
-
Object.entries(access),
|
|
395
|
-
);
|
|
490
|
+
const accessRecord: Record<string, unknown> = Object.fromEntries(Object.entries(access));
|
|
396
491
|
for (const key of Object.keys(accessRecord)) {
|
|
397
492
|
if (key !== "visibility") {
|
|
398
493
|
throw new ValidationError(`Unknown field "${key}" on access.`, {
|
|
@@ -437,31 +532,26 @@ function validateProviderProxy(config: {
|
|
|
437
532
|
},
|
|
438
533
|
);
|
|
439
534
|
}
|
|
440
|
-
rejectUnknownFields(
|
|
441
|
-
|
|
442
|
-
new Set(["mode", "provider", "geo", "session"]),
|
|
443
|
-
"proxy",
|
|
444
|
-
);
|
|
445
|
-
assertLiteralField(
|
|
446
|
-
proxy.mode,
|
|
447
|
-
"proxy.mode",
|
|
448
|
-
VALID_PROVIDER_PROXY_MODES,
|
|
449
|
-
config.id,
|
|
450
|
-
);
|
|
535
|
+
rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
|
|
536
|
+
assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
|
|
451
537
|
if (proxy.provider !== undefined) {
|
|
452
|
-
assertLiteralField(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
538
|
+
assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
539
|
+
}
|
|
540
|
+
if (proxy.providers !== undefined) {
|
|
541
|
+
if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
|
|
542
|
+
throw new ValidationError(
|
|
543
|
+
`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`,
|
|
544
|
+
{
|
|
545
|
+
fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
|
|
546
|
+
},
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
for (const vendor of proxy.providers) {
|
|
550
|
+
assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
551
|
+
}
|
|
458
552
|
}
|
|
459
553
|
if (proxy.geo !== undefined) {
|
|
460
|
-
if (
|
|
461
|
-
!proxy.geo ||
|
|
462
|
-
typeof proxy.geo !== "object" ||
|
|
463
|
-
Array.isArray(proxy.geo)
|
|
464
|
-
) {
|
|
554
|
+
if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
|
|
465
555
|
throw new ValidationError(
|
|
466
556
|
`Provider "${config.id}" has invalid proxy.geo: must be an object.`,
|
|
467
557
|
{
|
|
@@ -469,11 +559,7 @@ function validateProviderProxy(config: {
|
|
|
469
559
|
},
|
|
470
560
|
);
|
|
471
561
|
}
|
|
472
|
-
rejectUnknownFields(
|
|
473
|
-
proxy.geo,
|
|
474
|
-
new Set(["country", "subdivision", "city"]),
|
|
475
|
-
"proxy.geo",
|
|
476
|
-
);
|
|
562
|
+
rejectUnknownFields(proxy.geo, new Set(["country", "subdivision", "city"]), "proxy.geo");
|
|
477
563
|
if (proxy.geo.country !== undefined) {
|
|
478
564
|
assertIsoCountry(proxy.geo.country, "proxy.geo.country");
|
|
479
565
|
}
|
|
@@ -487,11 +573,7 @@ function validateProviderProxy(config: {
|
|
|
487
573
|
}
|
|
488
574
|
}
|
|
489
575
|
if (proxy.session !== undefined) {
|
|
490
|
-
if (
|
|
491
|
-
!proxy.session ||
|
|
492
|
-
typeof proxy.session !== "object" ||
|
|
493
|
-
Array.isArray(proxy.session)
|
|
494
|
-
) {
|
|
576
|
+
if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
|
|
495
577
|
throw new ValidationError(
|
|
496
578
|
`Provider "${config.id}" has invalid proxy.session: must be an object.`,
|
|
497
579
|
{
|
|
@@ -501,7 +583,7 @@ function validateProviderProxy(config: {
|
|
|
501
583
|
}
|
|
502
584
|
rejectUnknownFields(
|
|
503
585
|
proxy.session,
|
|
504
|
-
new Set(["affinity", "lifetimeMinutes", "poolSize"]),
|
|
586
|
+
new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]),
|
|
505
587
|
"proxy.session",
|
|
506
588
|
);
|
|
507
589
|
if (proxy.session.affinity !== undefined) {
|
|
@@ -513,51 +595,103 @@ function validateProviderProxy(config: {
|
|
|
513
595
|
);
|
|
514
596
|
}
|
|
515
597
|
const lifetime = proxy.session.lifetimeMinutes;
|
|
516
|
-
if (
|
|
517
|
-
lifetime !== undefined &&
|
|
518
|
-
(!Number.isFinite(lifetime) || lifetime <= 0)
|
|
519
|
-
) {
|
|
598
|
+
if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
520
599
|
throw new ValidationError(
|
|
521
600
|
`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`,
|
|
522
601
|
);
|
|
523
602
|
}
|
|
524
603
|
const poolSize = proxy.session.poolSize;
|
|
604
|
+
if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
605
|
+
throw new ValidationError(
|
|
606
|
+
`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
const drainLeadSeconds = proxy.session.drainLeadSeconds;
|
|
525
610
|
if (
|
|
526
|
-
|
|
527
|
-
(!Number.
|
|
611
|
+
drainLeadSeconds !== undefined &&
|
|
612
|
+
(!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)
|
|
528
613
|
) {
|
|
529
614
|
throw new ValidationError(
|
|
530
|
-
`Provider "${config.id}" has invalid proxy.session.
|
|
615
|
+
`Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`,
|
|
616
|
+
{
|
|
617
|
+
fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
|
|
618
|
+
},
|
|
531
619
|
);
|
|
532
620
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
621
|
+
// A drain lead longer than the sticky lifetime would fire the expiring
|
|
622
|
+
// event before the session is even established, so the provider would
|
|
623
|
+
// never get a usable window. Reject the contradiction at build time.
|
|
624
|
+
if (
|
|
625
|
+
drainLeadSeconds !== undefined &&
|
|
626
|
+
lifetime !== undefined &&
|
|
627
|
+
Number.isFinite(lifetime) &&
|
|
628
|
+
drainLeadSeconds >= lifetime * 60
|
|
629
|
+
) {
|
|
540
630
|
throw new ValidationError(
|
|
541
|
-
`Provider "${config.id}"
|
|
631
|
+
`Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`,
|
|
542
632
|
{
|
|
543
|
-
fix: `
|
|
633
|
+
fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
|
|
544
634
|
},
|
|
545
635
|
);
|
|
546
636
|
}
|
|
547
637
|
}
|
|
638
|
+
// Every credentialed vendor in a required-mode chain must declare its
|
|
639
|
+
// provider secret(s) so a missing credential fails at build/validation time,
|
|
640
|
+
// not during a live outage. This covers the fallback legs too (not just the
|
|
641
|
+
// first vendor): a declared-but-uncredentialed nodemaven fallback would leave
|
|
642
|
+
// the chain silently down to a single vendor, reintroducing the SPOF the chain
|
|
643
|
+
// removes.
|
|
644
|
+
const vendorChain =
|
|
645
|
+
proxy.providers && proxy.providers.length > 0
|
|
646
|
+
? proxy.providers
|
|
647
|
+
: proxy.provider
|
|
648
|
+
? [proxy.provider]
|
|
649
|
+
: [];
|
|
650
|
+
if (proxy.mode === "required") {
|
|
651
|
+
for (const vendor of vendorChain) {
|
|
652
|
+
const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
|
|
653
|
+
if (!requiredSecrets) continue;
|
|
654
|
+
for (const secretName of requiredSecrets) {
|
|
655
|
+
// Match the canonical runtime gate (assertRequiredSecretsPresent /
|
|
656
|
+
// listMissingRequiredSecrets), which enforces only `required === true`
|
|
657
|
+
// declarations. A declaration that omits `required` (defaulting to
|
|
658
|
+
// optional) is skipped at runtime, so accepting it here would pass
|
|
659
|
+
// validation while leaving the credential unenforced until proxy
|
|
660
|
+
// resolution during a live request — the fail-open gap this check exists
|
|
661
|
+
// to close.
|
|
662
|
+
const declared = config.secrets?.some(
|
|
663
|
+
(secret) => secret.name === secretName && secret.required === true,
|
|
664
|
+
);
|
|
665
|
+
if (!declared) {
|
|
666
|
+
throw new ValidationError(
|
|
667
|
+
`Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`,
|
|
668
|
+
{
|
|
669
|
+
fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
|
|
670
|
+
},
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
// `decodo`/`custom` are deprecated vendor values (string-union members, so the
|
|
677
|
+
// @deprecated symbol gate can't catch them — warn at validation time instead).
|
|
678
|
+
const deprecatedVendors = vendorChain.filter(
|
|
679
|
+
(vendor) => vendor === "decodo" || vendor === "custom",
|
|
680
|
+
);
|
|
681
|
+
if (deprecatedVendors.length > 0) {
|
|
682
|
+
console.warn(
|
|
683
|
+
`[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.`,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
548
686
|
}
|
|
549
687
|
|
|
550
|
-
function validateProviderStt(config: {
|
|
551
|
-
id: string;
|
|
552
|
-
stt?: ProviderSttConfig;
|
|
553
|
-
}): void {
|
|
688
|
+
function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): void {
|
|
554
689
|
const stt = config.stt;
|
|
555
690
|
if (stt === undefined) return;
|
|
556
691
|
if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
|
|
557
|
-
throw new ValidationError(
|
|
558
|
-
`
|
|
559
|
-
|
|
560
|
-
);
|
|
692
|
+
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
693
|
+
fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
|
|
694
|
+
});
|
|
561
695
|
}
|
|
562
696
|
rejectUnknownFields(stt, new Set(["mode"]), "stt");
|
|
563
697
|
assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
|
|
@@ -584,16 +718,9 @@ function validateOperationIds(
|
|
|
584
718
|
);
|
|
585
719
|
}
|
|
586
720
|
}
|
|
587
|
-
const OPERATION_CONTRACT_VERSION_REGEX =
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
/^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
591
|
-
const VALID_OPERATION_LIFECYCLES = [
|
|
592
|
-
"stable",
|
|
593
|
-
"beta",
|
|
594
|
-
"deprecated",
|
|
595
|
-
"removed",
|
|
596
|
-
] as const;
|
|
721
|
+
const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
722
|
+
const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
723
|
+
const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"] as const;
|
|
597
724
|
|
|
598
725
|
function assertNonEmptyString(
|
|
599
726
|
value: unknown,
|
|
@@ -624,10 +751,7 @@ function validateToolRouterMetadata(
|
|
|
624
751
|
},
|
|
625
752
|
);
|
|
626
753
|
}
|
|
627
|
-
if (
|
|
628
|
-
toolRouter.name !== undefined &&
|
|
629
|
-
!MCP_TOOL_NAME_REGEX.test(toolRouter.name)
|
|
630
|
-
) {
|
|
754
|
+
if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
631
755
|
throw new ValidationError(
|
|
632
756
|
`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`,
|
|
633
757
|
{
|
|
@@ -699,10 +823,7 @@ function validateOperationContracts(
|
|
|
699
823
|
providerId,
|
|
700
824
|
);
|
|
701
825
|
}
|
|
702
|
-
if (
|
|
703
|
-
contract.lifecycle === "deprecated" ||
|
|
704
|
-
contract.lifecycle === "removed"
|
|
705
|
-
) {
|
|
826
|
+
if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
|
|
706
827
|
if (!contract.deprecation || typeof contract.deprecation !== "object") {
|
|
707
828
|
throw new ValidationError(
|
|
708
829
|
`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`,
|
|
@@ -750,10 +871,7 @@ function validateOperationAnnotations(
|
|
|
750
871
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
|
|
751
872
|
},
|
|
752
873
|
);
|
|
753
|
-
if (
|
|
754
|
-
timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
|
|
755
|
-
timeoutMs > OPERATION_TIMEOUT_MS_MAX
|
|
756
|
-
)
|
|
874
|
+
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
757
875
|
throw new ValidationError(
|
|
758
876
|
`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`,
|
|
759
877
|
{
|
|
@@ -821,6 +939,36 @@ function validateOperationObservability(
|
|
|
821
939
|
}
|
|
822
940
|
}
|
|
823
941
|
|
|
942
|
+
function validateOperationErrorCodes(
|
|
943
|
+
providerId: string,
|
|
944
|
+
operations: Record<string, ProviderOperation>,
|
|
945
|
+
): void {
|
|
946
|
+
for (const [operationName, operation] of Object.entries(operations)) {
|
|
947
|
+
for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
|
|
948
|
+
if (
|
|
949
|
+
errorCode.status !== undefined &&
|
|
950
|
+
!VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)
|
|
951
|
+
) {
|
|
952
|
+
const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
|
|
953
|
+
throw new ValidationError(
|
|
954
|
+
`Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`,
|
|
955
|
+
{
|
|
956
|
+
fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
|
|
957
|
+
},
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
if (
|
|
961
|
+
errorCode.status !== undefined &&
|
|
962
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)
|
|
963
|
+
) {
|
|
964
|
+
console.warn(
|
|
965
|
+
`[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.`,
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
824
972
|
const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
|
|
825
973
|
const SSE_TRANSPORT_FIELDS = new Set([
|
|
826
974
|
"kind",
|
|
@@ -951,12 +1099,7 @@ function validateOperationTransports(
|
|
|
951
1099
|
},
|
|
952
1100
|
);
|
|
953
1101
|
}
|
|
954
|
-
assertLiteralField(
|
|
955
|
-
kind,
|
|
956
|
-
`${fieldPath}.kind`,
|
|
957
|
-
VALID_OPERATION_TRANSPORT_KINDS,
|
|
958
|
-
providerId,
|
|
959
|
-
);
|
|
1102
|
+
assertLiteralField(kind, `${fieldPath}.kind`, VALID_OPERATION_TRANSPORT_KINDS, providerId);
|
|
960
1103
|
|
|
961
1104
|
switch (kind) {
|
|
962
1105
|
case "json":
|
|
@@ -988,16 +1131,9 @@ function validateOperationTransports(
|
|
|
988
1131
|
STREAM_MAX_DURATION_MS_MAX,
|
|
989
1132
|
"max duration",
|
|
990
1133
|
);
|
|
991
|
-
assertPositiveBytes(
|
|
992
|
-
Reflect.get(transport, "maxEventBytes"),
|
|
993
|
-
`${fieldPath}.maxEventBytes`,
|
|
994
|
-
);
|
|
1134
|
+
assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
|
|
995
1135
|
const resumable = Reflect.get(transport, "resumable");
|
|
996
|
-
if (
|
|
997
|
-
resumable !== undefined &&
|
|
998
|
-
resumable !== false &&
|
|
999
|
-
resumable !== "last-event-id"
|
|
1000
|
-
) {
|
|
1136
|
+
if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
|
|
1001
1137
|
throw new ValidationError(
|
|
1002
1138
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`,
|
|
1003
1139
|
{
|
|
@@ -1017,12 +1153,7 @@ function validateOperationTransports(
|
|
|
1017
1153
|
rejectUnknownFields(transport, HTTP_STREAM_TRANSPORT_FIELDS, fieldPath);
|
|
1018
1154
|
const contentType = Reflect.get(transport, "contentType");
|
|
1019
1155
|
if (contentType !== undefined) {
|
|
1020
|
-
assertNonEmptyString(
|
|
1021
|
-
contentType,
|
|
1022
|
-
`${fieldPath}.contentType`,
|
|
1023
|
-
providerId,
|
|
1024
|
-
operationName,
|
|
1025
|
-
);
|
|
1156
|
+
assertNonEmptyString(contentType, `${fieldPath}.contentType`, providerId, operationName);
|
|
1026
1157
|
}
|
|
1027
1158
|
assertStreamMs(
|
|
1028
1159
|
Reflect.get(transport, "idleTimeoutMs"),
|
|
@@ -1038,10 +1169,7 @@ function validateOperationTransports(
|
|
|
1038
1169
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1039
1170
|
"max duration",
|
|
1040
1171
|
);
|
|
1041
|
-
assertPositiveBytes(
|
|
1042
|
-
Reflect.get(transport, "maxChunkBytes"),
|
|
1043
|
-
`${fieldPath}.maxChunkBytes`,
|
|
1044
|
-
);
|
|
1172
|
+
assertPositiveBytes(Reflect.get(transport, "maxChunkBytes"), `${fieldPath}.maxChunkBytes`);
|
|
1045
1173
|
break;
|
|
1046
1174
|
}
|
|
1047
1175
|
case "websocket": {
|
|
@@ -1066,10 +1194,7 @@ function validateOperationTransports(
|
|
|
1066
1194
|
);
|
|
1067
1195
|
}
|
|
1068
1196
|
for (const subprotocol of subprotocols) {
|
|
1069
|
-
if (
|
|
1070
|
-
typeof subprotocol !== "string" ||
|
|
1071
|
-
!WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)
|
|
1072
|
-
) {
|
|
1197
|
+
if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
1073
1198
|
throw new ValidationError(
|
|
1074
1199
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`,
|
|
1075
1200
|
{
|
|
@@ -1093,10 +1218,7 @@ function validateOperationTransports(
|
|
|
1093
1218
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1094
1219
|
"max duration",
|
|
1095
1220
|
);
|
|
1096
|
-
assertPositiveBytes(
|
|
1097
|
-
Reflect.get(transport, "maxFrameBytes"),
|
|
1098
|
-
`${fieldPath}.maxFrameBytes`,
|
|
1099
|
-
);
|
|
1221
|
+
assertPositiveBytes(Reflect.get(transport, "maxFrameBytes"), `${fieldPath}.maxFrameBytes`);
|
|
1100
1222
|
break;
|
|
1101
1223
|
}
|
|
1102
1224
|
}
|
|
@@ -1159,10 +1281,7 @@ function levenshtein(a: string, b: string): number {
|
|
|
1159
1281
|
return prev[n] ?? 0;
|
|
1160
1282
|
}
|
|
1161
1283
|
|
|
1162
|
-
function suggestField(
|
|
1163
|
-
unknown: string,
|
|
1164
|
-
candidates: ReadonlySet<string>,
|
|
1165
|
-
): string | undefined {
|
|
1284
|
+
function suggestField(unknown: string, candidates: ReadonlySet<string>): string | undefined {
|
|
1166
1285
|
let best: string | undefined;
|
|
1167
1286
|
let bestDist = 3;
|
|
1168
1287
|
for (const candidate of candidates) {
|
|
@@ -1175,11 +1294,7 @@ function suggestField(
|
|
|
1175
1294
|
return best;
|
|
1176
1295
|
}
|
|
1177
1296
|
|
|
1178
|
-
function rejectUnknownFields(
|
|
1179
|
-
value: object,
|
|
1180
|
-
allowed: ReadonlySet<string>,
|
|
1181
|
-
fieldPath: string,
|
|
1182
|
-
): void {
|
|
1297
|
+
function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldPath: string): void {
|
|
1183
1298
|
for (const key of Object.keys(value)) {
|
|
1184
1299
|
if (allowed.has(key)) continue;
|
|
1185
1300
|
const hint = suggestField(key, allowed);
|
|
@@ -1218,23 +1333,12 @@ function validateProviderHealthMonitor(
|
|
|
1218
1333
|
field: "healthMonitor" | "healthProbe" = "healthMonitor",
|
|
1219
1334
|
): void {
|
|
1220
1335
|
if (healthMonitor === undefined) return;
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
)
|
|
1226
|
-
throw new ValidationError(
|
|
1227
|
-
`Provider "${providerId}" has invalid ${field}: must be an object.`,
|
|
1228
|
-
{
|
|
1229
|
-
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
1230
|
-
},
|
|
1231
|
-
);
|
|
1336
|
+
if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
|
|
1337
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
|
|
1338
|
+
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
1339
|
+
});
|
|
1232
1340
|
const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
|
|
1233
|
-
rejectUnknownFields(
|
|
1234
|
-
healthMonitorRecord,
|
|
1235
|
-
PROVIDER_HEALTH_MONITOR_FIELDS,
|
|
1236
|
-
field,
|
|
1237
|
-
);
|
|
1341
|
+
rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
|
|
1238
1342
|
if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
|
|
1239
1343
|
assertBoundedIntegerMs(
|
|
1240
1344
|
healthMonitorRecord.defaultProbeTimeoutMs,
|
|
@@ -1302,11 +1406,7 @@ function validateProviderHealthMonitor(
|
|
|
1302
1406
|
|
|
1303
1407
|
const probeOverrides = healthMonitorRecord.probeOverrides;
|
|
1304
1408
|
if (probeOverrides !== undefined) {
|
|
1305
|
-
if (
|
|
1306
|
-
!probeOverrides ||
|
|
1307
|
-
typeof probeOverrides !== "object" ||
|
|
1308
|
-
Array.isArray(probeOverrides)
|
|
1309
|
-
)
|
|
1409
|
+
if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
|
|
1310
1410
|
throw new ValidationError(
|
|
1311
1411
|
`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`,
|
|
1312
1412
|
);
|
|
@@ -1372,14 +1472,8 @@ function validateHealthCheckCase(
|
|
|
1372
1472
|
): void {
|
|
1373
1473
|
const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
|
|
1374
1474
|
if (!caseValue || typeof caseValue !== "object" || Array.isArray(caseValue))
|
|
1375
|
-
throw new ValidationError(
|
|
1376
|
-
|
|
1377
|
-
);
|
|
1378
|
-
rejectUnknownFields(
|
|
1379
|
-
caseValue as Record<string, unknown>,
|
|
1380
|
-
HEALTH_CHECK_CASE_FIELDS,
|
|
1381
|
-
fieldPath,
|
|
1382
|
-
);
|
|
1475
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1476
|
+
rejectUnknownFields(caseValue as Record<string, unknown>, HEALTH_CHECK_CASE_FIELDS, fieldPath);
|
|
1383
1477
|
const c = caseValue as HealthCheckCase;
|
|
1384
1478
|
if (typeof c.name !== "string" || c.name.length === 0)
|
|
1385
1479
|
throw new ValidationError(
|
|
@@ -1407,15 +1501,11 @@ function validateHealthCheckCase(
|
|
|
1407
1501
|
`Provider "${providerId}" ${fieldPath}.degradedThresholdMs must be an integer degraded threshold in [${HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN}, ${HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX}] ms.`,
|
|
1408
1502
|
);
|
|
1409
1503
|
if (c.timeoutMs !== undefined) {
|
|
1410
|
-
assertBoundedIntegerMs(
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1416
|
-
label: "timeout",
|
|
1417
|
-
},
|
|
1418
|
-
);
|
|
1504
|
+
assertBoundedIntegerMs(c.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
|
|
1505
|
+
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1506
|
+
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1507
|
+
label: "timeout",
|
|
1508
|
+
});
|
|
1419
1509
|
}
|
|
1420
1510
|
if (
|
|
1421
1511
|
c.expectedStatus !== undefined &&
|
|
@@ -1431,21 +1521,11 @@ function validateHealthCheckCase(
|
|
|
1431
1521
|
);
|
|
1432
1522
|
}
|
|
1433
1523
|
|
|
1434
|
-
function validateHealthCheckSuite(
|
|
1435
|
-
providerId: string,
|
|
1436
|
-
operationName: string,
|
|
1437
|
-
suite: unknown,
|
|
1438
|
-
): void {
|
|
1524
|
+
function validateHealthCheckSuite(providerId: string, operationName: string, suite: unknown): void {
|
|
1439
1525
|
const fieldPath = `operations.${operationName}.healthCheck`;
|
|
1440
1526
|
if (!suite || typeof suite !== "object" || Array.isArray(suite))
|
|
1441
|
-
throw new ValidationError(
|
|
1442
|
-
|
|
1443
|
-
);
|
|
1444
|
-
rejectUnknownFields(
|
|
1445
|
-
suite as Record<string, unknown>,
|
|
1446
|
-
HEALTH_CHECK_SUITE_FIELDS,
|
|
1447
|
-
fieldPath,
|
|
1448
|
-
);
|
|
1527
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1528
|
+
rejectUnknownFields(suite as Record<string, unknown>, HEALTH_CHECK_SUITE_FIELDS, fieldPath);
|
|
1449
1529
|
const s = suite as HealthCheckSuite;
|
|
1450
1530
|
if (!isPositiveMsDurationString(s.interval))
|
|
1451
1531
|
throw new ValidationError(
|
|
@@ -1455,11 +1535,7 @@ function validateHealthCheckSuite(
|
|
|
1455
1535
|
},
|
|
1456
1536
|
);
|
|
1457
1537
|
if (s.schedule !== undefined) {
|
|
1458
|
-
if (
|
|
1459
|
-
!s.schedule ||
|
|
1460
|
-
typeof s.schedule !== "object" ||
|
|
1461
|
-
Array.isArray(s.schedule)
|
|
1462
|
-
) {
|
|
1538
|
+
if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
|
|
1463
1539
|
throw new ValidationError(
|
|
1464
1540
|
`Provider "${providerId}" ${fieldPath}.schedule must be an object.`,
|
|
1465
1541
|
);
|
|
@@ -1469,11 +1545,7 @@ function validateHealthCheckSuite(
|
|
|
1469
1545
|
`Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`,
|
|
1470
1546
|
);
|
|
1471
1547
|
}
|
|
1472
|
-
rejectUnknownFields(
|
|
1473
|
-
s.schedule,
|
|
1474
|
-
new Set(["randomize"]),
|
|
1475
|
-
`${fieldPath}.schedule`,
|
|
1476
|
-
);
|
|
1548
|
+
rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
|
|
1477
1549
|
const randomize = Reflect.get(s.schedule, "randomize");
|
|
1478
1550
|
if (randomize !== undefined) {
|
|
1479
1551
|
validateScheduleRandomization(
|
|
@@ -1484,15 +1556,11 @@ function validateHealthCheckSuite(
|
|
|
1484
1556
|
}
|
|
1485
1557
|
}
|
|
1486
1558
|
if (s.timeoutMs !== undefined) {
|
|
1487
|
-
assertBoundedIntegerMs(
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1493
|
-
label: "timeout",
|
|
1494
|
-
},
|
|
1495
|
-
);
|
|
1559
|
+
assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
|
|
1560
|
+
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1561
|
+
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1562
|
+
label: "timeout",
|
|
1563
|
+
});
|
|
1496
1564
|
}
|
|
1497
1565
|
if (s.degradedThresholdMs !== undefined) {
|
|
1498
1566
|
assertBoundedIntegerMs(
|
|
@@ -1505,10 +1573,7 @@ function validateHealthCheckSuite(
|
|
|
1505
1573
|
},
|
|
1506
1574
|
);
|
|
1507
1575
|
}
|
|
1508
|
-
if (
|
|
1509
|
-
s.requiresConnection !== undefined &&
|
|
1510
|
-
typeof s.requiresConnection !== "boolean"
|
|
1511
|
-
)
|
|
1576
|
+
if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
|
|
1512
1577
|
throw new ValidationError(
|
|
1513
1578
|
`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`,
|
|
1514
1579
|
);
|
|
@@ -1540,14 +1605,8 @@ function validateHealthCheckUnsupported(
|
|
|
1540
1605
|
unsupported: unknown,
|
|
1541
1606
|
): void {
|
|
1542
1607
|
const fieldPath = `operations.${operationName}.healthCheckUnsupported`;
|
|
1543
|
-
if (
|
|
1544
|
-
|
|
1545
|
-
typeof unsupported !== "object" ||
|
|
1546
|
-
Array.isArray(unsupported)
|
|
1547
|
-
)
|
|
1548
|
-
throw new ValidationError(
|
|
1549
|
-
`Provider "${providerId}" ${fieldPath} must be an object.`,
|
|
1550
|
-
);
|
|
1608
|
+
if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
|
|
1609
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1551
1610
|
rejectUnknownFields(
|
|
1552
1611
|
unsupported as Record<string, unknown>,
|
|
1553
1612
|
HEALTH_CHECK_UNSUPPORTED_FIELDS,
|
|
@@ -1581,12 +1640,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
|
|
|
1581
1640
|
"steps",
|
|
1582
1641
|
"run",
|
|
1583
1642
|
]);
|
|
1584
|
-
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
|
|
1585
|
-
"kind",
|
|
1586
|
-
"interval",
|
|
1587
|
-
"jitter",
|
|
1588
|
-
"randomize",
|
|
1589
|
-
]);
|
|
1643
|
+
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
|
|
1590
1644
|
const HEALTH_JOURNEY_STEP_FIELDS = new Set([
|
|
1591
1645
|
"id",
|
|
1592
1646
|
"description",
|
|
@@ -1605,10 +1659,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
|
|
|
1605
1659
|
"minManualInterval",
|
|
1606
1660
|
"publicRationale",
|
|
1607
1661
|
]);
|
|
1608
|
-
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
|
|
1609
|
-
"enabled",
|
|
1610
|
-
"reason",
|
|
1611
|
-
]);
|
|
1662
|
+
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
|
|
1612
1663
|
const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
|
|
1613
1664
|
"enabled",
|
|
1614
1665
|
"requiresAcknowledgement",
|
|
@@ -1628,32 +1679,18 @@ function validateHealthJourneyManualTrigger(
|
|
|
1628
1679
|
manualTrigger: unknown,
|
|
1629
1680
|
): void {
|
|
1630
1681
|
const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
|
|
1631
|
-
if (
|
|
1632
|
-
!manualTrigger ||
|
|
1633
|
-
typeof manualTrigger !== "object" ||
|
|
1634
|
-
Array.isArray(manualTrigger)
|
|
1635
|
-
) {
|
|
1682
|
+
if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
|
|
1636
1683
|
throw new ValidationError(
|
|
1637
1684
|
`Provider "${providerId}" ${fieldPath} must be an object when present.`,
|
|
1638
1685
|
);
|
|
1639
1686
|
}
|
|
1640
|
-
rejectUnknownFields(
|
|
1641
|
-
manualTrigger,
|
|
1642
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS,
|
|
1643
|
-
fieldPath,
|
|
1644
|
-
);
|
|
1687
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
|
|
1645
1688
|
const enabled = Reflect.get(manualTrigger, "enabled");
|
|
1646
1689
|
if (typeof enabled !== "boolean") {
|
|
1647
|
-
throw new ValidationError(
|
|
1648
|
-
`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`,
|
|
1649
|
-
);
|
|
1690
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`);
|
|
1650
1691
|
}
|
|
1651
1692
|
if (enabled === false) {
|
|
1652
|
-
rejectUnknownFields(
|
|
1653
|
-
manualTrigger,
|
|
1654
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS,
|
|
1655
|
-
fieldPath,
|
|
1656
|
-
);
|
|
1693
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS, fieldPath);
|
|
1657
1694
|
if (
|
|
1658
1695
|
Reflect.get(manualTrigger, "reason") !== undefined &&
|
|
1659
1696
|
(typeof Reflect.get(manualTrigger, "reason") !== "string" ||
|
|
@@ -1665,25 +1702,15 @@ function validateHealthJourneyManualTrigger(
|
|
|
1665
1702
|
}
|
|
1666
1703
|
return;
|
|
1667
1704
|
}
|
|
1668
|
-
rejectUnknownFields(
|
|
1669
|
-
|
|
1670
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS,
|
|
1671
|
-
fieldPath,
|
|
1672
|
-
);
|
|
1673
|
-
const requiresAcknowledgement = Reflect.get(
|
|
1674
|
-
manualTrigger,
|
|
1675
|
-
"requiresAcknowledgement",
|
|
1676
|
-
);
|
|
1705
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS, fieldPath);
|
|
1706
|
+
const requiresAcknowledgement = Reflect.get(manualTrigger, "requiresAcknowledgement");
|
|
1677
1707
|
if (typeof requiresAcknowledgement !== "boolean") {
|
|
1678
1708
|
throw new ValidationError(
|
|
1679
1709
|
`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`,
|
|
1680
1710
|
);
|
|
1681
1711
|
}
|
|
1682
1712
|
const risk = Reflect.get(manualTrigger, "risk");
|
|
1683
|
-
if (
|
|
1684
|
-
typeof risk !== "string" ||
|
|
1685
|
-
!HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)
|
|
1686
|
-
) {
|
|
1713
|
+
if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
1687
1714
|
throw new ValidationError(
|
|
1688
1715
|
`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`,
|
|
1689
1716
|
);
|
|
@@ -1694,10 +1721,7 @@ function validateHealthJourneyManualTrigger(
|
|
|
1694
1721
|
);
|
|
1695
1722
|
}
|
|
1696
1723
|
const minManualInterval = Reflect.get(manualTrigger, "minManualInterval");
|
|
1697
|
-
assertIsoDuration(
|
|
1698
|
-
minManualInterval,
|
|
1699
|
-
`Provider "${providerId}" ${fieldPath}.minManualInterval`,
|
|
1700
|
-
);
|
|
1724
|
+
assertIsoDuration(minManualInterval, `Provider "${providerId}" ${fieldPath}.minManualInterval`);
|
|
1701
1725
|
if (isoDurationMs(minManualInterval) <= 0) {
|
|
1702
1726
|
throw new ValidationError(
|
|
1703
1727
|
`Provider "${providerId}" ${fieldPath}.minManualInterval must be a positive duration.`,
|
|
@@ -1728,18 +1752,14 @@ const SMS_ORIGIN_FIELDS_BY_KIND: Record<string, ReadonlySet<string>> = {
|
|
|
1728
1752
|
e164: new Set(["kind", "value", "display"]),
|
|
1729
1753
|
nationalServiceCode: new Set(["kind", "country", "value", "display"]),
|
|
1730
1754
|
};
|
|
1731
|
-
const DURATION_RE =
|
|
1732
|
-
/^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
|
|
1755
|
+
const DURATION_RE = /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
|
|
1733
1756
|
const E164_RE = /^\+[1-9]\d{1,14}$/;
|
|
1734
1757
|
const ISO_COUNTRY_RE = /^[A-Z]{2}$/;
|
|
1735
1758
|
const NATIONAL_SERVICE_CODE_RE = /^[0-9]{2,15}$/;
|
|
1736
1759
|
const BCP47_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
1737
1760
|
const JOURNEY_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
1738
1761
|
|
|
1739
|
-
function assertIsoDuration(
|
|
1740
|
-
value: unknown,
|
|
1741
|
-
fieldPath: string,
|
|
1742
|
-
): asserts value is string {
|
|
1762
|
+
function assertIsoDuration(value: unknown, fieldPath: string): asserts value is string {
|
|
1743
1763
|
if (typeof value !== "string" || !DURATION_RE.test(value)) {
|
|
1744
1764
|
throw new ValidationError(
|
|
1745
1765
|
`${fieldPath} must be an ISO 8601 duration for example PT8H or PT2M30S.`,
|
|
@@ -1754,15 +1774,10 @@ function isoDurationMs(value: string): number {
|
|
|
1754
1774
|
const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
|
|
1755
1775
|
const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
|
|
1756
1776
|
const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
|
|
1757
|
-
return
|
|
1758
|
-
days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000
|
|
1759
|
-
);
|
|
1777
|
+
return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
|
|
1760
1778
|
}
|
|
1761
1779
|
|
|
1762
|
-
function scheduleRandomizationMs(
|
|
1763
|
-
randomize: unknown,
|
|
1764
|
-
fieldPath: string,
|
|
1765
|
-
): number {
|
|
1780
|
+
function scheduleRandomizationMs(randomize: unknown, fieldPath: string): number {
|
|
1766
1781
|
const mode = Reflect.get(randomize as object, "mode");
|
|
1767
1782
|
switch (mode) {
|
|
1768
1783
|
case "centered": {
|
|
@@ -1776,9 +1791,7 @@ function scheduleRandomizationMs(
|
|
|
1776
1791
|
return isoDurationMs(maxDelay);
|
|
1777
1792
|
}
|
|
1778
1793
|
default:
|
|
1779
|
-
throw new ValidationError(
|
|
1780
|
-
`${fieldPath}.mode must be "centered" or "delayed".`,
|
|
1781
|
-
);
|
|
1794
|
+
throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
|
|
1782
1795
|
}
|
|
1783
1796
|
}
|
|
1784
1797
|
|
|
@@ -1792,25 +1805,18 @@ function validateScheduleRandomization(
|
|
|
1792
1805
|
}
|
|
1793
1806
|
const mode = Reflect.get(randomize, "mode");
|
|
1794
1807
|
const allowedFields =
|
|
1795
|
-
mode === "centered"
|
|
1796
|
-
? new Set(["mode", "maxOffset"])
|
|
1797
|
-
: new Set(["mode", "maxDelay"]);
|
|
1808
|
+
mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
|
|
1798
1809
|
rejectUnknownFields(randomize, allowedFields, fieldPath);
|
|
1799
1810
|
const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
|
|
1800
1811
|
if (offsetMs <= 0) {
|
|
1801
1812
|
throw new ValidationError(`${fieldPath} duration must be positive.`);
|
|
1802
1813
|
}
|
|
1803
1814
|
if (offsetMs >= intervalMs) {
|
|
1804
|
-
throw new ValidationError(
|
|
1805
|
-
`${fieldPath} duration must be shorter than schedule interval.`,
|
|
1806
|
-
);
|
|
1815
|
+
throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
|
|
1807
1816
|
}
|
|
1808
1817
|
}
|
|
1809
1818
|
|
|
1810
|
-
function assertIsoCountry(
|
|
1811
|
-
value: unknown,
|
|
1812
|
-
fieldPath: string,
|
|
1813
|
-
): asserts value is string {
|
|
1819
|
+
function assertIsoCountry(value: unknown, fieldPath: string): asserts value is string {
|
|
1814
1820
|
if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
|
|
1815
1821
|
throw new ValidationError(
|
|
1816
1822
|
`${fieldPath} must be an ISO 3166-1 alpha-2 country code for example KR.`,
|
|
@@ -1833,9 +1839,7 @@ function normalizeIntervalDuration(input: string): string {
|
|
|
1833
1839
|
? durationMs / 3_600_000
|
|
1834
1840
|
: durationMs / 86_400_000;
|
|
1835
1841
|
if (!Number.isInteger(amount) || amount <= 0) {
|
|
1836
|
-
throw new ValidationError(
|
|
1837
|
-
`Journey schedule interval must be a positive duration.`,
|
|
1838
|
-
);
|
|
1842
|
+
throw new ValidationError(`Journey schedule interval must be a positive duration.`);
|
|
1839
1843
|
}
|
|
1840
1844
|
if (unit === "s") return `PT${amount}S`;
|
|
1841
1845
|
if (unit === "m") return `PT${amount}M`;
|
|
@@ -1894,11 +1898,7 @@ function countCapturingGroups(pattern: RegExp): number {
|
|
|
1894
1898
|
if (inCharacterClass || char !== "(") continue;
|
|
1895
1899
|
const next = source[i + 1];
|
|
1896
1900
|
if (next === "?" && source[i + 2] !== "<") continue;
|
|
1897
|
-
if (
|
|
1898
|
-
next === "?" &&
|
|
1899
|
-
source[i + 2] === "<" &&
|
|
1900
|
-
(source[i + 3] === "=" || source[i + 3] === "!")
|
|
1901
|
-
)
|
|
1901
|
+
if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1902
1902
|
continue;
|
|
1903
1903
|
count += 1;
|
|
1904
1904
|
}
|
|
@@ -1917,9 +1917,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1917
1917
|
}
|
|
1918
1918
|
const kind = Reflect.get(origin, "kind");
|
|
1919
1919
|
if (kind !== "e164" && kind !== "nationalServiceCode") {
|
|
1920
|
-
throw new ValidationError(
|
|
1921
|
-
`${fieldPath}.kind must be "e164" or "nationalServiceCode".`,
|
|
1922
|
-
);
|
|
1920
|
+
throw new ValidationError(`${fieldPath}.kind must be "e164" or "nationalServiceCode".`);
|
|
1923
1921
|
}
|
|
1924
1922
|
rejectUnknownFields(origin, SMS_ORIGIN_FIELDS_BY_KIND[kind], fieldPath);
|
|
1925
1923
|
if (kind === "e164") {
|
|
@@ -1946,9 +1944,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1946
1944
|
Reflect.get(origin, "display") !== undefined &&
|
|
1947
1945
|
typeof Reflect.get(origin, "display") !== "string"
|
|
1948
1946
|
) {
|
|
1949
|
-
throw new ValidationError(
|
|
1950
|
-
`${fieldPath}.display must be a string when present.`,
|
|
1951
|
-
);
|
|
1947
|
+
throw new ValidationError(`${fieldPath}.display must be a string when present.`);
|
|
1952
1948
|
}
|
|
1953
1949
|
}
|
|
1954
1950
|
|
|
@@ -1962,9 +1958,7 @@ function validateSmsOtpMatcher(
|
|
|
1962
1958
|
rejectUnknownFields(matcher, SMS_OTP_MATCHER_FIELDS, fieldPath);
|
|
1963
1959
|
const matcherId = Reflect.get(matcher, "id");
|
|
1964
1960
|
if (typeof matcherId !== "string" || !JOURNEY_ID_RE.test(matcherId)) {
|
|
1965
|
-
throw new ValidationError(
|
|
1966
|
-
`${fieldPath}.id must be a kebab-case identifier.`,
|
|
1967
|
-
);
|
|
1961
|
+
throw new ValidationError(`${fieldPath}.id must be a kebab-case identifier.`);
|
|
1968
1962
|
}
|
|
1969
1963
|
assertIsoCountry(Reflect.get(matcher, "country"), `${fieldPath}.country`);
|
|
1970
1964
|
if (
|
|
@@ -1972,24 +1966,18 @@ function validateSmsOtpMatcher(
|
|
|
1972
1966
|
(typeof Reflect.get(matcher, "locale") !== "string" ||
|
|
1973
1967
|
!BCP47_RE.test(Reflect.get(matcher, "locale")))
|
|
1974
1968
|
) {
|
|
1975
|
-
throw new ValidationError(
|
|
1976
|
-
`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`,
|
|
1977
|
-
);
|
|
1969
|
+
throw new ValidationError(`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`);
|
|
1978
1970
|
}
|
|
1979
1971
|
if (
|
|
1980
1972
|
Reflect.get(matcher, "phoneNumber") !== undefined &&
|
|
1981
1973
|
(typeof Reflect.get(matcher, "phoneNumber") !== "string" ||
|
|
1982
1974
|
!E164_RE.test(Reflect.get(matcher, "phoneNumber")))
|
|
1983
1975
|
) {
|
|
1984
|
-
throw new ValidationError(
|
|
1985
|
-
`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`,
|
|
1986
|
-
);
|
|
1976
|
+
throw new ValidationError(`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`);
|
|
1987
1977
|
}
|
|
1988
1978
|
const origins = Reflect.get(matcher, "origins");
|
|
1989
1979
|
if (!Array.isArray(origins) || origins.length === 0) {
|
|
1990
|
-
throw new ValidationError(
|
|
1991
|
-
`${fieldPath}.origins must be a non-empty array.`,
|
|
1992
|
-
);
|
|
1980
|
+
throw new ValidationError(`${fieldPath}.origins must be a non-empty array.`);
|
|
1993
1981
|
}
|
|
1994
1982
|
for (const [index, origin] of origins.entries()) {
|
|
1995
1983
|
validateSmsOrigin(origin, `${fieldPath}.origins[${index}]`);
|
|
@@ -2010,10 +1998,7 @@ function validateSmsOtpMatcher(
|
|
|
2010
1998
|
);
|
|
2011
1999
|
}
|
|
2012
2000
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
2013
|
-
if (
|
|
2014
|
-
countCapturingGroups(regex) !== 1 &&
|
|
2015
|
-
Reflect.get(code, "capture") === undefined
|
|
2016
|
-
) {
|
|
2001
|
+
if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
|
|
2017
2002
|
throw new ValidationError(
|
|
2018
2003
|
`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`,
|
|
2019
2004
|
);
|
|
@@ -2023,20 +2008,12 @@ function validateSmsOtpMatcher(
|
|
|
2023
2008
|
typeof Reflect.get(code, "capture") !== "string" &&
|
|
2024
2009
|
typeof Reflect.get(code, "capture") !== "number"
|
|
2025
2010
|
) {
|
|
2026
|
-
throw new ValidationError(
|
|
2027
|
-
`${fieldPath}.code.capture must be a string or number when present.`,
|
|
2028
|
-
);
|
|
2011
|
+
throw new ValidationError(`${fieldPath}.code.capture must be a string or number when present.`);
|
|
2029
2012
|
}
|
|
2030
2013
|
assertIsoDuration(Reflect.get(matcher, "maxAge"), `${fieldPath}.maxAge`);
|
|
2031
|
-
assertIsoDuration(
|
|
2032
|
-
Reflect.get(matcher, "waitTimeout"),
|
|
2033
|
-
`${fieldPath}.waitTimeout`,
|
|
2034
|
-
);
|
|
2014
|
+
assertIsoDuration(Reflect.get(matcher, "waitTimeout"), `${fieldPath}.waitTimeout`);
|
|
2035
2015
|
if (Reflect.get(matcher, "clockSkew") !== undefined)
|
|
2036
|
-
assertIsoDuration(
|
|
2037
|
-
Reflect.get(matcher, "clockSkew"),
|
|
2038
|
-
`${fieldPath}.clockSkew`,
|
|
2039
|
-
);
|
|
2016
|
+
assertIsoDuration(Reflect.get(matcher, "clockSkew"), `${fieldPath}.clockSkew`);
|
|
2040
2017
|
}
|
|
2041
2018
|
|
|
2042
2019
|
export function defineSmsOtpMatcher(
|
|
@@ -2068,9 +2045,7 @@ export function defineSmsOtpMatcher(
|
|
|
2068
2045
|
return matcher;
|
|
2069
2046
|
}
|
|
2070
2047
|
|
|
2071
|
-
export function defineHealthJourney(
|
|
2072
|
-
config: HealthJourneyDefinition,
|
|
2073
|
-
): HealthJourneyDefinition {
|
|
2048
|
+
export function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition {
|
|
2074
2049
|
return config;
|
|
2075
2050
|
}
|
|
2076
2051
|
|
|
@@ -2081,22 +2056,15 @@ function validateHealthJourneySchedule(
|
|
|
2081
2056
|
): void {
|
|
2082
2057
|
const fieldPath = `healthJourneys.${journeyId}.schedule`;
|
|
2083
2058
|
if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) {
|
|
2084
|
-
throw new ValidationError(
|
|
2085
|
-
`Provider "${providerId}" ${fieldPath} must be an object.`,
|
|
2086
|
-
);
|
|
2059
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
2087
2060
|
}
|
|
2088
2061
|
rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
|
|
2089
2062
|
if (Reflect.get(schedule, "kind") !== "interval")
|
|
2090
|
-
throw new ValidationError(
|
|
2091
|
-
`Provider "${providerId}" ${fieldPath}.kind must be "interval".`,
|
|
2092
|
-
);
|
|
2063
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
|
|
2093
2064
|
const interval = Reflect.get(schedule, "interval");
|
|
2094
2065
|
assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
|
|
2095
2066
|
const randomize = Reflect.get(schedule, "randomize");
|
|
2096
|
-
if (
|
|
2097
|
-
Reflect.get(schedule, "jitter") !== undefined &&
|
|
2098
|
-
randomize !== undefined
|
|
2099
|
-
) {
|
|
2067
|
+
if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
|
|
2100
2068
|
throw new ValidationError(
|
|
2101
2069
|
`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`,
|
|
2102
2070
|
);
|
|
@@ -2123,17 +2091,13 @@ function validateHealthJourneys(
|
|
|
2123
2091
|
const covered = new Set<string>();
|
|
2124
2092
|
if (healthJourneys === undefined) return covered;
|
|
2125
2093
|
if (!Array.isArray(healthJourneys)) {
|
|
2126
|
-
throw new ValidationError(
|
|
2127
|
-
`Provider "${providerId}" healthJourneys must be an array.`,
|
|
2128
|
-
);
|
|
2094
|
+
throw new ValidationError(`Provider "${providerId}" healthJourneys must be an array.`);
|
|
2129
2095
|
}
|
|
2130
2096
|
const journeyIds = new Set<string>();
|
|
2131
2097
|
for (const [index, journey] of healthJourneys.entries()) {
|
|
2132
2098
|
const prefix = `healthJourneys[${index}]`;
|
|
2133
2099
|
if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
|
|
2134
|
-
throw new ValidationError(
|
|
2135
|
-
`Provider "${providerId}" ${prefix} must be an object.`,
|
|
2136
|
-
);
|
|
2100
|
+
throw new ValidationError(`Provider "${providerId}" ${prefix} must be an object.`);
|
|
2137
2101
|
}
|
|
2138
2102
|
rejectUnknownFields(journey, HEALTH_JOURNEY_FIELDS, prefix);
|
|
2139
2103
|
if (typeof journey.id !== "string" || !JOURNEY_ID_RE.test(journey.id)) {
|
|
@@ -2147,10 +2111,7 @@ function validateHealthJourneys(
|
|
|
2147
2111
|
);
|
|
2148
2112
|
journeyIds.add(journey.id);
|
|
2149
2113
|
validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
|
|
2150
|
-
if (
|
|
2151
|
-
!Array.isArray(journey.coversOperations) ||
|
|
2152
|
-
journey.coversOperations.length === 0
|
|
2153
|
-
) {
|
|
2114
|
+
if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
|
|
2154
2115
|
throw new ValidationError(
|
|
2155
2116
|
`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`,
|
|
2156
2117
|
);
|
|
@@ -2185,10 +2146,7 @@ function validateHealthJourneys(
|
|
|
2185
2146
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers must be an array.`,
|
|
2186
2147
|
);
|
|
2187
2148
|
for (const [matcherIndex, matcher] of journey.smsMatchers.entries()) {
|
|
2188
|
-
validateSmsOtpMatcher(
|
|
2189
|
-
matcher,
|
|
2190
|
-
`healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`,
|
|
2191
|
-
);
|
|
2149
|
+
validateSmsOtpMatcher(matcher, `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`);
|
|
2192
2150
|
if (matcherIds.has(matcher.id))
|
|
2193
2151
|
throw new ValidationError(
|
|
2194
2152
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers has duplicate matcher id "${matcher.id}".`,
|
|
@@ -2199,9 +2157,7 @@ function validateHealthJourneys(
|
|
|
2199
2157
|
for (const [stepIndex, step] of journey.steps.entries()) {
|
|
2200
2158
|
const stepPath = `healthJourneys.${journey.id}.steps[${stepIndex}]`;
|
|
2201
2159
|
if (!step || typeof step !== "object" || Array.isArray(step))
|
|
2202
|
-
throw new ValidationError(
|
|
2203
|
-
`Provider "${providerId}" ${stepPath} must be an object.`,
|
|
2204
|
-
);
|
|
2160
|
+
throw new ValidationError(`Provider "${providerId}" ${stepPath} must be an object.`);
|
|
2205
2161
|
rejectUnknownFields(step, HEALTH_JOURNEY_STEP_FIELDS, stepPath);
|
|
2206
2162
|
if (typeof step.id !== "string" || !JOURNEY_ID_RE.test(step.id))
|
|
2207
2163
|
throw new ValidationError(
|
|
@@ -2211,20 +2167,13 @@ function validateHealthJourneys(
|
|
|
2211
2167
|
throw new ValidationError(
|
|
2212
2168
|
`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`,
|
|
2213
2169
|
);
|
|
2214
|
-
if (
|
|
2215
|
-
step.usesSmsMatcher !== undefined &&
|
|
2216
|
-
!matcherIds.has(step.usesSmsMatcher)
|
|
2217
|
-
)
|
|
2170
|
+
if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
|
|
2218
2171
|
throw new ValidationError(
|
|
2219
2172
|
`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`,
|
|
2220
2173
|
);
|
|
2221
2174
|
}
|
|
2222
2175
|
if (journey.manualTrigger !== undefined)
|
|
2223
|
-
validateHealthJourneyManualTrigger(
|
|
2224
|
-
providerId,
|
|
2225
|
-
journey.id,
|
|
2226
|
-
journey.manualTrigger,
|
|
2227
|
-
);
|
|
2176
|
+
validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
|
|
2228
2177
|
if (journey.timeout !== undefined)
|
|
2229
2178
|
assertIsoDuration(
|
|
2230
2179
|
journey.timeout,
|
|
@@ -2270,23 +2219,10 @@ function validateOperationHealthChecks(
|
|
|
2270
2219
|
fix: `Remove either operations.${operationName}.healthCheck or operations.${operationName}.healthCheckUnsupported.`,
|
|
2271
2220
|
},
|
|
2272
2221
|
);
|
|
2273
|
-
if (hasCheck)
|
|
2274
|
-
validateHealthCheckSuite(
|
|
2275
|
-
providerId,
|
|
2276
|
-
operationName,
|
|
2277
|
-
operation.healthCheck,
|
|
2278
|
-
);
|
|
2222
|
+
if (hasCheck) validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
|
|
2279
2223
|
if (hasUnsupported)
|
|
2280
|
-
validateHealthCheckUnsupported(
|
|
2281
|
-
|
|
2282
|
-
operationName,
|
|
2283
|
-
operation.healthCheckUnsupported,
|
|
2284
|
-
);
|
|
2285
|
-
if (
|
|
2286
|
-
!hasCheck &&
|
|
2287
|
-
!hasUnsupported &&
|
|
2288
|
-
!journeyCoveredOperations.has(operationName)
|
|
2289
|
-
)
|
|
2224
|
+
validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
|
|
2225
|
+
if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
|
|
2290
2226
|
throw new ValidationError(
|
|
2291
2227
|
`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`,
|
|
2292
2228
|
{
|
|
@@ -2308,6 +2244,26 @@ function validateOperationFixtures(
|
|
|
2308
2244
|
fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
|
|
2309
2245
|
},
|
|
2310
2246
|
);
|
|
2247
|
+
if (operation.fixtures?.recordedAt !== undefined) {
|
|
2248
|
+
const recordedAt = operation.fixtures.recordedAt;
|
|
2249
|
+
const parsed =
|
|
2250
|
+
typeof recordedAt === "string"
|
|
2251
|
+
? new Date(`${recordedAt}T00:00:00.000Z`)
|
|
2252
|
+
: new Date(Number.NaN);
|
|
2253
|
+
const isCalendarDate =
|
|
2254
|
+
typeof recordedAt === "string" &&
|
|
2255
|
+
/^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
|
|
2256
|
+
!Number.isNaN(parsed.getTime()) &&
|
|
2257
|
+
parsed.toISOString().slice(0, 10) === recordedAt;
|
|
2258
|
+
const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
2259
|
+
if (!isCalendarDate || recordedAt > kstToday)
|
|
2260
|
+
throw new ValidationError(
|
|
2261
|
+
`Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`,
|
|
2262
|
+
{
|
|
2263
|
+
fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
|
|
2264
|
+
},
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2311
2267
|
if (operation.fixtures?.request !== undefined) {
|
|
2312
2268
|
const result = safeParseSchemaSync(
|
|
2313
2269
|
operation.input,
|
|
@@ -2341,6 +2297,44 @@ function validateOperationFixtures(
|
|
|
2341
2297
|
}
|
|
2342
2298
|
}
|
|
2343
2299
|
|
|
2300
|
+
function resolveOperationFixtureRequests<TOperations extends Record<string, ProviderOperation>>(
|
|
2301
|
+
operations: TOperations,
|
|
2302
|
+
): TOperations {
|
|
2303
|
+
let changed = false;
|
|
2304
|
+
const resolvedOperations = Object.fromEntries(
|
|
2305
|
+
Object.entries(operations).map(([operationName, operation]) => {
|
|
2306
|
+
if (operation.fixtures?.request === undefined) return [operationName, operation];
|
|
2307
|
+
const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
|
|
2308
|
+
if (request === operation.fixtures.request) return [operationName, operation];
|
|
2309
|
+
changed = true;
|
|
2310
|
+
return [
|
|
2311
|
+
operationName,
|
|
2312
|
+
{
|
|
2313
|
+
...operation,
|
|
2314
|
+
fixtures: {
|
|
2315
|
+
...operation.fixtures,
|
|
2316
|
+
request,
|
|
2317
|
+
},
|
|
2318
|
+
},
|
|
2319
|
+
];
|
|
2320
|
+
}),
|
|
2321
|
+
) as TOperations;
|
|
2322
|
+
return changed ? resolvedOperations : operations;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
/**
|
|
2326
|
+
* Shallow shape guard only: the `deployment` object is passed through
|
|
2327
|
+
* verbatim and deliberately not deep-validated by the SDK — the APIFuse
|
|
2328
|
+
* registry builder owns deployment validation and profile resolution.
|
|
2329
|
+
*/
|
|
2330
|
+
function validateProviderDeployment(providerId: string, deployment: unknown): void {
|
|
2331
|
+
if (deployment === undefined) return;
|
|
2332
|
+
if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
|
|
2333
|
+
throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
|
|
2334
|
+
fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2344
2338
|
export function defineProvider<
|
|
2345
2339
|
TOperations extends Record<string, ProviderOperation>,
|
|
2346
2340
|
TConfig extends ProviderConfig<TOperations>,
|
|
@@ -2348,20 +2342,19 @@ export function defineProvider<
|
|
|
2348
2342
|
config: TConfig & AuthStartNoInputGuard<TConfig>,
|
|
2349
2343
|
): ProviderDefinition & { operations: OperationMapConfig<TOperations> } {
|
|
2350
2344
|
validateProviderShape(config);
|
|
2345
|
+
const operations = resolveOperationFixtureRequests(config.operations);
|
|
2351
2346
|
if (!CONNECTOR_ID_REGEX.test(config.id))
|
|
2352
2347
|
throw new ProviderError(`Invalid provider id: "${config.id}"`, {
|
|
2353
2348
|
fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
|
|
2354
2349
|
});
|
|
2355
2350
|
if (Object.keys(config.operations).length === 0)
|
|
2356
|
-
throw new ProviderError(
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
fix: "Add at least one operation to the operations object",
|
|
2360
|
-
},
|
|
2361
|
-
);
|
|
2351
|
+
throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
|
|
2352
|
+
fix: "Add at least one operation to the operations object",
|
|
2353
|
+
});
|
|
2362
2354
|
validateOperationIds(config.id, config.operations);
|
|
2363
2355
|
validateOperationAnnotations(config.id, config.operations);
|
|
2364
2356
|
validateOperationObservability(config.id, config.operations);
|
|
2357
|
+
validateOperationErrorCodes(config.id, config.operations);
|
|
2365
2358
|
validateOperationTransports(config.id, config.operations);
|
|
2366
2359
|
validateOperationContracts(config.id, config.operations);
|
|
2367
2360
|
validateToolRouterMetadata(config.id, config.operations);
|
|
@@ -2370,11 +2363,7 @@ export function defineProvider<
|
|
|
2370
2363
|
config.operations,
|
|
2371
2364
|
config.healthJourneys,
|
|
2372
2365
|
);
|
|
2373
|
-
validateOperationHealthChecks(
|
|
2374
|
-
config.id,
|
|
2375
|
-
config.operations,
|
|
2376
|
-
journeyCoveredOperations,
|
|
2377
|
-
);
|
|
2366
|
+
validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
|
|
2378
2367
|
if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
|
|
2379
2368
|
throw new ValidationError(
|
|
2380
2369
|
`Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
|
|
@@ -2387,7 +2376,15 @@ export function defineProvider<
|
|
|
2387
2376
|
config.healthProbe ?? config.healthMonitor,
|
|
2388
2377
|
config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
|
|
2389
2378
|
);
|
|
2390
|
-
validateOperationFixtures(config.id,
|
|
2379
|
+
validateOperationFixtures(config.id, operations);
|
|
2380
|
+
validateProviderDeployment(config.id, config.deployment);
|
|
2381
|
+
try {
|
|
2382
|
+
validateNativeProviderConfig(config.native);
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
if (error instanceof NativeEgressPolicyValidationError)
|
|
2385
|
+
throw new ValidationError(error.message);
|
|
2386
|
+
throw error;
|
|
2387
|
+
}
|
|
2391
2388
|
validateProviderProxy(config);
|
|
2392
2389
|
validateProviderStt(config);
|
|
2393
2390
|
if (config.runtime === "browser" && !config.browser)
|
|
@@ -2406,7 +2403,11 @@ export function defineProvider<
|
|
|
2406
2403
|
id: config.id,
|
|
2407
2404
|
version: config.version,
|
|
2408
2405
|
runtime: config.runtime,
|
|
2406
|
+
// Verbatim passthrough: deployment validation and profile resolution
|
|
2407
|
+
// are owned by the APIFuse registry builder, not the SDK.
|
|
2408
|
+
deployment: config.deployment,
|
|
2409
2409
|
allowedHosts: config.allowedHosts,
|
|
2410
|
+
native: config.native,
|
|
2410
2411
|
stealth: config.stealth,
|
|
2411
2412
|
proxy: config.proxy,
|
|
2412
2413
|
stt: config.stt,
|
|
@@ -2418,7 +2419,7 @@ export function defineProvider<
|
|
|
2418
2419
|
credential: config.credential,
|
|
2419
2420
|
context: config.context,
|
|
2420
2421
|
meta: config.meta,
|
|
2421
|
-
operations
|
|
2422
|
+
operations,
|
|
2422
2423
|
// Transitional healthMonitor → healthProbe alias: mirror whichever field
|
|
2423
2424
|
// was declared onto both so old and new consumers keep working.
|
|
2424
2425
|
healthMonitor: config.healthMonitor ?? config.healthProbe,
|