@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.20
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 +85 -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 +1 -1
- package/dist/ceremonies/index.js +8 -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 +208 -125
- 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.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 +1 -1
- package/dist/runtime/auth-flow.js +3 -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 +15 -4
- 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 +115 -4
- package/dist/server/serve.js +792 -127
- package/dist/server/types.d.ts +30 -9
- package/dist/server/types.js +8 -7
- 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 +477 -33
- package/dist/types.d.ts +323 -11
- 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 +24 -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 +368 -504
- 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 +111 -42
- package/src/lint.ts +89 -159
- 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 +65 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +5 -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 +41 -110
- 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 +1141 -324
- package/src/server/types.ts +12 -19
- 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 +652 -79
- package/src/types.ts +369 -11
- 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"
|
|
@@ -74,23 +84,10 @@ interface ProviderImplementationProfile {
|
|
|
74
84
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
75
85
|
const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
76
86
|
const VALID_RUNTIMES = ["standard", "shared", "browser"] as const;
|
|
77
|
-
const VALID_AUTH_MODES = [
|
|
78
|
-
"none",
|
|
79
|
-
"platform-managed",
|
|
80
|
-
"credentials",
|
|
81
|
-
"oauth2",
|
|
82
|
-
] as const;
|
|
87
|
+
const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"] as const;
|
|
83
88
|
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
|
|
84
|
-
const VALID_PROVIDER_PROXY_MODES = [
|
|
85
|
-
|
|
86
|
-
"optional",
|
|
87
|
-
"required",
|
|
88
|
-
] as const;
|
|
89
|
-
const VALID_PROVIDER_PROXY_PROVIDERS = [
|
|
90
|
-
"smartproxy",
|
|
91
|
-
"decodo",
|
|
92
|
-
"custom",
|
|
93
|
-
] as const;
|
|
89
|
+
const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"] as const;
|
|
90
|
+
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"] as const;
|
|
94
91
|
const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
95
92
|
"request",
|
|
96
93
|
"operation",
|
|
@@ -99,25 +96,25 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
|
99
96
|
] as const;
|
|
100
97
|
const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
|
|
101
98
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
99
|
+
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
100
|
+
const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
101
|
+
// Per-vendor provider-declared credential secrets. A required-mode chain must
|
|
102
|
+
// declare every secret of every credentialed vendor it names, so a missing
|
|
103
|
+
// credential fails at build/validation time rather than during a live outage: a
|
|
104
|
+
// declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
|
|
105
|
+
// exactly the failure class the multi-vendor chain exists to remove. Vendors
|
|
106
|
+
// absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
|
|
107
|
+
// `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
|
|
108
|
+
// no declaration requirement.
|
|
109
|
+
const VENDOR_REQUIRED_SECRETS: Partial<Record<ProviderProxyProvider, readonly string[]>> = {
|
|
110
|
+
smartproxy: [SMARTPROXY_APP_KEY_SECRET],
|
|
111
|
+
nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
|
|
112
|
+
};
|
|
102
113
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
103
114
|
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;
|
|
115
|
+
const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
|
|
116
|
+
const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"] as const;
|
|
117
|
+
const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"] as const;
|
|
121
118
|
const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
122
119
|
const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
123
120
|
|
|
@@ -135,18 +132,16 @@ function msDurationMs(value: string): number {
|
|
|
135
132
|
function parsePositiveMsDuration(value: string): number | undefined {
|
|
136
133
|
const trimmed = value.trim();
|
|
137
134
|
if (!MS_DURATION_PATTERN.test(trimmed)) return undefined;
|
|
138
|
-
const parsed = ms(
|
|
139
|
-
(trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue,
|
|
140
|
-
);
|
|
135
|
+
const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue);
|
|
141
136
|
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
142
137
|
return parsed;
|
|
143
138
|
}
|
|
144
139
|
|
|
145
140
|
type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
|
|
146
|
-
type OperationConfig<
|
|
147
|
-
TInput
|
|
148
|
-
|
|
149
|
-
>
|
|
141
|
+
type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
142
|
+
OperationDefinition<TInput, TOutput>,
|
|
143
|
+
"handler"
|
|
144
|
+
> & {
|
|
150
145
|
handler(
|
|
151
146
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
152
147
|
input: InferSchemaOutput<TInput>,
|
|
@@ -154,59 +149,44 @@ type OperationConfig<
|
|
|
154
149
|
| OperationHandlerResult<InferSchemaOutput<TOutput>>
|
|
155
150
|
| Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
|
|
156
151
|
};
|
|
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
|
-
> =
|
|
152
|
+
type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
|
|
153
|
+
[K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
|
|
154
|
+
? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
|
|
155
|
+
: never;
|
|
156
|
+
};
|
|
157
|
+
type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
|
|
170
158
|
| SseOperationConfig<TInput, TOutput>
|
|
171
159
|
| HttpStreamOperationConfig<TInput, TOutput>
|
|
172
160
|
| WebSocketOperationConfig<TInput, TOutput>;
|
|
173
|
-
type SseOperationConfig<
|
|
174
|
-
TInput
|
|
175
|
-
|
|
176
|
-
>
|
|
161
|
+
type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
162
|
+
OperationConfig<TInput, TOutput>,
|
|
163
|
+
"handler" | "transport"
|
|
164
|
+
> & {
|
|
177
165
|
transport: OperationSseTransport;
|
|
178
166
|
handler(
|
|
179
167
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
180
168
|
input: InferSchemaOutput<TInput>,
|
|
181
|
-
):
|
|
182
|
-
| AsyncIterable<ProviderStreamEvent>
|
|
183
|
-
| Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
169
|
+
): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
184
170
|
};
|
|
185
|
-
type HttpStreamOperationConfig<
|
|
186
|
-
TInput
|
|
187
|
-
|
|
188
|
-
>
|
|
171
|
+
type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
172
|
+
OperationConfig<TInput, TOutput>,
|
|
173
|
+
"handler" | "transport"
|
|
174
|
+
> & {
|
|
189
175
|
transport: OperationHttpStreamTransport;
|
|
190
176
|
handler(
|
|
191
177
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
192
178
|
input: InferSchemaOutput<TInput>,
|
|
193
|
-
):
|
|
194
|
-
| Response
|
|
195
|
-
| ReadableStream<Uint8Array>
|
|
196
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
179
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
197
180
|
};
|
|
198
|
-
type WebSocketOperationConfig<
|
|
199
|
-
TInput
|
|
200
|
-
|
|
201
|
-
>
|
|
181
|
+
type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
182
|
+
OperationConfig<TInput, TOutput>,
|
|
183
|
+
"handler" | "transport"
|
|
184
|
+
> & {
|
|
202
185
|
transport: OperationWebSocketTransport;
|
|
203
186
|
handler(
|
|
204
187
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
205
188
|
input: InferSchemaOutput<TInput>,
|
|
206
|
-
):
|
|
207
|
-
| Response
|
|
208
|
-
| ReadableStream<Uint8Array>
|
|
209
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
189
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
210
190
|
};
|
|
211
191
|
|
|
212
192
|
type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
@@ -221,13 +201,19 @@ type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
|
221
201
|
: unknown
|
|
222
202
|
: unknown;
|
|
223
203
|
|
|
224
|
-
export interface ProviderConfig<
|
|
225
|
-
TOperations extends Record<string, ProviderOperation>,
|
|
226
|
-
> {
|
|
204
|
+
export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
|
|
227
205
|
id: string;
|
|
228
206
|
version: string;
|
|
229
207
|
runtime: "standard" | "shared" | "browser";
|
|
208
|
+
/**
|
|
209
|
+
* Optional deployment overrides, passed through verbatim onto the returned
|
|
210
|
+
* provider definition. The SDK types this field but does not deep-validate
|
|
211
|
+
* it — the APIFuse registry builder owns deployment validation and
|
|
212
|
+
* resolves omitted fields against the runtime deployment profiles.
|
|
213
|
+
*/
|
|
214
|
+
deployment?: ProviderDeploymentOverrides;
|
|
230
215
|
allowedHosts?: string[];
|
|
216
|
+
native?: NativeProviderConfig;
|
|
231
217
|
stealth?: {
|
|
232
218
|
profile: string;
|
|
233
219
|
platform: StealthPlatform;
|
|
@@ -270,35 +256,24 @@ export interface ProviderConfig<
|
|
|
270
256
|
}
|
|
271
257
|
|
|
272
258
|
/** Define one provider operation with schema-driven handler inference. */
|
|
273
|
-
export function defineOperation<
|
|
274
|
-
TInput extends SchemaLike,
|
|
275
|
-
TOutput extends SchemaLike,
|
|
276
|
-
>(
|
|
259
|
+
export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
277
260
|
operation: OperationConfig<TInput, TOutput>,
|
|
278
261
|
): OperationDefinition<TInput, TOutput> {
|
|
279
262
|
return operation;
|
|
280
263
|
}
|
|
281
264
|
|
|
282
265
|
/** Define a non-JSON provider operation with explicit transport metadata. */
|
|
283
|
-
export function defineStreamOperation<
|
|
284
|
-
TInput extends SchemaLike,
|
|
285
|
-
TOutput extends SchemaLike,
|
|
286
|
-
>(
|
|
266
|
+
export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
287
267
|
operation: StreamOperationConfig<TInput, TOutput>,
|
|
288
268
|
): OperationDefinition<TInput, TOutput> {
|
|
289
269
|
return operation;
|
|
290
270
|
}
|
|
291
271
|
|
|
292
|
-
function assertObjectConfig(
|
|
293
|
-
value: unknown,
|
|
294
|
-
): asserts value is Record<string, unknown> {
|
|
272
|
+
function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
|
|
295
273
|
if (!value || typeof value !== "object") {
|
|
296
|
-
throw new ProviderError(
|
|
297
|
-
"defineProvider
|
|
298
|
-
|
|
299
|
-
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
300
|
-
},
|
|
301
|
-
);
|
|
274
|
+
throw new ProviderError("defineProvider config must be an object. Offending field: config", {
|
|
275
|
+
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
276
|
+
});
|
|
302
277
|
}
|
|
303
278
|
}
|
|
304
279
|
function assertRequiredField(
|
|
@@ -336,25 +311,10 @@ function validateProviderShape(config: unknown): void {
|
|
|
336
311
|
assertRequiredField(config, "meta", String(config.id));
|
|
337
312
|
assertRequiredField(config, "operations", String(config.id));
|
|
338
313
|
if (typeof config.runtime === "string")
|
|
339
|
-
assertLiteralField(
|
|
340
|
-
config.runtime,
|
|
341
|
-
"runtime",
|
|
342
|
-
VALID_RUNTIMES,
|
|
343
|
-
String(config.id),
|
|
344
|
-
);
|
|
314
|
+
assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
|
|
345
315
|
const auth = config.auth;
|
|
346
|
-
if (
|
|
347
|
-
auth
|
|
348
|
-
typeof auth === "object" &&
|
|
349
|
-
"mode" in auth &&
|
|
350
|
-
typeof auth.mode === "string"
|
|
351
|
-
)
|
|
352
|
-
assertLiteralField(
|
|
353
|
-
auth.mode,
|
|
354
|
-
"auth.mode",
|
|
355
|
-
VALID_AUTH_MODES,
|
|
356
|
-
String(config.id),
|
|
357
|
-
);
|
|
316
|
+
if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
|
|
317
|
+
assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
|
|
358
318
|
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
359
319
|
throw new ProviderError(
|
|
360
320
|
`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
|
|
@@ -390,9 +350,7 @@ function validateProviderShape(config: unknown): void {
|
|
|
390
350
|
},
|
|
391
351
|
);
|
|
392
352
|
}
|
|
393
|
-
const accessRecord: Record<string, unknown> = Object.fromEntries(
|
|
394
|
-
Object.entries(access),
|
|
395
|
-
);
|
|
353
|
+
const accessRecord: Record<string, unknown> = Object.fromEntries(Object.entries(access));
|
|
396
354
|
for (const key of Object.keys(accessRecord)) {
|
|
397
355
|
if (key !== "visibility") {
|
|
398
356
|
throw new ValidationError(`Unknown field "${key}" on access.`, {
|
|
@@ -437,31 +395,26 @@ function validateProviderProxy(config: {
|
|
|
437
395
|
},
|
|
438
396
|
);
|
|
439
397
|
}
|
|
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
|
-
);
|
|
398
|
+
rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
|
|
399
|
+
assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
|
|
451
400
|
if (proxy.provider !== undefined) {
|
|
452
|
-
assertLiteralField(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
401
|
+
assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
402
|
+
}
|
|
403
|
+
if (proxy.providers !== undefined) {
|
|
404
|
+
if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
|
|
405
|
+
throw new ValidationError(
|
|
406
|
+
`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`,
|
|
407
|
+
{
|
|
408
|
+
fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
for (const vendor of proxy.providers) {
|
|
413
|
+
assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
414
|
+
}
|
|
458
415
|
}
|
|
459
416
|
if (proxy.geo !== undefined) {
|
|
460
|
-
if (
|
|
461
|
-
!proxy.geo ||
|
|
462
|
-
typeof proxy.geo !== "object" ||
|
|
463
|
-
Array.isArray(proxy.geo)
|
|
464
|
-
) {
|
|
417
|
+
if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
|
|
465
418
|
throw new ValidationError(
|
|
466
419
|
`Provider "${config.id}" has invalid proxy.geo: must be an object.`,
|
|
467
420
|
{
|
|
@@ -469,11 +422,7 @@ function validateProviderProxy(config: {
|
|
|
469
422
|
},
|
|
470
423
|
);
|
|
471
424
|
}
|
|
472
|
-
rejectUnknownFields(
|
|
473
|
-
proxy.geo,
|
|
474
|
-
new Set(["country", "subdivision", "city"]),
|
|
475
|
-
"proxy.geo",
|
|
476
|
-
);
|
|
425
|
+
rejectUnknownFields(proxy.geo, new Set(["country", "subdivision", "city"]), "proxy.geo");
|
|
477
426
|
if (proxy.geo.country !== undefined) {
|
|
478
427
|
assertIsoCountry(proxy.geo.country, "proxy.geo.country");
|
|
479
428
|
}
|
|
@@ -487,11 +436,7 @@ function validateProviderProxy(config: {
|
|
|
487
436
|
}
|
|
488
437
|
}
|
|
489
438
|
if (proxy.session !== undefined) {
|
|
490
|
-
if (
|
|
491
|
-
!proxy.session ||
|
|
492
|
-
typeof proxy.session !== "object" ||
|
|
493
|
-
Array.isArray(proxy.session)
|
|
494
|
-
) {
|
|
439
|
+
if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
|
|
495
440
|
throw new ValidationError(
|
|
496
441
|
`Provider "${config.id}" has invalid proxy.session: must be an object.`,
|
|
497
442
|
{
|
|
@@ -501,7 +446,7 @@ function validateProviderProxy(config: {
|
|
|
501
446
|
}
|
|
502
447
|
rejectUnknownFields(
|
|
503
448
|
proxy.session,
|
|
504
|
-
new Set(["affinity", "lifetimeMinutes", "poolSize"]),
|
|
449
|
+
new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]),
|
|
505
450
|
"proxy.session",
|
|
506
451
|
);
|
|
507
452
|
if (proxy.session.affinity !== undefined) {
|
|
@@ -513,51 +458,103 @@ function validateProviderProxy(config: {
|
|
|
513
458
|
);
|
|
514
459
|
}
|
|
515
460
|
const lifetime = proxy.session.lifetimeMinutes;
|
|
516
|
-
if (
|
|
517
|
-
lifetime !== undefined &&
|
|
518
|
-
(!Number.isFinite(lifetime) || lifetime <= 0)
|
|
519
|
-
) {
|
|
461
|
+
if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
520
462
|
throw new ValidationError(
|
|
521
463
|
`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`,
|
|
522
464
|
);
|
|
523
465
|
}
|
|
524
466
|
const poolSize = proxy.session.poolSize;
|
|
467
|
+
if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
468
|
+
throw new ValidationError(
|
|
469
|
+
`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
const drainLeadSeconds = proxy.session.drainLeadSeconds;
|
|
525
473
|
if (
|
|
526
|
-
|
|
527
|
-
(!Number.
|
|
474
|
+
drainLeadSeconds !== undefined &&
|
|
475
|
+
(!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)
|
|
528
476
|
) {
|
|
529
477
|
throw new ValidationError(
|
|
530
|
-
`Provider "${config.id}" has invalid proxy.session.
|
|
478
|
+
`Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`,
|
|
479
|
+
{
|
|
480
|
+
fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
|
|
481
|
+
},
|
|
531
482
|
);
|
|
532
483
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
484
|
+
// A drain lead longer than the sticky lifetime would fire the expiring
|
|
485
|
+
// event before the session is even established, so the provider would
|
|
486
|
+
// never get a usable window. Reject the contradiction at build time.
|
|
487
|
+
if (
|
|
488
|
+
drainLeadSeconds !== undefined &&
|
|
489
|
+
lifetime !== undefined &&
|
|
490
|
+
Number.isFinite(lifetime) &&
|
|
491
|
+
drainLeadSeconds >= lifetime * 60
|
|
492
|
+
) {
|
|
540
493
|
throw new ValidationError(
|
|
541
|
-
`Provider "${config.id}"
|
|
494
|
+
`Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`,
|
|
542
495
|
{
|
|
543
|
-
fix: `
|
|
496
|
+
fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
|
|
544
497
|
},
|
|
545
498
|
);
|
|
546
499
|
}
|
|
547
500
|
}
|
|
501
|
+
// Every credentialed vendor in a required-mode chain must declare its
|
|
502
|
+
// provider secret(s) so a missing credential fails at build/validation time,
|
|
503
|
+
// not during a live outage. This covers the fallback legs too (not just the
|
|
504
|
+
// first vendor): a declared-but-uncredentialed nodemaven fallback would leave
|
|
505
|
+
// the chain silently down to a single vendor, reintroducing the SPOF the chain
|
|
506
|
+
// removes.
|
|
507
|
+
const vendorChain =
|
|
508
|
+
proxy.providers && proxy.providers.length > 0
|
|
509
|
+
? proxy.providers
|
|
510
|
+
: proxy.provider
|
|
511
|
+
? [proxy.provider]
|
|
512
|
+
: [];
|
|
513
|
+
if (proxy.mode === "required") {
|
|
514
|
+
for (const vendor of vendorChain) {
|
|
515
|
+
const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
|
|
516
|
+
if (!requiredSecrets) continue;
|
|
517
|
+
for (const secretName of requiredSecrets) {
|
|
518
|
+
// Match the canonical runtime gate (assertRequiredSecretsPresent /
|
|
519
|
+
// listMissingRequiredSecrets), which enforces only `required === true`
|
|
520
|
+
// declarations. A declaration that omits `required` (defaulting to
|
|
521
|
+
// optional) is skipped at runtime, so accepting it here would pass
|
|
522
|
+
// validation while leaving the credential unenforced until proxy
|
|
523
|
+
// resolution during a live request — the fail-open gap this check exists
|
|
524
|
+
// to close.
|
|
525
|
+
const declared = config.secrets?.some(
|
|
526
|
+
(secret) => secret.name === secretName && secret.required === true,
|
|
527
|
+
);
|
|
528
|
+
if (!declared) {
|
|
529
|
+
throw new ValidationError(
|
|
530
|
+
`Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`,
|
|
531
|
+
{
|
|
532
|
+
fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
|
|
533
|
+
},
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
// `decodo`/`custom` are deprecated vendor values (string-union members, so the
|
|
540
|
+
// @deprecated symbol gate can't catch them — warn at validation time instead).
|
|
541
|
+
const deprecatedVendors = vendorChain.filter(
|
|
542
|
+
(vendor) => vendor === "decodo" || vendor === "custom",
|
|
543
|
+
);
|
|
544
|
+
if (deprecatedVendors.length > 0) {
|
|
545
|
+
console.warn(
|
|
546
|
+
`[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.`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
548
549
|
}
|
|
549
550
|
|
|
550
|
-
function validateProviderStt(config: {
|
|
551
|
-
id: string;
|
|
552
|
-
stt?: ProviderSttConfig;
|
|
553
|
-
}): void {
|
|
551
|
+
function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): void {
|
|
554
552
|
const stt = config.stt;
|
|
555
553
|
if (stt === undefined) return;
|
|
556
554
|
if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
|
|
557
|
-
throw new ValidationError(
|
|
558
|
-
`
|
|
559
|
-
|
|
560
|
-
);
|
|
555
|
+
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
556
|
+
fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
|
|
557
|
+
});
|
|
561
558
|
}
|
|
562
559
|
rejectUnknownFields(stt, new Set(["mode"]), "stt");
|
|
563
560
|
assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
|
|
@@ -584,16 +581,9 @@ function validateOperationIds(
|
|
|
584
581
|
);
|
|
585
582
|
}
|
|
586
583
|
}
|
|
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;
|
|
584
|
+
const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
585
|
+
const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
586
|
+
const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"] as const;
|
|
597
587
|
|
|
598
588
|
function assertNonEmptyString(
|
|
599
589
|
value: unknown,
|
|
@@ -624,10 +614,7 @@ function validateToolRouterMetadata(
|
|
|
624
614
|
},
|
|
625
615
|
);
|
|
626
616
|
}
|
|
627
|
-
if (
|
|
628
|
-
toolRouter.name !== undefined &&
|
|
629
|
-
!MCP_TOOL_NAME_REGEX.test(toolRouter.name)
|
|
630
|
-
) {
|
|
617
|
+
if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
631
618
|
throw new ValidationError(
|
|
632
619
|
`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`,
|
|
633
620
|
{
|
|
@@ -699,10 +686,7 @@ function validateOperationContracts(
|
|
|
699
686
|
providerId,
|
|
700
687
|
);
|
|
701
688
|
}
|
|
702
|
-
if (
|
|
703
|
-
contract.lifecycle === "deprecated" ||
|
|
704
|
-
contract.lifecycle === "removed"
|
|
705
|
-
) {
|
|
689
|
+
if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
|
|
706
690
|
if (!contract.deprecation || typeof contract.deprecation !== "object") {
|
|
707
691
|
throw new ValidationError(
|
|
708
692
|
`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`,
|
|
@@ -750,10 +734,7 @@ function validateOperationAnnotations(
|
|
|
750
734
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
|
|
751
735
|
},
|
|
752
736
|
);
|
|
753
|
-
if (
|
|
754
|
-
timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
|
|
755
|
-
timeoutMs > OPERATION_TIMEOUT_MS_MAX
|
|
756
|
-
)
|
|
737
|
+
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
757
738
|
throw new ValidationError(
|
|
758
739
|
`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`,
|
|
759
740
|
{
|
|
@@ -821,6 +802,36 @@ function validateOperationObservability(
|
|
|
821
802
|
}
|
|
822
803
|
}
|
|
823
804
|
|
|
805
|
+
function validateOperationErrorCodes(
|
|
806
|
+
providerId: string,
|
|
807
|
+
operations: Record<string, ProviderOperation>,
|
|
808
|
+
): void {
|
|
809
|
+
for (const [operationName, operation] of Object.entries(operations)) {
|
|
810
|
+
for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
|
|
811
|
+
if (
|
|
812
|
+
errorCode.status !== undefined &&
|
|
813
|
+
!VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)
|
|
814
|
+
) {
|
|
815
|
+
const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
|
|
816
|
+
throw new ValidationError(
|
|
817
|
+
`Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`,
|
|
818
|
+
{
|
|
819
|
+
fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
|
|
820
|
+
},
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
if (
|
|
824
|
+
errorCode.status !== undefined &&
|
|
825
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)
|
|
826
|
+
) {
|
|
827
|
+
console.warn(
|
|
828
|
+
`[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.`,
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
824
835
|
const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
|
|
825
836
|
const SSE_TRANSPORT_FIELDS = new Set([
|
|
826
837
|
"kind",
|
|
@@ -951,12 +962,7 @@ function validateOperationTransports(
|
|
|
951
962
|
},
|
|
952
963
|
);
|
|
953
964
|
}
|
|
954
|
-
assertLiteralField(
|
|
955
|
-
kind,
|
|
956
|
-
`${fieldPath}.kind`,
|
|
957
|
-
VALID_OPERATION_TRANSPORT_KINDS,
|
|
958
|
-
providerId,
|
|
959
|
-
);
|
|
965
|
+
assertLiteralField(kind, `${fieldPath}.kind`, VALID_OPERATION_TRANSPORT_KINDS, providerId);
|
|
960
966
|
|
|
961
967
|
switch (kind) {
|
|
962
968
|
case "json":
|
|
@@ -988,16 +994,9 @@ function validateOperationTransports(
|
|
|
988
994
|
STREAM_MAX_DURATION_MS_MAX,
|
|
989
995
|
"max duration",
|
|
990
996
|
);
|
|
991
|
-
assertPositiveBytes(
|
|
992
|
-
Reflect.get(transport, "maxEventBytes"),
|
|
993
|
-
`${fieldPath}.maxEventBytes`,
|
|
994
|
-
);
|
|
997
|
+
assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
|
|
995
998
|
const resumable = Reflect.get(transport, "resumable");
|
|
996
|
-
if (
|
|
997
|
-
resumable !== undefined &&
|
|
998
|
-
resumable !== false &&
|
|
999
|
-
resumable !== "last-event-id"
|
|
1000
|
-
) {
|
|
999
|
+
if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
|
|
1001
1000
|
throw new ValidationError(
|
|
1002
1001
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`,
|
|
1003
1002
|
{
|
|
@@ -1017,12 +1016,7 @@ function validateOperationTransports(
|
|
|
1017
1016
|
rejectUnknownFields(transport, HTTP_STREAM_TRANSPORT_FIELDS, fieldPath);
|
|
1018
1017
|
const contentType = Reflect.get(transport, "contentType");
|
|
1019
1018
|
if (contentType !== undefined) {
|
|
1020
|
-
assertNonEmptyString(
|
|
1021
|
-
contentType,
|
|
1022
|
-
`${fieldPath}.contentType`,
|
|
1023
|
-
providerId,
|
|
1024
|
-
operationName,
|
|
1025
|
-
);
|
|
1019
|
+
assertNonEmptyString(contentType, `${fieldPath}.contentType`, providerId, operationName);
|
|
1026
1020
|
}
|
|
1027
1021
|
assertStreamMs(
|
|
1028
1022
|
Reflect.get(transport, "idleTimeoutMs"),
|
|
@@ -1038,10 +1032,7 @@ function validateOperationTransports(
|
|
|
1038
1032
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1039
1033
|
"max duration",
|
|
1040
1034
|
);
|
|
1041
|
-
assertPositiveBytes(
|
|
1042
|
-
Reflect.get(transport, "maxChunkBytes"),
|
|
1043
|
-
`${fieldPath}.maxChunkBytes`,
|
|
1044
|
-
);
|
|
1035
|
+
assertPositiveBytes(Reflect.get(transport, "maxChunkBytes"), `${fieldPath}.maxChunkBytes`);
|
|
1045
1036
|
break;
|
|
1046
1037
|
}
|
|
1047
1038
|
case "websocket": {
|
|
@@ -1066,10 +1057,7 @@ function validateOperationTransports(
|
|
|
1066
1057
|
);
|
|
1067
1058
|
}
|
|
1068
1059
|
for (const subprotocol of subprotocols) {
|
|
1069
|
-
if (
|
|
1070
|
-
typeof subprotocol !== "string" ||
|
|
1071
|
-
!WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)
|
|
1072
|
-
) {
|
|
1060
|
+
if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
1073
1061
|
throw new ValidationError(
|
|
1074
1062
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`,
|
|
1075
1063
|
{
|
|
@@ -1093,10 +1081,7 @@ function validateOperationTransports(
|
|
|
1093
1081
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1094
1082
|
"max duration",
|
|
1095
1083
|
);
|
|
1096
|
-
assertPositiveBytes(
|
|
1097
|
-
Reflect.get(transport, "maxFrameBytes"),
|
|
1098
|
-
`${fieldPath}.maxFrameBytes`,
|
|
1099
|
-
);
|
|
1084
|
+
assertPositiveBytes(Reflect.get(transport, "maxFrameBytes"), `${fieldPath}.maxFrameBytes`);
|
|
1100
1085
|
break;
|
|
1101
1086
|
}
|
|
1102
1087
|
}
|
|
@@ -1159,10 +1144,7 @@ function levenshtein(a: string, b: string): number {
|
|
|
1159
1144
|
return prev[n] ?? 0;
|
|
1160
1145
|
}
|
|
1161
1146
|
|
|
1162
|
-
function suggestField(
|
|
1163
|
-
unknown: string,
|
|
1164
|
-
candidates: ReadonlySet<string>,
|
|
1165
|
-
): string | undefined {
|
|
1147
|
+
function suggestField(unknown: string, candidates: ReadonlySet<string>): string | undefined {
|
|
1166
1148
|
let best: string | undefined;
|
|
1167
1149
|
let bestDist = 3;
|
|
1168
1150
|
for (const candidate of candidates) {
|
|
@@ -1175,11 +1157,7 @@ function suggestField(
|
|
|
1175
1157
|
return best;
|
|
1176
1158
|
}
|
|
1177
1159
|
|
|
1178
|
-
function rejectUnknownFields(
|
|
1179
|
-
value: object,
|
|
1180
|
-
allowed: ReadonlySet<string>,
|
|
1181
|
-
fieldPath: string,
|
|
1182
|
-
): void {
|
|
1160
|
+
function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldPath: string): void {
|
|
1183
1161
|
for (const key of Object.keys(value)) {
|
|
1184
1162
|
if (allowed.has(key)) continue;
|
|
1185
1163
|
const hint = suggestField(key, allowed);
|
|
@@ -1218,23 +1196,12 @@ function validateProviderHealthMonitor(
|
|
|
1218
1196
|
field: "healthMonitor" | "healthProbe" = "healthMonitor",
|
|
1219
1197
|
): void {
|
|
1220
1198
|
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
|
-
);
|
|
1199
|
+
if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
|
|
1200
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
|
|
1201
|
+
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
1202
|
+
});
|
|
1232
1203
|
const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
|
|
1233
|
-
rejectUnknownFields(
|
|
1234
|
-
healthMonitorRecord,
|
|
1235
|
-
PROVIDER_HEALTH_MONITOR_FIELDS,
|
|
1236
|
-
field,
|
|
1237
|
-
);
|
|
1204
|
+
rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
|
|
1238
1205
|
if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
|
|
1239
1206
|
assertBoundedIntegerMs(
|
|
1240
1207
|
healthMonitorRecord.defaultProbeTimeoutMs,
|
|
@@ -1302,11 +1269,7 @@ function validateProviderHealthMonitor(
|
|
|
1302
1269
|
|
|
1303
1270
|
const probeOverrides = healthMonitorRecord.probeOverrides;
|
|
1304
1271
|
if (probeOverrides !== undefined) {
|
|
1305
|
-
if (
|
|
1306
|
-
!probeOverrides ||
|
|
1307
|
-
typeof probeOverrides !== "object" ||
|
|
1308
|
-
Array.isArray(probeOverrides)
|
|
1309
|
-
)
|
|
1272
|
+
if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
|
|
1310
1273
|
throw new ValidationError(
|
|
1311
1274
|
`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`,
|
|
1312
1275
|
);
|
|
@@ -1372,14 +1335,8 @@ function validateHealthCheckCase(
|
|
|
1372
1335
|
): void {
|
|
1373
1336
|
const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
|
|
1374
1337
|
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
|
-
);
|
|
1338
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1339
|
+
rejectUnknownFields(caseValue as Record<string, unknown>, HEALTH_CHECK_CASE_FIELDS, fieldPath);
|
|
1383
1340
|
const c = caseValue as HealthCheckCase;
|
|
1384
1341
|
if (typeof c.name !== "string" || c.name.length === 0)
|
|
1385
1342
|
throw new ValidationError(
|
|
@@ -1407,15 +1364,11 @@ function validateHealthCheckCase(
|
|
|
1407
1364
|
`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
1365
|
);
|
|
1409
1366
|
if (c.timeoutMs !== undefined) {
|
|
1410
|
-
assertBoundedIntegerMs(
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1416
|
-
label: "timeout",
|
|
1417
|
-
},
|
|
1418
|
-
);
|
|
1367
|
+
assertBoundedIntegerMs(c.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
|
|
1368
|
+
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1369
|
+
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1370
|
+
label: "timeout",
|
|
1371
|
+
});
|
|
1419
1372
|
}
|
|
1420
1373
|
if (
|
|
1421
1374
|
c.expectedStatus !== undefined &&
|
|
@@ -1431,21 +1384,11 @@ function validateHealthCheckCase(
|
|
|
1431
1384
|
);
|
|
1432
1385
|
}
|
|
1433
1386
|
|
|
1434
|
-
function validateHealthCheckSuite(
|
|
1435
|
-
providerId: string,
|
|
1436
|
-
operationName: string,
|
|
1437
|
-
suite: unknown,
|
|
1438
|
-
): void {
|
|
1387
|
+
function validateHealthCheckSuite(providerId: string, operationName: string, suite: unknown): void {
|
|
1439
1388
|
const fieldPath = `operations.${operationName}.healthCheck`;
|
|
1440
1389
|
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
|
-
);
|
|
1390
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1391
|
+
rejectUnknownFields(suite as Record<string, unknown>, HEALTH_CHECK_SUITE_FIELDS, fieldPath);
|
|
1449
1392
|
const s = suite as HealthCheckSuite;
|
|
1450
1393
|
if (!isPositiveMsDurationString(s.interval))
|
|
1451
1394
|
throw new ValidationError(
|
|
@@ -1455,11 +1398,7 @@ function validateHealthCheckSuite(
|
|
|
1455
1398
|
},
|
|
1456
1399
|
);
|
|
1457
1400
|
if (s.schedule !== undefined) {
|
|
1458
|
-
if (
|
|
1459
|
-
!s.schedule ||
|
|
1460
|
-
typeof s.schedule !== "object" ||
|
|
1461
|
-
Array.isArray(s.schedule)
|
|
1462
|
-
) {
|
|
1401
|
+
if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
|
|
1463
1402
|
throw new ValidationError(
|
|
1464
1403
|
`Provider "${providerId}" ${fieldPath}.schedule must be an object.`,
|
|
1465
1404
|
);
|
|
@@ -1469,11 +1408,7 @@ function validateHealthCheckSuite(
|
|
|
1469
1408
|
`Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`,
|
|
1470
1409
|
);
|
|
1471
1410
|
}
|
|
1472
|
-
rejectUnknownFields(
|
|
1473
|
-
s.schedule,
|
|
1474
|
-
new Set(["randomize"]),
|
|
1475
|
-
`${fieldPath}.schedule`,
|
|
1476
|
-
);
|
|
1411
|
+
rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
|
|
1477
1412
|
const randomize = Reflect.get(s.schedule, "randomize");
|
|
1478
1413
|
if (randomize !== undefined) {
|
|
1479
1414
|
validateScheduleRandomization(
|
|
@@ -1484,15 +1419,11 @@ function validateHealthCheckSuite(
|
|
|
1484
1419
|
}
|
|
1485
1420
|
}
|
|
1486
1421
|
if (s.timeoutMs !== undefined) {
|
|
1487
|
-
assertBoundedIntegerMs(
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1493
|
-
label: "timeout",
|
|
1494
|
-
},
|
|
1495
|
-
);
|
|
1422
|
+
assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
|
|
1423
|
+
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1424
|
+
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1425
|
+
label: "timeout",
|
|
1426
|
+
});
|
|
1496
1427
|
}
|
|
1497
1428
|
if (s.degradedThresholdMs !== undefined) {
|
|
1498
1429
|
assertBoundedIntegerMs(
|
|
@@ -1505,10 +1436,7 @@ function validateHealthCheckSuite(
|
|
|
1505
1436
|
},
|
|
1506
1437
|
);
|
|
1507
1438
|
}
|
|
1508
|
-
if (
|
|
1509
|
-
s.requiresConnection !== undefined &&
|
|
1510
|
-
typeof s.requiresConnection !== "boolean"
|
|
1511
|
-
)
|
|
1439
|
+
if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
|
|
1512
1440
|
throw new ValidationError(
|
|
1513
1441
|
`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`,
|
|
1514
1442
|
);
|
|
@@ -1540,14 +1468,8 @@ function validateHealthCheckUnsupported(
|
|
|
1540
1468
|
unsupported: unknown,
|
|
1541
1469
|
): void {
|
|
1542
1470
|
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
|
-
);
|
|
1471
|
+
if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
|
|
1472
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1551
1473
|
rejectUnknownFields(
|
|
1552
1474
|
unsupported as Record<string, unknown>,
|
|
1553
1475
|
HEALTH_CHECK_UNSUPPORTED_FIELDS,
|
|
@@ -1581,12 +1503,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
|
|
|
1581
1503
|
"steps",
|
|
1582
1504
|
"run",
|
|
1583
1505
|
]);
|
|
1584
|
-
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
|
|
1585
|
-
"kind",
|
|
1586
|
-
"interval",
|
|
1587
|
-
"jitter",
|
|
1588
|
-
"randomize",
|
|
1589
|
-
]);
|
|
1506
|
+
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
|
|
1590
1507
|
const HEALTH_JOURNEY_STEP_FIELDS = new Set([
|
|
1591
1508
|
"id",
|
|
1592
1509
|
"description",
|
|
@@ -1605,10 +1522,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
|
|
|
1605
1522
|
"minManualInterval",
|
|
1606
1523
|
"publicRationale",
|
|
1607
1524
|
]);
|
|
1608
|
-
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
|
|
1609
|
-
"enabled",
|
|
1610
|
-
"reason",
|
|
1611
|
-
]);
|
|
1525
|
+
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
|
|
1612
1526
|
const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
|
|
1613
1527
|
"enabled",
|
|
1614
1528
|
"requiresAcknowledgement",
|
|
@@ -1628,32 +1542,18 @@ function validateHealthJourneyManualTrigger(
|
|
|
1628
1542
|
manualTrigger: unknown,
|
|
1629
1543
|
): void {
|
|
1630
1544
|
const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
|
|
1631
|
-
if (
|
|
1632
|
-
!manualTrigger ||
|
|
1633
|
-
typeof manualTrigger !== "object" ||
|
|
1634
|
-
Array.isArray(manualTrigger)
|
|
1635
|
-
) {
|
|
1545
|
+
if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
|
|
1636
1546
|
throw new ValidationError(
|
|
1637
1547
|
`Provider "${providerId}" ${fieldPath} must be an object when present.`,
|
|
1638
1548
|
);
|
|
1639
1549
|
}
|
|
1640
|
-
rejectUnknownFields(
|
|
1641
|
-
manualTrigger,
|
|
1642
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS,
|
|
1643
|
-
fieldPath,
|
|
1644
|
-
);
|
|
1550
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
|
|
1645
1551
|
const enabled = Reflect.get(manualTrigger, "enabled");
|
|
1646
1552
|
if (typeof enabled !== "boolean") {
|
|
1647
|
-
throw new ValidationError(
|
|
1648
|
-
`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`,
|
|
1649
|
-
);
|
|
1553
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`);
|
|
1650
1554
|
}
|
|
1651
1555
|
if (enabled === false) {
|
|
1652
|
-
rejectUnknownFields(
|
|
1653
|
-
manualTrigger,
|
|
1654
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS,
|
|
1655
|
-
fieldPath,
|
|
1656
|
-
);
|
|
1556
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS, fieldPath);
|
|
1657
1557
|
if (
|
|
1658
1558
|
Reflect.get(manualTrigger, "reason") !== undefined &&
|
|
1659
1559
|
(typeof Reflect.get(manualTrigger, "reason") !== "string" ||
|
|
@@ -1665,25 +1565,15 @@ function validateHealthJourneyManualTrigger(
|
|
|
1665
1565
|
}
|
|
1666
1566
|
return;
|
|
1667
1567
|
}
|
|
1668
|
-
rejectUnknownFields(
|
|
1669
|
-
|
|
1670
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS,
|
|
1671
|
-
fieldPath,
|
|
1672
|
-
);
|
|
1673
|
-
const requiresAcknowledgement = Reflect.get(
|
|
1674
|
-
manualTrigger,
|
|
1675
|
-
"requiresAcknowledgement",
|
|
1676
|
-
);
|
|
1568
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS, fieldPath);
|
|
1569
|
+
const requiresAcknowledgement = Reflect.get(manualTrigger, "requiresAcknowledgement");
|
|
1677
1570
|
if (typeof requiresAcknowledgement !== "boolean") {
|
|
1678
1571
|
throw new ValidationError(
|
|
1679
1572
|
`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`,
|
|
1680
1573
|
);
|
|
1681
1574
|
}
|
|
1682
1575
|
const risk = Reflect.get(manualTrigger, "risk");
|
|
1683
|
-
if (
|
|
1684
|
-
typeof risk !== "string" ||
|
|
1685
|
-
!HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)
|
|
1686
|
-
) {
|
|
1576
|
+
if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
1687
1577
|
throw new ValidationError(
|
|
1688
1578
|
`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`,
|
|
1689
1579
|
);
|
|
@@ -1694,10 +1584,7 @@ function validateHealthJourneyManualTrigger(
|
|
|
1694
1584
|
);
|
|
1695
1585
|
}
|
|
1696
1586
|
const minManualInterval = Reflect.get(manualTrigger, "minManualInterval");
|
|
1697
|
-
assertIsoDuration(
|
|
1698
|
-
minManualInterval,
|
|
1699
|
-
`Provider "${providerId}" ${fieldPath}.minManualInterval`,
|
|
1700
|
-
);
|
|
1587
|
+
assertIsoDuration(minManualInterval, `Provider "${providerId}" ${fieldPath}.minManualInterval`);
|
|
1701
1588
|
if (isoDurationMs(minManualInterval) <= 0) {
|
|
1702
1589
|
throw new ValidationError(
|
|
1703
1590
|
`Provider "${providerId}" ${fieldPath}.minManualInterval must be a positive duration.`,
|
|
@@ -1728,18 +1615,14 @@ const SMS_ORIGIN_FIELDS_BY_KIND: Record<string, ReadonlySet<string>> = {
|
|
|
1728
1615
|
e164: new Set(["kind", "value", "display"]),
|
|
1729
1616
|
nationalServiceCode: new Set(["kind", "country", "value", "display"]),
|
|
1730
1617
|
};
|
|
1731
|
-
const DURATION_RE =
|
|
1732
|
-
/^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
|
|
1618
|
+
const DURATION_RE = /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
|
|
1733
1619
|
const E164_RE = /^\+[1-9]\d{1,14}$/;
|
|
1734
1620
|
const ISO_COUNTRY_RE = /^[A-Z]{2}$/;
|
|
1735
1621
|
const NATIONAL_SERVICE_CODE_RE = /^[0-9]{2,15}$/;
|
|
1736
1622
|
const BCP47_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
1737
1623
|
const JOURNEY_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
1738
1624
|
|
|
1739
|
-
function assertIsoDuration(
|
|
1740
|
-
value: unknown,
|
|
1741
|
-
fieldPath: string,
|
|
1742
|
-
): asserts value is string {
|
|
1625
|
+
function assertIsoDuration(value: unknown, fieldPath: string): asserts value is string {
|
|
1743
1626
|
if (typeof value !== "string" || !DURATION_RE.test(value)) {
|
|
1744
1627
|
throw new ValidationError(
|
|
1745
1628
|
`${fieldPath} must be an ISO 8601 duration for example PT8H or PT2M30S.`,
|
|
@@ -1754,15 +1637,10 @@ function isoDurationMs(value: string): number {
|
|
|
1754
1637
|
const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
|
|
1755
1638
|
const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
|
|
1756
1639
|
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
|
-
);
|
|
1640
|
+
return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
|
|
1760
1641
|
}
|
|
1761
1642
|
|
|
1762
|
-
function scheduleRandomizationMs(
|
|
1763
|
-
randomize: unknown,
|
|
1764
|
-
fieldPath: string,
|
|
1765
|
-
): number {
|
|
1643
|
+
function scheduleRandomizationMs(randomize: unknown, fieldPath: string): number {
|
|
1766
1644
|
const mode = Reflect.get(randomize as object, "mode");
|
|
1767
1645
|
switch (mode) {
|
|
1768
1646
|
case "centered": {
|
|
@@ -1776,9 +1654,7 @@ function scheduleRandomizationMs(
|
|
|
1776
1654
|
return isoDurationMs(maxDelay);
|
|
1777
1655
|
}
|
|
1778
1656
|
default:
|
|
1779
|
-
throw new ValidationError(
|
|
1780
|
-
`${fieldPath}.mode must be "centered" or "delayed".`,
|
|
1781
|
-
);
|
|
1657
|
+
throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
|
|
1782
1658
|
}
|
|
1783
1659
|
}
|
|
1784
1660
|
|
|
@@ -1792,25 +1668,18 @@ function validateScheduleRandomization(
|
|
|
1792
1668
|
}
|
|
1793
1669
|
const mode = Reflect.get(randomize, "mode");
|
|
1794
1670
|
const allowedFields =
|
|
1795
|
-
mode === "centered"
|
|
1796
|
-
? new Set(["mode", "maxOffset"])
|
|
1797
|
-
: new Set(["mode", "maxDelay"]);
|
|
1671
|
+
mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
|
|
1798
1672
|
rejectUnknownFields(randomize, allowedFields, fieldPath);
|
|
1799
1673
|
const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
|
|
1800
1674
|
if (offsetMs <= 0) {
|
|
1801
1675
|
throw new ValidationError(`${fieldPath} duration must be positive.`);
|
|
1802
1676
|
}
|
|
1803
1677
|
if (offsetMs >= intervalMs) {
|
|
1804
|
-
throw new ValidationError(
|
|
1805
|
-
`${fieldPath} duration must be shorter than schedule interval.`,
|
|
1806
|
-
);
|
|
1678
|
+
throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
|
|
1807
1679
|
}
|
|
1808
1680
|
}
|
|
1809
1681
|
|
|
1810
|
-
function assertIsoCountry(
|
|
1811
|
-
value: unknown,
|
|
1812
|
-
fieldPath: string,
|
|
1813
|
-
): asserts value is string {
|
|
1682
|
+
function assertIsoCountry(value: unknown, fieldPath: string): asserts value is string {
|
|
1814
1683
|
if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
|
|
1815
1684
|
throw new ValidationError(
|
|
1816
1685
|
`${fieldPath} must be an ISO 3166-1 alpha-2 country code for example KR.`,
|
|
@@ -1833,9 +1702,7 @@ function normalizeIntervalDuration(input: string): string {
|
|
|
1833
1702
|
? durationMs / 3_600_000
|
|
1834
1703
|
: durationMs / 86_400_000;
|
|
1835
1704
|
if (!Number.isInteger(amount) || amount <= 0) {
|
|
1836
|
-
throw new ValidationError(
|
|
1837
|
-
`Journey schedule interval must be a positive duration.`,
|
|
1838
|
-
);
|
|
1705
|
+
throw new ValidationError(`Journey schedule interval must be a positive duration.`);
|
|
1839
1706
|
}
|
|
1840
1707
|
if (unit === "s") return `PT${amount}S`;
|
|
1841
1708
|
if (unit === "m") return `PT${amount}M`;
|
|
@@ -1894,11 +1761,7 @@ function countCapturingGroups(pattern: RegExp): number {
|
|
|
1894
1761
|
if (inCharacterClass || char !== "(") continue;
|
|
1895
1762
|
const next = source[i + 1];
|
|
1896
1763
|
if (next === "?" && source[i + 2] !== "<") continue;
|
|
1897
|
-
if (
|
|
1898
|
-
next === "?" &&
|
|
1899
|
-
source[i + 2] === "<" &&
|
|
1900
|
-
(source[i + 3] === "=" || source[i + 3] === "!")
|
|
1901
|
-
)
|
|
1764
|
+
if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1902
1765
|
continue;
|
|
1903
1766
|
count += 1;
|
|
1904
1767
|
}
|
|
@@ -1917,9 +1780,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1917
1780
|
}
|
|
1918
1781
|
const kind = Reflect.get(origin, "kind");
|
|
1919
1782
|
if (kind !== "e164" && kind !== "nationalServiceCode") {
|
|
1920
|
-
throw new ValidationError(
|
|
1921
|
-
`${fieldPath}.kind must be "e164" or "nationalServiceCode".`,
|
|
1922
|
-
);
|
|
1783
|
+
throw new ValidationError(`${fieldPath}.kind must be "e164" or "nationalServiceCode".`);
|
|
1923
1784
|
}
|
|
1924
1785
|
rejectUnknownFields(origin, SMS_ORIGIN_FIELDS_BY_KIND[kind], fieldPath);
|
|
1925
1786
|
if (kind === "e164") {
|
|
@@ -1946,9 +1807,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1946
1807
|
Reflect.get(origin, "display") !== undefined &&
|
|
1947
1808
|
typeof Reflect.get(origin, "display") !== "string"
|
|
1948
1809
|
) {
|
|
1949
|
-
throw new ValidationError(
|
|
1950
|
-
`${fieldPath}.display must be a string when present.`,
|
|
1951
|
-
);
|
|
1810
|
+
throw new ValidationError(`${fieldPath}.display must be a string when present.`);
|
|
1952
1811
|
}
|
|
1953
1812
|
}
|
|
1954
1813
|
|
|
@@ -1962,9 +1821,7 @@ function validateSmsOtpMatcher(
|
|
|
1962
1821
|
rejectUnknownFields(matcher, SMS_OTP_MATCHER_FIELDS, fieldPath);
|
|
1963
1822
|
const matcherId = Reflect.get(matcher, "id");
|
|
1964
1823
|
if (typeof matcherId !== "string" || !JOURNEY_ID_RE.test(matcherId)) {
|
|
1965
|
-
throw new ValidationError(
|
|
1966
|
-
`${fieldPath}.id must be a kebab-case identifier.`,
|
|
1967
|
-
);
|
|
1824
|
+
throw new ValidationError(`${fieldPath}.id must be a kebab-case identifier.`);
|
|
1968
1825
|
}
|
|
1969
1826
|
assertIsoCountry(Reflect.get(matcher, "country"), `${fieldPath}.country`);
|
|
1970
1827
|
if (
|
|
@@ -1972,24 +1829,18 @@ function validateSmsOtpMatcher(
|
|
|
1972
1829
|
(typeof Reflect.get(matcher, "locale") !== "string" ||
|
|
1973
1830
|
!BCP47_RE.test(Reflect.get(matcher, "locale")))
|
|
1974
1831
|
) {
|
|
1975
|
-
throw new ValidationError(
|
|
1976
|
-
`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`,
|
|
1977
|
-
);
|
|
1832
|
+
throw new ValidationError(`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`);
|
|
1978
1833
|
}
|
|
1979
1834
|
if (
|
|
1980
1835
|
Reflect.get(matcher, "phoneNumber") !== undefined &&
|
|
1981
1836
|
(typeof Reflect.get(matcher, "phoneNumber") !== "string" ||
|
|
1982
1837
|
!E164_RE.test(Reflect.get(matcher, "phoneNumber")))
|
|
1983
1838
|
) {
|
|
1984
|
-
throw new ValidationError(
|
|
1985
|
-
`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`,
|
|
1986
|
-
);
|
|
1839
|
+
throw new ValidationError(`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`);
|
|
1987
1840
|
}
|
|
1988
1841
|
const origins = Reflect.get(matcher, "origins");
|
|
1989
1842
|
if (!Array.isArray(origins) || origins.length === 0) {
|
|
1990
|
-
throw new ValidationError(
|
|
1991
|
-
`${fieldPath}.origins must be a non-empty array.`,
|
|
1992
|
-
);
|
|
1843
|
+
throw new ValidationError(`${fieldPath}.origins must be a non-empty array.`);
|
|
1993
1844
|
}
|
|
1994
1845
|
for (const [index, origin] of origins.entries()) {
|
|
1995
1846
|
validateSmsOrigin(origin, `${fieldPath}.origins[${index}]`);
|
|
@@ -2010,10 +1861,7 @@ function validateSmsOtpMatcher(
|
|
|
2010
1861
|
);
|
|
2011
1862
|
}
|
|
2012
1863
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
2013
|
-
if (
|
|
2014
|
-
countCapturingGroups(regex) !== 1 &&
|
|
2015
|
-
Reflect.get(code, "capture") === undefined
|
|
2016
|
-
) {
|
|
1864
|
+
if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
|
|
2017
1865
|
throw new ValidationError(
|
|
2018
1866
|
`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`,
|
|
2019
1867
|
);
|
|
@@ -2023,20 +1871,12 @@ function validateSmsOtpMatcher(
|
|
|
2023
1871
|
typeof Reflect.get(code, "capture") !== "string" &&
|
|
2024
1872
|
typeof Reflect.get(code, "capture") !== "number"
|
|
2025
1873
|
) {
|
|
2026
|
-
throw new ValidationError(
|
|
2027
|
-
`${fieldPath}.code.capture must be a string or number when present.`,
|
|
2028
|
-
);
|
|
1874
|
+
throw new ValidationError(`${fieldPath}.code.capture must be a string or number when present.`);
|
|
2029
1875
|
}
|
|
2030
1876
|
assertIsoDuration(Reflect.get(matcher, "maxAge"), `${fieldPath}.maxAge`);
|
|
2031
|
-
assertIsoDuration(
|
|
2032
|
-
Reflect.get(matcher, "waitTimeout"),
|
|
2033
|
-
`${fieldPath}.waitTimeout`,
|
|
2034
|
-
);
|
|
1877
|
+
assertIsoDuration(Reflect.get(matcher, "waitTimeout"), `${fieldPath}.waitTimeout`);
|
|
2035
1878
|
if (Reflect.get(matcher, "clockSkew") !== undefined)
|
|
2036
|
-
assertIsoDuration(
|
|
2037
|
-
Reflect.get(matcher, "clockSkew"),
|
|
2038
|
-
`${fieldPath}.clockSkew`,
|
|
2039
|
-
);
|
|
1879
|
+
assertIsoDuration(Reflect.get(matcher, "clockSkew"), `${fieldPath}.clockSkew`);
|
|
2040
1880
|
}
|
|
2041
1881
|
|
|
2042
1882
|
export function defineSmsOtpMatcher(
|
|
@@ -2068,9 +1908,7 @@ export function defineSmsOtpMatcher(
|
|
|
2068
1908
|
return matcher;
|
|
2069
1909
|
}
|
|
2070
1910
|
|
|
2071
|
-
export function defineHealthJourney(
|
|
2072
|
-
config: HealthJourneyDefinition,
|
|
2073
|
-
): HealthJourneyDefinition {
|
|
1911
|
+
export function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition {
|
|
2074
1912
|
return config;
|
|
2075
1913
|
}
|
|
2076
1914
|
|
|
@@ -2081,22 +1919,15 @@ function validateHealthJourneySchedule(
|
|
|
2081
1919
|
): void {
|
|
2082
1920
|
const fieldPath = `healthJourneys.${journeyId}.schedule`;
|
|
2083
1921
|
if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) {
|
|
2084
|
-
throw new ValidationError(
|
|
2085
|
-
`Provider "${providerId}" ${fieldPath} must be an object.`,
|
|
2086
|
-
);
|
|
1922
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
2087
1923
|
}
|
|
2088
1924
|
rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
|
|
2089
1925
|
if (Reflect.get(schedule, "kind") !== "interval")
|
|
2090
|
-
throw new ValidationError(
|
|
2091
|
-
`Provider "${providerId}" ${fieldPath}.kind must be "interval".`,
|
|
2092
|
-
);
|
|
1926
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
|
|
2093
1927
|
const interval = Reflect.get(schedule, "interval");
|
|
2094
1928
|
assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
|
|
2095
1929
|
const randomize = Reflect.get(schedule, "randomize");
|
|
2096
|
-
if (
|
|
2097
|
-
Reflect.get(schedule, "jitter") !== undefined &&
|
|
2098
|
-
randomize !== undefined
|
|
2099
|
-
) {
|
|
1930
|
+
if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
|
|
2100
1931
|
throw new ValidationError(
|
|
2101
1932
|
`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`,
|
|
2102
1933
|
);
|
|
@@ -2123,17 +1954,13 @@ function validateHealthJourneys(
|
|
|
2123
1954
|
const covered = new Set<string>();
|
|
2124
1955
|
if (healthJourneys === undefined) return covered;
|
|
2125
1956
|
if (!Array.isArray(healthJourneys)) {
|
|
2126
|
-
throw new ValidationError(
|
|
2127
|
-
`Provider "${providerId}" healthJourneys must be an array.`,
|
|
2128
|
-
);
|
|
1957
|
+
throw new ValidationError(`Provider "${providerId}" healthJourneys must be an array.`);
|
|
2129
1958
|
}
|
|
2130
1959
|
const journeyIds = new Set<string>();
|
|
2131
1960
|
for (const [index, journey] of healthJourneys.entries()) {
|
|
2132
1961
|
const prefix = `healthJourneys[${index}]`;
|
|
2133
1962
|
if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
|
|
2134
|
-
throw new ValidationError(
|
|
2135
|
-
`Provider "${providerId}" ${prefix} must be an object.`,
|
|
2136
|
-
);
|
|
1963
|
+
throw new ValidationError(`Provider "${providerId}" ${prefix} must be an object.`);
|
|
2137
1964
|
}
|
|
2138
1965
|
rejectUnknownFields(journey, HEALTH_JOURNEY_FIELDS, prefix);
|
|
2139
1966
|
if (typeof journey.id !== "string" || !JOURNEY_ID_RE.test(journey.id)) {
|
|
@@ -2147,10 +1974,7 @@ function validateHealthJourneys(
|
|
|
2147
1974
|
);
|
|
2148
1975
|
journeyIds.add(journey.id);
|
|
2149
1976
|
validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
|
|
2150
|
-
if (
|
|
2151
|
-
!Array.isArray(journey.coversOperations) ||
|
|
2152
|
-
journey.coversOperations.length === 0
|
|
2153
|
-
) {
|
|
1977
|
+
if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
|
|
2154
1978
|
throw new ValidationError(
|
|
2155
1979
|
`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`,
|
|
2156
1980
|
);
|
|
@@ -2185,10 +2009,7 @@ function validateHealthJourneys(
|
|
|
2185
2009
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers must be an array.`,
|
|
2186
2010
|
);
|
|
2187
2011
|
for (const [matcherIndex, matcher] of journey.smsMatchers.entries()) {
|
|
2188
|
-
validateSmsOtpMatcher(
|
|
2189
|
-
matcher,
|
|
2190
|
-
`healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`,
|
|
2191
|
-
);
|
|
2012
|
+
validateSmsOtpMatcher(matcher, `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`);
|
|
2192
2013
|
if (matcherIds.has(matcher.id))
|
|
2193
2014
|
throw new ValidationError(
|
|
2194
2015
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers has duplicate matcher id "${matcher.id}".`,
|
|
@@ -2199,9 +2020,7 @@ function validateHealthJourneys(
|
|
|
2199
2020
|
for (const [stepIndex, step] of journey.steps.entries()) {
|
|
2200
2021
|
const stepPath = `healthJourneys.${journey.id}.steps[${stepIndex}]`;
|
|
2201
2022
|
if (!step || typeof step !== "object" || Array.isArray(step))
|
|
2202
|
-
throw new ValidationError(
|
|
2203
|
-
`Provider "${providerId}" ${stepPath} must be an object.`,
|
|
2204
|
-
);
|
|
2023
|
+
throw new ValidationError(`Provider "${providerId}" ${stepPath} must be an object.`);
|
|
2205
2024
|
rejectUnknownFields(step, HEALTH_JOURNEY_STEP_FIELDS, stepPath);
|
|
2206
2025
|
if (typeof step.id !== "string" || !JOURNEY_ID_RE.test(step.id))
|
|
2207
2026
|
throw new ValidationError(
|
|
@@ -2211,20 +2030,13 @@ function validateHealthJourneys(
|
|
|
2211
2030
|
throw new ValidationError(
|
|
2212
2031
|
`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`,
|
|
2213
2032
|
);
|
|
2214
|
-
if (
|
|
2215
|
-
step.usesSmsMatcher !== undefined &&
|
|
2216
|
-
!matcherIds.has(step.usesSmsMatcher)
|
|
2217
|
-
)
|
|
2033
|
+
if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
|
|
2218
2034
|
throw new ValidationError(
|
|
2219
2035
|
`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`,
|
|
2220
2036
|
);
|
|
2221
2037
|
}
|
|
2222
2038
|
if (journey.manualTrigger !== undefined)
|
|
2223
|
-
validateHealthJourneyManualTrigger(
|
|
2224
|
-
providerId,
|
|
2225
|
-
journey.id,
|
|
2226
|
-
journey.manualTrigger,
|
|
2227
|
-
);
|
|
2039
|
+
validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
|
|
2228
2040
|
if (journey.timeout !== undefined)
|
|
2229
2041
|
assertIsoDuration(
|
|
2230
2042
|
journey.timeout,
|
|
@@ -2270,23 +2082,10 @@ function validateOperationHealthChecks(
|
|
|
2270
2082
|
fix: `Remove either operations.${operationName}.healthCheck or operations.${operationName}.healthCheckUnsupported.`,
|
|
2271
2083
|
},
|
|
2272
2084
|
);
|
|
2273
|
-
if (hasCheck)
|
|
2274
|
-
validateHealthCheckSuite(
|
|
2275
|
-
providerId,
|
|
2276
|
-
operationName,
|
|
2277
|
-
operation.healthCheck,
|
|
2278
|
-
);
|
|
2085
|
+
if (hasCheck) validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
|
|
2279
2086
|
if (hasUnsupported)
|
|
2280
|
-
validateHealthCheckUnsupported(
|
|
2281
|
-
|
|
2282
|
-
operationName,
|
|
2283
|
-
operation.healthCheckUnsupported,
|
|
2284
|
-
);
|
|
2285
|
-
if (
|
|
2286
|
-
!hasCheck &&
|
|
2287
|
-
!hasUnsupported &&
|
|
2288
|
-
!journeyCoveredOperations.has(operationName)
|
|
2289
|
-
)
|
|
2087
|
+
validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
|
|
2088
|
+
if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
|
|
2290
2089
|
throw new ValidationError(
|
|
2291
2090
|
`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`,
|
|
2292
2091
|
{
|
|
@@ -2308,6 +2107,26 @@ function validateOperationFixtures(
|
|
|
2308
2107
|
fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
|
|
2309
2108
|
},
|
|
2310
2109
|
);
|
|
2110
|
+
if (operation.fixtures?.recordedAt !== undefined) {
|
|
2111
|
+
const recordedAt = operation.fixtures.recordedAt;
|
|
2112
|
+
const parsed =
|
|
2113
|
+
typeof recordedAt === "string"
|
|
2114
|
+
? new Date(`${recordedAt}T00:00:00.000Z`)
|
|
2115
|
+
: new Date(Number.NaN);
|
|
2116
|
+
const isCalendarDate =
|
|
2117
|
+
typeof recordedAt === "string" &&
|
|
2118
|
+
/^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
|
|
2119
|
+
!Number.isNaN(parsed.getTime()) &&
|
|
2120
|
+
parsed.toISOString().slice(0, 10) === recordedAt;
|
|
2121
|
+
const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
2122
|
+
if (!isCalendarDate || recordedAt > kstToday)
|
|
2123
|
+
throw new ValidationError(
|
|
2124
|
+
`Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`,
|
|
2125
|
+
{
|
|
2126
|
+
fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
|
|
2127
|
+
},
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2311
2130
|
if (operation.fixtures?.request !== undefined) {
|
|
2312
2131
|
const result = safeParseSchemaSync(
|
|
2313
2132
|
operation.input,
|
|
@@ -2341,6 +2160,44 @@ function validateOperationFixtures(
|
|
|
2341
2160
|
}
|
|
2342
2161
|
}
|
|
2343
2162
|
|
|
2163
|
+
function resolveOperationFixtureRequests<TOperations extends Record<string, ProviderOperation>>(
|
|
2164
|
+
operations: TOperations,
|
|
2165
|
+
): TOperations {
|
|
2166
|
+
let changed = false;
|
|
2167
|
+
const resolvedOperations = Object.fromEntries(
|
|
2168
|
+
Object.entries(operations).map(([operationName, operation]) => {
|
|
2169
|
+
if (operation.fixtures?.request === undefined) return [operationName, operation];
|
|
2170
|
+
const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
|
|
2171
|
+
if (request === operation.fixtures.request) return [operationName, operation];
|
|
2172
|
+
changed = true;
|
|
2173
|
+
return [
|
|
2174
|
+
operationName,
|
|
2175
|
+
{
|
|
2176
|
+
...operation,
|
|
2177
|
+
fixtures: {
|
|
2178
|
+
...operation.fixtures,
|
|
2179
|
+
request,
|
|
2180
|
+
},
|
|
2181
|
+
},
|
|
2182
|
+
];
|
|
2183
|
+
}),
|
|
2184
|
+
) as TOperations;
|
|
2185
|
+
return changed ? resolvedOperations : operations;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
/**
|
|
2189
|
+
* Shallow shape guard only: the `deployment` object is passed through
|
|
2190
|
+
* verbatim and deliberately not deep-validated by the SDK — the APIFuse
|
|
2191
|
+
* registry builder owns deployment validation and profile resolution.
|
|
2192
|
+
*/
|
|
2193
|
+
function validateProviderDeployment(providerId: string, deployment: unknown): void {
|
|
2194
|
+
if (deployment === undefined) return;
|
|
2195
|
+
if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
|
|
2196
|
+
throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
|
|
2197
|
+
fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2344
2201
|
export function defineProvider<
|
|
2345
2202
|
TOperations extends Record<string, ProviderOperation>,
|
|
2346
2203
|
TConfig extends ProviderConfig<TOperations>,
|
|
@@ -2348,20 +2205,19 @@ export function defineProvider<
|
|
|
2348
2205
|
config: TConfig & AuthStartNoInputGuard<TConfig>,
|
|
2349
2206
|
): ProviderDefinition & { operations: OperationMapConfig<TOperations> } {
|
|
2350
2207
|
validateProviderShape(config);
|
|
2208
|
+
const operations = resolveOperationFixtureRequests(config.operations);
|
|
2351
2209
|
if (!CONNECTOR_ID_REGEX.test(config.id))
|
|
2352
2210
|
throw new ProviderError(`Invalid provider id: "${config.id}"`, {
|
|
2353
2211
|
fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
|
|
2354
2212
|
});
|
|
2355
2213
|
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
|
-
);
|
|
2214
|
+
throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
|
|
2215
|
+
fix: "Add at least one operation to the operations object",
|
|
2216
|
+
});
|
|
2362
2217
|
validateOperationIds(config.id, config.operations);
|
|
2363
2218
|
validateOperationAnnotations(config.id, config.operations);
|
|
2364
2219
|
validateOperationObservability(config.id, config.operations);
|
|
2220
|
+
validateOperationErrorCodes(config.id, config.operations);
|
|
2365
2221
|
validateOperationTransports(config.id, config.operations);
|
|
2366
2222
|
validateOperationContracts(config.id, config.operations);
|
|
2367
2223
|
validateToolRouterMetadata(config.id, config.operations);
|
|
@@ -2370,11 +2226,7 @@ export function defineProvider<
|
|
|
2370
2226
|
config.operations,
|
|
2371
2227
|
config.healthJourneys,
|
|
2372
2228
|
);
|
|
2373
|
-
validateOperationHealthChecks(
|
|
2374
|
-
config.id,
|
|
2375
|
-
config.operations,
|
|
2376
|
-
journeyCoveredOperations,
|
|
2377
|
-
);
|
|
2229
|
+
validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
|
|
2378
2230
|
if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
|
|
2379
2231
|
throw new ValidationError(
|
|
2380
2232
|
`Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
|
|
@@ -2387,7 +2239,15 @@ export function defineProvider<
|
|
|
2387
2239
|
config.healthProbe ?? config.healthMonitor,
|
|
2388
2240
|
config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
|
|
2389
2241
|
);
|
|
2390
|
-
validateOperationFixtures(config.id,
|
|
2242
|
+
validateOperationFixtures(config.id, operations);
|
|
2243
|
+
validateProviderDeployment(config.id, config.deployment);
|
|
2244
|
+
try {
|
|
2245
|
+
validateNativeProviderConfig(config.native);
|
|
2246
|
+
} catch (error) {
|
|
2247
|
+
if (error instanceof NativeEgressPolicyValidationError)
|
|
2248
|
+
throw new ValidationError(error.message);
|
|
2249
|
+
throw error;
|
|
2250
|
+
}
|
|
2391
2251
|
validateProviderProxy(config);
|
|
2392
2252
|
validateProviderStt(config);
|
|
2393
2253
|
if (config.runtime === "browser" && !config.browser)
|
|
@@ -2406,7 +2266,11 @@ export function defineProvider<
|
|
|
2406
2266
|
id: config.id,
|
|
2407
2267
|
version: config.version,
|
|
2408
2268
|
runtime: config.runtime,
|
|
2269
|
+
// Verbatim passthrough: deployment validation and profile resolution
|
|
2270
|
+
// are owned by the APIFuse registry builder, not the SDK.
|
|
2271
|
+
deployment: config.deployment,
|
|
2409
2272
|
allowedHosts: config.allowedHosts,
|
|
2273
|
+
native: config.native,
|
|
2410
2274
|
stealth: config.stealth,
|
|
2411
2275
|
proxy: config.proxy,
|
|
2412
2276
|
stt: config.stt,
|
|
@@ -2418,7 +2282,7 @@ export function defineProvider<
|
|
|
2418
2282
|
credential: config.credential,
|
|
2419
2283
|
context: config.context,
|
|
2420
2284
|
meta: config.meta,
|
|
2421
|
-
operations
|
|
2285
|
+
operations,
|
|
2422
2286
|
// Transitional healthMonitor → healthProbe alias: mirror whichever field
|
|
2423
2287
|
// was declared onto both so old and new consumers keep working.
|
|
2424
2288
|
healthMonitor: config.healthMonitor ?? config.healthProbe,
|