@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.31
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 +493 -5
- package/CHANGELOG.md +131 -1
- package/README.md +52 -6
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +106 -62
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +63 -55
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +78 -82
- package/bin/apifuse-pack-types.ts +583 -0
- package/bin/apifuse-perf.ts +59 -140
- package/bin/apifuse-record.ts +698 -113
- package/bin/apifuse-submit-check.ts +517 -44
- 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.ts +1 -1
- package/dist/auth-turn/index.d.ts +4 -4
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +16 -2
- package/dist/auth.js +76 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +117 -29
- 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/cli/templates/provider/README.md.tpl +4 -4
- package/dist/config/loader.d.ts +176 -17
- package/dist/config/loader.js +434 -161
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +7 -14
- package/dist/contract-types.d.ts +3 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +6 -6
- package/dist/declaration-validation.d.ts +23 -0
- package/dist/declaration-validation.js +159 -0
- package/dist/define.d.ts +13 -1
- package/dist/define.js +391 -122
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +123 -0
- package/dist/errors.d.ts +19 -1
- package/dist/errors.js +41 -3
- 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 +50 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +6 -1
- package/dist/lint.js +370 -18
- 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 +13 -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 +3 -1
- package/dist/runtime/auth-flow.js +8 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +138 -40
- package/dist/runtime/cache.d.ts +2 -1
- package/dist/runtime/cache.js +173 -23
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +14 -1
- package/dist/runtime/choice.js +566 -101
- 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 +25 -3
- package/dist/runtime/http.d.ts +3 -2
- package/dist/runtime/http.js +517 -55
- 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 +371 -23
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -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 +58 -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 +5 -5
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +3 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +20 -0
- package/dist/runtime/resolver-vendors/browser.js +282 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +368 -0
- package/dist/runtime/resolver-vendors/types.d.ts +83 -0
- package/dist/runtime/resolver-vendors/types.js +69 -0
- package/dist/runtime/resolver.d.ts +59 -0
- package/dist/runtime/resolver.js +705 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +5 -2
- package/dist/runtime/state.js +280 -74
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +30 -5
- package/dist/runtime/stealth.js +523 -259
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +12 -27
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- 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 +117 -3
- package/dist/server/self-test.js +787 -151
- package/dist/server/serve-implementation.d.ts +210 -0
- package/dist/server/serve-implementation.js +2078 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1143
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -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 +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -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 +21 -21
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +3 -2
- package/dist/testing/index.js +3 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +488 -28
- package/dist/types.d.ts +566 -19
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +42 -7
- package/src/auth-turn/index.ts +2 -2
- package/src/auth.ts +146 -86
- package/src/ceremonies/index.ts +167 -92
- 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/cli/templates/provider/README.md.tpl +4 -4
- package/src/config/loader.ts +667 -289
- package/src/contract-serialization.ts +10 -18
- package/src/contract-types.ts +3 -2
- package/src/contract.ts +14 -28
- package/src/declaration-validation.ts +202 -0
- package/src/define.ts +631 -495
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +128 -0
- package/src/errors.ts +56 -11
- 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 +158 -44
- package/src/lint.ts +488 -154
- 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 +13 -7
- package/src/runtime/browser.ts +252 -207
- package/src/runtime/cache.ts +209 -81
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +758 -197
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +37 -19
- package/src/runtime/http.ts +645 -65
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +530 -67
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -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 +84 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +10 -13
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +18 -0
- package/src/runtime/resolver-shared.ts +17 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +408 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
- package/src/runtime/resolver-vendors/types.ts +173 -0
- package/src/runtime/resolver.ts +1060 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +399 -161
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +681 -295
- package/src/runtime/stt.ts +39 -113
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +6 -1
- package/src/server/index.ts +30 -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 +1030 -180
- package/src/server/serve-implementation.ts +3062 -0
- package/src/server/serve.ts +1 -1781
- package/src/server/types.ts +12 -13
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -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 +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +27 -33
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +16 -20
- package/src/testing/index.ts +11 -2
- package/src/testing/run.ts +668 -74
- package/src/types.ts +665 -35
- 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,14 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { validateFailClosedDeclaration } from "./declaration-validation.js";
|
|
4
|
+
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
5
|
+
import { ProviderError, ValidationError } from "./errors.js";
|
|
6
|
+
import {
|
|
7
|
+
NativeEgressPolicyValidationError,
|
|
8
|
+
validateNativeProviderConfig,
|
|
9
|
+
} from "./native-egress-policy.js";
|
|
10
|
+
import { safeParseSchemaSync } from "./schema.js";
|
|
11
|
+
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
5
12
|
import type {
|
|
6
13
|
AuthConfig,
|
|
7
14
|
BrowserEngine,
|
|
@@ -14,6 +21,7 @@ import type {
|
|
|
14
21
|
HealthJourneySchedule,
|
|
15
22
|
HealthScheduleRandomization,
|
|
16
23
|
InferSchemaOutput,
|
|
24
|
+
NativeProviderConfig,
|
|
17
25
|
OperationDefinition,
|
|
18
26
|
OperationHandlerResult,
|
|
19
27
|
OperationHttpStreamTransport,
|
|
@@ -21,10 +29,16 @@ import type {
|
|
|
21
29
|
OperationTransport,
|
|
22
30
|
OperationWebSocketTransport,
|
|
23
31
|
ProviderAccessConfig,
|
|
32
|
+
ProviderChallengeKind,
|
|
24
33
|
ProviderDefinition,
|
|
34
|
+
ProviderOcrConfig,
|
|
35
|
+
ProviderDeploymentOverrides,
|
|
25
36
|
ProviderHealthMonitorConfig,
|
|
26
37
|
ProviderProxyConfig,
|
|
38
|
+
ProviderProxyProvider,
|
|
27
39
|
ProviderPublicProfile,
|
|
40
|
+
ProviderResolverConfig,
|
|
41
|
+
ProviderResolverVendor,
|
|
28
42
|
ProviderReviewed,
|
|
29
43
|
ProviderSecretDeclaration,
|
|
30
44
|
ProviderStreamEvent,
|
|
@@ -32,7 +46,7 @@ import type {
|
|
|
32
46
|
SchemaLike,
|
|
33
47
|
SmsOtpMatcherDefinition,
|
|
34
48
|
StealthPlatform,
|
|
35
|
-
} from "./types";
|
|
49
|
+
} from "./types.js";
|
|
36
50
|
import {
|
|
37
51
|
HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
38
52
|
HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
|
|
@@ -48,7 +62,8 @@ import {
|
|
|
48
62
|
STREAM_IDLE_TIMEOUT_MS_MIN,
|
|
49
63
|
STREAM_MAX_DURATION_MS_MAX,
|
|
50
64
|
STREAM_MAX_DURATION_MS_MIN,
|
|
51
|
-
|
|
65
|
+
VALID_OPERATION_ERROR_STATUSES,
|
|
66
|
+
} from "./types.js";
|
|
52
67
|
|
|
53
68
|
type ProviderImplementationSourceAccess =
|
|
54
69
|
| "official_api"
|
|
@@ -79,45 +94,87 @@ const VALID_AUTH_MODES = [
|
|
|
79
94
|
"platform-managed",
|
|
80
95
|
"credentials",
|
|
81
96
|
"oauth2",
|
|
97
|
+
"oauth2_proxied",
|
|
82
98
|
] as const;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
const VALID_PROVIDER_PROXY_PROVIDERS = [
|
|
90
|
-
"smartproxy",
|
|
91
|
-
"decodo",
|
|
92
|
-
"custom",
|
|
99
|
+
const PROXIED_OAUTH_REQUIRED_FIELDS = [
|
|
100
|
+
"authorizeUrl",
|
|
101
|
+
"tokenUrl",
|
|
102
|
+
"customScheme",
|
|
103
|
+
"rewriteProfile",
|
|
104
|
+
"clientIdEnvKey",
|
|
93
105
|
] as const;
|
|
106
|
+
const PROXIED_OAUTH_ALLOWED_FIELDS = new Set([
|
|
107
|
+
...PROXIED_OAUTH_REQUIRED_FIELDS,
|
|
108
|
+
"pkce",
|
|
109
|
+
"authorizeParams",
|
|
110
|
+
"tokenParams",
|
|
111
|
+
]);
|
|
112
|
+
const PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS = new Set([
|
|
113
|
+
"client_id",
|
|
114
|
+
"response_type",
|
|
115
|
+
"state",
|
|
116
|
+
"code_challenge",
|
|
117
|
+
"code_challenge_method",
|
|
118
|
+
]);
|
|
119
|
+
const PROXIED_OAUTH_PROFILE_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
120
|
+
const PROXIED_OAUTH_ENV_KEY_REGEX = /^[A-Z][A-Z0-9_]*__[A-Z0-9_]+$/;
|
|
121
|
+
const CUSTOM_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+$/;
|
|
122
|
+
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
|
|
123
|
+
const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"] as const;
|
|
124
|
+
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"] as const;
|
|
94
125
|
const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
95
126
|
"request",
|
|
96
127
|
"operation",
|
|
97
128
|
"auth-flow",
|
|
98
129
|
"connection",
|
|
99
130
|
] as const;
|
|
131
|
+
const VALID_PROVIDER_OCR_MODES = ["optional", "required"] as const;
|
|
100
132
|
const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
|
|
133
|
+
function exhaustiveLiteralArray<TUnion extends string>() {
|
|
134
|
+
return <const TValues extends readonly TUnion[]>(
|
|
135
|
+
values: TValues,
|
|
136
|
+
..._missing: Exclude<TUnion, TValues[number]> extends never
|
|
137
|
+
? []
|
|
138
|
+
: ["Missing runtime values", Exclude<TUnion, TValues[number]>]
|
|
139
|
+
): TValues => values;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const VALID_PROVIDER_RESOLVER_VENDORS = exhaustiveLiteralArray<ProviderResolverVendor>()([
|
|
143
|
+
"browser",
|
|
144
|
+
"capsolver",
|
|
145
|
+
"capmonster",
|
|
146
|
+
"2captcha",
|
|
147
|
+
"custom",
|
|
148
|
+
] as const);
|
|
149
|
+
export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray<ProviderChallengeKind>()([
|
|
150
|
+
"turnstile",
|
|
151
|
+
"recaptcha_v2",
|
|
152
|
+
"recaptcha_v3",
|
|
153
|
+
"hcaptcha",
|
|
154
|
+
"cloudflare_interstitial",
|
|
155
|
+
"aws_waf",
|
|
156
|
+
"akamai_sec_cpt",
|
|
157
|
+
"akamai_sensor",
|
|
158
|
+
] as const);
|
|
101
159
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
160
|
+
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
161
|
+
const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
162
|
+
// Per-vendor provider-declared credential secrets. A required-mode chain must
|
|
163
|
+
// declare every secret of every credentialed vendor it names, so a missing
|
|
164
|
+
// credential fails at build/validation time rather than during a live outage: a
|
|
165
|
+
// declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
|
|
166
|
+
// exactly the failure class the multi-vendor chain exists to remove. Vendors
|
|
167
|
+
// absent from this map (the deprecated `custom`/`decodo` values have no managed
|
|
168
|
+
// adapter) impose no declaration requirement.
|
|
169
|
+
const VENDOR_REQUIRED_SECRETS: Partial<Record<ProviderProxyProvider, readonly string[]>> = {
|
|
170
|
+
smartproxy: [SMARTPROXY_APP_KEY_SECRET],
|
|
171
|
+
nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
|
|
172
|
+
};
|
|
102
173
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
103
174
|
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;
|
|
175
|
+
const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
|
|
176
|
+
const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"] as const;
|
|
177
|
+
const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"] as const;
|
|
121
178
|
const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
122
179
|
const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
123
180
|
|
|
@@ -135,18 +192,16 @@ function msDurationMs(value: string): number {
|
|
|
135
192
|
function parsePositiveMsDuration(value: string): number | undefined {
|
|
136
193
|
const trimmed = value.trim();
|
|
137
194
|
if (!MS_DURATION_PATTERN.test(trimmed)) return undefined;
|
|
138
|
-
const parsed = ms(
|
|
139
|
-
(trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue,
|
|
140
|
-
);
|
|
195
|
+
const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue);
|
|
141
196
|
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
142
197
|
return parsed;
|
|
143
198
|
}
|
|
144
199
|
|
|
145
200
|
type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
|
|
146
|
-
type OperationConfig<
|
|
147
|
-
TInput
|
|
148
|
-
|
|
149
|
-
>
|
|
201
|
+
type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
202
|
+
OperationDefinition<TInput, TOutput>,
|
|
203
|
+
"handler"
|
|
204
|
+
> & {
|
|
150
205
|
handler(
|
|
151
206
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
152
207
|
input: InferSchemaOutput<TInput>,
|
|
@@ -154,59 +209,44 @@ type OperationConfig<
|
|
|
154
209
|
| OperationHandlerResult<InferSchemaOutput<TOutput>>
|
|
155
210
|
| Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
|
|
156
211
|
};
|
|
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
|
-
> =
|
|
212
|
+
type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
|
|
213
|
+
[K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
|
|
214
|
+
? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
|
|
215
|
+
: never;
|
|
216
|
+
};
|
|
217
|
+
type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
|
|
170
218
|
| SseOperationConfig<TInput, TOutput>
|
|
171
219
|
| HttpStreamOperationConfig<TInput, TOutput>
|
|
172
220
|
| WebSocketOperationConfig<TInput, TOutput>;
|
|
173
|
-
type SseOperationConfig<
|
|
174
|
-
TInput
|
|
175
|
-
|
|
176
|
-
>
|
|
221
|
+
type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
222
|
+
OperationConfig<TInput, TOutput>,
|
|
223
|
+
"handler" | "transport"
|
|
224
|
+
> & {
|
|
177
225
|
transport: OperationSseTransport;
|
|
178
226
|
handler(
|
|
179
227
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
180
228
|
input: InferSchemaOutput<TInput>,
|
|
181
|
-
):
|
|
182
|
-
| AsyncIterable<ProviderStreamEvent>
|
|
183
|
-
| Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
229
|
+
): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
184
230
|
};
|
|
185
|
-
type HttpStreamOperationConfig<
|
|
186
|
-
TInput
|
|
187
|
-
|
|
188
|
-
>
|
|
231
|
+
type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
232
|
+
OperationConfig<TInput, TOutput>,
|
|
233
|
+
"handler" | "transport"
|
|
234
|
+
> & {
|
|
189
235
|
transport: OperationHttpStreamTransport;
|
|
190
236
|
handler(
|
|
191
237
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
192
238
|
input: InferSchemaOutput<TInput>,
|
|
193
|
-
):
|
|
194
|
-
| Response
|
|
195
|
-
| ReadableStream<Uint8Array>
|
|
196
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
239
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
197
240
|
};
|
|
198
|
-
type WebSocketOperationConfig<
|
|
199
|
-
TInput
|
|
200
|
-
|
|
201
|
-
>
|
|
241
|
+
type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
242
|
+
OperationConfig<TInput, TOutput>,
|
|
243
|
+
"handler" | "transport"
|
|
244
|
+
> & {
|
|
202
245
|
transport: OperationWebSocketTransport;
|
|
203
246
|
handler(
|
|
204
247
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
205
248
|
input: InferSchemaOutput<TInput>,
|
|
206
|
-
):
|
|
207
|
-
| Response
|
|
208
|
-
| ReadableStream<Uint8Array>
|
|
209
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
249
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
210
250
|
};
|
|
211
251
|
|
|
212
252
|
type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
@@ -221,19 +261,27 @@ type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
|
221
261
|
: unknown
|
|
222
262
|
: unknown;
|
|
223
263
|
|
|
224
|
-
export interface ProviderConfig<
|
|
225
|
-
TOperations extends Record<string, ProviderOperation>,
|
|
226
|
-
> {
|
|
264
|
+
export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
|
|
227
265
|
id: string;
|
|
228
266
|
version: string;
|
|
229
267
|
runtime: "standard" | "shared" | "browser";
|
|
268
|
+
/**
|
|
269
|
+
* Optional deployment overrides, passed through verbatim onto the returned
|
|
270
|
+
* provider definition. The SDK types this field but does not deep-validate
|
|
271
|
+
* it — the APIFuse registry builder owns deployment validation and
|
|
272
|
+
* resolves omitted fields against the runtime deployment profiles.
|
|
273
|
+
*/
|
|
274
|
+
deployment?: ProviderDeploymentOverrides;
|
|
230
275
|
allowedHosts?: string[];
|
|
276
|
+
native?: NativeProviderConfig;
|
|
231
277
|
stealth?: {
|
|
232
278
|
profile: string;
|
|
233
279
|
platform: StealthPlatform;
|
|
234
280
|
};
|
|
235
281
|
proxy?: ProviderProxyConfig;
|
|
282
|
+
ocr?: ProviderOcrConfig;
|
|
236
283
|
stt?: ProviderSttConfig;
|
|
284
|
+
resolver?: ProviderResolverConfig;
|
|
237
285
|
browser?: { engine: BrowserEngine };
|
|
238
286
|
auth?: AuthConfig;
|
|
239
287
|
reviewed?: ProviderReviewed;
|
|
@@ -270,35 +318,24 @@ export interface ProviderConfig<
|
|
|
270
318
|
}
|
|
271
319
|
|
|
272
320
|
/** Define one provider operation with schema-driven handler inference. */
|
|
273
|
-
export function defineOperation<
|
|
274
|
-
TInput extends SchemaLike,
|
|
275
|
-
TOutput extends SchemaLike,
|
|
276
|
-
>(
|
|
321
|
+
export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
277
322
|
operation: OperationConfig<TInput, TOutput>,
|
|
278
323
|
): OperationDefinition<TInput, TOutput> {
|
|
279
324
|
return operation;
|
|
280
325
|
}
|
|
281
326
|
|
|
282
327
|
/** Define a non-JSON provider operation with explicit transport metadata. */
|
|
283
|
-
export function defineStreamOperation<
|
|
284
|
-
TInput extends SchemaLike,
|
|
285
|
-
TOutput extends SchemaLike,
|
|
286
|
-
>(
|
|
328
|
+
export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
287
329
|
operation: StreamOperationConfig<TInput, TOutput>,
|
|
288
330
|
): OperationDefinition<TInput, TOutput> {
|
|
289
331
|
return operation;
|
|
290
332
|
}
|
|
291
333
|
|
|
292
|
-
function assertObjectConfig(
|
|
293
|
-
value: unknown,
|
|
294
|
-
): asserts value is Record<string, unknown> {
|
|
334
|
+
function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
|
|
295
335
|
if (!value || typeof value !== "object") {
|
|
296
|
-
throw new ProviderError(
|
|
297
|
-
"defineProvider
|
|
298
|
-
|
|
299
|
-
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
300
|
-
},
|
|
301
|
-
);
|
|
336
|
+
throw new ProviderError("defineProvider config must be an object. Offending field: config", {
|
|
337
|
+
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
338
|
+
});
|
|
302
339
|
}
|
|
303
340
|
}
|
|
304
341
|
function assertRequiredField(
|
|
@@ -328,6 +365,111 @@ function assertLiteralField<TValue extends string>(
|
|
|
328
365
|
);
|
|
329
366
|
}
|
|
330
367
|
}
|
|
368
|
+
|
|
369
|
+
function validateProxiedOAuthParams(
|
|
370
|
+
value: unknown,
|
|
371
|
+
field: "authorizeParams" | "tokenParams",
|
|
372
|
+
providerId: string,
|
|
373
|
+
): void {
|
|
374
|
+
if (value === undefined) return;
|
|
375
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
376
|
+
throw new ValidationError(
|
|
377
|
+
`Provider "${providerId}" auth.proxied.${field} must be an object of string values.`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
for (const [key, paramValue] of Object.entries(value)) {
|
|
381
|
+
if (!key.trim() || typeof paramValue !== "string") {
|
|
382
|
+
throw new ValidationError(
|
|
383
|
+
`Provider "${providerId}" auth.proxied.${field} must contain non-empty keys and string values.`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (
|
|
387
|
+
field === "authorizeParams" &&
|
|
388
|
+
PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())
|
|
389
|
+
) {
|
|
390
|
+
throw new ValidationError(
|
|
391
|
+
`Provider "${providerId}" auth.proxied.authorizeParams cannot override reserved parameter "${key}".`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function validateProxiedOAuthAuth(auth: Record<string, unknown>, providerId: string): void {
|
|
398
|
+
const proxied = auth.proxied;
|
|
399
|
+
if (auth.mode !== "oauth2_proxied") {
|
|
400
|
+
if (proxied !== undefined) {
|
|
401
|
+
throw new ValidationError(
|
|
402
|
+
`Provider "${providerId}" auth.proxied is only valid when auth.mode is "oauth2_proxied".`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (!proxied || typeof proxied !== "object" || Array.isArray(proxied)) {
|
|
408
|
+
throw new ValidationError(
|
|
409
|
+
`Provider "${providerId}" with auth.mode "oauth2_proxied" must declare auth.proxied.`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
const config = Object.fromEntries(Object.entries(proxied));
|
|
413
|
+
for (const key of Object.keys(config)) {
|
|
414
|
+
if (!PROXIED_OAUTH_ALLOWED_FIELDS.has(key)) {
|
|
415
|
+
throw new ValidationError(
|
|
416
|
+
`Provider "${providerId}" has unknown auth.proxied field "${key}".`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
for (const field of PROXIED_OAUTH_REQUIRED_FIELDS) {
|
|
421
|
+
if (typeof config[field] !== "string" || !config[field].trim()) {
|
|
422
|
+
throw new ValidationError(
|
|
423
|
+
`Provider "${providerId}" auth.proxied.${field} must be a non-empty string.`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const field of ["authorizeUrl", "tokenUrl"] as const) {
|
|
428
|
+
try {
|
|
429
|
+
const endpoint = new URL(String(config[field]));
|
|
430
|
+
if (
|
|
431
|
+
endpoint.protocol !== "https:" ||
|
|
432
|
+
endpoint.username ||
|
|
433
|
+
endpoint.password ||
|
|
434
|
+
endpoint.hash
|
|
435
|
+
) {
|
|
436
|
+
throw new Error("invalid endpoint");
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
throw new ValidationError(
|
|
440
|
+
`Provider "${providerId}" auth.proxied.${field} must be an absolute HTTPS URL without credentials or a fragment.`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const customScheme = String(config.customScheme);
|
|
445
|
+
if (
|
|
446
|
+
!CUSTOM_SCHEME_REGEX.test(customScheme) ||
|
|
447
|
+
customScheme.toLowerCase().startsWith("http://") ||
|
|
448
|
+
customScheme.toLowerCase().startsWith("https://")
|
|
449
|
+
) {
|
|
450
|
+
throw new ValidationError(
|
|
451
|
+
`Provider "${providerId}" auth.proxied.customScheme must be a non-HTTP custom-scheme URL prefix.`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
if (!PROXIED_OAUTH_PROFILE_REGEX.test(String(config.rewriteProfile))) {
|
|
455
|
+
throw new ValidationError(
|
|
456
|
+
`Provider "${providerId}" auth.proxied.rewriteProfile must be a kebab-case profile name.`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
if (!PROXIED_OAUTH_ENV_KEY_REGEX.test(String(config.clientIdEnvKey))) {
|
|
460
|
+
throw new ValidationError(
|
|
461
|
+
`Provider "${providerId}" auth.proxied.clientIdEnvKey must be an APIFuse-style uppercase environment key.`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (config.pkce !== undefined && config.pkce !== "S256" && config.pkce !== "none") {
|
|
465
|
+
throw new ValidationError(
|
|
466
|
+
`Provider "${providerId}" auth.proxied.pkce must be "S256" or "none".`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
validateProxiedOAuthParams(config.authorizeParams, "authorizeParams", providerId);
|
|
470
|
+
validateProxiedOAuthParams(config.tokenParams, "tokenParams", providerId);
|
|
471
|
+
}
|
|
472
|
+
|
|
331
473
|
function validateProviderShape(config: unknown): void {
|
|
332
474
|
assertObjectConfig(config);
|
|
333
475
|
assertRequiredField(config, "id");
|
|
@@ -336,25 +478,13 @@ function validateProviderShape(config: unknown): void {
|
|
|
336
478
|
assertRequiredField(config, "meta", String(config.id));
|
|
337
479
|
assertRequiredField(config, "operations", String(config.id));
|
|
338
480
|
if (typeof config.runtime === "string")
|
|
339
|
-
assertLiteralField(
|
|
340
|
-
config.runtime,
|
|
341
|
-
"runtime",
|
|
342
|
-
VALID_RUNTIMES,
|
|
343
|
-
String(config.id),
|
|
344
|
-
);
|
|
481
|
+
assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
|
|
345
482
|
const auth = config.auth;
|
|
346
|
-
if (
|
|
347
|
-
auth
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
)
|
|
352
|
-
assertLiteralField(
|
|
353
|
-
auth.mode,
|
|
354
|
-
"auth.mode",
|
|
355
|
-
VALID_AUTH_MODES,
|
|
356
|
-
String(config.id),
|
|
357
|
-
);
|
|
483
|
+
if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
|
|
484
|
+
assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
|
|
485
|
+
if (auth && typeof auth === "object" && !Array.isArray(auth)) {
|
|
486
|
+
validateProxiedOAuthAuth(Object.fromEntries(Object.entries(auth)), String(config.id));
|
|
487
|
+
}
|
|
358
488
|
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
359
489
|
throw new ProviderError(
|
|
360
490
|
`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
|
|
@@ -390,9 +520,7 @@ function validateProviderShape(config: unknown): void {
|
|
|
390
520
|
},
|
|
391
521
|
);
|
|
392
522
|
}
|
|
393
|
-
const accessRecord: Record<string, unknown> = Object.fromEntries(
|
|
394
|
-
Object.entries(access),
|
|
395
|
-
);
|
|
523
|
+
const accessRecord: Record<string, unknown> = Object.fromEntries(Object.entries(access));
|
|
396
524
|
for (const key of Object.keys(accessRecord)) {
|
|
397
525
|
if (key !== "visibility") {
|
|
398
526
|
throw new ValidationError(`Unknown field "${key}" on access.`, {
|
|
@@ -437,31 +565,26 @@ function validateProviderProxy(config: {
|
|
|
437
565
|
},
|
|
438
566
|
);
|
|
439
567
|
}
|
|
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
|
-
);
|
|
568
|
+
rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
|
|
569
|
+
assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
|
|
451
570
|
if (proxy.provider !== undefined) {
|
|
452
|
-
assertLiteralField(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
571
|
+
assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
572
|
+
}
|
|
573
|
+
if (proxy.providers !== undefined) {
|
|
574
|
+
if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
|
|
575
|
+
throw new ValidationError(
|
|
576
|
+
`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`,
|
|
577
|
+
{
|
|
578
|
+
fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
|
|
579
|
+
},
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
for (const vendor of proxy.providers) {
|
|
583
|
+
assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
584
|
+
}
|
|
458
585
|
}
|
|
459
586
|
if (proxy.geo !== undefined) {
|
|
460
|
-
if (
|
|
461
|
-
!proxy.geo ||
|
|
462
|
-
typeof proxy.geo !== "object" ||
|
|
463
|
-
Array.isArray(proxy.geo)
|
|
464
|
-
) {
|
|
587
|
+
if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
|
|
465
588
|
throw new ValidationError(
|
|
466
589
|
`Provider "${config.id}" has invalid proxy.geo: must be an object.`,
|
|
467
590
|
{
|
|
@@ -469,11 +592,7 @@ function validateProviderProxy(config: {
|
|
|
469
592
|
},
|
|
470
593
|
);
|
|
471
594
|
}
|
|
472
|
-
rejectUnknownFields(
|
|
473
|
-
proxy.geo,
|
|
474
|
-
new Set(["country", "subdivision", "city"]),
|
|
475
|
-
"proxy.geo",
|
|
476
|
-
);
|
|
595
|
+
rejectUnknownFields(proxy.geo, new Set(["country", "subdivision", "city"]), "proxy.geo");
|
|
477
596
|
if (proxy.geo.country !== undefined) {
|
|
478
597
|
assertIsoCountry(proxy.geo.country, "proxy.geo.country");
|
|
479
598
|
}
|
|
@@ -487,11 +606,7 @@ function validateProviderProxy(config: {
|
|
|
487
606
|
}
|
|
488
607
|
}
|
|
489
608
|
if (proxy.session !== undefined) {
|
|
490
|
-
if (
|
|
491
|
-
!proxy.session ||
|
|
492
|
-
typeof proxy.session !== "object" ||
|
|
493
|
-
Array.isArray(proxy.session)
|
|
494
|
-
) {
|
|
609
|
+
if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
|
|
495
610
|
throw new ValidationError(
|
|
496
611
|
`Provider "${config.id}" has invalid proxy.session: must be an object.`,
|
|
497
612
|
{
|
|
@@ -501,7 +616,7 @@ function validateProviderProxy(config: {
|
|
|
501
616
|
}
|
|
502
617
|
rejectUnknownFields(
|
|
503
618
|
proxy.session,
|
|
504
|
-
new Set(["affinity", "lifetimeMinutes", "poolSize"]),
|
|
619
|
+
new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]),
|
|
505
620
|
"proxy.session",
|
|
506
621
|
);
|
|
507
622
|
if (proxy.session.affinity !== undefined) {
|
|
@@ -513,56 +628,195 @@ function validateProviderProxy(config: {
|
|
|
513
628
|
);
|
|
514
629
|
}
|
|
515
630
|
const lifetime = proxy.session.lifetimeMinutes;
|
|
516
|
-
if (
|
|
517
|
-
lifetime !== undefined &&
|
|
518
|
-
(!Number.isFinite(lifetime) || lifetime <= 0)
|
|
519
|
-
) {
|
|
631
|
+
if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
520
632
|
throw new ValidationError(
|
|
521
633
|
`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`,
|
|
522
634
|
);
|
|
523
635
|
}
|
|
524
636
|
const poolSize = proxy.session.poolSize;
|
|
637
|
+
if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
638
|
+
throw new ValidationError(
|
|
639
|
+
`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
const drainLeadSeconds = proxy.session.drainLeadSeconds;
|
|
525
643
|
if (
|
|
526
|
-
|
|
527
|
-
(!Number.
|
|
644
|
+
drainLeadSeconds !== undefined &&
|
|
645
|
+
(!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)
|
|
528
646
|
) {
|
|
529
647
|
throw new ValidationError(
|
|
530
|
-
`Provider "${config.id}" has invalid proxy.session.
|
|
648
|
+
`Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`,
|
|
649
|
+
{
|
|
650
|
+
fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
|
|
651
|
+
},
|
|
531
652
|
);
|
|
532
653
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
654
|
+
// A drain lead longer than the sticky lifetime would fire the expiring
|
|
655
|
+
// event before the session is even established, so the provider would
|
|
656
|
+
// never get a usable window. Reject the contradiction at build time.
|
|
657
|
+
if (
|
|
658
|
+
drainLeadSeconds !== undefined &&
|
|
659
|
+
lifetime !== undefined &&
|
|
660
|
+
Number.isFinite(lifetime) &&
|
|
661
|
+
drainLeadSeconds >= lifetime * 60
|
|
662
|
+
) {
|
|
540
663
|
throw new ValidationError(
|
|
541
|
-
`Provider "${config.id}"
|
|
664
|
+
`Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`,
|
|
542
665
|
{
|
|
543
|
-
fix: `
|
|
666
|
+
fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
|
|
544
667
|
},
|
|
545
668
|
);
|
|
546
669
|
}
|
|
547
670
|
}
|
|
671
|
+
// Every credentialed vendor in a required-mode chain must declare its
|
|
672
|
+
// provider secret(s) so a missing credential fails at build/validation time,
|
|
673
|
+
// not during a live outage. This covers the fallback legs too (not just the
|
|
674
|
+
// first vendor): a declared-but-uncredentialed nodemaven fallback would leave
|
|
675
|
+
// the chain silently down to a single vendor, reintroducing the SPOF the chain
|
|
676
|
+
// removes.
|
|
677
|
+
const vendorChain =
|
|
678
|
+
proxy.providers && proxy.providers.length > 0
|
|
679
|
+
? proxy.providers
|
|
680
|
+
: proxy.provider
|
|
681
|
+
? [proxy.provider]
|
|
682
|
+
: [];
|
|
683
|
+
if (proxy.mode === "required") {
|
|
684
|
+
for (const vendor of vendorChain) {
|
|
685
|
+
const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
|
|
686
|
+
if (!requiredSecrets) continue;
|
|
687
|
+
for (const secretName of requiredSecrets) {
|
|
688
|
+
// Match the canonical runtime gate (assertRequiredSecretsPresent /
|
|
689
|
+
// listMissingRequiredSecrets), which enforces only `required === true`
|
|
690
|
+
// declarations. A declaration that omits `required` (defaulting to
|
|
691
|
+
// optional) is skipped at runtime, so accepting it here would pass
|
|
692
|
+
// validation while leaving the credential unenforced until proxy
|
|
693
|
+
// resolution during a live request — the fail-open gap this check exists
|
|
694
|
+
// to close.
|
|
695
|
+
const declared = config.secrets?.some(
|
|
696
|
+
(secret) => secret.name === secretName && secret.required === true,
|
|
697
|
+
);
|
|
698
|
+
if (!declared) {
|
|
699
|
+
throw new ValidationError(
|
|
700
|
+
`Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`,
|
|
701
|
+
{
|
|
702
|
+
fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
|
|
703
|
+
},
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// `decodo`/`custom` are deprecated vendor values (string-union members, so the
|
|
710
|
+
// @deprecated symbol gate can't catch them — warn at validation time instead).
|
|
711
|
+
const deprecatedVendors = vendorChain.filter(
|
|
712
|
+
(vendor) => vendor === "decodo" || vendor === "custom",
|
|
713
|
+
);
|
|
714
|
+
if (
|
|
715
|
+
proxy.mode === "required" &&
|
|
716
|
+
vendorChain.length > 0 &&
|
|
717
|
+
deprecatedVendors.length === vendorChain.length
|
|
718
|
+
) {
|
|
719
|
+
throw new ValidationError(
|
|
720
|
+
`Provider "${config.id}" requires proxy egress but declares only deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}.`,
|
|
721
|
+
{
|
|
722
|
+
fix: `Use proxy.provider or proxy.providers with "smartproxy" and/or "nodemaven".`,
|
|
723
|
+
},
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
if (deprecatedVendors.length > 0) {
|
|
727
|
+
console.warn(
|
|
728
|
+
`[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven".`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
548
731
|
}
|
|
549
732
|
|
|
550
|
-
function validateProviderStt(config: {
|
|
551
|
-
id: string;
|
|
552
|
-
stt?: ProviderSttConfig;
|
|
553
|
-
}): void {
|
|
733
|
+
function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): void {
|
|
554
734
|
const stt = config.stt;
|
|
555
735
|
if (stt === undefined) return;
|
|
556
736
|
if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
|
|
557
|
-
throw new ValidationError(
|
|
558
|
-
`
|
|
559
|
-
|
|
560
|
-
);
|
|
737
|
+
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
738
|
+
fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
|
|
739
|
+
});
|
|
561
740
|
}
|
|
562
741
|
rejectUnknownFields(stt, new Set(["mode"]), "stt");
|
|
563
742
|
assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
|
|
564
743
|
}
|
|
565
744
|
|
|
745
|
+
function validateProviderOcr(config: { id: string; ocr?: ProviderOcrConfig }): void {
|
|
746
|
+
const ocr = config.ocr;
|
|
747
|
+
if (ocr === undefined) return;
|
|
748
|
+
if (!ocr || typeof ocr !== "object" || Array.isArray(ocr)) {
|
|
749
|
+
throw new ValidationError(`Provider "${config.id}" has invalid ocr: must be an object.`, {
|
|
750
|
+
fix: `Use ocr: { mode: "required" } or ocr: { mode: "optional" }.`,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
rejectUnknownFields(ocr, new Set(["mode"]), "ocr");
|
|
754
|
+
assertLiteralField(ocr.mode, "ocr.mode", VALID_PROVIDER_OCR_MODES, config.id);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function validateProviderResolver(config: { id: string; resolver?: ProviderResolverConfig }): void {
|
|
758
|
+
const resolver = config.resolver;
|
|
759
|
+
if (resolver === undefined) return;
|
|
760
|
+
if (!resolver || typeof resolver !== "object" || Array.isArray(resolver)) {
|
|
761
|
+
throw new ValidationError(`Provider "${config.id}" has invalid resolver: must be an object.`, {
|
|
762
|
+
fix: `Set resolver for provider "${config.id}" to { vendors: ["2captcha"], kinds: ["turnstile"] }.`,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
rejectUnknownFields(
|
|
766
|
+
resolver,
|
|
767
|
+
new Set(["vendors", "kinds", "clientProfile"]),
|
|
768
|
+
"resolver",
|
|
769
|
+
config.id,
|
|
770
|
+
);
|
|
771
|
+
validateResolverLiteralArray(
|
|
772
|
+
resolver.vendors,
|
|
773
|
+
"resolver.vendors",
|
|
774
|
+
VALID_PROVIDER_RESOLVER_VENDORS,
|
|
775
|
+
config.id,
|
|
776
|
+
);
|
|
777
|
+
validateResolverLiteralArray(
|
|
778
|
+
resolver.kinds,
|
|
779
|
+
"resolver.kinds",
|
|
780
|
+
VALID_PROVIDER_CHALLENGE_KINDS,
|
|
781
|
+
config.id,
|
|
782
|
+
);
|
|
783
|
+
if (
|
|
784
|
+
resolver.clientProfile !== undefined &&
|
|
785
|
+
(typeof resolver.clientProfile !== "string" || !resolver.clientProfile.trim())
|
|
786
|
+
) {
|
|
787
|
+
throw new ValidationError(
|
|
788
|
+
`Provider "${config.id}" has invalid resolver.clientProfile: must be a non-empty string.`,
|
|
789
|
+
{
|
|
790
|
+
fix: `Set resolver.clientProfile for provider "${config.id}" to a transport-owned profile name.`,
|
|
791
|
+
},
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function validateResolverLiteralArray<TValue extends string>(
|
|
797
|
+
value: readonly TValue[],
|
|
798
|
+
field: string,
|
|
799
|
+
validValues: readonly TValue[],
|
|
800
|
+
providerId: string,
|
|
801
|
+
): void {
|
|
802
|
+
if (!Array.isArray(value)) {
|
|
803
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an array.`, {
|
|
804
|
+
fix: `Set ${field} for provider "${providerId}" to an array containing only: ${validValues.join(", ")}.`,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
for (const [index, item] of value.entries()) {
|
|
808
|
+
if (typeof item === "string" && validValues.some((validValue) => validValue === item)) {
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
throw new ValidationError(
|
|
812
|
+
`Provider "${providerId}" has invalid ${field}[${index}]: ${JSON.stringify(item)}. Expected one of: ${validValues.join(", ")}`,
|
|
813
|
+
{
|
|
814
|
+
fix: `Set ${field}[${index}] for provider "${providerId}" to one of ${validValues.map((validValue) => `"${validValue}"`).join(", ")}.`,
|
|
815
|
+
},
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
566
820
|
function validateOperationIds(
|
|
567
821
|
providerId: string,
|
|
568
822
|
operations: Record<string, ProviderOperation>,
|
|
@@ -584,16 +838,9 @@ function validateOperationIds(
|
|
|
584
838
|
);
|
|
585
839
|
}
|
|
586
840
|
}
|
|
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;
|
|
841
|
+
const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
842
|
+
const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
843
|
+
const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"] as const;
|
|
597
844
|
|
|
598
845
|
function assertNonEmptyString(
|
|
599
846
|
value: unknown,
|
|
@@ -624,10 +871,7 @@ function validateToolRouterMetadata(
|
|
|
624
871
|
},
|
|
625
872
|
);
|
|
626
873
|
}
|
|
627
|
-
if (
|
|
628
|
-
toolRouter.name !== undefined &&
|
|
629
|
-
!MCP_TOOL_NAME_REGEX.test(toolRouter.name)
|
|
630
|
-
) {
|
|
874
|
+
if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
631
875
|
throw new ValidationError(
|
|
632
876
|
`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`,
|
|
633
877
|
{
|
|
@@ -699,10 +943,7 @@ function validateOperationContracts(
|
|
|
699
943
|
providerId,
|
|
700
944
|
);
|
|
701
945
|
}
|
|
702
|
-
if (
|
|
703
|
-
contract.lifecycle === "deprecated" ||
|
|
704
|
-
contract.lifecycle === "removed"
|
|
705
|
-
) {
|
|
946
|
+
if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
|
|
706
947
|
if (!contract.deprecation || typeof contract.deprecation !== "object") {
|
|
707
948
|
throw new ValidationError(
|
|
708
949
|
`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`,
|
|
@@ -750,10 +991,7 @@ function validateOperationAnnotations(
|
|
|
750
991
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
|
|
751
992
|
},
|
|
752
993
|
);
|
|
753
|
-
if (
|
|
754
|
-
timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
|
|
755
|
-
timeoutMs > OPERATION_TIMEOUT_MS_MAX
|
|
756
|
-
)
|
|
994
|
+
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
757
995
|
throw new ValidationError(
|
|
758
996
|
`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`,
|
|
759
997
|
{
|
|
@@ -821,6 +1059,36 @@ function validateOperationObservability(
|
|
|
821
1059
|
}
|
|
822
1060
|
}
|
|
823
1061
|
|
|
1062
|
+
function validateOperationErrorCodes(
|
|
1063
|
+
providerId: string,
|
|
1064
|
+
operations: Record<string, ProviderOperation>,
|
|
1065
|
+
): void {
|
|
1066
|
+
for (const [operationName, operation] of Object.entries(operations)) {
|
|
1067
|
+
for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
|
|
1068
|
+
if (
|
|
1069
|
+
errorCode.status !== undefined &&
|
|
1070
|
+
!VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)
|
|
1071
|
+
) {
|
|
1072
|
+
const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
|
|
1073
|
+
throw new ValidationError(
|
|
1074
|
+
`Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`,
|
|
1075
|
+
{
|
|
1076
|
+
fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
|
|
1077
|
+
},
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
if (
|
|
1081
|
+
errorCode.status !== undefined &&
|
|
1082
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)
|
|
1083
|
+
) {
|
|
1084
|
+
console.warn(
|
|
1085
|
+
`[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.`,
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
824
1092
|
const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
|
|
825
1093
|
const SSE_TRANSPORT_FIELDS = new Set([
|
|
826
1094
|
"kind",
|
|
@@ -951,12 +1219,7 @@ function validateOperationTransports(
|
|
|
951
1219
|
},
|
|
952
1220
|
);
|
|
953
1221
|
}
|
|
954
|
-
assertLiteralField(
|
|
955
|
-
kind,
|
|
956
|
-
`${fieldPath}.kind`,
|
|
957
|
-
VALID_OPERATION_TRANSPORT_KINDS,
|
|
958
|
-
providerId,
|
|
959
|
-
);
|
|
1222
|
+
assertLiteralField(kind, `${fieldPath}.kind`, VALID_OPERATION_TRANSPORT_KINDS, providerId);
|
|
960
1223
|
|
|
961
1224
|
switch (kind) {
|
|
962
1225
|
case "json":
|
|
@@ -988,16 +1251,9 @@ function validateOperationTransports(
|
|
|
988
1251
|
STREAM_MAX_DURATION_MS_MAX,
|
|
989
1252
|
"max duration",
|
|
990
1253
|
);
|
|
991
|
-
assertPositiveBytes(
|
|
992
|
-
Reflect.get(transport, "maxEventBytes"),
|
|
993
|
-
`${fieldPath}.maxEventBytes`,
|
|
994
|
-
);
|
|
1254
|
+
assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
|
|
995
1255
|
const resumable = Reflect.get(transport, "resumable");
|
|
996
|
-
if (
|
|
997
|
-
resumable !== undefined &&
|
|
998
|
-
resumable !== false &&
|
|
999
|
-
resumable !== "last-event-id"
|
|
1000
|
-
) {
|
|
1256
|
+
if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
|
|
1001
1257
|
throw new ValidationError(
|
|
1002
1258
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`,
|
|
1003
1259
|
{
|
|
@@ -1017,12 +1273,7 @@ function validateOperationTransports(
|
|
|
1017
1273
|
rejectUnknownFields(transport, HTTP_STREAM_TRANSPORT_FIELDS, fieldPath);
|
|
1018
1274
|
const contentType = Reflect.get(transport, "contentType");
|
|
1019
1275
|
if (contentType !== undefined) {
|
|
1020
|
-
assertNonEmptyString(
|
|
1021
|
-
contentType,
|
|
1022
|
-
`${fieldPath}.contentType`,
|
|
1023
|
-
providerId,
|
|
1024
|
-
operationName,
|
|
1025
|
-
);
|
|
1276
|
+
assertNonEmptyString(contentType, `${fieldPath}.contentType`, providerId, operationName);
|
|
1026
1277
|
}
|
|
1027
1278
|
assertStreamMs(
|
|
1028
1279
|
Reflect.get(transport, "idleTimeoutMs"),
|
|
@@ -1038,10 +1289,7 @@ function validateOperationTransports(
|
|
|
1038
1289
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1039
1290
|
"max duration",
|
|
1040
1291
|
);
|
|
1041
|
-
assertPositiveBytes(
|
|
1042
|
-
Reflect.get(transport, "maxChunkBytes"),
|
|
1043
|
-
`${fieldPath}.maxChunkBytes`,
|
|
1044
|
-
);
|
|
1292
|
+
assertPositiveBytes(Reflect.get(transport, "maxChunkBytes"), `${fieldPath}.maxChunkBytes`);
|
|
1045
1293
|
break;
|
|
1046
1294
|
}
|
|
1047
1295
|
case "websocket": {
|
|
@@ -1066,10 +1314,7 @@ function validateOperationTransports(
|
|
|
1066
1314
|
);
|
|
1067
1315
|
}
|
|
1068
1316
|
for (const subprotocol of subprotocols) {
|
|
1069
|
-
if (
|
|
1070
|
-
typeof subprotocol !== "string" ||
|
|
1071
|
-
!WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)
|
|
1072
|
-
) {
|
|
1317
|
+
if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
1073
1318
|
throw new ValidationError(
|
|
1074
1319
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`,
|
|
1075
1320
|
{
|
|
@@ -1093,10 +1338,7 @@ function validateOperationTransports(
|
|
|
1093
1338
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1094
1339
|
"max duration",
|
|
1095
1340
|
);
|
|
1096
|
-
assertPositiveBytes(
|
|
1097
|
-
Reflect.get(transport, "maxFrameBytes"),
|
|
1098
|
-
`${fieldPath}.maxFrameBytes`,
|
|
1099
|
-
);
|
|
1341
|
+
assertPositiveBytes(Reflect.get(transport, "maxFrameBytes"), `${fieldPath}.maxFrameBytes`);
|
|
1100
1342
|
break;
|
|
1101
1343
|
}
|
|
1102
1344
|
}
|
|
@@ -1159,10 +1401,7 @@ function levenshtein(a: string, b: string): number {
|
|
|
1159
1401
|
return prev[n] ?? 0;
|
|
1160
1402
|
}
|
|
1161
1403
|
|
|
1162
|
-
function suggestField(
|
|
1163
|
-
unknown: string,
|
|
1164
|
-
candidates: ReadonlySet<string>,
|
|
1165
|
-
): string | undefined {
|
|
1404
|
+
function suggestField(unknown: string, candidates: ReadonlySet<string>): string | undefined {
|
|
1166
1405
|
let best: string | undefined;
|
|
1167
1406
|
let bestDist = 3;
|
|
1168
1407
|
for (const candidate of candidates) {
|
|
@@ -1179,6 +1418,7 @@ function rejectUnknownFields(
|
|
|
1179
1418
|
value: object,
|
|
1180
1419
|
allowed: ReadonlySet<string>,
|
|
1181
1420
|
fieldPath: string,
|
|
1421
|
+
providerId?: string,
|
|
1182
1422
|
): void {
|
|
1183
1423
|
for (const key of Object.keys(value)) {
|
|
1184
1424
|
if (allowed.has(key)) continue;
|
|
@@ -1187,7 +1427,11 @@ function rejectUnknownFields(
|
|
|
1187
1427
|
hint
|
|
1188
1428
|
? `Unknown field "${key}" on ${fieldPath}. Did you mean "${hint}"?`
|
|
1189
1429
|
: `Unknown field "${key}" on ${fieldPath}.`,
|
|
1190
|
-
{
|
|
1430
|
+
{
|
|
1431
|
+
fix: providerId
|
|
1432
|
+
? `Remove ${fieldPath}.${key} from provider "${providerId}" or rename it.`
|
|
1433
|
+
: `Remove ${fieldPath}.${key} or rename it.`,
|
|
1434
|
+
},
|
|
1191
1435
|
);
|
|
1192
1436
|
}
|
|
1193
1437
|
}
|
|
@@ -1218,23 +1462,12 @@ function validateProviderHealthMonitor(
|
|
|
1218
1462
|
field: "healthMonitor" | "healthProbe" = "healthMonitor",
|
|
1219
1463
|
): void {
|
|
1220
1464
|
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
|
-
);
|
|
1465
|
+
if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
|
|
1466
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
|
|
1467
|
+
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
1468
|
+
});
|
|
1232
1469
|
const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
|
|
1233
|
-
rejectUnknownFields(
|
|
1234
|
-
healthMonitorRecord,
|
|
1235
|
-
PROVIDER_HEALTH_MONITOR_FIELDS,
|
|
1236
|
-
field,
|
|
1237
|
-
);
|
|
1470
|
+
rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
|
|
1238
1471
|
if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
|
|
1239
1472
|
assertBoundedIntegerMs(
|
|
1240
1473
|
healthMonitorRecord.defaultProbeTimeoutMs,
|
|
@@ -1302,11 +1535,7 @@ function validateProviderHealthMonitor(
|
|
|
1302
1535
|
|
|
1303
1536
|
const probeOverrides = healthMonitorRecord.probeOverrides;
|
|
1304
1537
|
if (probeOverrides !== undefined) {
|
|
1305
|
-
if (
|
|
1306
|
-
!probeOverrides ||
|
|
1307
|
-
typeof probeOverrides !== "object" ||
|
|
1308
|
-
Array.isArray(probeOverrides)
|
|
1309
|
-
)
|
|
1538
|
+
if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
|
|
1310
1539
|
throw new ValidationError(
|
|
1311
1540
|
`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`,
|
|
1312
1541
|
);
|
|
@@ -1372,14 +1601,8 @@ function validateHealthCheckCase(
|
|
|
1372
1601
|
): void {
|
|
1373
1602
|
const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
|
|
1374
1603
|
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
|
-
);
|
|
1604
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1605
|
+
rejectUnknownFields(caseValue as Record<string, unknown>, HEALTH_CHECK_CASE_FIELDS, fieldPath);
|
|
1383
1606
|
const c = caseValue as HealthCheckCase;
|
|
1384
1607
|
if (typeof c.name !== "string" || c.name.length === 0)
|
|
1385
1608
|
throw new ValidationError(
|
|
@@ -1407,15 +1630,11 @@ function validateHealthCheckCase(
|
|
|
1407
1630
|
`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
1631
|
);
|
|
1409
1632
|
if (c.timeoutMs !== undefined) {
|
|
1410
|
-
assertBoundedIntegerMs(
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1416
|
-
label: "timeout",
|
|
1417
|
-
},
|
|
1418
|
-
);
|
|
1633
|
+
assertBoundedIntegerMs(c.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
|
|
1634
|
+
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1635
|
+
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1636
|
+
label: "timeout",
|
|
1637
|
+
});
|
|
1419
1638
|
}
|
|
1420
1639
|
if (
|
|
1421
1640
|
c.expectedStatus !== undefined &&
|
|
@@ -1431,21 +1650,11 @@ function validateHealthCheckCase(
|
|
|
1431
1650
|
);
|
|
1432
1651
|
}
|
|
1433
1652
|
|
|
1434
|
-
function validateHealthCheckSuite(
|
|
1435
|
-
providerId: string,
|
|
1436
|
-
operationName: string,
|
|
1437
|
-
suite: unknown,
|
|
1438
|
-
): void {
|
|
1653
|
+
function validateHealthCheckSuite(providerId: string, operationName: string, suite: unknown): void {
|
|
1439
1654
|
const fieldPath = `operations.${operationName}.healthCheck`;
|
|
1440
1655
|
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
|
-
);
|
|
1656
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1657
|
+
rejectUnknownFields(suite as Record<string, unknown>, HEALTH_CHECK_SUITE_FIELDS, fieldPath);
|
|
1449
1658
|
const s = suite as HealthCheckSuite;
|
|
1450
1659
|
if (!isPositiveMsDurationString(s.interval))
|
|
1451
1660
|
throw new ValidationError(
|
|
@@ -1455,11 +1664,7 @@ function validateHealthCheckSuite(
|
|
|
1455
1664
|
},
|
|
1456
1665
|
);
|
|
1457
1666
|
if (s.schedule !== undefined) {
|
|
1458
|
-
if (
|
|
1459
|
-
!s.schedule ||
|
|
1460
|
-
typeof s.schedule !== "object" ||
|
|
1461
|
-
Array.isArray(s.schedule)
|
|
1462
|
-
) {
|
|
1667
|
+
if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
|
|
1463
1668
|
throw new ValidationError(
|
|
1464
1669
|
`Provider "${providerId}" ${fieldPath}.schedule must be an object.`,
|
|
1465
1670
|
);
|
|
@@ -1469,11 +1674,7 @@ function validateHealthCheckSuite(
|
|
|
1469
1674
|
`Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`,
|
|
1470
1675
|
);
|
|
1471
1676
|
}
|
|
1472
|
-
rejectUnknownFields(
|
|
1473
|
-
s.schedule,
|
|
1474
|
-
new Set(["randomize"]),
|
|
1475
|
-
`${fieldPath}.schedule`,
|
|
1476
|
-
);
|
|
1677
|
+
rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
|
|
1477
1678
|
const randomize = Reflect.get(s.schedule, "randomize");
|
|
1478
1679
|
if (randomize !== undefined) {
|
|
1479
1680
|
validateScheduleRandomization(
|
|
@@ -1484,15 +1685,11 @@ function validateHealthCheckSuite(
|
|
|
1484
1685
|
}
|
|
1485
1686
|
}
|
|
1486
1687
|
if (s.timeoutMs !== undefined) {
|
|
1487
|
-
assertBoundedIntegerMs(
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1493
|
-
label: "timeout",
|
|
1494
|
-
},
|
|
1495
|
-
);
|
|
1688
|
+
assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
|
|
1689
|
+
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1690
|
+
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1691
|
+
label: "timeout",
|
|
1692
|
+
});
|
|
1496
1693
|
}
|
|
1497
1694
|
if (s.degradedThresholdMs !== undefined) {
|
|
1498
1695
|
assertBoundedIntegerMs(
|
|
@@ -1505,10 +1702,7 @@ function validateHealthCheckSuite(
|
|
|
1505
1702
|
},
|
|
1506
1703
|
);
|
|
1507
1704
|
}
|
|
1508
|
-
if (
|
|
1509
|
-
s.requiresConnection !== undefined &&
|
|
1510
|
-
typeof s.requiresConnection !== "boolean"
|
|
1511
|
-
)
|
|
1705
|
+
if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
|
|
1512
1706
|
throw new ValidationError(
|
|
1513
1707
|
`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`,
|
|
1514
1708
|
);
|
|
@@ -1540,14 +1734,8 @@ function validateHealthCheckUnsupported(
|
|
|
1540
1734
|
unsupported: unknown,
|
|
1541
1735
|
): void {
|
|
1542
1736
|
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
|
-
);
|
|
1737
|
+
if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
|
|
1738
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1551
1739
|
rejectUnknownFields(
|
|
1552
1740
|
unsupported as Record<string, unknown>,
|
|
1553
1741
|
HEALTH_CHECK_UNSUPPORTED_FIELDS,
|
|
@@ -1581,12 +1769,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
|
|
|
1581
1769
|
"steps",
|
|
1582
1770
|
"run",
|
|
1583
1771
|
]);
|
|
1584
|
-
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
|
|
1585
|
-
"kind",
|
|
1586
|
-
"interval",
|
|
1587
|
-
"jitter",
|
|
1588
|
-
"randomize",
|
|
1589
|
-
]);
|
|
1772
|
+
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
|
|
1590
1773
|
const HEALTH_JOURNEY_STEP_FIELDS = new Set([
|
|
1591
1774
|
"id",
|
|
1592
1775
|
"description",
|
|
@@ -1605,10 +1788,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
|
|
|
1605
1788
|
"minManualInterval",
|
|
1606
1789
|
"publicRationale",
|
|
1607
1790
|
]);
|
|
1608
|
-
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
|
|
1609
|
-
"enabled",
|
|
1610
|
-
"reason",
|
|
1611
|
-
]);
|
|
1791
|
+
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
|
|
1612
1792
|
const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
|
|
1613
1793
|
"enabled",
|
|
1614
1794
|
"requiresAcknowledgement",
|
|
@@ -1628,32 +1808,18 @@ function validateHealthJourneyManualTrigger(
|
|
|
1628
1808
|
manualTrigger: unknown,
|
|
1629
1809
|
): void {
|
|
1630
1810
|
const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
|
|
1631
|
-
if (
|
|
1632
|
-
!manualTrigger ||
|
|
1633
|
-
typeof manualTrigger !== "object" ||
|
|
1634
|
-
Array.isArray(manualTrigger)
|
|
1635
|
-
) {
|
|
1811
|
+
if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
|
|
1636
1812
|
throw new ValidationError(
|
|
1637
1813
|
`Provider "${providerId}" ${fieldPath} must be an object when present.`,
|
|
1638
1814
|
);
|
|
1639
1815
|
}
|
|
1640
|
-
rejectUnknownFields(
|
|
1641
|
-
manualTrigger,
|
|
1642
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS,
|
|
1643
|
-
fieldPath,
|
|
1644
|
-
);
|
|
1816
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
|
|
1645
1817
|
const enabled = Reflect.get(manualTrigger, "enabled");
|
|
1646
1818
|
if (typeof enabled !== "boolean") {
|
|
1647
|
-
throw new ValidationError(
|
|
1648
|
-
`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`,
|
|
1649
|
-
);
|
|
1819
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`);
|
|
1650
1820
|
}
|
|
1651
1821
|
if (enabled === false) {
|
|
1652
|
-
rejectUnknownFields(
|
|
1653
|
-
manualTrigger,
|
|
1654
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS,
|
|
1655
|
-
fieldPath,
|
|
1656
|
-
);
|
|
1822
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS, fieldPath);
|
|
1657
1823
|
if (
|
|
1658
1824
|
Reflect.get(manualTrigger, "reason") !== undefined &&
|
|
1659
1825
|
(typeof Reflect.get(manualTrigger, "reason") !== "string" ||
|
|
@@ -1665,25 +1831,15 @@ function validateHealthJourneyManualTrigger(
|
|
|
1665
1831
|
}
|
|
1666
1832
|
return;
|
|
1667
1833
|
}
|
|
1668
|
-
rejectUnknownFields(
|
|
1669
|
-
|
|
1670
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS,
|
|
1671
|
-
fieldPath,
|
|
1672
|
-
);
|
|
1673
|
-
const requiresAcknowledgement = Reflect.get(
|
|
1674
|
-
manualTrigger,
|
|
1675
|
-
"requiresAcknowledgement",
|
|
1676
|
-
);
|
|
1834
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS, fieldPath);
|
|
1835
|
+
const requiresAcknowledgement = Reflect.get(manualTrigger, "requiresAcknowledgement");
|
|
1677
1836
|
if (typeof requiresAcknowledgement !== "boolean") {
|
|
1678
1837
|
throw new ValidationError(
|
|
1679
1838
|
`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`,
|
|
1680
1839
|
);
|
|
1681
1840
|
}
|
|
1682
1841
|
const risk = Reflect.get(manualTrigger, "risk");
|
|
1683
|
-
if (
|
|
1684
|
-
typeof risk !== "string" ||
|
|
1685
|
-
!HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)
|
|
1686
|
-
) {
|
|
1842
|
+
if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
1687
1843
|
throw new ValidationError(
|
|
1688
1844
|
`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`,
|
|
1689
1845
|
);
|
|
@@ -1694,10 +1850,7 @@ function validateHealthJourneyManualTrigger(
|
|
|
1694
1850
|
);
|
|
1695
1851
|
}
|
|
1696
1852
|
const minManualInterval = Reflect.get(manualTrigger, "minManualInterval");
|
|
1697
|
-
assertIsoDuration(
|
|
1698
|
-
minManualInterval,
|
|
1699
|
-
`Provider "${providerId}" ${fieldPath}.minManualInterval`,
|
|
1700
|
-
);
|
|
1853
|
+
assertIsoDuration(minManualInterval, `Provider "${providerId}" ${fieldPath}.minManualInterval`);
|
|
1701
1854
|
if (isoDurationMs(minManualInterval) <= 0) {
|
|
1702
1855
|
throw new ValidationError(
|
|
1703
1856
|
`Provider "${providerId}" ${fieldPath}.minManualInterval must be a positive duration.`,
|
|
@@ -1728,18 +1881,14 @@ const SMS_ORIGIN_FIELDS_BY_KIND: Record<string, ReadonlySet<string>> = {
|
|
|
1728
1881
|
e164: new Set(["kind", "value", "display"]),
|
|
1729
1882
|
nationalServiceCode: new Set(["kind", "country", "value", "display"]),
|
|
1730
1883
|
};
|
|
1731
|
-
const DURATION_RE =
|
|
1732
|
-
/^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
|
|
1884
|
+
const DURATION_RE = /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
|
|
1733
1885
|
const E164_RE = /^\+[1-9]\d{1,14}$/;
|
|
1734
1886
|
const ISO_COUNTRY_RE = /^[A-Z]{2}$/;
|
|
1735
1887
|
const NATIONAL_SERVICE_CODE_RE = /^[0-9]{2,15}$/;
|
|
1736
1888
|
const BCP47_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
1737
1889
|
const JOURNEY_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
1738
1890
|
|
|
1739
|
-
function assertIsoDuration(
|
|
1740
|
-
value: unknown,
|
|
1741
|
-
fieldPath: string,
|
|
1742
|
-
): asserts value is string {
|
|
1891
|
+
function assertIsoDuration(value: unknown, fieldPath: string): asserts value is string {
|
|
1743
1892
|
if (typeof value !== "string" || !DURATION_RE.test(value)) {
|
|
1744
1893
|
throw new ValidationError(
|
|
1745
1894
|
`${fieldPath} must be an ISO 8601 duration for example PT8H or PT2M30S.`,
|
|
@@ -1754,15 +1903,10 @@ function isoDurationMs(value: string): number {
|
|
|
1754
1903
|
const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
|
|
1755
1904
|
const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
|
|
1756
1905
|
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
|
-
);
|
|
1906
|
+
return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
|
|
1760
1907
|
}
|
|
1761
1908
|
|
|
1762
|
-
function scheduleRandomizationMs(
|
|
1763
|
-
randomize: unknown,
|
|
1764
|
-
fieldPath: string,
|
|
1765
|
-
): number {
|
|
1909
|
+
function scheduleRandomizationMs(randomize: unknown, fieldPath: string): number {
|
|
1766
1910
|
const mode = Reflect.get(randomize as object, "mode");
|
|
1767
1911
|
switch (mode) {
|
|
1768
1912
|
case "centered": {
|
|
@@ -1776,9 +1920,7 @@ function scheduleRandomizationMs(
|
|
|
1776
1920
|
return isoDurationMs(maxDelay);
|
|
1777
1921
|
}
|
|
1778
1922
|
default:
|
|
1779
|
-
throw new ValidationError(
|
|
1780
|
-
`${fieldPath}.mode must be "centered" or "delayed".`,
|
|
1781
|
-
);
|
|
1923
|
+
throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
|
|
1782
1924
|
}
|
|
1783
1925
|
}
|
|
1784
1926
|
|
|
@@ -1792,25 +1934,18 @@ function validateScheduleRandomization(
|
|
|
1792
1934
|
}
|
|
1793
1935
|
const mode = Reflect.get(randomize, "mode");
|
|
1794
1936
|
const allowedFields =
|
|
1795
|
-
mode === "centered"
|
|
1796
|
-
? new Set(["mode", "maxOffset"])
|
|
1797
|
-
: new Set(["mode", "maxDelay"]);
|
|
1937
|
+
mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
|
|
1798
1938
|
rejectUnknownFields(randomize, allowedFields, fieldPath);
|
|
1799
1939
|
const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
|
|
1800
1940
|
if (offsetMs <= 0) {
|
|
1801
1941
|
throw new ValidationError(`${fieldPath} duration must be positive.`);
|
|
1802
1942
|
}
|
|
1803
1943
|
if (offsetMs >= intervalMs) {
|
|
1804
|
-
throw new ValidationError(
|
|
1805
|
-
`${fieldPath} duration must be shorter than schedule interval.`,
|
|
1806
|
-
);
|
|
1944
|
+
throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
|
|
1807
1945
|
}
|
|
1808
1946
|
}
|
|
1809
1947
|
|
|
1810
|
-
function assertIsoCountry(
|
|
1811
|
-
value: unknown,
|
|
1812
|
-
fieldPath: string,
|
|
1813
|
-
): asserts value is string {
|
|
1948
|
+
function assertIsoCountry(value: unknown, fieldPath: string): asserts value is string {
|
|
1814
1949
|
if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
|
|
1815
1950
|
throw new ValidationError(
|
|
1816
1951
|
`${fieldPath} must be an ISO 3166-1 alpha-2 country code for example KR.`,
|
|
@@ -1833,9 +1968,7 @@ function normalizeIntervalDuration(input: string): string {
|
|
|
1833
1968
|
? durationMs / 3_600_000
|
|
1834
1969
|
: durationMs / 86_400_000;
|
|
1835
1970
|
if (!Number.isInteger(amount) || amount <= 0) {
|
|
1836
|
-
throw new ValidationError(
|
|
1837
|
-
`Journey schedule interval must be a positive duration.`,
|
|
1838
|
-
);
|
|
1971
|
+
throw new ValidationError(`Journey schedule interval must be a positive duration.`);
|
|
1839
1972
|
}
|
|
1840
1973
|
if (unit === "s") return `PT${amount}S`;
|
|
1841
1974
|
if (unit === "m") return `PT${amount}M`;
|
|
@@ -1894,11 +2027,7 @@ function countCapturingGroups(pattern: RegExp): number {
|
|
|
1894
2027
|
if (inCharacterClass || char !== "(") continue;
|
|
1895
2028
|
const next = source[i + 1];
|
|
1896
2029
|
if (next === "?" && source[i + 2] !== "<") continue;
|
|
1897
|
-
if (
|
|
1898
|
-
next === "?" &&
|
|
1899
|
-
source[i + 2] === "<" &&
|
|
1900
|
-
(source[i + 3] === "=" || source[i + 3] === "!")
|
|
1901
|
-
)
|
|
2030
|
+
if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1902
2031
|
continue;
|
|
1903
2032
|
count += 1;
|
|
1904
2033
|
}
|
|
@@ -1917,9 +2046,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1917
2046
|
}
|
|
1918
2047
|
const kind = Reflect.get(origin, "kind");
|
|
1919
2048
|
if (kind !== "e164" && kind !== "nationalServiceCode") {
|
|
1920
|
-
throw new ValidationError(
|
|
1921
|
-
`${fieldPath}.kind must be "e164" or "nationalServiceCode".`,
|
|
1922
|
-
);
|
|
2049
|
+
throw new ValidationError(`${fieldPath}.kind must be "e164" or "nationalServiceCode".`);
|
|
1923
2050
|
}
|
|
1924
2051
|
rejectUnknownFields(origin, SMS_ORIGIN_FIELDS_BY_KIND[kind], fieldPath);
|
|
1925
2052
|
if (kind === "e164") {
|
|
@@ -1946,9 +2073,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1946
2073
|
Reflect.get(origin, "display") !== undefined &&
|
|
1947
2074
|
typeof Reflect.get(origin, "display") !== "string"
|
|
1948
2075
|
) {
|
|
1949
|
-
throw new ValidationError(
|
|
1950
|
-
`${fieldPath}.display must be a string when present.`,
|
|
1951
|
-
);
|
|
2076
|
+
throw new ValidationError(`${fieldPath}.display must be a string when present.`);
|
|
1952
2077
|
}
|
|
1953
2078
|
}
|
|
1954
2079
|
|
|
@@ -1962,9 +2087,7 @@ function validateSmsOtpMatcher(
|
|
|
1962
2087
|
rejectUnknownFields(matcher, SMS_OTP_MATCHER_FIELDS, fieldPath);
|
|
1963
2088
|
const matcherId = Reflect.get(matcher, "id");
|
|
1964
2089
|
if (typeof matcherId !== "string" || !JOURNEY_ID_RE.test(matcherId)) {
|
|
1965
|
-
throw new ValidationError(
|
|
1966
|
-
`${fieldPath}.id must be a kebab-case identifier.`,
|
|
1967
|
-
);
|
|
2090
|
+
throw new ValidationError(`${fieldPath}.id must be a kebab-case identifier.`);
|
|
1968
2091
|
}
|
|
1969
2092
|
assertIsoCountry(Reflect.get(matcher, "country"), `${fieldPath}.country`);
|
|
1970
2093
|
if (
|
|
@@ -1972,24 +2095,18 @@ function validateSmsOtpMatcher(
|
|
|
1972
2095
|
(typeof Reflect.get(matcher, "locale") !== "string" ||
|
|
1973
2096
|
!BCP47_RE.test(Reflect.get(matcher, "locale")))
|
|
1974
2097
|
) {
|
|
1975
|
-
throw new ValidationError(
|
|
1976
|
-
`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`,
|
|
1977
|
-
);
|
|
2098
|
+
throw new ValidationError(`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`);
|
|
1978
2099
|
}
|
|
1979
2100
|
if (
|
|
1980
2101
|
Reflect.get(matcher, "phoneNumber") !== undefined &&
|
|
1981
2102
|
(typeof Reflect.get(matcher, "phoneNumber") !== "string" ||
|
|
1982
2103
|
!E164_RE.test(Reflect.get(matcher, "phoneNumber")))
|
|
1983
2104
|
) {
|
|
1984
|
-
throw new ValidationError(
|
|
1985
|
-
`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`,
|
|
1986
|
-
);
|
|
2105
|
+
throw new ValidationError(`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`);
|
|
1987
2106
|
}
|
|
1988
2107
|
const origins = Reflect.get(matcher, "origins");
|
|
1989
2108
|
if (!Array.isArray(origins) || origins.length === 0) {
|
|
1990
|
-
throw new ValidationError(
|
|
1991
|
-
`${fieldPath}.origins must be a non-empty array.`,
|
|
1992
|
-
);
|
|
2109
|
+
throw new ValidationError(`${fieldPath}.origins must be a non-empty array.`);
|
|
1993
2110
|
}
|
|
1994
2111
|
for (const [index, origin] of origins.entries()) {
|
|
1995
2112
|
validateSmsOrigin(origin, `${fieldPath}.origins[${index}]`);
|
|
@@ -2010,10 +2127,7 @@ function validateSmsOtpMatcher(
|
|
|
2010
2127
|
);
|
|
2011
2128
|
}
|
|
2012
2129
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
2013
|
-
if (
|
|
2014
|
-
countCapturingGroups(regex) !== 1 &&
|
|
2015
|
-
Reflect.get(code, "capture") === undefined
|
|
2016
|
-
) {
|
|
2130
|
+
if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
|
|
2017
2131
|
throw new ValidationError(
|
|
2018
2132
|
`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`,
|
|
2019
2133
|
);
|
|
@@ -2023,20 +2137,12 @@ function validateSmsOtpMatcher(
|
|
|
2023
2137
|
typeof Reflect.get(code, "capture") !== "string" &&
|
|
2024
2138
|
typeof Reflect.get(code, "capture") !== "number"
|
|
2025
2139
|
) {
|
|
2026
|
-
throw new ValidationError(
|
|
2027
|
-
`${fieldPath}.code.capture must be a string or number when present.`,
|
|
2028
|
-
);
|
|
2140
|
+
throw new ValidationError(`${fieldPath}.code.capture must be a string or number when present.`);
|
|
2029
2141
|
}
|
|
2030
2142
|
assertIsoDuration(Reflect.get(matcher, "maxAge"), `${fieldPath}.maxAge`);
|
|
2031
|
-
assertIsoDuration(
|
|
2032
|
-
Reflect.get(matcher, "waitTimeout"),
|
|
2033
|
-
`${fieldPath}.waitTimeout`,
|
|
2034
|
-
);
|
|
2143
|
+
assertIsoDuration(Reflect.get(matcher, "waitTimeout"), `${fieldPath}.waitTimeout`);
|
|
2035
2144
|
if (Reflect.get(matcher, "clockSkew") !== undefined)
|
|
2036
|
-
assertIsoDuration(
|
|
2037
|
-
Reflect.get(matcher, "clockSkew"),
|
|
2038
|
-
`${fieldPath}.clockSkew`,
|
|
2039
|
-
);
|
|
2145
|
+
assertIsoDuration(Reflect.get(matcher, "clockSkew"), `${fieldPath}.clockSkew`);
|
|
2040
2146
|
}
|
|
2041
2147
|
|
|
2042
2148
|
export function defineSmsOtpMatcher(
|
|
@@ -2068,9 +2174,7 @@ export function defineSmsOtpMatcher(
|
|
|
2068
2174
|
return matcher;
|
|
2069
2175
|
}
|
|
2070
2176
|
|
|
2071
|
-
export function defineHealthJourney(
|
|
2072
|
-
config: HealthJourneyDefinition,
|
|
2073
|
-
): HealthJourneyDefinition {
|
|
2177
|
+
export function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition {
|
|
2074
2178
|
return config;
|
|
2075
2179
|
}
|
|
2076
2180
|
|
|
@@ -2081,22 +2185,15 @@ function validateHealthJourneySchedule(
|
|
|
2081
2185
|
): void {
|
|
2082
2186
|
const fieldPath = `healthJourneys.${journeyId}.schedule`;
|
|
2083
2187
|
if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) {
|
|
2084
|
-
throw new ValidationError(
|
|
2085
|
-
`Provider "${providerId}" ${fieldPath} must be an object.`,
|
|
2086
|
-
);
|
|
2188
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
2087
2189
|
}
|
|
2088
2190
|
rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
|
|
2089
2191
|
if (Reflect.get(schedule, "kind") !== "interval")
|
|
2090
|
-
throw new ValidationError(
|
|
2091
|
-
`Provider "${providerId}" ${fieldPath}.kind must be "interval".`,
|
|
2092
|
-
);
|
|
2192
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
|
|
2093
2193
|
const interval = Reflect.get(schedule, "interval");
|
|
2094
2194
|
assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
|
|
2095
2195
|
const randomize = Reflect.get(schedule, "randomize");
|
|
2096
|
-
if (
|
|
2097
|
-
Reflect.get(schedule, "jitter") !== undefined &&
|
|
2098
|
-
randomize !== undefined
|
|
2099
|
-
) {
|
|
2196
|
+
if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
|
|
2100
2197
|
throw new ValidationError(
|
|
2101
2198
|
`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`,
|
|
2102
2199
|
);
|
|
@@ -2123,17 +2220,13 @@ function validateHealthJourneys(
|
|
|
2123
2220
|
const covered = new Set<string>();
|
|
2124
2221
|
if (healthJourneys === undefined) return covered;
|
|
2125
2222
|
if (!Array.isArray(healthJourneys)) {
|
|
2126
|
-
throw new ValidationError(
|
|
2127
|
-
`Provider "${providerId}" healthJourneys must be an array.`,
|
|
2128
|
-
);
|
|
2223
|
+
throw new ValidationError(`Provider "${providerId}" healthJourneys must be an array.`);
|
|
2129
2224
|
}
|
|
2130
2225
|
const journeyIds = new Set<string>();
|
|
2131
2226
|
for (const [index, journey] of healthJourneys.entries()) {
|
|
2132
2227
|
const prefix = `healthJourneys[${index}]`;
|
|
2133
2228
|
if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
|
|
2134
|
-
throw new ValidationError(
|
|
2135
|
-
`Provider "${providerId}" ${prefix} must be an object.`,
|
|
2136
|
-
);
|
|
2229
|
+
throw new ValidationError(`Provider "${providerId}" ${prefix} must be an object.`);
|
|
2137
2230
|
}
|
|
2138
2231
|
rejectUnknownFields(journey, HEALTH_JOURNEY_FIELDS, prefix);
|
|
2139
2232
|
if (typeof journey.id !== "string" || !JOURNEY_ID_RE.test(journey.id)) {
|
|
@@ -2147,10 +2240,7 @@ function validateHealthJourneys(
|
|
|
2147
2240
|
);
|
|
2148
2241
|
journeyIds.add(journey.id);
|
|
2149
2242
|
validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
|
|
2150
|
-
if (
|
|
2151
|
-
!Array.isArray(journey.coversOperations) ||
|
|
2152
|
-
journey.coversOperations.length === 0
|
|
2153
|
-
) {
|
|
2243
|
+
if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
|
|
2154
2244
|
throw new ValidationError(
|
|
2155
2245
|
`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`,
|
|
2156
2246
|
);
|
|
@@ -2185,10 +2275,7 @@ function validateHealthJourneys(
|
|
|
2185
2275
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers must be an array.`,
|
|
2186
2276
|
);
|
|
2187
2277
|
for (const [matcherIndex, matcher] of journey.smsMatchers.entries()) {
|
|
2188
|
-
validateSmsOtpMatcher(
|
|
2189
|
-
matcher,
|
|
2190
|
-
`healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`,
|
|
2191
|
-
);
|
|
2278
|
+
validateSmsOtpMatcher(matcher, `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`);
|
|
2192
2279
|
if (matcherIds.has(matcher.id))
|
|
2193
2280
|
throw new ValidationError(
|
|
2194
2281
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers has duplicate matcher id "${matcher.id}".`,
|
|
@@ -2199,9 +2286,7 @@ function validateHealthJourneys(
|
|
|
2199
2286
|
for (const [stepIndex, step] of journey.steps.entries()) {
|
|
2200
2287
|
const stepPath = `healthJourneys.${journey.id}.steps[${stepIndex}]`;
|
|
2201
2288
|
if (!step || typeof step !== "object" || Array.isArray(step))
|
|
2202
|
-
throw new ValidationError(
|
|
2203
|
-
`Provider "${providerId}" ${stepPath} must be an object.`,
|
|
2204
|
-
);
|
|
2289
|
+
throw new ValidationError(`Provider "${providerId}" ${stepPath} must be an object.`);
|
|
2205
2290
|
rejectUnknownFields(step, HEALTH_JOURNEY_STEP_FIELDS, stepPath);
|
|
2206
2291
|
if (typeof step.id !== "string" || !JOURNEY_ID_RE.test(step.id))
|
|
2207
2292
|
throw new ValidationError(
|
|
@@ -2211,20 +2296,13 @@ function validateHealthJourneys(
|
|
|
2211
2296
|
throw new ValidationError(
|
|
2212
2297
|
`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`,
|
|
2213
2298
|
);
|
|
2214
|
-
if (
|
|
2215
|
-
step.usesSmsMatcher !== undefined &&
|
|
2216
|
-
!matcherIds.has(step.usesSmsMatcher)
|
|
2217
|
-
)
|
|
2299
|
+
if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
|
|
2218
2300
|
throw new ValidationError(
|
|
2219
2301
|
`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`,
|
|
2220
2302
|
);
|
|
2221
2303
|
}
|
|
2222
2304
|
if (journey.manualTrigger !== undefined)
|
|
2223
|
-
validateHealthJourneyManualTrigger(
|
|
2224
|
-
providerId,
|
|
2225
|
-
journey.id,
|
|
2226
|
-
journey.manualTrigger,
|
|
2227
|
-
);
|
|
2305
|
+
validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
|
|
2228
2306
|
if (journey.timeout !== undefined)
|
|
2229
2307
|
assertIsoDuration(
|
|
2230
2308
|
journey.timeout,
|
|
@@ -2270,23 +2348,10 @@ function validateOperationHealthChecks(
|
|
|
2270
2348
|
fix: `Remove either operations.${operationName}.healthCheck or operations.${operationName}.healthCheckUnsupported.`,
|
|
2271
2349
|
},
|
|
2272
2350
|
);
|
|
2273
|
-
if (hasCheck)
|
|
2274
|
-
validateHealthCheckSuite(
|
|
2275
|
-
providerId,
|
|
2276
|
-
operationName,
|
|
2277
|
-
operation.healthCheck,
|
|
2278
|
-
);
|
|
2351
|
+
if (hasCheck) validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
|
|
2279
2352
|
if (hasUnsupported)
|
|
2280
|
-
validateHealthCheckUnsupported(
|
|
2281
|
-
|
|
2282
|
-
operationName,
|
|
2283
|
-
operation.healthCheckUnsupported,
|
|
2284
|
-
);
|
|
2285
|
-
if (
|
|
2286
|
-
!hasCheck &&
|
|
2287
|
-
!hasUnsupported &&
|
|
2288
|
-
!journeyCoveredOperations.has(operationName)
|
|
2289
|
-
)
|
|
2353
|
+
validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
|
|
2354
|
+
if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
|
|
2290
2355
|
throw new ValidationError(
|
|
2291
2356
|
`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`,
|
|
2292
2357
|
{
|
|
@@ -2308,6 +2373,26 @@ function validateOperationFixtures(
|
|
|
2308
2373
|
fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
|
|
2309
2374
|
},
|
|
2310
2375
|
);
|
|
2376
|
+
if (operation.fixtures?.recordedAt !== undefined) {
|
|
2377
|
+
const recordedAt = operation.fixtures.recordedAt;
|
|
2378
|
+
const parsed =
|
|
2379
|
+
typeof recordedAt === "string"
|
|
2380
|
+
? new Date(`${recordedAt}T00:00:00.000Z`)
|
|
2381
|
+
: new Date(Number.NaN);
|
|
2382
|
+
const isCalendarDate =
|
|
2383
|
+
typeof recordedAt === "string" &&
|
|
2384
|
+
/^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
|
|
2385
|
+
!Number.isNaN(parsed.getTime()) &&
|
|
2386
|
+
parsed.toISOString().slice(0, 10) === recordedAt;
|
|
2387
|
+
const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
2388
|
+
if (!isCalendarDate || recordedAt > kstToday)
|
|
2389
|
+
throw new ValidationError(
|
|
2390
|
+
`Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`,
|
|
2391
|
+
{
|
|
2392
|
+
fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
|
|
2393
|
+
},
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2311
2396
|
if (operation.fixtures?.request !== undefined) {
|
|
2312
2397
|
const result = safeParseSchemaSync(
|
|
2313
2398
|
operation.input,
|
|
@@ -2341,6 +2426,44 @@ function validateOperationFixtures(
|
|
|
2341
2426
|
}
|
|
2342
2427
|
}
|
|
2343
2428
|
|
|
2429
|
+
function resolveOperationFixtureRequests<TOperations extends Record<string, ProviderOperation>>(
|
|
2430
|
+
operations: TOperations,
|
|
2431
|
+
): TOperations {
|
|
2432
|
+
let changed = false;
|
|
2433
|
+
const resolvedOperations = Object.fromEntries(
|
|
2434
|
+
Object.entries(operations).map(([operationName, operation]) => {
|
|
2435
|
+
if (operation.fixtures?.request === undefined) return [operationName, operation];
|
|
2436
|
+
const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
|
|
2437
|
+
if (request === operation.fixtures.request) return [operationName, operation];
|
|
2438
|
+
changed = true;
|
|
2439
|
+
return [
|
|
2440
|
+
operationName,
|
|
2441
|
+
{
|
|
2442
|
+
...operation,
|
|
2443
|
+
fixtures: {
|
|
2444
|
+
...operation.fixtures,
|
|
2445
|
+
request,
|
|
2446
|
+
},
|
|
2447
|
+
},
|
|
2448
|
+
];
|
|
2449
|
+
}),
|
|
2450
|
+
) as TOperations;
|
|
2451
|
+
return changed ? resolvedOperations : operations;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
/**
|
|
2455
|
+
* Shallow shape guard only: the `deployment` object is passed through
|
|
2456
|
+
* verbatim and deliberately not deep-validated by the SDK — the APIFuse
|
|
2457
|
+
* registry builder owns deployment validation and profile resolution.
|
|
2458
|
+
*/
|
|
2459
|
+
function validateProviderDeployment(providerId: string, deployment: unknown): void {
|
|
2460
|
+
if (deployment === undefined) return;
|
|
2461
|
+
if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
|
|
2462
|
+
throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
|
|
2463
|
+
fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2344
2467
|
export function defineProvider<
|
|
2345
2468
|
TOperations extends Record<string, ProviderOperation>,
|
|
2346
2469
|
TConfig extends ProviderConfig<TOperations>,
|
|
@@ -2348,20 +2471,19 @@ export function defineProvider<
|
|
|
2348
2471
|
config: TConfig & AuthStartNoInputGuard<TConfig>,
|
|
2349
2472
|
): ProviderDefinition & { operations: OperationMapConfig<TOperations> } {
|
|
2350
2473
|
validateProviderShape(config);
|
|
2474
|
+
const operations = resolveOperationFixtureRequests(config.operations);
|
|
2351
2475
|
if (!CONNECTOR_ID_REGEX.test(config.id))
|
|
2352
2476
|
throw new ProviderError(`Invalid provider id: "${config.id}"`, {
|
|
2353
2477
|
fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
|
|
2354
2478
|
});
|
|
2355
2479
|
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
|
-
);
|
|
2480
|
+
throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
|
|
2481
|
+
fix: "Add at least one operation to the operations object",
|
|
2482
|
+
});
|
|
2362
2483
|
validateOperationIds(config.id, config.operations);
|
|
2363
2484
|
validateOperationAnnotations(config.id, config.operations);
|
|
2364
2485
|
validateOperationObservability(config.id, config.operations);
|
|
2486
|
+
validateOperationErrorCodes(config.id, config.operations);
|
|
2365
2487
|
validateOperationTransports(config.id, config.operations);
|
|
2366
2488
|
validateOperationContracts(config.id, config.operations);
|
|
2367
2489
|
validateToolRouterMetadata(config.id, config.operations);
|
|
@@ -2370,11 +2492,7 @@ export function defineProvider<
|
|
|
2370
2492
|
config.operations,
|
|
2371
2493
|
config.healthJourneys,
|
|
2372
2494
|
);
|
|
2373
|
-
validateOperationHealthChecks(
|
|
2374
|
-
config.id,
|
|
2375
|
-
config.operations,
|
|
2376
|
-
journeyCoveredOperations,
|
|
2377
|
-
);
|
|
2495
|
+
validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
|
|
2378
2496
|
if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
|
|
2379
2497
|
throw new ValidationError(
|
|
2380
2498
|
`Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
|
|
@@ -2387,9 +2505,19 @@ export function defineProvider<
|
|
|
2387
2505
|
config.healthProbe ?? config.healthMonitor,
|
|
2388
2506
|
config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
|
|
2389
2507
|
);
|
|
2390
|
-
validateOperationFixtures(config.id,
|
|
2508
|
+
validateOperationFixtures(config.id, operations);
|
|
2509
|
+
validateProviderDeployment(config.id, config.deployment);
|
|
2510
|
+
try {
|
|
2511
|
+
validateNativeProviderConfig(config.native);
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
if (error instanceof NativeEgressPolicyValidationError)
|
|
2514
|
+
throw new ValidationError(error.message);
|
|
2515
|
+
throw error;
|
|
2516
|
+
}
|
|
2391
2517
|
validateProviderProxy(config);
|
|
2518
|
+
validateProviderOcr(config);
|
|
2392
2519
|
validateProviderStt(config);
|
|
2520
|
+
validateProviderResolver(config);
|
|
2393
2521
|
if (config.runtime === "browser" && !config.browser)
|
|
2394
2522
|
throw new ProviderError(
|
|
2395
2523
|
`Provider "${config.id}" must define browser.engine when runtime is "browser"`,
|
|
@@ -2402,14 +2530,20 @@ export function defineProvider<
|
|
|
2402
2530
|
`Provider "${config.id}" cannot define browser config unless runtime is "browser"`,
|
|
2403
2531
|
{ fix: 'Set runtime: "browser" or remove the browser config' },
|
|
2404
2532
|
);
|
|
2405
|
-
|
|
2533
|
+
const provider: ProviderDefinition & { operations: OperationMapConfig<TOperations> } = {
|
|
2406
2534
|
id: config.id,
|
|
2407
2535
|
version: config.version,
|
|
2408
2536
|
runtime: config.runtime,
|
|
2537
|
+
// Verbatim passthrough: deployment validation and profile resolution
|
|
2538
|
+
// are owned by the APIFuse registry builder, not the SDK.
|
|
2539
|
+
deployment: config.deployment,
|
|
2409
2540
|
allowedHosts: config.allowedHosts,
|
|
2541
|
+
native: config.native,
|
|
2410
2542
|
stealth: config.stealth,
|
|
2411
2543
|
proxy: config.proxy,
|
|
2544
|
+
ocr: config.ocr,
|
|
2412
2545
|
stt: config.stt,
|
|
2546
|
+
resolver: config.resolver,
|
|
2413
2547
|
browser: config.browser,
|
|
2414
2548
|
auth: config.auth,
|
|
2415
2549
|
reviewed: config.reviewed,
|
|
@@ -2418,11 +2552,13 @@ export function defineProvider<
|
|
|
2418
2552
|
credential: config.credential,
|
|
2419
2553
|
context: config.context,
|
|
2420
2554
|
meta: config.meta,
|
|
2421
|
-
operations
|
|
2555
|
+
operations,
|
|
2422
2556
|
// Transitional healthMonitor → healthProbe alias: mirror whichever field
|
|
2423
2557
|
// was declared onto both so old and new consumers keep working.
|
|
2424
2558
|
healthMonitor: config.healthMonitor ?? config.healthProbe,
|
|
2425
2559
|
healthProbe: config.healthProbe ?? config.healthMonitor,
|
|
2426
2560
|
healthJourneys: config.healthJourneys,
|
|
2427
2561
|
};
|
|
2562
|
+
validateFailClosedDeclaration(provider);
|
|
2563
|
+
return provider;
|
|
2428
2564
|
}
|