@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +487 -0
- package/CHANGELOG.md +85 -0
- package/README.md +49 -3
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-pack-smoke.ts +36 -81
- package/bin/apifuse-pack-types.ts +305 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +659 -111
- package/bin/apifuse-submit-check.ts +546 -48
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +1 -1
- package/dist/ceremonies/index.js +8 -18
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/config/loader.d.ts +176 -8
- package/dist/config/loader.js +424 -95
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +9 -1
- package/dist/define.js +208 -125
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/errors.d.ts +22 -1
- package/dist/errors.js +88 -0
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +46 -42
- package/dist/index.js +41 -37
- package/dist/lint.js +8 -15
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +12 -11
- package/dist/provider.js +10 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +1 -1
- package/dist/runtime/auth-flow.js +3 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +31 -4
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +387 -47
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +345 -22
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network.d.ts +127 -0
- package/dist/runtime/native-network.js +1298 -0
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +15 -4
- package/dist/runtime/stealth.d.ts +5 -3
- package/dist/runtime/stealth.js +423 -143
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +104 -3
- package/dist/server/self-test.js +673 -115
- package/dist/server/serve.d.ts +115 -4
- package/dist/server/serve.js +792 -127
- package/dist/server/types.d.ts +30 -9
- package/dist/server/types.js +8 -7
- package/dist/stateful/errors.d.ts +14 -0
- package/dist/stateful/errors.js +14 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +71 -0
- package/dist/stateful/stateful-provider-session-routing.js +353 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +477 -33
- package/dist/types.d.ts +323 -11
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +24 -78
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +652 -204
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +368 -504
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +91 -0
- package/src/errors.ts +110 -5
- package/src/fixture-sanitization.ts +247 -0
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +111 -42
- package/src/lint.ts +89 -159
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +65 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +5 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +43 -20
- package/src/runtime/http.ts +494 -57
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +495 -66
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network.ts +1686 -0
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +41 -110
- package/src/runtime/stealth.ts +505 -154
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +29 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +1141 -324
- package/src/server/types.ts +12 -19
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +23 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +555 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +10 -26
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +10 -2
- package/src/testing/run.ts +652 -79
- package/src/types.ts +369 -11
- package/src/user-input.ts +118 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/dist/define.js
CHANGED
|
@@ -1,27 +1,17 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
3
|
+
import { ProviderError, ValidationError } from "./errors.js";
|
|
4
|
+
import { NativeEgressPolicyValidationError, validateNativeProviderConfig, } from "./native-egress-policy.js";
|
|
5
|
+
import { safeParseSchemaSync } from "./schema.js";
|
|
6
|
+
import { resolveHealthCheckInputDateTokens } from "./server/self-test-input-tokens.js";
|
|
7
|
+
import { HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX, HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN, HEALTH_CHECK_TIMEOUT_MS_MAX, HEALTH_CHECK_TIMEOUT_MS_MIN, OPERATION_TIMEOUT_MS_MAX, OPERATION_TIMEOUT_MS_MIN, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, VALID_OPERATION_ERROR_STATUSES, } from "./types.js";
|
|
5
8
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
6
9
|
const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
7
10
|
const VALID_RUNTIMES = ["standard", "shared", "browser"];
|
|
8
|
-
const VALID_AUTH_MODES = [
|
|
9
|
-
"none",
|
|
10
|
-
"platform-managed",
|
|
11
|
-
"credentials",
|
|
12
|
-
"oauth2",
|
|
13
|
-
];
|
|
11
|
+
const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"];
|
|
14
12
|
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"];
|
|
15
|
-
const VALID_PROVIDER_PROXY_MODES = [
|
|
16
|
-
|
|
17
|
-
"optional",
|
|
18
|
-
"required",
|
|
19
|
-
];
|
|
20
|
-
const VALID_PROVIDER_PROXY_PROVIDERS = [
|
|
21
|
-
"smartproxy",
|
|
22
|
-
"decodo",
|
|
23
|
-
"custom",
|
|
24
|
-
];
|
|
13
|
+
const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"];
|
|
14
|
+
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"];
|
|
25
15
|
const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
26
16
|
"request",
|
|
27
17
|
"operation",
|
|
@@ -30,25 +20,25 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
|
30
20
|
];
|
|
31
21
|
const VALID_PROVIDER_STT_MODES = ["optional", "required"];
|
|
32
22
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
23
|
+
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
24
|
+
const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
25
|
+
// Per-vendor provider-declared credential secrets. A required-mode chain must
|
|
26
|
+
// declare every secret of every credentialed vendor it names, so a missing
|
|
27
|
+
// credential fails at build/validation time rather than during a live outage: a
|
|
28
|
+
// declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
|
|
29
|
+
// exactly the failure class the multi-vendor chain exists to remove. Vendors
|
|
30
|
+
// absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
|
|
31
|
+
// `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
|
|
32
|
+
// no declaration requirement.
|
|
33
|
+
const VENDOR_REQUIRED_SECRETS = {
|
|
34
|
+
smartproxy: [SMARTPROXY_APP_KEY_SECRET],
|
|
35
|
+
nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
|
|
36
|
+
};
|
|
33
37
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
34
38
|
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
|
-
];
|
|
39
|
+
const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"];
|
|
40
|
+
const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"];
|
|
41
|
+
const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"];
|
|
52
42
|
const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
53
43
|
const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
54
44
|
const MS_DURATION_PATTERN = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*([a-zA-Z]+)?$/;
|
|
@@ -109,10 +99,7 @@ function validateProviderShape(config) {
|
|
|
109
99
|
if (typeof config.runtime === "string")
|
|
110
100
|
assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
|
|
111
101
|
const auth = config.auth;
|
|
112
|
-
if (auth &&
|
|
113
|
-
typeof auth === "object" &&
|
|
114
|
-
"mode" in auth &&
|
|
115
|
-
typeof auth.mode === "string")
|
|
102
|
+
if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
|
|
116
103
|
assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
|
|
117
104
|
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
118
105
|
throw new ProviderError(`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`, {
|
|
@@ -167,15 +154,23 @@ function validateProviderProxy(config) {
|
|
|
167
154
|
fix: `Use proxy: { mode: "required", provider: "smartproxy", geo: { country: "KR" }, session: { affinity: "connection", lifetimeMinutes: 30 } }`,
|
|
168
155
|
});
|
|
169
156
|
}
|
|
170
|
-
rejectUnknownFields(proxy, new Set(["mode", "provider", "geo", "session"]), "proxy");
|
|
157
|
+
rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
|
|
171
158
|
assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
|
|
172
159
|
if (proxy.provider !== undefined) {
|
|
173
160
|
assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
174
161
|
}
|
|
162
|
+
if (proxy.providers !== undefined) {
|
|
163
|
+
if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
|
|
164
|
+
throw new ValidationError(`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`, {
|
|
165
|
+
fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
for (const vendor of proxy.providers) {
|
|
169
|
+
assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
175
172
|
if (proxy.geo !== undefined) {
|
|
176
|
-
if (!proxy.geo ||
|
|
177
|
-
typeof proxy.geo !== "object" ||
|
|
178
|
-
Array.isArray(proxy.geo)) {
|
|
173
|
+
if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
|
|
179
174
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.geo: must be an object.`, {
|
|
180
175
|
fix: `Use proxy.geo: { country: "KR" } with ISO alpha-2 country codes.`,
|
|
181
176
|
});
|
|
@@ -192,43 +187,90 @@ function validateProviderProxy(config) {
|
|
|
192
187
|
}
|
|
193
188
|
}
|
|
194
189
|
if (proxy.session !== undefined) {
|
|
195
|
-
if (!proxy.session ||
|
|
196
|
-
typeof proxy.session !== "object" ||
|
|
197
|
-
Array.isArray(proxy.session)) {
|
|
190
|
+
if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
|
|
198
191
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session: must be an object.`, {
|
|
199
192
|
fix: `Use proxy.session: { affinity: "connection", lifetimeMinutes: 30 }.`,
|
|
200
193
|
});
|
|
201
194
|
}
|
|
202
|
-
rejectUnknownFields(proxy.session, new Set(["affinity", "lifetimeMinutes", "poolSize"]), "proxy.session");
|
|
195
|
+
rejectUnknownFields(proxy.session, new Set(["affinity", "lifetimeMinutes", "poolSize", "drainLeadSeconds"]), "proxy.session");
|
|
203
196
|
if (proxy.session.affinity !== undefined) {
|
|
204
197
|
assertLiteralField(proxy.session.affinity, "proxy.session.affinity", VALID_PROVIDER_PROXY_AFFINITIES, config.id);
|
|
205
198
|
}
|
|
206
199
|
const lifetime = proxy.session.lifetimeMinutes;
|
|
207
|
-
if (lifetime !== undefined &&
|
|
208
|
-
(!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
200
|
+
if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
209
201
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`);
|
|
210
202
|
}
|
|
211
203
|
const poolSize = proxy.session.poolSize;
|
|
212
|
-
if (poolSize !== undefined &&
|
|
213
|
-
(!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
204
|
+
if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
214
205
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`);
|
|
215
206
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
207
|
+
const drainLeadSeconds = proxy.session.drainLeadSeconds;
|
|
208
|
+
if (drainLeadSeconds !== undefined &&
|
|
209
|
+
(!Number.isFinite(drainLeadSeconds) || drainLeadSeconds <= 0)) {
|
|
210
|
+
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.drainLeadSeconds: must be a positive number of seconds.`, {
|
|
211
|
+
fix: `Use proxy.session.drainLeadSeconds: 120 to receive the sticky-expiry drain event 120s before hard expiry.`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// A drain lead longer than the sticky lifetime would fire the expiring
|
|
215
|
+
// event before the session is even established, so the provider would
|
|
216
|
+
// never get a usable window. Reject the contradiction at build time.
|
|
217
|
+
if (drainLeadSeconds !== undefined &&
|
|
218
|
+
lifetime !== undefined &&
|
|
219
|
+
Number.isFinite(lifetime) &&
|
|
220
|
+
drainLeadSeconds >= lifetime * 60) {
|
|
221
|
+
throw new ValidationError(`Provider "${config.id}" has proxy.session.drainLeadSeconds (${drainLeadSeconds}s) greater than or equal to proxy.session.lifetimeMinutes (${lifetime}m).`, {
|
|
222
|
+
fix: `Lower drainLeadSeconds below the sticky lifetime so the drain event leaves a usable session window.`,
|
|
222
223
|
});
|
|
223
224
|
}
|
|
224
225
|
}
|
|
226
|
+
// Every credentialed vendor in a required-mode chain must declare its
|
|
227
|
+
// provider secret(s) so a missing credential fails at build/validation time,
|
|
228
|
+
// not during a live outage. This covers the fallback legs too (not just the
|
|
229
|
+
// first vendor): a declared-but-uncredentialed nodemaven fallback would leave
|
|
230
|
+
// the chain silently down to a single vendor, reintroducing the SPOF the chain
|
|
231
|
+
// removes.
|
|
232
|
+
const vendorChain = proxy.providers && proxy.providers.length > 0
|
|
233
|
+
? proxy.providers
|
|
234
|
+
: proxy.provider
|
|
235
|
+
? [proxy.provider]
|
|
236
|
+
: [];
|
|
237
|
+
if (proxy.mode === "required") {
|
|
238
|
+
for (const vendor of vendorChain) {
|
|
239
|
+
const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
|
|
240
|
+
if (!requiredSecrets)
|
|
241
|
+
continue;
|
|
242
|
+
for (const secretName of requiredSecrets) {
|
|
243
|
+
// Match the canonical runtime gate (assertRequiredSecretsPresent /
|
|
244
|
+
// listMissingRequiredSecrets), which enforces only `required === true`
|
|
245
|
+
// declarations. A declaration that omits `required` (defaulting to
|
|
246
|
+
// optional) is skipped at runtime, so accepting it here would pass
|
|
247
|
+
// validation while leaving the credential unenforced until proxy
|
|
248
|
+
// resolution during a live request — the fail-open gap this check exists
|
|
249
|
+
// to close.
|
|
250
|
+
const declared = config.secrets?.some((secret) => secret.name === secretName && secret.required === true);
|
|
251
|
+
if (!declared) {
|
|
252
|
+
throw new ValidationError(`Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`, {
|
|
253
|
+
fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// `decodo`/`custom` are deprecated vendor values (string-union members, so the
|
|
260
|
+
// @deprecated symbol gate can't catch them — warn at validation time instead).
|
|
261
|
+
const deprecatedVendors = vendorChain.filter((vendor) => vendor === "decodo" || vendor === "custom");
|
|
262
|
+
if (deprecatedVendors.length > 0) {
|
|
263
|
+
console.warn(`[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven", or the APIFUSE__PROXY__URL bring-your-own escape hatch.`);
|
|
264
|
+
}
|
|
225
265
|
}
|
|
226
266
|
function validateProviderStt(config) {
|
|
227
267
|
const stt = config.stt;
|
|
228
268
|
if (stt === undefined)
|
|
229
269
|
return;
|
|
230
270
|
if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
|
|
231
|
-
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
271
|
+
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
272
|
+
fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
|
|
273
|
+
});
|
|
232
274
|
}
|
|
233
275
|
rejectUnknownFields(stt, new Set(["mode"]), "stt");
|
|
234
276
|
assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
|
|
@@ -247,12 +289,7 @@ function validateOperationIds(providerId, operations) {
|
|
|
247
289
|
}
|
|
248
290
|
const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
249
291
|
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
|
-
];
|
|
292
|
+
const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"];
|
|
256
293
|
function assertNonEmptyString(value, field, providerId, operationName) {
|
|
257
294
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
258
295
|
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 +305,7 @@ function validateToolRouterMetadata(providerId, operations) {
|
|
|
268
305
|
fix: `Remove operations.${operationName}.toolRouter or provide MCP-safe metadata.`,
|
|
269
306
|
});
|
|
270
307
|
}
|
|
271
|
-
if (toolRouter.name !== undefined &&
|
|
272
|
-
!MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
308
|
+
if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
273
309
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`, {
|
|
274
310
|
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
311
|
});
|
|
@@ -307,8 +343,7 @@ function validateOperationContracts(providerId, operations) {
|
|
|
307
343
|
if (contract.lifecycle !== undefined) {
|
|
308
344
|
assertLiteralField(contract.lifecycle, `operations.${operationName}.contract.lifecycle`, VALID_OPERATION_LIFECYCLES, providerId);
|
|
309
345
|
}
|
|
310
|
-
if (contract.lifecycle === "deprecated" ||
|
|
311
|
-
contract.lifecycle === "removed") {
|
|
346
|
+
if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
|
|
312
347
|
if (!contract.deprecation || typeof contract.deprecation !== "object") {
|
|
313
348
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`, {
|
|
314
349
|
fix: `Add announcedAt, removalAfter, and migrationGuide to operations.${operationName}.contract.deprecation.`,
|
|
@@ -333,8 +368,7 @@ function validateOperationAnnotations(providerId, operations) {
|
|
|
333
368
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an integer number of milliseconds.`, {
|
|
334
369
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
|
|
335
370
|
});
|
|
336
|
-
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
|
|
337
|
-
timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
371
|
+
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
338
372
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`, {
|
|
339
373
|
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
374
|
});
|
|
@@ -377,6 +411,23 @@ function validateOperationObservability(providerId, operations) {
|
|
|
377
411
|
}
|
|
378
412
|
}
|
|
379
413
|
}
|
|
414
|
+
function validateOperationErrorCodes(providerId, operations) {
|
|
415
|
+
for (const [operationName, operation] of Object.entries(operations)) {
|
|
416
|
+
for (const [index, errorCode] of (operation.docs?.errorCodes ?? []).entries()) {
|
|
417
|
+
if (errorCode.status !== undefined &&
|
|
418
|
+
!VALID_OPERATION_ERROR_STATUSES.some((status) => status === errorCode.status)) {
|
|
419
|
+
const field = `operations.${operationName}.docs.errorCodes[${index}].status`;
|
|
420
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: ${String(errorCode.status)} is not an emittable provider error status.`, {
|
|
421
|
+
fix: `Set ${field} to one of ${VALID_OPERATION_ERROR_STATUSES.join(", ")}, or omit it.`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (errorCode.status !== undefined &&
|
|
425
|
+
SDK_RUNTIME_OWNED_ERROR_CODES.has(errorCode.code)) {
|
|
426
|
+
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.`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
380
431
|
const JSON_TRANSPORT_FIELDS = new Set(["kind"]);
|
|
381
432
|
const SSE_TRANSPORT_FIELDS = new Set([
|
|
382
433
|
"kind",
|
|
@@ -478,9 +529,7 @@ function validateOperationTransports(providerId, operations) {
|
|
|
478
529
|
assertStreamMs(maxDurationMs, `${fieldPath}.maxDurationMs`, STREAM_MAX_DURATION_MS_MIN, STREAM_MAX_DURATION_MS_MAX, "max duration");
|
|
479
530
|
assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
|
|
480
531
|
const resumable = Reflect.get(transport, "resumable");
|
|
481
|
-
if (resumable !== undefined &&
|
|
482
|
-
resumable !== false &&
|
|
483
|
-
resumable !== "last-event-id") {
|
|
532
|
+
if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
|
|
484
533
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`, {
|
|
485
534
|
fix: `Use ${fieldPath}.resumable: "last-event-id" for SSE Last-Event-ID resume support, or false to disable resume.`,
|
|
486
535
|
});
|
|
@@ -515,8 +564,7 @@ function validateOperationTransports(providerId, operations) {
|
|
|
515
564
|
});
|
|
516
565
|
}
|
|
517
566
|
for (const subprotocol of subprotocols) {
|
|
518
|
-
if (typeof subprotocol !== "string" ||
|
|
519
|
-
!WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
567
|
+
if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
520
568
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`, {
|
|
521
569
|
fix: `Use values such as "apifuse.v1" without spaces or separators that are invalid for Sec-WebSocket-Protocol.`,
|
|
522
570
|
});
|
|
@@ -624,9 +672,7 @@ function assertBoundedIntegerMs(value, fieldPath, options) {
|
|
|
624
672
|
function validateProviderHealthMonitor(providerId, healthMonitor, field = "healthMonitor") {
|
|
625
673
|
if (healthMonitor === undefined)
|
|
626
674
|
return;
|
|
627
|
-
if (!healthMonitor ||
|
|
628
|
-
typeof healthMonitor !== "object" ||
|
|
629
|
-
Array.isArray(healthMonitor))
|
|
675
|
+
if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
|
|
630
676
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
|
|
631
677
|
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
632
678
|
});
|
|
@@ -676,9 +722,7 @@ function validateProviderHealthMonitor(providerId, healthMonitor, field = "healt
|
|
|
676
722
|
}
|
|
677
723
|
const probeOverrides = healthMonitorRecord.probeOverrides;
|
|
678
724
|
if (probeOverrides !== undefined) {
|
|
679
|
-
if (!probeOverrides ||
|
|
680
|
-
typeof probeOverrides !== "object" ||
|
|
681
|
-
Array.isArray(probeOverrides))
|
|
725
|
+
if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
|
|
682
726
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`);
|
|
683
727
|
for (const [probeId, override] of Object.entries(probeOverrides)) {
|
|
684
728
|
if (probeId.length === 0)
|
|
@@ -756,9 +800,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
756
800
|
fix: `Set ${fieldPath}.interval to a positive ms-style duration string.`,
|
|
757
801
|
});
|
|
758
802
|
if (s.schedule !== undefined) {
|
|
759
|
-
if (!s.schedule ||
|
|
760
|
-
typeof s.schedule !== "object" ||
|
|
761
|
-
Array.isArray(s.schedule)) {
|
|
803
|
+
if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
|
|
762
804
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.schedule must be an object.`);
|
|
763
805
|
}
|
|
764
806
|
if (Reflect.get(s.schedule, "jitter") !== undefined) {
|
|
@@ -784,8 +826,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
784
826
|
label: "degraded threshold",
|
|
785
827
|
});
|
|
786
828
|
}
|
|
787
|
-
if (s.requiresConnection !== undefined &&
|
|
788
|
-
typeof s.requiresConnection !== "boolean")
|
|
829
|
+
if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
|
|
789
830
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`);
|
|
790
831
|
if (!Array.isArray(s.cases) || s.cases.length === 0)
|
|
791
832
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.cases must be a non-empty array.`, {
|
|
@@ -804,9 +845,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
804
845
|
}
|
|
805
846
|
function validateHealthCheckUnsupported(providerId, operationName, unsupported) {
|
|
806
847
|
const fieldPath = `operations.${operationName}.healthCheckUnsupported`;
|
|
807
|
-
if (!unsupported ||
|
|
808
|
-
typeof unsupported !== "object" ||
|
|
809
|
-
Array.isArray(unsupported))
|
|
848
|
+
if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
|
|
810
849
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
811
850
|
rejectUnknownFields(unsupported, HEALTH_CHECK_UNSUPPORTED_FIELDS, fieldPath);
|
|
812
851
|
const u = unsupported;
|
|
@@ -831,12 +870,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
|
|
|
831
870
|
"steps",
|
|
832
871
|
"run",
|
|
833
872
|
]);
|
|
834
|
-
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
|
|
835
|
-
"kind",
|
|
836
|
-
"interval",
|
|
837
|
-
"jitter",
|
|
838
|
-
"randomize",
|
|
839
|
-
]);
|
|
873
|
+
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
|
|
840
874
|
const HEALTH_JOURNEY_STEP_FIELDS = new Set([
|
|
841
875
|
"id",
|
|
842
876
|
"description",
|
|
@@ -854,10 +888,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
|
|
|
854
888
|
"minManualInterval",
|
|
855
889
|
"publicRationale",
|
|
856
890
|
]);
|
|
857
|
-
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
|
|
858
|
-
"enabled",
|
|
859
|
-
"reason",
|
|
860
|
-
]);
|
|
891
|
+
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
|
|
861
892
|
const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
|
|
862
893
|
"enabled",
|
|
863
894
|
"requiresAcknowledgement",
|
|
@@ -872,9 +903,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS = new Set([
|
|
|
872
903
|
]);
|
|
873
904
|
function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger) {
|
|
874
905
|
const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
|
|
875
|
-
if (!manualTrigger ||
|
|
876
|
-
typeof manualTrigger !== "object" ||
|
|
877
|
-
Array.isArray(manualTrigger)) {
|
|
906
|
+
if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
|
|
878
907
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object when present.`);
|
|
879
908
|
}
|
|
880
909
|
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
|
|
@@ -897,8 +926,7 @@ function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger
|
|
|
897
926
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`);
|
|
898
927
|
}
|
|
899
928
|
const risk = Reflect.get(manualTrigger, "risk");
|
|
900
|
-
if (typeof risk !== "string" ||
|
|
901
|
-
!HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
929
|
+
if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
902
930
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`);
|
|
903
931
|
}
|
|
904
932
|
if (risk !== "read_only" && requiresAcknowledgement !== true) {
|
|
@@ -950,7 +978,7 @@ function isoDurationMs(value) {
|
|
|
950
978
|
const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
|
|
951
979
|
const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
|
|
952
980
|
const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
|
|
953
|
-
return
|
|
981
|
+
return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
|
|
954
982
|
}
|
|
955
983
|
function scheduleRandomizationMs(randomize, fieldPath) {
|
|
956
984
|
const mode = Reflect.get(randomize, "mode");
|
|
@@ -974,9 +1002,7 @@ function validateScheduleRandomization(randomize, fieldPath, intervalMs) {
|
|
|
974
1002
|
throw new ValidationError(`${fieldPath} must be an object.`);
|
|
975
1003
|
}
|
|
976
1004
|
const mode = Reflect.get(randomize, "mode");
|
|
977
|
-
const allowedFields = mode === "centered"
|
|
978
|
-
? new Set(["mode", "maxOffset"])
|
|
979
|
-
: new Set(["mode", "maxDelay"]);
|
|
1005
|
+
const allowedFields = mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
|
|
980
1006
|
rejectUnknownFields(randomize, allowedFields, fieldPath);
|
|
981
1007
|
const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
|
|
982
1008
|
if (offsetMs <= 0) {
|
|
@@ -1062,9 +1088,7 @@ function countCapturingGroups(pattern) {
|
|
|
1062
1088
|
const next = source[i + 1];
|
|
1063
1089
|
if (next === "?" && source[i + 2] !== "<")
|
|
1064
1090
|
continue;
|
|
1065
|
-
if (next === "?" &&
|
|
1066
|
-
source[i + 2] === "<" &&
|
|
1067
|
-
(source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1091
|
+
if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1068
1092
|
continue;
|
|
1069
1093
|
count += 1;
|
|
1070
1094
|
}
|
|
@@ -1142,8 +1166,7 @@ function validateSmsOtpMatcher(matcher, fieldPath) {
|
|
|
1142
1166
|
throw new ValidationError(`${fieldPath}.code.pattern must be a RegExp or pattern source string.`);
|
|
1143
1167
|
}
|
|
1144
1168
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
1145
|
-
if (countCapturingGroups(regex) !== 1 &&
|
|
1146
|
-
Reflect.get(code, "capture") === undefined) {
|
|
1169
|
+
if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
|
|
1147
1170
|
throw new ValidationError(`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`);
|
|
1148
1171
|
}
|
|
1149
1172
|
if (Reflect.get(code, "capture") !== undefined &&
|
|
@@ -1195,8 +1218,7 @@ function validateHealthJourneySchedule(providerId, journeyId, schedule) {
|
|
|
1195
1218
|
const interval = Reflect.get(schedule, "interval");
|
|
1196
1219
|
assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
|
|
1197
1220
|
const randomize = Reflect.get(schedule, "randomize");
|
|
1198
|
-
if (Reflect.get(schedule, "jitter") !== undefined &&
|
|
1199
|
-
randomize !== undefined) {
|
|
1221
|
+
if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
|
|
1200
1222
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`);
|
|
1201
1223
|
}
|
|
1202
1224
|
if (Reflect.get(schedule, "jitter") !== undefined)
|
|
@@ -1226,8 +1248,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
|
|
|
1226
1248
|
throw new ValidationError(`Provider "${providerId}" has duplicate health journey id "${journey.id}".`);
|
|
1227
1249
|
journeyIds.add(journey.id);
|
|
1228
1250
|
validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
|
|
1229
|
-
if (!Array.isArray(journey.coversOperations) ||
|
|
1230
|
-
journey.coversOperations.length === 0) {
|
|
1251
|
+
if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
|
|
1231
1252
|
throw new ValidationError(`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`);
|
|
1232
1253
|
}
|
|
1233
1254
|
for (const operationId of journey.coversOperations) {
|
|
@@ -1265,8 +1286,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
|
|
|
1265
1286
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.id must be a kebab-case identifier.`);
|
|
1266
1287
|
if (step.operationId !== undefined && !operations[step.operationId])
|
|
1267
1288
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`);
|
|
1268
|
-
if (step.usesSmsMatcher !== undefined &&
|
|
1269
|
-
!matcherIds.has(step.usesSmsMatcher))
|
|
1289
|
+
if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
|
|
1270
1290
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`);
|
|
1271
1291
|
}
|
|
1272
1292
|
if (journey.manualTrigger !== undefined)
|
|
@@ -1300,9 +1320,7 @@ function validateOperationHealthChecks(providerId, operations, journeyCoveredOpe
|
|
|
1300
1320
|
validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
|
|
1301
1321
|
if (hasUnsupported)
|
|
1302
1322
|
validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
|
|
1303
|
-
if (!hasCheck &&
|
|
1304
|
-
!hasUnsupported &&
|
|
1305
|
-
!journeyCoveredOperations.has(operationName))
|
|
1323
|
+
if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
|
|
1306
1324
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`, {
|
|
1307
1325
|
fix: `Add \`healthCheck: { interval, cases: [...] }\` or \`healthCheckUnsupported: { reason: "..." }\` to operations.${operationName}.`,
|
|
1308
1326
|
});
|
|
@@ -1314,6 +1332,21 @@ function validateOperationFixtures(providerId, operations) {
|
|
|
1314
1332
|
throw new ValidationError(`Operation handler must be defined for provider "${providerId}" operation "${operationName}"`, {
|
|
1315
1333
|
fix: `Add operations.${operationName}.handler as an async function with signature (ctx, input) => Promise<output>`,
|
|
1316
1334
|
});
|
|
1335
|
+
if (operation.fixtures?.recordedAt !== undefined) {
|
|
1336
|
+
const recordedAt = operation.fixtures.recordedAt;
|
|
1337
|
+
const parsed = typeof recordedAt === "string"
|
|
1338
|
+
? new Date(`${recordedAt}T00:00:00.000Z`)
|
|
1339
|
+
: new Date(Number.NaN);
|
|
1340
|
+
const isCalendarDate = typeof recordedAt === "string" &&
|
|
1341
|
+
/^\d{4}-\d{2}-\d{2}$/.test(recordedAt) &&
|
|
1342
|
+
!Number.isNaN(parsed.getTime()) &&
|
|
1343
|
+
parsed.toISOString().slice(0, 10) === recordedAt;
|
|
1344
|
+
const kstToday = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
1345
|
+
if (!isCalendarDate || recordedAt > kstToday)
|
|
1346
|
+
throw new ValidationError(`Fixture recordedAt must be a valid, non-future KST calendar date for provider "${providerId}" operation "${operationName}"`, {
|
|
1347
|
+
fix: `Set operations.${operationName}.fixtures.recordedAt to the KST capture date in YYYY-MM-DD format; it must not be in the future.`,
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1317
1350
|
if (operation.fixtures?.request !== undefined) {
|
|
1318
1351
|
const result = safeParseSchemaSync(operation.input, operation.fixtures.request, `operations.${operationName}.fixtures.request`);
|
|
1319
1352
|
if (!result.success)
|
|
@@ -1332,8 +1365,44 @@ function validateOperationFixtures(providerId, operations) {
|
|
|
1332
1365
|
}
|
|
1333
1366
|
}
|
|
1334
1367
|
}
|
|
1368
|
+
function resolveOperationFixtureRequests(operations) {
|
|
1369
|
+
let changed = false;
|
|
1370
|
+
const resolvedOperations = Object.fromEntries(Object.entries(operations).map(([operationName, operation]) => {
|
|
1371
|
+
if (operation.fixtures?.request === undefined)
|
|
1372
|
+
return [operationName, operation];
|
|
1373
|
+
const request = resolveHealthCheckInputDateTokens(operation.fixtures.request);
|
|
1374
|
+
if (request === operation.fixtures.request)
|
|
1375
|
+
return [operationName, operation];
|
|
1376
|
+
changed = true;
|
|
1377
|
+
return [
|
|
1378
|
+
operationName,
|
|
1379
|
+
{
|
|
1380
|
+
...operation,
|
|
1381
|
+
fixtures: {
|
|
1382
|
+
...operation.fixtures,
|
|
1383
|
+
request,
|
|
1384
|
+
},
|
|
1385
|
+
},
|
|
1386
|
+
];
|
|
1387
|
+
}));
|
|
1388
|
+
return changed ? resolvedOperations : operations;
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Shallow shape guard only: the `deployment` object is passed through
|
|
1392
|
+
* verbatim and deliberately not deep-validated by the SDK — the APIFuse
|
|
1393
|
+
* registry builder owns deployment validation and profile resolution.
|
|
1394
|
+
*/
|
|
1395
|
+
function validateProviderDeployment(providerId, deployment) {
|
|
1396
|
+
if (deployment === undefined)
|
|
1397
|
+
return;
|
|
1398
|
+
if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
|
|
1399
|
+
throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
|
|
1400
|
+
fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1335
1403
|
export function defineProvider(config) {
|
|
1336
1404
|
validateProviderShape(config);
|
|
1405
|
+
const operations = resolveOperationFixtureRequests(config.operations);
|
|
1337
1406
|
if (!CONNECTOR_ID_REGEX.test(config.id))
|
|
1338
1407
|
throw new ProviderError(`Invalid provider id: "${config.id}"`, {
|
|
1339
1408
|
fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
|
|
@@ -1345,6 +1414,7 @@ export function defineProvider(config) {
|
|
|
1345
1414
|
validateOperationIds(config.id, config.operations);
|
|
1346
1415
|
validateOperationAnnotations(config.id, config.operations);
|
|
1347
1416
|
validateOperationObservability(config.id, config.operations);
|
|
1417
|
+
validateOperationErrorCodes(config.id, config.operations);
|
|
1348
1418
|
validateOperationTransports(config.id, config.operations);
|
|
1349
1419
|
validateOperationContracts(config.id, config.operations);
|
|
1350
1420
|
validateToolRouterMetadata(config.id, config.operations);
|
|
@@ -1355,7 +1425,16 @@ export function defineProvider(config) {
|
|
|
1355
1425
|
fix: "Keep healthProbe (the new name) and delete the healthMonitor block.",
|
|
1356
1426
|
});
|
|
1357
1427
|
validateProviderHealthMonitor(config.id, config.healthProbe ?? config.healthMonitor, config.healthProbe !== undefined ? "healthProbe" : "healthMonitor");
|
|
1358
|
-
validateOperationFixtures(config.id,
|
|
1428
|
+
validateOperationFixtures(config.id, operations);
|
|
1429
|
+
validateProviderDeployment(config.id, config.deployment);
|
|
1430
|
+
try {
|
|
1431
|
+
validateNativeProviderConfig(config.native);
|
|
1432
|
+
}
|
|
1433
|
+
catch (error) {
|
|
1434
|
+
if (error instanceof NativeEgressPolicyValidationError)
|
|
1435
|
+
throw new ValidationError(error.message);
|
|
1436
|
+
throw error;
|
|
1437
|
+
}
|
|
1359
1438
|
validateProviderProxy(config);
|
|
1360
1439
|
validateProviderStt(config);
|
|
1361
1440
|
if (config.runtime === "browser" && !config.browser)
|
|
@@ -1368,7 +1447,11 @@ export function defineProvider(config) {
|
|
|
1368
1447
|
id: config.id,
|
|
1369
1448
|
version: config.version,
|
|
1370
1449
|
runtime: config.runtime,
|
|
1450
|
+
// Verbatim passthrough: deployment validation and profile resolution
|
|
1451
|
+
// are owned by the APIFuse registry builder, not the SDK.
|
|
1452
|
+
deployment: config.deployment,
|
|
1371
1453
|
allowedHosts: config.allowedHosts,
|
|
1454
|
+
native: config.native,
|
|
1372
1455
|
stealth: config.stealth,
|
|
1373
1456
|
proxy: config.proxy,
|
|
1374
1457
|
stt: config.stt,
|
|
@@ -1380,7 +1463,7 @@ export function defineProvider(config) {
|
|
|
1380
1463
|
credential: config.credential,
|
|
1381
1464
|
context: config.context,
|
|
1382
1465
|
meta: config.meta,
|
|
1383
|
-
operations
|
|
1466
|
+
operations,
|
|
1384
1467
|
// Transitional healthMonitor → healthProbe alias: mirror whichever field
|
|
1385
1468
|
// was declared onto both so old and new consumers keep working.
|
|
1386
1469
|
healthMonitor: config.healthMonitor ?? config.healthProbe,
|
package/dist/dev.d.ts
CHANGED
package/dist/dev.js
CHANGED