@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/dist/define.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { validateFailClosedDeclaration } from "./declaration-validation.js";
|
|
3
|
+
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
4
|
+
import { ProviderError, ValidationError } from "./errors.js";
|
|
5
|
+
import { NativeEgressPolicyValidationError, validateNativeProviderConfig, } from "./native-egress-policy.js";
|
|
6
|
+
import { safeParseSchemaSync } from "./schema.js";
|
|
7
|
+
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
8
|
+
import { HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX, HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN, HEALTH_CHECK_TIMEOUT_MS_MAX, HEALTH_CHECK_TIMEOUT_MS_MIN, OPERATION_TIMEOUT_MS_MAX, OPERATION_TIMEOUT_MS_MIN, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, VALID_OPERATION_ERROR_STATUSES, } from "./types.js";
|
|
5
9
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
6
10
|
const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
7
11
|
const VALID_RUNTIMES = ["standard", "shared", "browser"];
|
|
@@ -10,45 +14,81 @@ const VALID_AUTH_MODES = [
|
|
|
10
14
|
"platform-managed",
|
|
11
15
|
"credentials",
|
|
12
16
|
"oauth2",
|
|
17
|
+
"oauth2_proxied",
|
|
13
18
|
];
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
const VALID_PROVIDER_PROXY_PROVIDERS = [
|
|
21
|
-
"smartproxy",
|
|
22
|
-
"decodo",
|
|
23
|
-
"custom",
|
|
19
|
+
const PROXIED_OAUTH_REQUIRED_FIELDS = [
|
|
20
|
+
"authorizeUrl",
|
|
21
|
+
"tokenUrl",
|
|
22
|
+
"customScheme",
|
|
23
|
+
"rewriteProfile",
|
|
24
|
+
"clientIdEnvKey",
|
|
24
25
|
];
|
|
26
|
+
const PROXIED_OAUTH_ALLOWED_FIELDS = new Set([
|
|
27
|
+
...PROXIED_OAUTH_REQUIRED_FIELDS,
|
|
28
|
+
"pkce",
|
|
29
|
+
"authorizeParams",
|
|
30
|
+
"tokenParams",
|
|
31
|
+
]);
|
|
32
|
+
const PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS = new Set([
|
|
33
|
+
"client_id",
|
|
34
|
+
"response_type",
|
|
35
|
+
"state",
|
|
36
|
+
"code_challenge",
|
|
37
|
+
"code_challenge_method",
|
|
38
|
+
]);
|
|
39
|
+
const PROXIED_OAUTH_PROFILE_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
40
|
+
const PROXIED_OAUTH_ENV_KEY_REGEX = /^[A-Z][A-Z0-9_]*__[A-Z0-9_]+$/;
|
|
41
|
+
const CUSTOM_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+$/;
|
|
42
|
+
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"];
|
|
43
|
+
const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"];
|
|
44
|
+
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"];
|
|
25
45
|
const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
26
46
|
"request",
|
|
27
47
|
"operation",
|
|
28
48
|
"auth-flow",
|
|
29
49
|
"connection",
|
|
30
50
|
];
|
|
51
|
+
const VALID_PROVIDER_OCR_MODES = ["optional", "required"];
|
|
31
52
|
const VALID_PROVIDER_STT_MODES = ["optional", "required"];
|
|
53
|
+
function exhaustiveLiteralArray() {
|
|
54
|
+
return (values, ..._missing) => values;
|
|
55
|
+
}
|
|
56
|
+
export const VALID_PROVIDER_RESOLVER_VENDORS = exhaustiveLiteralArray()([
|
|
57
|
+
"browser",
|
|
58
|
+
"capsolver",
|
|
59
|
+
"capmonster",
|
|
60
|
+
"2captcha",
|
|
61
|
+
"custom",
|
|
62
|
+
]);
|
|
63
|
+
export const VALID_PROVIDER_CHALLENGE_KINDS = exhaustiveLiteralArray()([
|
|
64
|
+
"turnstile",
|
|
65
|
+
"recaptcha_v2",
|
|
66
|
+
"recaptcha_v3",
|
|
67
|
+
"hcaptcha",
|
|
68
|
+
"cloudflare_interstitial",
|
|
69
|
+
"aws_waf",
|
|
70
|
+
"akamai_sec_cpt",
|
|
71
|
+
"akamai_sensor",
|
|
72
|
+
]);
|
|
32
73
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
74
|
+
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
75
|
+
const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
76
|
+
// Per-vendor provider-declared credential secrets. A required-mode chain must
|
|
77
|
+
// declare every secret of every credentialed vendor it names, so a missing
|
|
78
|
+
// credential fails at build/validation time rather than during a live outage: a
|
|
79
|
+
// declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
|
|
80
|
+
// exactly the failure class the multi-vendor chain exists to remove. Vendors
|
|
81
|
+
// absent from this map (the deprecated `custom`/`decodo` values have no managed
|
|
82
|
+
// adapter) impose no declaration requirement.
|
|
83
|
+
const VENDOR_REQUIRED_SECRETS = {
|
|
84
|
+
smartproxy: [SMARTPROXY_APP_KEY_SECRET],
|
|
85
|
+
nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
|
|
86
|
+
};
|
|
33
87
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
34
88
|
const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
|
|
35
|
-
const VALID_OPERATION_RISK_CLASSES = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"destructive",
|
|
39
|
-
"external-send",
|
|
40
|
-
];
|
|
41
|
-
const VALID_OPERATION_APPROVAL_POLICIES = [
|
|
42
|
-
"never",
|
|
43
|
-
"risk-based",
|
|
44
|
-
"always",
|
|
45
|
-
];
|
|
46
|
-
const VALID_OPERATION_TRANSPORT_KINDS = [
|
|
47
|
-
"json",
|
|
48
|
-
"sse",
|
|
49
|
-
"http-stream",
|
|
50
|
-
"websocket",
|
|
51
|
-
];
|
|
89
|
+
const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"];
|
|
90
|
+
const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"];
|
|
91
|
+
const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"];
|
|
52
92
|
const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
53
93
|
const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
54
94
|
const MS_DURATION_PATTERN = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*([a-zA-Z]+)?$/;
|
|
@@ -99,6 +139,76 @@ function assertLiteralField(value, field, validValues, providerId) {
|
|
|
99
139
|
});
|
|
100
140
|
}
|
|
101
141
|
}
|
|
142
|
+
function validateProxiedOAuthParams(value, field, providerId) {
|
|
143
|
+
if (value === undefined)
|
|
144
|
+
return;
|
|
145
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
146
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be an object of string values.`);
|
|
147
|
+
}
|
|
148
|
+
for (const [key, paramValue] of Object.entries(value)) {
|
|
149
|
+
if (!key.trim() || typeof paramValue !== "string") {
|
|
150
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must contain non-empty keys and string values.`);
|
|
151
|
+
}
|
|
152
|
+
if (field === "authorizeParams" &&
|
|
153
|
+
PROXIED_OAUTH_RESERVED_AUTHORIZE_PARAMS.has(key.toLowerCase())) {
|
|
154
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.authorizeParams cannot override reserved parameter "${key}".`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function validateProxiedOAuthAuth(auth, providerId) {
|
|
159
|
+
const proxied = auth.proxied;
|
|
160
|
+
if (auth.mode !== "oauth2_proxied") {
|
|
161
|
+
if (proxied !== undefined) {
|
|
162
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied is only valid when auth.mode is "oauth2_proxied".`);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!proxied || typeof proxied !== "object" || Array.isArray(proxied)) {
|
|
167
|
+
throw new ValidationError(`Provider "${providerId}" with auth.mode "oauth2_proxied" must declare auth.proxied.`);
|
|
168
|
+
}
|
|
169
|
+
const config = Object.fromEntries(Object.entries(proxied));
|
|
170
|
+
for (const key of Object.keys(config)) {
|
|
171
|
+
if (!PROXIED_OAUTH_ALLOWED_FIELDS.has(key)) {
|
|
172
|
+
throw new ValidationError(`Provider "${providerId}" has unknown auth.proxied field "${key}".`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const field of PROXIED_OAUTH_REQUIRED_FIELDS) {
|
|
176
|
+
if (typeof config[field] !== "string" || !config[field].trim()) {
|
|
177
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be a non-empty string.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const field of ["authorizeUrl", "tokenUrl"]) {
|
|
181
|
+
try {
|
|
182
|
+
const endpoint = new URL(String(config[field]));
|
|
183
|
+
if (endpoint.protocol !== "https:" ||
|
|
184
|
+
endpoint.username ||
|
|
185
|
+
endpoint.password ||
|
|
186
|
+
endpoint.hash) {
|
|
187
|
+
throw new Error("invalid endpoint");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.${field} must be an absolute HTTPS URL without credentials or a fragment.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const customScheme = String(config.customScheme);
|
|
195
|
+
if (!CUSTOM_SCHEME_REGEX.test(customScheme) ||
|
|
196
|
+
customScheme.toLowerCase().startsWith("http://") ||
|
|
197
|
+
customScheme.toLowerCase().startsWith("https://")) {
|
|
198
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.customScheme must be a non-HTTP custom-scheme URL prefix.`);
|
|
199
|
+
}
|
|
200
|
+
if (!PROXIED_OAUTH_PROFILE_REGEX.test(String(config.rewriteProfile))) {
|
|
201
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.rewriteProfile must be a kebab-case profile name.`);
|
|
202
|
+
}
|
|
203
|
+
if (!PROXIED_OAUTH_ENV_KEY_REGEX.test(String(config.clientIdEnvKey))) {
|
|
204
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.clientIdEnvKey must be an APIFuse-style uppercase environment key.`);
|
|
205
|
+
}
|
|
206
|
+
if (config.pkce !== undefined && config.pkce !== "S256" && config.pkce !== "none") {
|
|
207
|
+
throw new ValidationError(`Provider "${providerId}" auth.proxied.pkce must be "S256" or "none".`);
|
|
208
|
+
}
|
|
209
|
+
validateProxiedOAuthParams(config.authorizeParams, "authorizeParams", providerId);
|
|
210
|
+
validateProxiedOAuthParams(config.tokenParams, "tokenParams", providerId);
|
|
211
|
+
}
|
|
102
212
|
function validateProviderShape(config) {
|
|
103
213
|
assertObjectConfig(config);
|
|
104
214
|
assertRequiredField(config, "id");
|
|
@@ -109,11 +219,11 @@ function validateProviderShape(config) {
|
|
|
109
219
|
if (typeof config.runtime === "string")
|
|
110
220
|
assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
|
|
111
221
|
const auth = config.auth;
|
|
112
|
-
if (auth &&
|
|
113
|
-
typeof auth === "object" &&
|
|
114
|
-
"mode" in auth &&
|
|
115
|
-
typeof auth.mode === "string")
|
|
222
|
+
if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
|
|
116
223
|
assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
|
|
224
|
+
if (auth && typeof auth === "object" && !Array.isArray(auth)) {
|
|
225
|
+
validateProxiedOAuthAuth(Object.fromEntries(Object.entries(auth)), String(config.id));
|
|
226
|
+
}
|
|
117
227
|
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
118
228
|
throw new ProviderError(`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`, {
|
|
119
229
|
fix: "Use the single canonical auth interface: auth.flow. Gateway calls auth.flow.start/continue/poll/abort/refresh only and persists complete turn data.credential as-is, so put login/token/session exchange inside auth.flow.continue.",
|
|
@@ -167,15 +277,23 @@ function validateProviderProxy(config) {
|
|
|
167
277
|
fix: `Use proxy: { mode: "required", provider: "smartproxy", geo: { country: "KR" }, session: { affinity: "connection", lifetimeMinutes: 30 } }`,
|
|
168
278
|
});
|
|
169
279
|
}
|
|
170
|
-
rejectUnknownFields(proxy, new Set(["mode", "provider", "geo", "session"]), "proxy");
|
|
280
|
+
rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
|
|
171
281
|
assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
|
|
172
282
|
if (proxy.provider !== undefined) {
|
|
173
283
|
assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
174
284
|
}
|
|
285
|
+
if (proxy.providers !== undefined) {
|
|
286
|
+
if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
|
|
287
|
+
throw new ValidationError(`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`, {
|
|
288
|
+
fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
for (const vendor of proxy.providers) {
|
|
292
|
+
assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
175
295
|
if (proxy.geo !== undefined) {
|
|
176
|
-
if (!proxy.geo ||
|
|
177
|
-
typeof proxy.geo !== "object" ||
|
|
178
|
-
Array.isArray(proxy.geo)) {
|
|
296
|
+
if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
|
|
179
297
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.geo: must be an object.`, {
|
|
180
298
|
fix: `Use proxy.geo: { country: "KR" } with ISO alpha-2 country codes.`,
|
|
181
299
|
});
|
|
@@ -192,47 +310,147 @@ function validateProviderProxy(config) {
|
|
|
192
310
|
}
|
|
193
311
|
}
|
|
194
312
|
if (proxy.session !== undefined) {
|
|
195
|
-
if (!proxy.session ||
|
|
196
|
-
typeof proxy.session !== "object" ||
|
|
197
|
-
Array.isArray(proxy.session)) {
|
|
313
|
+
if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
|
|
198
314
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session: must be an object.`, {
|
|
199
315
|
fix: `Use proxy.session: { affinity: "connection", lifetimeMinutes: 30 }.`,
|
|
200
316
|
});
|
|
201
317
|
}
|
|
202
|
-
rejectUnknownFields(proxy.session, new Set(["affinity", "lifetimeMinutes", "poolSize"]), "proxy.session");
|
|
318
|
+
rejectUnknownFields(proxy.session, new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]), "proxy.session");
|
|
203
319
|
if (proxy.session.affinity !== undefined) {
|
|
204
320
|
assertLiteralField(proxy.session.affinity, "proxy.session.affinity", VALID_PROVIDER_PROXY_AFFINITIES, config.id);
|
|
205
321
|
}
|
|
206
322
|
const lifetime = proxy.session.lifetimeMinutes;
|
|
207
|
-
if (lifetime !== undefined &&
|
|
208
|
-
(!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
323
|
+
if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
209
324
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`);
|
|
210
325
|
}
|
|
211
326
|
const poolSize = proxy.session.poolSize;
|
|
212
|
-
if (poolSize !== undefined &&
|
|
213
|
-
(!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
327
|
+
if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
214
328
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`);
|
|
215
329
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
330
|
+
const drainLeadSeconds = proxy.session.drainLeadSeconds;
|
|
331
|
+
if (drainLeadSeconds !== undefined &&
|
|
332
|
+
(!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)) {
|
|
333
|
+
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`, {
|
|
334
|
+
fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
// A drain lead longer than the sticky lifetime would fire the expiring
|
|
338
|
+
// event before the session is even established, so the provider would
|
|
339
|
+
// never get a usable window. Reject the contradiction at build time.
|
|
340
|
+
if (drainLeadSeconds !== undefined &&
|
|
341
|
+
lifetime !== undefined &&
|
|
342
|
+
Number.isFinite(lifetime) &&
|
|
343
|
+
drainLeadSeconds >= lifetime * 60) {
|
|
344
|
+
throw new ValidationError(`Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`, {
|
|
345
|
+
fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
|
|
222
346
|
});
|
|
223
347
|
}
|
|
224
348
|
}
|
|
349
|
+
// Every credentialed vendor in a required-mode chain must declare its
|
|
350
|
+
// provider secret(s) so a missing credential fails at build/validation time,
|
|
351
|
+
// not during a live outage. This covers the fallback legs too (not just the
|
|
352
|
+
// first vendor): a declared-but-uncredentialed nodemaven fallback would leave
|
|
353
|
+
// the chain silently down to a single vendor, reintroducing the SPOF the chain
|
|
354
|
+
// removes.
|
|
355
|
+
const vendorChain = proxy.providers && proxy.providers.length > 0
|
|
356
|
+
? proxy.providers
|
|
357
|
+
: proxy.provider
|
|
358
|
+
? [proxy.provider]
|
|
359
|
+
: [];
|
|
360
|
+
if (proxy.mode === "required") {
|
|
361
|
+
for (const vendor of vendorChain) {
|
|
362
|
+
const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
|
|
363
|
+
if (!requiredSecrets)
|
|
364
|
+
continue;
|
|
365
|
+
for (const secretName of requiredSecrets) {
|
|
366
|
+
// Match the canonical runtime gate (assertRequiredSecretsPresent /
|
|
367
|
+
// listMissingRequiredSecrets), which enforces only `required === true`
|
|
368
|
+
// declarations. A declaration that omits `required` (defaulting to
|
|
369
|
+
// optional) is skipped at runtime, so accepting it here would pass
|
|
370
|
+
// validation while leaving the credential unenforced until proxy
|
|
371
|
+
// resolution during a live request — the fail-open gap this check exists
|
|
372
|
+
// to close.
|
|
373
|
+
const declared = config.secrets?.some((secret) => secret.name === secretName && secret.required === true);
|
|
374
|
+
if (!declared) {
|
|
375
|
+
throw new ValidationError(`Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`, {
|
|
376
|
+
fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// `decodo`/`custom` are deprecated vendor values (string-union members, so the
|
|
383
|
+
// @deprecated symbol gate can't catch them — warn at validation time instead).
|
|
384
|
+
const deprecatedVendors = vendorChain.filter((vendor) => vendor === "decodo" || vendor === "custom");
|
|
385
|
+
if (proxy.mode === "required" &&
|
|
386
|
+
vendorChain.length > 0 &&
|
|
387
|
+
deprecatedVendors.length === vendorChain.length) {
|
|
388
|
+
throw new ValidationError(`Provider "${config.id}" requires proxy egress but declares only deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}.`, {
|
|
389
|
+
fix: `Use proxy.provider or proxy.providers with "smartproxy" and/or "nodemaven".`,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (deprecatedVendors.length > 0) {
|
|
393
|
+
console.warn(`[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven".`);
|
|
394
|
+
}
|
|
225
395
|
}
|
|
226
396
|
function validateProviderStt(config) {
|
|
227
397
|
const stt = config.stt;
|
|
228
398
|
if (stt === undefined)
|
|
229
399
|
return;
|
|
230
400
|
if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
|
|
231
|
-
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
401
|
+
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
402
|
+
fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
|
|
403
|
+
});
|
|
232
404
|
}
|
|
233
405
|
rejectUnknownFields(stt, new Set(["mode"]), "stt");
|
|
234
406
|
assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
|
|
235
407
|
}
|
|
408
|
+
function validateProviderOcr(config) {
|
|
409
|
+
const ocr = config.ocr;
|
|
410
|
+
if (ocr === undefined)
|
|
411
|
+
return;
|
|
412
|
+
if (!ocr || typeof ocr !== "object" || Array.isArray(ocr)) {
|
|
413
|
+
throw new ValidationError(`Provider "${config.id}" has invalid ocr: must be an object.`, {
|
|
414
|
+
fix: `Use ocr: { mode: "required" } or ocr: { mode: "optional" }.`,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
rejectUnknownFields(ocr, new Set(["mode"]), "ocr");
|
|
418
|
+
assertLiteralField(ocr.mode, "ocr.mode", VALID_PROVIDER_OCR_MODES, config.id);
|
|
419
|
+
}
|
|
420
|
+
function validateProviderResolver(config) {
|
|
421
|
+
const resolver = config.resolver;
|
|
422
|
+
if (resolver === undefined)
|
|
423
|
+
return;
|
|
424
|
+
if (!resolver || typeof resolver !== "object" || Array.isArray(resolver)) {
|
|
425
|
+
throw new ValidationError(`Provider "${config.id}" has invalid resolver: must be an object.`, {
|
|
426
|
+
fix: `Set resolver for provider "${config.id}" to { vendors: ["2captcha"], kinds: ["turnstile"] }.`,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
rejectUnknownFields(resolver, new Set(["vendors", "kinds", "clientProfile"]), "resolver", config.id);
|
|
430
|
+
validateResolverLiteralArray(resolver.vendors, "resolver.vendors", VALID_PROVIDER_RESOLVER_VENDORS, config.id);
|
|
431
|
+
validateResolverLiteralArray(resolver.kinds, "resolver.kinds", VALID_PROVIDER_CHALLENGE_KINDS, config.id);
|
|
432
|
+
if (resolver.clientProfile !== undefined &&
|
|
433
|
+
(typeof resolver.clientProfile !== "string" || !resolver.clientProfile.trim())) {
|
|
434
|
+
throw new ValidationError(`Provider "${config.id}" has invalid resolver.clientProfile: must be a non-empty string.`, {
|
|
435
|
+
fix: `Set resolver.clientProfile for provider "${config.id}" to a transport-owned profile name.`,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function validateResolverLiteralArray(value, field, validValues, providerId) {
|
|
440
|
+
if (!Array.isArray(value)) {
|
|
441
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an array.`, {
|
|
442
|
+
fix: `Set ${field} for provider "${providerId}" to an array containing only: ${validValues.join(", ")}.`,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
for (const [index, item] of value.entries()) {
|
|
446
|
+
if (typeof item === "string" && validValues.some((validValue) => validValue === item)) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}[${index}]: ${JSON.stringify(item)}. Expected one of: ${validValues.join(", ")}`, {
|
|
450
|
+
fix: `Set ${field}[${index}] for provider "${providerId}" to one of ${validValues.map((validValue) => `"${validValue}"`).join(", ")}.`,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
236
454
|
function validateOperationIds(providerId, operations) {
|
|
237
455
|
for (const operationName of Object.keys(operations)) {
|
|
238
456
|
if (!OPERATION_ID_REGEX.test(operationName))
|
|
@@ -247,12 +465,7 @@ function validateOperationIds(providerId, operations) {
|
|
|
247
465
|
}
|
|
248
466
|
const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
249
467
|
const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
250
|
-
const VALID_OPERATION_LIFECYCLES = [
|
|
251
|
-
"stable",
|
|
252
|
-
"beta",
|
|
253
|
-
"deprecated",
|
|
254
|
-
"removed",
|
|
255
|
-
];
|
|
468
|
+
const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"];
|
|
256
469
|
function assertNonEmptyString(value, field, providerId, operationName) {
|
|
257
470
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
258
471
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${field}: must be a non-empty string.`, { fix: `Set ${field} to a non-empty customer-facing value.` });
|
|
@@ -268,8 +481,7 @@ function validateToolRouterMetadata(providerId, operations) {
|
|
|
268
481
|
fix: `Remove operations.${operationName}.toolRouter or provide MCP-safe metadata.`,
|
|
269
482
|
});
|
|
270
483
|
}
|
|
271
|
-
if (toolRouter.name !== undefined &&
|
|
272
|
-
!MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
484
|
+
if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
273
485
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`, {
|
|
274
486
|
fix: `Use letters, numbers, and underscores only, starting with a letter, for example "${providerId.replace(/[^A-Za-z0-9]+/g, "_")}__${operationName.replace(/[^A-Za-z0-9]+/g, "_")}".`,
|
|
275
487
|
});
|
|
@@ -307,8 +519,7 @@ function validateOperationContracts(providerId, operations) {
|
|
|
307
519
|
if (contract.lifecycle !== undefined) {
|
|
308
520
|
assertLiteralField(contract.lifecycle, `operations.${operationName}.contract.lifecycle`, VALID_OPERATION_LIFECYCLES, providerId);
|
|
309
521
|
}
|
|
310
|
-
if (contract.lifecycle === "deprecated" ||
|
|
311
|
-
contract.lifecycle === "removed") {
|
|
522
|
+
if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
|
|
312
523
|
if (!contract.deprecation || typeof contract.deprecation !== "object") {
|
|
313
524
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`, {
|
|
314
525
|
fix: `Add announcedAt, removalAfter, and migrationGuide to operations.${operationName}.contract.deprecation.`,
|
|
@@ -333,8 +544,7 @@ function validateOperationAnnotations(providerId, operations) {
|
|
|
333
544
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an integer number of milliseconds.`, {
|
|
334
545
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
|
|
335
546
|
});
|
|
336
|
-
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
|
|
337
|
-
timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
547
|
+
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
338
548
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`, {
|
|
339
549
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms (the upper bound stays below the gateway/ALB ceiling).`,
|
|
340
550
|
});
|
|
@@ -377,6 +587,23 @@ function validateOperationObservability(providerId, operations) {
|
|
|
377
587
|
}
|
|
378
588
|
}
|
|
379
589
|
}
|
|
590
|
+
function validateOperationErrorCodes(providerId, operations) {
|
|
591
|
+
for (const [operationName, operation] of Object.entries(operations)) {
|
|
592
|
+
for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
|
|
593
|
+
if (errorCode.status !== undefined &&
|
|
594
|
+
!VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)) {
|
|
595
|
+
const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
|
|
596
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`, {
|
|
597
|
+
fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
if (errorCode.status !== undefined &&
|
|
601
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)) {
|
|
602
|
+
console.warn(`[provider-sdk] Provider "${providerId}" operation "${operationName}" declares status ${errorCode.status} for SDK-owned error code "${errorCode.code}"; the declared status is documentation-only and will be ignored at runtime.`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
380
607
|
const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
|
|
381
608
|
const SSE_TRANSPORT_FIELDS = new Set([
|
|
382
609
|
"kind",
|
|
@@ -478,9 +705,7 @@ function validateOperationTransports(providerId, operations) {
|
|
|
478
705
|
assertStreamMs(maxDurationMs, `${fieldPath}.maxDurationMs`, STREAM_MAX_DURATION_MS_MIN, STREAM_MAX_DURATION_MS_MAX, "max duration");
|
|
479
706
|
assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
|
|
480
707
|
const resumable = Reflect.get(transport, "resumable");
|
|
481
|
-
if (resumable !== undefined &&
|
|
482
|
-
resumable !== false &&
|
|
483
|
-
resumable !== "last-event-id") {
|
|
708
|
+
if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
|
|
484
709
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`, {
|
|
485
710
|
fix: `Use ${fieldPath}.resumable: "last-event-id" for SSE Last-Event-ID resume support, or false to disable resume.`,
|
|
486
711
|
});
|
|
@@ -515,8 +740,7 @@ function validateOperationTransports(providerId, operations) {
|
|
|
515
740
|
});
|
|
516
741
|
}
|
|
517
742
|
for (const subprotocol of subprotocols) {
|
|
518
|
-
if (typeof subprotocol !== "string" ||
|
|
519
|
-
!WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
743
|
+
if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
520
744
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`, {
|
|
521
745
|
fix: `Use values such as "apifuse.v1" without spaces or separators that are invalid for Sec-WebSocket-Protocol.`,
|
|
522
746
|
});
|
|
@@ -601,14 +825,18 @@ function suggestField(unknown, candidates) {
|
|
|
601
825
|
}
|
|
602
826
|
return best;
|
|
603
827
|
}
|
|
604
|
-
function rejectUnknownFields(value, allowed, fieldPath) {
|
|
828
|
+
function rejectUnknownFields(value, allowed, fieldPath, providerId) {
|
|
605
829
|
for (const key of Object.keys(value)) {
|
|
606
830
|
if (allowed.has(key))
|
|
607
831
|
continue;
|
|
608
832
|
const hint = suggestField(key, allowed);
|
|
609
833
|
throw new ValidationError(hint
|
|
610
834
|
? `Unknown field "${key}" on ${fieldPath}. Did you mean "${hint}"?`
|
|
611
|
-
: `Unknown field "${key}" on ${fieldPath}.`, {
|
|
835
|
+
: `Unknown field "${key}" on ${fieldPath}.`, {
|
|
836
|
+
fix: providerId
|
|
837
|
+
? `Remove ${fieldPath}.${key} from provider "${providerId}" or rename it.`
|
|
838
|
+
: `Remove ${fieldPath}.${key} or rename it.`,
|
|
839
|
+
});
|
|
612
840
|
}
|
|
613
841
|
}
|
|
614
842
|
function assertBoundedIntegerMs(value, fieldPath, options) {
|
|
@@ -624,9 +852,7 @@ function assertBoundedIntegerMs(value, fieldPath, options) {
|
|
|
624
852
|
function validateProviderHealthMonitor(providerId, healthMonitor, field = "healthMonitor") {
|
|
625
853
|
if (healthMonitor === undefined)
|
|
626
854
|
return;
|
|
627
|
-
if (!healthMonitor ||
|
|
628
|
-
typeof healthMonitor !== "object" ||
|
|
629
|
-
Array.isArray(healthMonitor))
|
|
855
|
+
if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
|
|
630
856
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
|
|
631
857
|
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
632
858
|
});
|
|
@@ -676,9 +902,7 @@ function validateProviderHealthMonitor(providerId, healthMonitor, field = "healt
|
|
|
676
902
|
}
|
|
677
903
|
const probeOverrides = healthMonitorRecord.probeOverrides;
|
|
678
904
|
if (probeOverrides !== undefined) {
|
|
679
|
-
if (!probeOverrides ||
|
|
680
|
-
typeof probeOverrides !== "object" ||
|
|
681
|
-
Array.isArray(probeOverrides))
|
|
905
|
+
if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
|
|
682
906
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`);
|
|
683
907
|
for (const [probeId, override] of Object.entries(probeOverrides)) {
|
|
684
908
|
if (probeId.length === 0)
|
|
@@ -756,9 +980,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
756
980
|
fix: `Set ${fieldPath}.interval to a positive ms-style duration string.`,
|
|
757
981
|
});
|
|
758
982
|
if (s.schedule !== undefined) {
|
|
759
|
-
if (!s.schedule ||
|
|
760
|
-
typeof s.schedule !== "object" ||
|
|
761
|
-
Array.isArray(s.schedule)) {
|
|
983
|
+
if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
|
|
762
984
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.schedule must be an object.`);
|
|
763
985
|
}
|
|
764
986
|
if (Reflect.get(s.schedule, "jitter") !== undefined) {
|
|
@@ -784,8 +1006,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
784
1006
|
label: "degraded threshold",
|
|
785
1007
|
});
|
|
786
1008
|
}
|
|
787
|
-
if (s.requiresConnection !== undefined &&
|
|
788
|
-
typeof s.requiresConnection !== "boolean")
|
|
1009
|
+
if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
|
|
789
1010
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`);
|
|
790
1011
|
if (!Array.isArray(s.cases) || s.cases.length === 0)
|
|
791
1012
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.cases must be a non-empty array.`, {
|
|
@@ -804,9 +1025,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
804
1025
|
}
|
|
805
1026
|
function validateHealthCheckUnsupported(providerId, operationName, unsupported) {
|
|
806
1027
|
const fieldPath = `operations.${operationName}.healthCheckUnsupported`;
|
|
807
|
-
if (!unsupported ||
|
|
808
|
-
typeof unsupported !== "object" ||
|
|
809
|
-
Array.isArray(unsupported))
|
|
1028
|
+
if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
|
|
810
1029
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
811
1030
|
rejectUnknownFields(unsupported, HEALTH_CHECK_UNSUPPORTED_FIELDS, fieldPath);
|
|
812
1031
|
const u = unsupported;
|
|
@@ -831,12 +1050,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
|
|
|
831
1050
|
"steps",
|
|
832
1051
|
"run",
|
|
833
1052
|
]);
|
|
834
|
-
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
|
|
835
|
-
"kind",
|
|
836
|
-
"interval",
|
|
837
|
-
"jitter",
|
|
838
|
-
"randomize",
|
|
839
|
-
]);
|
|
1053
|
+
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
|
|
840
1054
|
const HEALTH_JOURNEY_STEP_FIELDS = new Set([
|
|
841
1055
|
"id",
|
|
842
1056
|
"description",
|
|
@@ -854,10 +1068,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
|
|
|
854
1068
|
"minManualInterval",
|
|
855
1069
|
"publicRationale",
|
|
856
1070
|
]);
|
|
857
|
-
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
|
|
858
|
-
"enabled",
|
|
859
|
-
"reason",
|
|
860
|
-
]);
|
|
1071
|
+
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
|
|
861
1072
|
const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
|
|
862
1073
|
"enabled",
|
|
863
1074
|
"requiresAcknowledgement",
|
|
@@ -872,9 +1083,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS = new Set([
|
|
|
872
1083
|
]);
|
|
873
1084
|
function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger) {
|
|
874
1085
|
const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
|
|
875
|
-
if (!manualTrigger ||
|
|
876
|
-
typeof manualTrigger !== "object" ||
|
|
877
|
-
Array.isArray(manualTrigger)) {
|
|
1086
|
+
if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
|
|
878
1087
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object when present.`);
|
|
879
1088
|
}
|
|
880
1089
|
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
|
|
@@ -897,8 +1106,7 @@ function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger
|
|
|
897
1106
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`);
|
|
898
1107
|
}
|
|
899
1108
|
const risk = Reflect.get(manualTrigger, "risk");
|
|
900
|
-
if (typeof risk !== "string" ||
|
|
901
|
-
!HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
1109
|
+
if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
902
1110
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`);
|
|
903
1111
|
}
|
|
904
1112
|
if (risk !== "read_only" && requiresAcknowledgement !== true) {
|
|
@@ -950,7 +1158,7 @@ function isoDurationMs(value) {
|
|
|
950
1158
|
const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
|
|
951
1159
|
const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
|
|
952
1160
|
const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
|
|
953
|
-
return
|
|
1161
|
+
return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
|
|
954
1162
|
}
|
|
955
1163
|
function scheduleRandomizationMs(randomize, fieldPath) {
|
|
956
1164
|
const mode = Reflect.get(randomize, "mode");
|
|
@@ -974,9 +1182,7 @@ function validateScheduleRandomization(randomize, fieldPath, intervalMs) {
|
|
|
974
1182
|
throw new ValidationError(`${fieldPath} must be an object.`);
|
|
975
1183
|
}
|
|
976
1184
|
const mode = Reflect.get(randomize, "mode");
|
|
977
|
-
const allowedFields = mode === "centered"
|
|
978
|
-
? new Set(["mode", "maxOffset"])
|
|
979
|
-
: new Set(["mode", "maxDelay"]);
|
|
1185
|
+
const allowedFields = mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
|
|
980
1186
|
rejectUnknownFields(randomize, allowedFields, fieldPath);
|
|
981
1187
|
const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
|
|
982
1188
|
if (offsetMs <= 0) {
|
|
@@ -1062,9 +1268,7 @@ function countCapturingGroups(pattern) {
|
|
|
1062
1268
|
const next = source[i + 1];
|
|
1063
1269
|
if (next === "?" && source[i + 2] !== "<")
|
|
1064
1270
|
continue;
|
|
1065
|
-
if (next === "?" &&
|
|
1066
|
-
source[i + 2] === "<" &&
|
|
1067
|
-
(source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1271
|
+
if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1068
1272
|
continue;
|
|
1069
1273
|
count += 1;
|
|
1070
1274
|
}
|
|
@@ -1142,8 +1346,7 @@ function validateSmsOtpMatcher(matcher, fieldPath) {
|
|
|
1142
1346
|
throw new ValidationError(`${fieldPath}.code.pattern must be a RegExp or pattern source string.`);
|
|
1143
1347
|
}
|
|
1144
1348
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
1145
|
-
if (countCapturingGroups(regex) !== 1 &&
|
|
1146
|
-
Reflect.get(code, "capture") === undefined) {
|
|
1349
|
+
if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
|
|
1147
1350
|
throw new ValidationError(`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`);
|
|
1148
1351
|
}
|
|
1149
1352
|
if (Reflect.get(code, "capture") !== undefined &&
|
|
@@ -1195,8 +1398,7 @@ function validateHealthJourneySchedule(providerId, journeyId, schedule) {
|
|
|
1195
1398
|
const interval = Reflect.get(schedule, "interval");
|
|
1196
1399
|
assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
|
|
1197
1400
|
const randomize = Reflect.get(schedule, "randomize");
|
|
1198
|
-
if (Reflect.get(schedule, "jitter") !== undefined &&
|
|
1199
|
-
randomize !== undefined) {
|
|
1401
|
+
if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
|
|
1200
1402
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`);
|
|
1201
1403
|
}
|
|
1202
1404
|
if (Reflect.get(schedule, "jitter") !== undefined)
|
|
@@ -1226,8 +1428,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
|
|
|
1226
1428
|
throw new ValidationError(`Provider "${providerId}" has duplicate health journey id "${journey.id}".`);
|
|
1227
1429
|
journeyIds.add(journey.id);
|
|
1228
1430
|
validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
|
|
1229
|
-
if (!Array.isArray(journey.coversOperations) ||
|
|
1230
|
-
journey.coversOperations.length === 0) {
|
|
1431
|
+
if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
|
|
1231
1432
|
throw new ValidationError(`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`);
|
|
1232
1433
|
}
|
|
1233
1434
|
for (const operationId of journey.coversOperations) {
|
|
@@ -1265,8 +1466,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
|
|
|
1265
1466
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.id must be a kebab-case identifier.`);
|
|
1266
1467
|
if (step.operationId !== undefined && !operations[step.operationId])
|
|
1267
1468
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`);
|
|
1268
|
-
if (step.usesSmsMatcher !== undefined &&
|
|
1269
|
-
!matcherIds.has(step.usesSmsMatcher))
|
|
1469
|
+
if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
|
|
1270
1470
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`);
|
|
1271
1471
|
}
|
|
1272
1472
|
if (journey.manualTrigger !== undefined)
|
|
@@ -1300,9 +1500,7 @@ function validateOperationHealthChecks(providerId, operations, journeyCoveredOpe
|
|
|
1300
1500
|
validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
|
|
1301
1501
|
if (hasUnsupported)
|
|
1302
1502
|
validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
|
|
1303
|
-
if (!hasCheck &&
|
|
1304
|
-
!hasUnsupported &&
|
|
1305
|
-
!journeyCoveredOperations.has(operationName))
|
|
1503
|
+
if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
|
|
1306
1504
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`, {
|
|
1307
1505
|
fix: `Add \`healthCheck: { interval, cases: [...] }\` or \`healthCheckUnsupported: { reason: "..." }\` to operations.${operationName}.`,
|
|
1308
1506
|
});
|
|
@@ -1314,6 +1512,21 @@ function validateOperationFixtures(providerId, operations) {
|
|
|
1314
1512
|
throw new ValidationError(`Operation handler must be defined for provider "${providerId}" operation "${operationName}"`, {
|
|
1315
1513
|
fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
|
|
1316
1514
|
});
|
|
1515
|
+
if (operation.fixtures?.recordedAt !== undefined) {
|
|
1516
|
+
const recordedAt = operation.fixtures.recordedAt;
|
|
1517
|
+
const parsed = typeof recordedAt === "string"
|
|
1518
|
+
? new Date(`${recordedAt}T00:00:00.000Z`)
|
|
1519
|
+
: new Date(Number.NaN);
|
|
1520
|
+
const isCalendarDate = typeof recordedAt === "string" &&
|
|
1521
|
+
/^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
|
|
1522
|
+
!Number.isNaN(parsed.getTime()) &&
|
|
1523
|
+
parsed.toISOString().slice(0, 10) === recordedAt;
|
|
1524
|
+
const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
1525
|
+
if (!isCalendarDate || recordedAt > kstToday)
|
|
1526
|
+
throw new ValidationError(`Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`, {
|
|
1527
|
+
fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1317
1530
|
if (operation.fixtures?.request !== undefined) {
|
|
1318
1531
|
const result = safeParseSchemaSync(operation.input, operation.fixtures.request, `operations.${operationName}.fixtures.request`);
|
|
1319
1532
|
if (!result.success)
|
|
@@ -1332,8 +1545,44 @@ function validateOperationFixtures(providerId, operations) {
|
|
|
1332
1545
|
}
|
|
1333
1546
|
}
|
|
1334
1547
|
}
|
|
1548
|
+
function resolveOperationFixtureRequests(operations) {
|
|
1549
|
+
let changed = false;
|
|
1550
|
+
const resolvedOperations = Object.fromEntries(Object.entries(operations).map(([operationName, operation]) => {
|
|
1551
|
+
if (operation.fixtures?.request === undefined)
|
|
1552
|
+
return [operationName, operation];
|
|
1553
|
+
const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
|
|
1554
|
+
if (request === operation.fixtures.request)
|
|
1555
|
+
return [operationName, operation];
|
|
1556
|
+
changed = true;
|
|
1557
|
+
return [
|
|
1558
|
+
operationName,
|
|
1559
|
+
{
|
|
1560
|
+
...operation,
|
|
1561
|
+
fixtures: {
|
|
1562
|
+
...operation.fixtures,
|
|
1563
|
+
request,
|
|
1564
|
+
},
|
|
1565
|
+
},
|
|
1566
|
+
];
|
|
1567
|
+
}));
|
|
1568
|
+
return changed ? resolvedOperations : operations;
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Shallow shape guard only: the `deployment` object is passed through
|
|
1572
|
+
* verbatim and deliberately not deep-validated by the SDK — the APIFuse
|
|
1573
|
+
* registry builder owns deployment validation and profile resolution.
|
|
1574
|
+
*/
|
|
1575
|
+
function validateProviderDeployment(providerId, deployment) {
|
|
1576
|
+
if (deployment === undefined)
|
|
1577
|
+
return;
|
|
1578
|
+
if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
|
|
1579
|
+
throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
|
|
1580
|
+
fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1335
1583
|
export function defineProvider(config) {
|
|
1336
1584
|
validateProviderShape(config);
|
|
1585
|
+
const operations = resolveOperationFixtureRequests(config.operations);
|
|
1337
1586
|
if (!CONNECTOR_ID_REGEX.test(config.id))
|
|
1338
1587
|
throw new ProviderError(`Invalid provider id: "${config.id}"`, {
|
|
1339
1588
|
fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
|
|
@@ -1345,6 +1594,7 @@ export function defineProvider(config) {
|
|
|
1345
1594
|
validateOperationIds(config.id, config.operations);
|
|
1346
1595
|
validateOperationAnnotations(config.id, config.operations);
|
|
1347
1596
|
validateOperationObservability(config.id, config.operations);
|
|
1597
|
+
validateOperationErrorCodes(config.id, config.operations);
|
|
1348
1598
|
validateOperationTransports(config.id, config.operations);
|
|
1349
1599
|
validateOperationContracts(config.id, config.operations);
|
|
1350
1600
|
validateToolRouterMetadata(config.id, config.operations);
|
|
@@ -1355,23 +1605,40 @@ export function defineProvider(config) {
|
|
|
1355
1605
|
fix: "Keep healthProbe (the new name) and delete the healthMonitor block.",
|
|
1356
1606
|
});
|
|
1357
1607
|
validateProviderHealthMonitor(config.id, config.healthProbe ?? config.healthMonitor, config.healthProbe !== undefined ? "healthProbe" : "healthMonitor");
|
|
1358
|
-
validateOperationFixtures(config.id,
|
|
1608
|
+
validateOperationFixtures(config.id, operations);
|
|
1609
|
+
validateProviderDeployment(config.id, config.deployment);
|
|
1610
|
+
try {
|
|
1611
|
+
validateNativeProviderConfig(config.native);
|
|
1612
|
+
}
|
|
1613
|
+
catch (error) {
|
|
1614
|
+
if (error instanceof NativeEgressPolicyValidationError)
|
|
1615
|
+
throw new ValidationError(error.message);
|
|
1616
|
+
throw error;
|
|
1617
|
+
}
|
|
1359
1618
|
validateProviderProxy(config);
|
|
1619
|
+
validateProviderOcr(config);
|
|
1360
1620
|
validateProviderStt(config);
|
|
1621
|
+
validateProviderResolver(config);
|
|
1361
1622
|
if (config.runtime === "browser" && !config.browser)
|
|
1362
1623
|
throw new ProviderError(`Provider "${config.id}" must define browser.engine when runtime is "browser"`, {
|
|
1363
1624
|
fix: 'Add browser: { engine: "playwright-stealth" } for TypeScript providers, or another supported engine for your runtime',
|
|
1364
1625
|
});
|
|
1365
1626
|
if (config.browser && config.runtime !== "browser")
|
|
1366
1627
|
throw new ProviderError(`Provider "${config.id}" cannot define browser config unless runtime is "browser"`, { fix: 'Set runtime: "browser" or remove the browser config' });
|
|
1367
|
-
|
|
1628
|
+
const provider = {
|
|
1368
1629
|
id: config.id,
|
|
1369
1630
|
version: config.version,
|
|
1370
1631
|
runtime: config.runtime,
|
|
1632
|
+
// Verbatim passthrough: deployment validation and profile resolution
|
|
1633
|
+
// are owned by the APIFuse registry builder, not the SDK.
|
|
1634
|
+
deployment: config.deployment,
|
|
1371
1635
|
allowedHosts: config.allowedHosts,
|
|
1636
|
+
native: config.native,
|
|
1372
1637
|
stealth: config.stealth,
|
|
1373
1638
|
proxy: config.proxy,
|
|
1639
|
+
ocr: config.ocr,
|
|
1374
1640
|
stt: config.stt,
|
|
1641
|
+
resolver: config.resolver,
|
|
1375
1642
|
browser: config.browser,
|
|
1376
1643
|
auth: config.auth,
|
|
1377
1644
|
reviewed: config.reviewed,
|
|
@@ -1380,11 +1647,13 @@ export function defineProvider(config) {
|
|
|
1380
1647
|
credential: config.credential,
|
|
1381
1648
|
context: config.context,
|
|
1382
1649
|
meta: config.meta,
|
|
1383
|
-
operations
|
|
1650
|
+
operations,
|
|
1384
1651
|
// Transitional healthMonitor → healthProbe alias: mirror whichever field
|
|
1385
1652
|
// was declared onto both so old and new consumers keep working.
|
|
1386
1653
|
healthMonitor: config.healthMonitor ?? config.healthProbe,
|
|
1387
1654
|
healthProbe: config.healthProbe ?? config.healthMonitor,
|
|
1388
1655
|
healthJourneys: config.healthJourneys,
|
|
1389
1656
|
};
|
|
1657
|
+
validateFailClosedDeclaration(provider);
|
|
1658
|
+
return provider;
|
|
1390
1659
|
}
|