@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/dist/define.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
3
|
+
import { ProviderError, ValidationError } from "./errors.js";
|
|
4
|
+
import { NativeEgressPolicyValidationError, validateNativeProviderConfig, } from "./native-egress-policy.js";
|
|
5
|
+
import { safeParseSchemaSync } from "./schema.js";
|
|
6
|
+
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
7
|
+
import { HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX, HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN, HEALTH_CHECK_TIMEOUT_MS_MAX, HEALTH_CHECK_TIMEOUT_MS_MIN, OPERATION_TIMEOUT_MS_MAX, OPERATION_TIMEOUT_MS_MIN, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, VALID_OPERATION_ERROR_STATUSES, } from "./types.js";
|
|
5
8
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
6
9
|
const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
7
10
|
const VALID_RUNTIMES = ["standard", "shared", "browser"];
|
|
@@ -10,18 +13,34 @@ const VALID_AUTH_MODES = [
|
|
|
10
13
|
"platform-managed",
|
|
11
14
|
"credentials",
|
|
12
15
|
"oauth2",
|
|
16
|
+
"oauth2_proxied",
|
|
13
17
|
];
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
const VALID_PROVIDER_PROXY_PROVIDERS = [
|
|
21
|
-
"smartproxy",
|
|
22
|
-
"decodo",
|
|
23
|
-
"custom",
|
|
18
|
+
const PROXIED_OAUTH_REQUIRED_FIELDS = [
|
|
19
|
+
"authorizeUrl",
|
|
20
|
+
"tokenUrl",
|
|
21
|
+
"customScheme",
|
|
22
|
+
"rewriteProfile",
|
|
23
|
+
"clientIdEnvKey",
|
|
24
24
|
];
|
|
25
|
+
const PROXIED_OAUTH_ALLOWED_FIELDS = new Set([
|
|
26
|
+
...PROXIED_OAUTH_REQUIRED_FIELDS,
|
|
27
|
+
"pkce",
|
|
28
|
+
"authorizeParams",
|
|
29
|
+
"tokenParams",
|
|
30
|
+
]);
|
|
31
|
+
const PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS = new Set([
|
|
32
|
+
"client_id",
|
|
33
|
+
"response_type",
|
|
34
|
+
"state",
|
|
35
|
+
"code_challenge",
|
|
36
|
+
"code_challenge_method",
|
|
37
|
+
]);
|
|
38
|
+
const PROXIED_OAUTH_PROFILE_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
39
|
+
const PROXIED_OAUTH_ENV_KEY_REGEX = /^[A-Z][A-Z0-9_]*__[A-Z0-9_]+$/;
|
|
40
|
+
const CUSTOM_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+$/;
|
|
41
|
+
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"];
|
|
42
|
+
const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"];
|
|
43
|
+
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"];
|
|
25
44
|
const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
26
45
|
"request",
|
|
27
46
|
"operation",
|
|
@@ -30,25 +49,25 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
|
30
49
|
];
|
|
31
50
|
const VALID_PROVIDER_STT_MODES = ["optional", "required"];
|
|
32
51
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
52
|
+
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
53
|
+
const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
54
|
+
// Per-vendor provider-declared credential secrets. A required-mode chain must
|
|
55
|
+
// declare every secret of every credentialed vendor it names, so a missing
|
|
56
|
+
// credential fails at build/validation time rather than during a live outage: a
|
|
57
|
+
// declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
|
|
58
|
+
// exactly the failure class the multi-vendor chain exists to remove. Vendors
|
|
59
|
+
// absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
|
|
60
|
+
// `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
|
|
61
|
+
// no declaration requirement.
|
|
62
|
+
const VENDOR_REQUIRED_SECRETS = {
|
|
63
|
+
smartproxy: [SMARTPROXY_APP_KEY_SECRET],
|
|
64
|
+
nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
|
|
65
|
+
};
|
|
33
66
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
34
67
|
const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
|
|
35
|
-
const VALID_OPERATION_RISK_CLASSES = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"destructive",
|
|
39
|
-
"external-send",
|
|
40
|
-
];
|
|
41
|
-
const VALID_OPERATION_APPROVAL_POLICIES = [
|
|
42
|
-
"never",
|
|
43
|
-
"risk-based",
|
|
44
|
-
"always",
|
|
45
|
-
];
|
|
46
|
-
const VALID_OPERATION_TRANSPORT_KINDS = [
|
|
47
|
-
"json",
|
|
48
|
-
"sse",
|
|
49
|
-
"http-stream",
|
|
50
|
-
"websocket",
|
|
51
|
-
];
|
|
68
|
+
const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"];
|
|
69
|
+
const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"];
|
|
70
|
+
const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"];
|
|
52
71
|
const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
53
72
|
const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
54
73
|
const MS_DURATION_PATTERN = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*([a-zA-Z]+)?$/;
|
|
@@ -99,6 +118,76 @@ function assertLiteralField(value, field, validValues, providerId) {
|
|
|
99
118
|
});
|
|
100
119
|
}
|
|
101
120
|
}
|
|
121
|
+
function validateProxiedOAuthParams(value, field, providerId) {
|
|
122
|
+
if (value === undefined)
|
|
123
|
+
return;
|
|
124
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
125
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be an object of string values.`);
|
|
126
|
+
}
|
|
127
|
+
for (const [key, paramValue] of Object.entries(value)) {
|
|
128
|
+
if (!key.trim() || typeof paramValue !== "string") {
|
|
129
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must contain non-empty keys and string values.`);
|
|
130
|
+
}
|
|
131
|
+
if (field === "authorizeParams" &&
|
|
132
|
+
PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())) {
|
|
133
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.authorizeParams cannot override reserved parameter "${key}".`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function validateProxiedOAuthAuth(auth, providerId) {
|
|
138
|
+
const proxied = auth.proxied;
|
|
139
|
+
if (auth.mode !== "oauth2_proxied") {
|
|
140
|
+
if (proxied !== undefined) {
|
|
141
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied is only valid when auth.mode is "oauth2_proxied".`);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!proxied || typeof proxied !== "object" || Array.isArray(proxied)) {
|
|
146
|
+
throw new ValidationError(`Provider "${providerId}" with auth.mode "oauth2_proxied" must declare auth.proxied.`);
|
|
147
|
+
}
|
|
148
|
+
const config = Object.fromEntries(Object.entries(proxied));
|
|
149
|
+
for (const key of Object.keys(config)) {
|
|
150
|
+
if (!PROXIED_OAUTH_ALLOWED_FIELDS.has(key)) {
|
|
151
|
+
throw new ValidationError(`Provider "${providerId}" has unknown auth.proxied field "${key}".`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const field of PROXIED_OAUTH_REQUIRED_FIELDS) {
|
|
155
|
+
if (typeof config[field] !== "string" || !config[field].trim()) {
|
|
156
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be a non-empty string.`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const field of ["authorizeUrl", "tokenUrl"]) {
|
|
160
|
+
try {
|
|
161
|
+
const endpoint = new URL(String(config[field]));
|
|
162
|
+
if (endpoint.protocol !== "https:" ||
|
|
163
|
+
endpoint.username ||
|
|
164
|
+
endpoint.password ||
|
|
165
|
+
endpoint.hash) {
|
|
166
|
+
throw new Error("invalid endpoint");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be an absolute HTTPS URL without credentials or a fragment.`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const customScheme = String(config.customScheme);
|
|
174
|
+
if (!CUSTOM_SCHEME_REGEX.test(customScheme) ||
|
|
175
|
+
customScheme.toLowerCase().startsWith("http://") ||
|
|
176
|
+
customScheme.toLowerCase().startsWith("https://")) {
|
|
177
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.customScheme must be a non-HTTP custom-scheme URL prefix.`);
|
|
178
|
+
}
|
|
179
|
+
if (!PROXIED_OAUTH_PROFILE_REGEX.test(String(config.rewriteProfile))) {
|
|
180
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.rewriteProfile must be a kebab-case profile name.`);
|
|
181
|
+
}
|
|
182
|
+
if (!PROXIED_OAUTH_ENV_KEY_REGEX.test(String(config.clientIdEnvKey))) {
|
|
183
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.clientIdEnvKey must be an APIFuse-style uppercase environment key.`);
|
|
184
|
+
}
|
|
185
|
+
if (config.pkce !== undefined && config.pkce !== "S256" && config.pkce !== "none") {
|
|
186
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.pkce must be "S256" or "none".`);
|
|
187
|
+
}
|
|
188
|
+
validateProxiedOAuthParams(config.authorizeParams, "authorizeParams", providerId);
|
|
189
|
+
validateProxiedOAuthParams(config.tokenParams, "tokenParams", providerId);
|
|
190
|
+
}
|
|
102
191
|
function validateProviderShape(config) {
|
|
103
192
|
assertObjectConfig(config);
|
|
104
193
|
assertRequiredField(config, "id");
|
|
@@ -109,11 +198,11 @@ function validateProviderShape(config) {
|
|
|
109
198
|
if (typeof config.runtime === "string")
|
|
110
199
|
assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
|
|
111
200
|
const auth = config.auth;
|
|
112
|
-
if (auth &&
|
|
113
|
-
typeof auth === "object" &&
|
|
114
|
-
"mode" in auth &&
|
|
115
|
-
typeof auth.mode === "string")
|
|
201
|
+
if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
|
|
116
202
|
assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
|
|
203
|
+
if (auth && typeof auth === "object" && !Array.isArray(auth)) {
|
|
204
|
+
validateProxiedOAuthAuth(Object.fromEntries(Object.entries(auth)), String(config.id));
|
|
205
|
+
}
|
|
117
206
|
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
118
207
|
throw new ProviderError(`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`, {
|
|
119
208
|
fix: "Use the single canonical auth interface: auth.flow. Gateway calls auth.flow.start/continue/poll/abort/refresh only and persists complete turn data.credential as-is, so put login/token/session exchange inside auth.flow.continue.",
|
|
@@ -167,15 +256,23 @@ function validateProviderProxy(config) {
|
|
|
167
256
|
fix: `Use proxy: { mode: "required", provider: "smartproxy", geo: { country: "KR" }, session: { affinity: "connection", lifetimeMinutes: 30 } }`,
|
|
168
257
|
});
|
|
169
258
|
}
|
|
170
|
-
rejectUnknownFields(proxy, new Set(["mode", "provider", "geo", "session"]), "proxy");
|
|
259
|
+
rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
|
|
171
260
|
assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
|
|
172
261
|
if (proxy.provider !== undefined) {
|
|
173
262
|
assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
174
263
|
}
|
|
264
|
+
if (proxy.providers !== undefined) {
|
|
265
|
+
if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
|
|
266
|
+
throw new ValidationError(`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`, {
|
|
267
|
+
fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
for (const vendor of proxy.providers) {
|
|
271
|
+
assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
175
274
|
if (proxy.geo !== undefined) {
|
|
176
|
-
if (!proxy.geo ||
|
|
177
|
-
typeof proxy.geo !== "object" ||
|
|
178
|
-
Array.isArray(proxy.geo)) {
|
|
275
|
+
if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
|
|
179
276
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.geo: must be an object.`, {
|
|
180
277
|
fix: `Use proxy.geo: { country: "KR" } with ISO alpha-2 country codes.`,
|
|
181
278
|
});
|
|
@@ -192,43 +289,90 @@ function validateProviderProxy(config) {
|
|
|
192
289
|
}
|
|
193
290
|
}
|
|
194
291
|
if (proxy.session !== undefined) {
|
|
195
|
-
if (!proxy.session ||
|
|
196
|
-
typeof proxy.session !== "object" ||
|
|
197
|
-
Array.isArray(proxy.session)) {
|
|
292
|
+
if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
|
|
198
293
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session: must be an object.`, {
|
|
199
294
|
fix: `Use proxy.session: { affinity: "connection", lifetimeMinutes: 30 }.`,
|
|
200
295
|
});
|
|
201
296
|
}
|
|
202
|
-
rejectUnknownFields(proxy.session, new Set(["affinity", "lifetimeMinutes", "poolSize"]), "proxy.session");
|
|
297
|
+
rejectUnknownFields(proxy.session, new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]), "proxy.session");
|
|
203
298
|
if (proxy.session.affinity !== undefined) {
|
|
204
299
|
assertLiteralField(proxy.session.affinity, "proxy.session.affinity", VALID_PROVIDER_PROXY_AFFINITIES, config.id);
|
|
205
300
|
}
|
|
206
301
|
const lifetime = proxy.session.lifetimeMinutes;
|
|
207
|
-
if (lifetime !== undefined &&
|
|
208
|
-
(!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
302
|
+
if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
209
303
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`);
|
|
210
304
|
}
|
|
211
305
|
const poolSize = proxy.session.poolSize;
|
|
212
|
-
if (poolSize !== undefined &&
|
|
213
|
-
(!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
306
|
+
if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
214
307
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`);
|
|
215
308
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
309
|
+
const drainLeadSeconds = proxy.session.drainLeadSeconds;
|
|
310
|
+
if (drainLeadSeconds !== undefined &&
|
|
311
|
+
(!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)) {
|
|
312
|
+
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`, {
|
|
313
|
+
fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
// A drain lead longer than the sticky lifetime would fire the expiring
|
|
317
|
+
// event before the session is even established, so the provider would
|
|
318
|
+
// never get a usable window. Reject the contradiction at build time.
|
|
319
|
+
if (drainLeadSeconds !== undefined &&
|
|
320
|
+
lifetime !== undefined &&
|
|
321
|
+
Number.isFinite(lifetime) &&
|
|
322
|
+
drainLeadSeconds >= lifetime * 60) {
|
|
323
|
+
throw new ValidationError(`Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`, {
|
|
324
|
+
fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
|
|
222
325
|
});
|
|
223
326
|
}
|
|
224
327
|
}
|
|
328
|
+
// Every credentialed vendor in a required-mode chain must declare its
|
|
329
|
+
// provider secret(s) so a missing credential fails at build/validation time,
|
|
330
|
+
// not during a live outage. This covers the fallback legs too (not just the
|
|
331
|
+
// first vendor): a declared-but-uncredentialed nodemaven fallback would leave
|
|
332
|
+
// the chain silently down to a single vendor, reintroducing the SPOF the chain
|
|
333
|
+
// removes.
|
|
334
|
+
const vendorChain = proxy.providers && proxy.providers.length > 0
|
|
335
|
+
? proxy.providers
|
|
336
|
+
: proxy.provider
|
|
337
|
+
? [proxy.provider]
|
|
338
|
+
: [];
|
|
339
|
+
if (proxy.mode === "required") {
|
|
340
|
+
for (const vendor of vendorChain) {
|
|
341
|
+
const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
|
|
342
|
+
if (!requiredSecrets)
|
|
343
|
+
continue;
|
|
344
|
+
for (const secretName of requiredSecrets) {
|
|
345
|
+
// Match the canonical runtime gate (assertRequiredSecretsPresent /
|
|
346
|
+
// listMissingRequiredSecrets), which enforces only `required === true`
|
|
347
|
+
// declarations. A declaration that omits `required` (defaulting to
|
|
348
|
+
// optional) is skipped at runtime, so accepting it here would pass
|
|
349
|
+
// validation while leaving the credential unenforced until proxy
|
|
350
|
+
// resolution during a live request — the fail-open gap this check exists
|
|
351
|
+
// to close.
|
|
352
|
+
const declared = config.secrets?.some((secret) => secret.name === secretName && secret.required === true);
|
|
353
|
+
if (!declared) {
|
|
354
|
+
throw new ValidationError(`Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`, {
|
|
355
|
+
fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// `decodo`/`custom` are deprecated vendor values (string-union members, so the
|
|
362
|
+
// @deprecated symbol gate can't catch them — warn at validation time instead).
|
|
363
|
+
const deprecatedVendors = vendorChain.filter((vendor) => vendor === "decodo" || vendor === "custom");
|
|
364
|
+
if (deprecatedVendors.length > 0) {
|
|
365
|
+
console.warn(`[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven", or the APIFUSE__PROXY__URL bring-your-own escape hatch.`);
|
|
366
|
+
}
|
|
225
367
|
}
|
|
226
368
|
function validateProviderStt(config) {
|
|
227
369
|
const stt = config.stt;
|
|
228
370
|
if (stt === undefined)
|
|
229
371
|
return;
|
|
230
372
|
if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
|
|
231
|
-
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
373
|
+
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
374
|
+
fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
|
|
375
|
+
});
|
|
232
376
|
}
|
|
233
377
|
rejectUnknownFields(stt, new Set(["mode"]), "stt");
|
|
234
378
|
assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
|
|
@@ -247,12 +391,7 @@ function validateOperationIds(providerId, operations) {
|
|
|
247
391
|
}
|
|
248
392
|
const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
249
393
|
const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
250
|
-
const VALID_OPERATION_LIFECYCLES = [
|
|
251
|
-
"stable",
|
|
252
|
-
"beta",
|
|
253
|
-
"deprecated",
|
|
254
|
-
"removed",
|
|
255
|
-
];
|
|
394
|
+
const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"];
|
|
256
395
|
function assertNonEmptyString(value, field, providerId, operationName) {
|
|
257
396
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
258
397
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${field}: must be a non-empty string.`, { fix: `Set ${field} to a non-empty customer-facing value.` });
|
|
@@ -268,8 +407,7 @@ function validateToolRouterMetadata(providerId, operations) {
|
|
|
268
407
|
fix: `Remove operations.${operationName}.toolRouter or provide MCP-safe metadata.`,
|
|
269
408
|
});
|
|
270
409
|
}
|
|
271
|
-
if (toolRouter.name !== undefined &&
|
|
272
|
-
!MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
410
|
+
if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
273
411
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`, {
|
|
274
412
|
fix: `Use letters, numbers, and underscores only, starting with a letter, for example "${providerId.replace(/[^A-Za-z0-9]+/g, "_")}__${operationName.replace(/[^A-Za-z0-9]+/g, "_")}".`,
|
|
275
413
|
});
|
|
@@ -307,8 +445,7 @@ function validateOperationContracts(providerId, operations) {
|
|
|
307
445
|
if (contract.lifecycle !== undefined) {
|
|
308
446
|
assertLiteralField(contract.lifecycle, `operations.${operationName}.contract.lifecycle`, VALID_OPERATION_LIFECYCLES, providerId);
|
|
309
447
|
}
|
|
310
|
-
if (contract.lifecycle === "deprecated" ||
|
|
311
|
-
contract.lifecycle === "removed") {
|
|
448
|
+
if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
|
|
312
449
|
if (!contract.deprecation || typeof contract.deprecation !== "object") {
|
|
313
450
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`, {
|
|
314
451
|
fix: `Add announcedAt, removalAfter, and migrationGuide to operations.${operationName}.contract.deprecation.`,
|
|
@@ -333,8 +470,7 @@ function validateOperationAnnotations(providerId, operations) {
|
|
|
333
470
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an integer number of milliseconds.`, {
|
|
334
471
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
|
|
335
472
|
});
|
|
336
|
-
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
|
|
337
|
-
timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
473
|
+
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
338
474
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`, {
|
|
339
475
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms (the upper bound stays below the gateway/ALB ceiling).`,
|
|
340
476
|
});
|
|
@@ -377,6 +513,23 @@ function validateOperationObservability(providerId, operations) {
|
|
|
377
513
|
}
|
|
378
514
|
}
|
|
379
515
|
}
|
|
516
|
+
function validateOperationErrorCodes(providerId, operations) {
|
|
517
|
+
for (const [operationName, operation] of Object.entries(operations)) {
|
|
518
|
+
for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
|
|
519
|
+
if (errorCode.status !== undefined &&
|
|
520
|
+
!VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)) {
|
|
521
|
+
const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
|
|
522
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`, {
|
|
523
|
+
fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (errorCode.status !== undefined &&
|
|
527
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)) {
|
|
528
|
+
console.warn(`[provider-sdk] Provider "${providerId}" operation "${operationName}" declares status ${errorCode.status} for SDK-owned error code "${errorCode.code}"; the declared status is documentation-only and will be ignored at runtime.`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
380
533
|
const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
|
|
381
534
|
const SSE_TRANSPORT_FIELDS = new Set([
|
|
382
535
|
"kind",
|
|
@@ -478,9 +631,7 @@ function validateOperationTransports(providerId, operations) {
|
|
|
478
631
|
assertStreamMs(maxDurationMs, `${fieldPath}.maxDurationMs`, STREAM_MAX_DURATION_MS_MIN, STREAM_MAX_DURATION_MS_MAX, "max duration");
|
|
479
632
|
assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
|
|
480
633
|
const resumable = Reflect.get(transport, "resumable");
|
|
481
|
-
if (resumable !== undefined &&
|
|
482
|
-
resumable !== false &&
|
|
483
|
-
resumable !== "last-event-id") {
|
|
634
|
+
if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
|
|
484
635
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`, {
|
|
485
636
|
fix: `Use ${fieldPath}.resumable: "last-event-id" for SSE Last-Event-ID resume support, or false to disable resume.`,
|
|
486
637
|
});
|
|
@@ -515,8 +666,7 @@ function validateOperationTransports(providerId, operations) {
|
|
|
515
666
|
});
|
|
516
667
|
}
|
|
517
668
|
for (const subprotocol of subprotocols) {
|
|
518
|
-
if (typeof subprotocol !== "string" ||
|
|
519
|
-
!WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
669
|
+
if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
520
670
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`, {
|
|
521
671
|
fix: `Use values such as "apifuse.v1" without spaces or separators that are invalid for Sec-WebSocket-Protocol.`,
|
|
522
672
|
});
|
|
@@ -624,9 +774,7 @@ function assertBoundedIntegerMs(value, fieldPath, options) {
|
|
|
624
774
|
function validateProviderHealthMonitor(providerId, healthMonitor, field = "healthMonitor") {
|
|
625
775
|
if (healthMonitor === undefined)
|
|
626
776
|
return;
|
|
627
|
-
if (!healthMonitor ||
|
|
628
|
-
typeof healthMonitor !== "object" ||
|
|
629
|
-
Array.isArray(healthMonitor))
|
|
777
|
+
if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
|
|
630
778
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
|
|
631
779
|
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
632
780
|
});
|
|
@@ -676,9 +824,7 @@ function validateProviderHealthMonitor(providerId, healthMonitor, field = "healt
|
|
|
676
824
|
}
|
|
677
825
|
const probeOverrides = healthMonitorRecord.probeOverrides;
|
|
678
826
|
if (probeOverrides !== undefined) {
|
|
679
|
-
if (!probeOverrides ||
|
|
680
|
-
typeof probeOverrides !== "object" ||
|
|
681
|
-
Array.isArray(probeOverrides))
|
|
827
|
+
if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
|
|
682
828
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`);
|
|
683
829
|
for (const [probeId, override] of Object.entries(probeOverrides)) {
|
|
684
830
|
if (probeId.length === 0)
|
|
@@ -756,9 +902,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
756
902
|
fix: `Set ${fieldPath}.interval to a positive ms-style duration string.`,
|
|
757
903
|
});
|
|
758
904
|
if (s.schedule !== undefined) {
|
|
759
|
-
if (!s.schedule ||
|
|
760
|
-
typeof s.schedule !== "object" ||
|
|
761
|
-
Array.isArray(s.schedule)) {
|
|
905
|
+
if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
|
|
762
906
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.schedule must be an object.`);
|
|
763
907
|
}
|
|
764
908
|
if (Reflect.get(s.schedule, "jitter") !== undefined) {
|
|
@@ -784,8 +928,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
784
928
|
label: "degraded threshold",
|
|
785
929
|
});
|
|
786
930
|
}
|
|
787
|
-
if (s.requiresConnection !== undefined &&
|
|
788
|
-
typeof s.requiresConnection !== "boolean")
|
|
931
|
+
if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
|
|
789
932
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`);
|
|
790
933
|
if (!Array.isArray(s.cases) || s.cases.length === 0)
|
|
791
934
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.cases must be a non-empty array.`, {
|
|
@@ -804,9 +947,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
804
947
|
}
|
|
805
948
|
function validateHealthCheckUnsupported(providerId, operationName, unsupported) {
|
|
806
949
|
const fieldPath = `operations.${operationName}.healthCheckUnsupported`;
|
|
807
|
-
if (!unsupported ||
|
|
808
|
-
typeof unsupported !== "object" ||
|
|
809
|
-
Array.isArray(unsupported))
|
|
950
|
+
if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
|
|
810
951
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
811
952
|
rejectUnknownFields(unsupported, HEALTH_CHECK_UNSUPPORTED_FIELDS, fieldPath);
|
|
812
953
|
const u = unsupported;
|
|
@@ -831,12 +972,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
|
|
|
831
972
|
"steps",
|
|
832
973
|
"run",
|
|
833
974
|
]);
|
|
834
|
-
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
|
|
835
|
-
"kind",
|
|
836
|
-
"interval",
|
|
837
|
-
"jitter",
|
|
838
|
-
"randomize",
|
|
839
|
-
]);
|
|
975
|
+
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
|
|
840
976
|
const HEALTH_JOURNEY_STEP_FIELDS = new Set([
|
|
841
977
|
"id",
|
|
842
978
|
"description",
|
|
@@ -854,10 +990,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
|
|
|
854
990
|
"minManualInterval",
|
|
855
991
|
"publicRationale",
|
|
856
992
|
]);
|
|
857
|
-
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
|
|
858
|
-
"enabled",
|
|
859
|
-
"reason",
|
|
860
|
-
]);
|
|
993
|
+
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
|
|
861
994
|
const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
|
|
862
995
|
"enabled",
|
|
863
996
|
"requiresAcknowledgement",
|
|
@@ -872,9 +1005,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS = new Set([
|
|
|
872
1005
|
]);
|
|
873
1006
|
function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger) {
|
|
874
1007
|
const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
|
|
875
|
-
if (!manualTrigger ||
|
|
876
|
-
typeof manualTrigger !== "object" ||
|
|
877
|
-
Array.isArray(manualTrigger)) {
|
|
1008
|
+
if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
|
|
878
1009
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object when present.`);
|
|
879
1010
|
}
|
|
880
1011
|
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
|
|
@@ -897,8 +1028,7 @@ function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger
|
|
|
897
1028
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`);
|
|
898
1029
|
}
|
|
899
1030
|
const risk = Reflect.get(manualTrigger, "risk");
|
|
900
|
-
if (typeof risk !== "string" ||
|
|
901
|
-
!HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
1031
|
+
if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
902
1032
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`);
|
|
903
1033
|
}
|
|
904
1034
|
if (risk !== "read_only" && requiresAcknowledgement !== true) {
|
|
@@ -950,7 +1080,7 @@ function isoDurationMs(value) {
|
|
|
950
1080
|
const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
|
|
951
1081
|
const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
|
|
952
1082
|
const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
|
|
953
|
-
return
|
|
1083
|
+
return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
|
|
954
1084
|
}
|
|
955
1085
|
function scheduleRandomizationMs(randomize, fieldPath) {
|
|
956
1086
|
const mode = Reflect.get(randomize, "mode");
|
|
@@ -974,9 +1104,7 @@ function validateScheduleRandomization(randomize, fieldPath, intervalMs) {
|
|
|
974
1104
|
throw new ValidationError(`${fieldPath} must be an object.`);
|
|
975
1105
|
}
|
|
976
1106
|
const mode = Reflect.get(randomize, "mode");
|
|
977
|
-
const allowedFields = mode === "centered"
|
|
978
|
-
? new Set(["mode", "maxOffset"])
|
|
979
|
-
: new Set(["mode", "maxDelay"]);
|
|
1107
|
+
const allowedFields = mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
|
|
980
1108
|
rejectUnknownFields(randomize, allowedFields, fieldPath);
|
|
981
1109
|
const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
|
|
982
1110
|
if (offsetMs <= 0) {
|
|
@@ -1062,9 +1190,7 @@ function countCapturingGroups(pattern) {
|
|
|
1062
1190
|
const next = source[i + 1];
|
|
1063
1191
|
if (next === "?" && source[i + 2] !== "<")
|
|
1064
1192
|
continue;
|
|
1065
|
-
if (next === "?" &&
|
|
1066
|
-
source[i + 2] === "<" &&
|
|
1067
|
-
(source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1193
|
+
if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1068
1194
|
continue;
|
|
1069
1195
|
count += 1;
|
|
1070
1196
|
}
|
|
@@ -1142,8 +1268,7 @@ function validateSmsOtpMatcher(matcher, fieldPath) {
|
|
|
1142
1268
|
throw new ValidationError(`${fieldPath}.code.pattern must be a RegExp or pattern source string.`);
|
|
1143
1269
|
}
|
|
1144
1270
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
1145
|
-
if (countCapturingGroups(regex) !== 1 &&
|
|
1146
|
-
Reflect.get(code, "capture") === undefined) {
|
|
1271
|
+
if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
|
|
1147
1272
|
throw new ValidationError(`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`);
|
|
1148
1273
|
}
|
|
1149
1274
|
if (Reflect.get(code, "capture") !== undefined &&
|
|
@@ -1195,8 +1320,7 @@ function validateHealthJourneySchedule(providerId, journeyId, schedule) {
|
|
|
1195
1320
|
const interval = Reflect.get(schedule, "interval");
|
|
1196
1321
|
assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
|
|
1197
1322
|
const randomize = Reflect.get(schedule, "randomize");
|
|
1198
|
-
if (Reflect.get(schedule, "jitter") !== undefined &&
|
|
1199
|
-
randomize !== undefined) {
|
|
1323
|
+
if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
|
|
1200
1324
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`);
|
|
1201
1325
|
}
|
|
1202
1326
|
if (Reflect.get(schedule, "jitter") !== undefined)
|
|
@@ -1226,8 +1350,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
|
|
|
1226
1350
|
throw new ValidationError(`Provider "${providerId}" has duplicate health journey id "${journey.id}".`);
|
|
1227
1351
|
journeyIds.add(journey.id);
|
|
1228
1352
|
validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
|
|
1229
|
-
if (!Array.isArray(journey.coversOperations) ||
|
|
1230
|
-
journey.coversOperations.length === 0) {
|
|
1353
|
+
if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
|
|
1231
1354
|
throw new ValidationError(`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`);
|
|
1232
1355
|
}
|
|
1233
1356
|
for (const operationId of journey.coversOperations) {
|
|
@@ -1265,8 +1388,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
|
|
|
1265
1388
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.id must be a kebab-case identifier.`);
|
|
1266
1389
|
if (step.operationId !== undefined && !operations[step.operationId])
|
|
1267
1390
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`);
|
|
1268
|
-
if (step.usesSmsMatcher !== undefined &&
|
|
1269
|
-
!matcherIds.has(step.usesSmsMatcher))
|
|
1391
|
+
if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
|
|
1270
1392
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`);
|
|
1271
1393
|
}
|
|
1272
1394
|
if (journey.manualTrigger !== undefined)
|
|
@@ -1300,9 +1422,7 @@ function validateOperationHealthChecks(providerId, operations, journeyCoveredOpe
|
|
|
1300
1422
|
validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
|
|
1301
1423
|
if (hasUnsupported)
|
|
1302
1424
|
validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
|
|
1303
|
-
if (!hasCheck &&
|
|
1304
|
-
!hasUnsupported &&
|
|
1305
|
-
!journeyCoveredOperations.has(operationName))
|
|
1425
|
+
if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
|
|
1306
1426
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`, {
|
|
1307
1427
|
fix: `Add \`healthCheck: { interval, cases: [...] }\` or \`healthCheckUnsupported: { reason: "..." }\` to operations.${operationName}.`,
|
|
1308
1428
|
});
|
|
@@ -1314,6 +1434,21 @@ function validateOperationFixtures(providerId, operations) {
|
|
|
1314
1434
|
throw new ValidationError(`Operation handler must be defined for provider "${providerId}" operation "${operationName}"`, {
|
|
1315
1435
|
fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
|
|
1316
1436
|
});
|
|
1437
|
+
if (operation.fixtures?.recordedAt !== undefined) {
|
|
1438
|
+
const recordedAt = operation.fixtures.recordedAt;
|
|
1439
|
+
const parsed = typeof recordedAt === "string"
|
|
1440
|
+
? new Date(`${recordedAt}T00:00:00.000Z`)
|
|
1441
|
+
: new Date(Number.NaN);
|
|
1442
|
+
const isCalendarDate = typeof recordedAt === "string" &&
|
|
1443
|
+
/^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
|
|
1444
|
+
!Number.isNaN(parsed.getTime()) &&
|
|
1445
|
+
parsed.toISOString().slice(0, 10) === recordedAt;
|
|
1446
|
+
const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
1447
|
+
if (!isCalendarDate || recordedAt > kstToday)
|
|
1448
|
+
throw new ValidationError(`Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`, {
|
|
1449
|
+
fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1317
1452
|
if (operation.fixtures?.request !== undefined) {
|
|
1318
1453
|
const result = safeParseSchemaSync(operation.input, operation.fixtures.request, `operations.${operationName}.fixtures.request`);
|
|
1319
1454
|
if (!result.success)
|
|
@@ -1332,8 +1467,44 @@ function validateOperationFixtures(providerId, operations) {
|
|
|
1332
1467
|
}
|
|
1333
1468
|
}
|
|
1334
1469
|
}
|
|
1470
|
+
function resolveOperationFixtureRequests(operations) {
|
|
1471
|
+
let changed = false;
|
|
1472
|
+
const resolvedOperations = Object.fromEntries(Object.entries(operations).map(([operationName, operation]) => {
|
|
1473
|
+
if (operation.fixtures?.request === undefined)
|
|
1474
|
+
return [operationName, operation];
|
|
1475
|
+
const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
|
|
1476
|
+
if (request === operation.fixtures.request)
|
|
1477
|
+
return [operationName, operation];
|
|
1478
|
+
changed = true;
|
|
1479
|
+
return [
|
|
1480
|
+
operationName,
|
|
1481
|
+
{
|
|
1482
|
+
...operation,
|
|
1483
|
+
fixtures: {
|
|
1484
|
+
...operation.fixtures,
|
|
1485
|
+
request,
|
|
1486
|
+
},
|
|
1487
|
+
},
|
|
1488
|
+
];
|
|
1489
|
+
}));
|
|
1490
|
+
return changed ? resolvedOperations : operations;
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Shallow shape guard only: the `deployment` object is passed through
|
|
1494
|
+
* verbatim and deliberately not deep-validated by the SDK — the APIFuse
|
|
1495
|
+
* registry builder owns deployment validation and profile resolution.
|
|
1496
|
+
*/
|
|
1497
|
+
function validateProviderDeployment(providerId, deployment) {
|
|
1498
|
+
if (deployment === undefined)
|
|
1499
|
+
return;
|
|
1500
|
+
if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
|
|
1501
|
+
throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
|
|
1502
|
+
fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1335
1505
|
export function defineProvider(config) {
|
|
1336
1506
|
validateProviderShape(config);
|
|
1507
|
+
const operations = resolveOperationFixtureRequests(config.operations);
|
|
1337
1508
|
if (!CONNECTOR_ID_REGEX.test(config.id))
|
|
1338
1509
|
throw new ProviderError(`Invalid provider id: "${config.id}"`, {
|
|
1339
1510
|
fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
|
|
@@ -1345,6 +1516,7 @@ export function defineProvider(config) {
|
|
|
1345
1516
|
validateOperationIds(config.id, config.operations);
|
|
1346
1517
|
validateOperationAnnotations(config.id, config.operations);
|
|
1347
1518
|
validateOperationObservability(config.id, config.operations);
|
|
1519
|
+
validateOperationErrorCodes(config.id, config.operations);
|
|
1348
1520
|
validateOperationTransports(config.id, config.operations);
|
|
1349
1521
|
validateOperationContracts(config.id, config.operations);
|
|
1350
1522
|
validateToolRouterMetadata(config.id, config.operations);
|
|
@@ -1355,7 +1527,16 @@ export function defineProvider(config) {
|
|
|
1355
1527
|
fix: "Keep healthProbe (the new name) and delete the healthMonitor block.",
|
|
1356
1528
|
});
|
|
1357
1529
|
validateProviderHealthMonitor(config.id, config.healthProbe ?? config.healthMonitor, config.healthProbe !== undefined ? "healthProbe" : "healthMonitor");
|
|
1358
|
-
validateOperationFixtures(config.id,
|
|
1530
|
+
validateOperationFixtures(config.id, operations);
|
|
1531
|
+
validateProviderDeployment(config.id, config.deployment);
|
|
1532
|
+
try {
|
|
1533
|
+
validateNativeProviderConfig(config.native);
|
|
1534
|
+
}
|
|
1535
|
+
catch (error) {
|
|
1536
|
+
if (error instanceof NativeEgressPolicyValidationError)
|
|
1537
|
+
throw new ValidationError(error.message);
|
|
1538
|
+
throw error;
|
|
1539
|
+
}
|
|
1359
1540
|
validateProviderProxy(config);
|
|
1360
1541
|
validateProviderStt(config);
|
|
1361
1542
|
if (config.runtime === "browser" && !config.browser)
|
|
@@ -1368,7 +1549,11 @@ export function defineProvider(config) {
|
|
|
1368
1549
|
id: config.id,
|
|
1369
1550
|
version: config.version,
|
|
1370
1551
|
runtime: config.runtime,
|
|
1552
|
+
// Verbatim passthrough: deployment validation and profile resolution
|
|
1553
|
+
// are owned by the APIFuse registry builder, not the SDK.
|
|
1554
|
+
deployment: config.deployment,
|
|
1371
1555
|
allowedHosts: config.allowedHosts,
|
|
1556
|
+
native: config.native,
|
|
1372
1557
|
stealth: config.stealth,
|
|
1373
1558
|
proxy: config.proxy,
|
|
1374
1559
|
stt: config.stt,
|
|
@@ -1380,7 +1565,7 @@ export function defineProvider(config) {
|
|
|
1380
1565
|
credential: config.credential,
|
|
1381
1566
|
context: config.context,
|
|
1382
1567
|
meta: config.meta,
|
|
1383
|
-
operations
|
|
1568
|
+
operations,
|
|
1384
1569
|
// Transitional healthMonitor → healthProbe alias: mirror whichever field
|
|
1385
1570
|
// was declared onto both so old and new consumers keep working.
|
|
1386
1571
|
healthMonitor: config.healthMonitor ?? config.healthProbe,
|