@apifuse/provider-sdk 2.2.0-beta.1 → 2.2.0-beta.11
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 +106 -0
- package/CHANGELOG.md +41 -0
- package/README.md +5 -1
- 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 +22 -81
- package/bin/apifuse-pack-types.ts +266 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +53 -70
- package/bin/apifuse-submit-check.ts +510 -45
- 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 +149 -8
- package/dist/config/loader.js +378 -89
- 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 +8 -1
- package/dist/define.js +119 -122
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +48 -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 +44 -42
- package/dist/index.js +39 -37
- package/dist/lint.js +8 -15
- package/dist/provider.d.ts +11 -11
- package/dist/provider.js +9 -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 +15 -3
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +144 -348
- 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 +56 -19
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- 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 +35 -0
- package/dist/runtime/proxy-nodemaven.js +128 -0
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +2 -4
- package/dist/runtime/request-options.d.ts +1 -1
- 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 +2 -2
- package/dist/runtime/stealth.js +108 -212
- 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-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 +105 -4
- package/dist/server/serve.js +588 -80
- package/dist/server/types.d.ts +10 -9
- package/dist/server/types.js +3 -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 +207 -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.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 +1 -1
- package/dist/testing/run.js +7 -14
- package/dist/types.d.ts +87 -1
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +14 -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 +543 -208
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +247 -505
- package/src/dev.ts +4 -9
- package/src/errors.ts +58 -5
- 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 +49 -42
- package/src/lint.ts +89 -159
- package/src/provider.ts +15 -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 +23 -20
- package/src/runtime/http.ts +195 -559
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +78 -65
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- 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 +178 -0
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +4 -13
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +41 -110
- package/src/runtime/stealth.ts +162 -377
- 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 +24 -7
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +859 -266
- package/src/server/types.ts +7 -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 +279 -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.ts +8 -19
- package/src/testing/index.ts +2 -2
- package/src/testing/run.ts +19 -67
- package/src/types.ts +88 -1
- package/src/user-input.ts +118 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/src/define.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
2
|
|
|
3
|
-
import { ProviderError, ValidationError } from "./errors";
|
|
4
|
-
import { safeParseSchemaSync } from "./schema";
|
|
3
|
+
import { ProviderError, ValidationError } from "./errors.js";
|
|
4
|
+
import { safeParseSchemaSync } from "./schema.js";
|
|
5
5
|
import type {
|
|
6
6
|
AuthConfig,
|
|
7
7
|
BrowserEngine,
|
|
@@ -22,8 +22,10 @@ import type {
|
|
|
22
22
|
OperationWebSocketTransport,
|
|
23
23
|
ProviderAccessConfig,
|
|
24
24
|
ProviderDefinition,
|
|
25
|
+
ProviderDeploymentOverrides,
|
|
25
26
|
ProviderHealthMonitorConfig,
|
|
26
27
|
ProviderProxyConfig,
|
|
28
|
+
ProviderProxyProvider,
|
|
27
29
|
ProviderPublicProfile,
|
|
28
30
|
ProviderReviewed,
|
|
29
31
|
ProviderSecretDeclaration,
|
|
@@ -32,7 +34,7 @@ import type {
|
|
|
32
34
|
SchemaLike,
|
|
33
35
|
SmsOtpMatcherDefinition,
|
|
34
36
|
StealthPlatform,
|
|
35
|
-
} from "./types";
|
|
37
|
+
} from "./types.js";
|
|
36
38
|
import {
|
|
37
39
|
HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
38
40
|
HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
|
|
@@ -48,7 +50,7 @@ import {
|
|
|
48
50
|
STREAM_IDLE_TIMEOUT_MS_MIN,
|
|
49
51
|
STREAM_MAX_DURATION_MS_MAX,
|
|
50
52
|
STREAM_MAX_DURATION_MS_MIN,
|
|
51
|
-
} from "./types";
|
|
53
|
+
} from "./types.js";
|
|
52
54
|
|
|
53
55
|
type ProviderImplementationSourceAccess =
|
|
54
56
|
| "official_api"
|
|
@@ -74,23 +76,10 @@ interface ProviderImplementationProfile {
|
|
|
74
76
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
75
77
|
const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
76
78
|
const VALID_RUNTIMES = ["standard", "shared", "browser"] as const;
|
|
77
|
-
const VALID_AUTH_MODES = [
|
|
78
|
-
"none",
|
|
79
|
-
"platform-managed",
|
|
80
|
-
"credentials",
|
|
81
|
-
"oauth2",
|
|
82
|
-
] as const;
|
|
79
|
+
const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"] as const;
|
|
83
80
|
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
|
|
84
|
-
const VALID_PROVIDER_PROXY_MODES = [
|
|
85
|
-
|
|
86
|
-
"optional",
|
|
87
|
-
"required",
|
|
88
|
-
] as const;
|
|
89
|
-
const VALID_PROVIDER_PROXY_PROVIDERS = [
|
|
90
|
-
"smartproxy",
|
|
91
|
-
"decodo",
|
|
92
|
-
"custom",
|
|
93
|
-
] as const;
|
|
81
|
+
const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"] as const;
|
|
82
|
+
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"] as const;
|
|
94
83
|
const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
95
84
|
"request",
|
|
96
85
|
"operation",
|
|
@@ -99,25 +88,25 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
|
99
88
|
] as const;
|
|
100
89
|
const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
|
|
101
90
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
91
|
+
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
92
|
+
const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
93
|
+
// Per-vendor provider-declared credential secrets. A required-mode chain must
|
|
94
|
+
// declare every secret of every credentialed vendor it names, so a missing
|
|
95
|
+
// credential fails at build/validation time rather than during a live outage: a
|
|
96
|
+
// declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
|
|
97
|
+
// exactly the failure class the multi-vendor chain exists to remove. Vendors
|
|
98
|
+
// absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
|
|
99
|
+
// `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
|
|
100
|
+
// no declaration requirement.
|
|
101
|
+
const VENDOR_REQUIRED_SECRETS: Partial<Record<ProviderProxyProvider, readonly string[]>> = {
|
|
102
|
+
smartproxy: [SMARTPROXY_APP_KEY_SECRET],
|
|
103
|
+
nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
|
|
104
|
+
};
|
|
102
105
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
103
106
|
const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
|
|
104
|
-
const VALID_OPERATION_RISK_CLASSES = [
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"destructive",
|
|
108
|
-
"external-send",
|
|
109
|
-
] as const;
|
|
110
|
-
const VALID_OPERATION_APPROVAL_POLICIES = [
|
|
111
|
-
"never",
|
|
112
|
-
"risk-based",
|
|
113
|
-
"always",
|
|
114
|
-
] as const;
|
|
115
|
-
const VALID_OPERATION_TRANSPORT_KINDS = [
|
|
116
|
-
"json",
|
|
117
|
-
"sse",
|
|
118
|
-
"http-stream",
|
|
119
|
-
"websocket",
|
|
120
|
-
] as const;
|
|
107
|
+
const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
|
|
108
|
+
const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"] as const;
|
|
109
|
+
const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"] as const;
|
|
121
110
|
const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
122
111
|
const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
123
112
|
|
|
@@ -135,18 +124,16 @@ function msDurationMs(value: string): number {
|
|
|
135
124
|
function parsePositiveMsDuration(value: string): number | undefined {
|
|
136
125
|
const trimmed = value.trim();
|
|
137
126
|
if (!MS_DURATION_PATTERN.test(trimmed)) return undefined;
|
|
138
|
-
const parsed = ms(
|
|
139
|
-
(trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue,
|
|
140
|
-
);
|
|
127
|
+
const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue);
|
|
141
128
|
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
142
129
|
return parsed;
|
|
143
130
|
}
|
|
144
131
|
|
|
145
132
|
type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
|
|
146
|
-
type OperationConfig<
|
|
147
|
-
TInput
|
|
148
|
-
|
|
149
|
-
>
|
|
133
|
+
type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
134
|
+
OperationDefinition<TInput, TOutput>,
|
|
135
|
+
"handler"
|
|
136
|
+
> & {
|
|
150
137
|
handler(
|
|
151
138
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
152
139
|
input: InferSchemaOutput<TInput>,
|
|
@@ -154,59 +141,44 @@ type OperationConfig<
|
|
|
154
141
|
| OperationHandlerResult<InferSchemaOutput<TOutput>>
|
|
155
142
|
| Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
|
|
156
143
|
};
|
|
157
|
-
type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
|
|
164
|
-
: never;
|
|
165
|
-
};
|
|
166
|
-
type StreamOperationConfig<
|
|
167
|
-
TInput extends SchemaLike,
|
|
168
|
-
TOutput extends SchemaLike,
|
|
169
|
-
> =
|
|
144
|
+
type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
|
|
145
|
+
[K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
|
|
146
|
+
? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
|
|
147
|
+
: never;
|
|
148
|
+
};
|
|
149
|
+
type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
|
|
170
150
|
| SseOperationConfig<TInput, TOutput>
|
|
171
151
|
| HttpStreamOperationConfig<TInput, TOutput>
|
|
172
152
|
| WebSocketOperationConfig<TInput, TOutput>;
|
|
173
|
-
type SseOperationConfig<
|
|
174
|
-
TInput
|
|
175
|
-
|
|
176
|
-
>
|
|
153
|
+
type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
154
|
+
OperationConfig<TInput, TOutput>,
|
|
155
|
+
"handler" | "transport"
|
|
156
|
+
> & {
|
|
177
157
|
transport: OperationSseTransport;
|
|
178
158
|
handler(
|
|
179
159
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
180
160
|
input: InferSchemaOutput<TInput>,
|
|
181
|
-
):
|
|
182
|
-
| AsyncIterable<ProviderStreamEvent>
|
|
183
|
-
| Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
161
|
+
): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
184
162
|
};
|
|
185
|
-
type HttpStreamOperationConfig<
|
|
186
|
-
TInput
|
|
187
|
-
|
|
188
|
-
>
|
|
163
|
+
type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
164
|
+
OperationConfig<TInput, TOutput>,
|
|
165
|
+
"handler" | "transport"
|
|
166
|
+
> & {
|
|
189
167
|
transport: OperationHttpStreamTransport;
|
|
190
168
|
handler(
|
|
191
169
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
192
170
|
input: InferSchemaOutput<TInput>,
|
|
193
|
-
):
|
|
194
|
-
| Response
|
|
195
|
-
| ReadableStream<Uint8Array>
|
|
196
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
171
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
197
172
|
};
|
|
198
|
-
type WebSocketOperationConfig<
|
|
199
|
-
TInput
|
|
200
|
-
|
|
201
|
-
>
|
|
173
|
+
type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
174
|
+
OperationConfig<TInput, TOutput>,
|
|
175
|
+
"handler" | "transport"
|
|
176
|
+
> & {
|
|
202
177
|
transport: OperationWebSocketTransport;
|
|
203
178
|
handler(
|
|
204
179
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
205
180
|
input: InferSchemaOutput<TInput>,
|
|
206
|
-
):
|
|
207
|
-
| Response
|
|
208
|
-
| ReadableStream<Uint8Array>
|
|
209
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
181
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
210
182
|
};
|
|
211
183
|
|
|
212
184
|
type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
@@ -221,12 +193,17 @@ type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
|
221
193
|
: unknown
|
|
222
194
|
: unknown;
|
|
223
195
|
|
|
224
|
-
export interface ProviderConfig<
|
|
225
|
-
TOperations extends Record<string, ProviderOperation>,
|
|
226
|
-
> {
|
|
196
|
+
export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
|
|
227
197
|
id: string;
|
|
228
198
|
version: string;
|
|
229
199
|
runtime: "standard" | "shared" | "browser";
|
|
200
|
+
/**
|
|
201
|
+
* Optional deployment overrides, passed through verbatim onto the returned
|
|
202
|
+
* provider definition. The SDK types this field but does not deep-validate
|
|
203
|
+
* it — the APIFuse registry builder owns deployment validation and
|
|
204
|
+
* resolves omitted fields against the runtime deployment profiles.
|
|
205
|
+
*/
|
|
206
|
+
deployment?: ProviderDeploymentOverrides;
|
|
230
207
|
allowedHosts?: string[];
|
|
231
208
|
stealth?: {
|
|
232
209
|
profile: string;
|
|
@@ -270,35 +247,24 @@ export interface ProviderConfig<
|
|
|
270
247
|
}
|
|
271
248
|
|
|
272
249
|
/** Define one provider operation with schema-driven handler inference. */
|
|
273
|
-
export function defineOperation<
|
|
274
|
-
TInput extends SchemaLike,
|
|
275
|
-
TOutput extends SchemaLike,
|
|
276
|
-
>(
|
|
250
|
+
export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
277
251
|
operation: OperationConfig<TInput, TOutput>,
|
|
278
252
|
): OperationDefinition<TInput, TOutput> {
|
|
279
253
|
return operation;
|
|
280
254
|
}
|
|
281
255
|
|
|
282
256
|
/** Define a non-JSON provider operation with explicit transport metadata. */
|
|
283
|
-
export function defineStreamOperation<
|
|
284
|
-
TInput extends SchemaLike,
|
|
285
|
-
TOutput extends SchemaLike,
|
|
286
|
-
>(
|
|
257
|
+
export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
287
258
|
operation: StreamOperationConfig<TInput, TOutput>,
|
|
288
259
|
): OperationDefinition<TInput, TOutput> {
|
|
289
260
|
return operation;
|
|
290
261
|
}
|
|
291
262
|
|
|
292
|
-
function assertObjectConfig(
|
|
293
|
-
value: unknown,
|
|
294
|
-
): asserts value is Record<string, unknown> {
|
|
263
|
+
function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
|
|
295
264
|
if (!value || typeof value !== "object") {
|
|
296
|
-
throw new ProviderError(
|
|
297
|
-
"defineProvider
|
|
298
|
-
|
|
299
|
-
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
300
|
-
},
|
|
301
|
-
);
|
|
265
|
+
throw new ProviderError("defineProvider config must be an object. Offending field: config", {
|
|
266
|
+
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
267
|
+
});
|
|
302
268
|
}
|
|
303
269
|
}
|
|
304
270
|
function assertRequiredField(
|
|
@@ -336,25 +302,10 @@ function validateProviderShape(config: unknown): void {
|
|
|
336
302
|
assertRequiredField(config, "meta", String(config.id));
|
|
337
303
|
assertRequiredField(config, "operations", String(config.id));
|
|
338
304
|
if (typeof config.runtime === "string")
|
|
339
|
-
assertLiteralField(
|
|
340
|
-
config.runtime,
|
|
341
|
-
"runtime",
|
|
342
|
-
VALID_RUNTIMES,
|
|
343
|
-
String(config.id),
|
|
344
|
-
);
|
|
305
|
+
assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
|
|
345
306
|
const auth = config.auth;
|
|
346
|
-
if (
|
|
347
|
-
auth
|
|
348
|
-
typeof auth === "object" &&
|
|
349
|
-
"mode" in auth &&
|
|
350
|
-
typeof auth.mode === "string"
|
|
351
|
-
)
|
|
352
|
-
assertLiteralField(
|
|
353
|
-
auth.mode,
|
|
354
|
-
"auth.mode",
|
|
355
|
-
VALID_AUTH_MODES,
|
|
356
|
-
String(config.id),
|
|
357
|
-
);
|
|
307
|
+
if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
|
|
308
|
+
assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
|
|
358
309
|
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
359
310
|
throw new ProviderError(
|
|
360
311
|
`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
|
|
@@ -390,9 +341,7 @@ function validateProviderShape(config: unknown): void {
|
|
|
390
341
|
},
|
|
391
342
|
);
|
|
392
343
|
}
|
|
393
|
-
const accessRecord: Record<string, unknown> = Object.fromEntries(
|
|
394
|
-
Object.entries(access),
|
|
395
|
-
);
|
|
344
|
+
const accessRecord: Record<string, unknown> = Object.fromEntries(Object.entries(access));
|
|
396
345
|
for (const key of Object.keys(accessRecord)) {
|
|
397
346
|
if (key !== "visibility") {
|
|
398
347
|
throw new ValidationError(`Unknown field "${key}" on access.`, {
|
|
@@ -437,31 +386,26 @@ function validateProviderProxy(config: {
|
|
|
437
386
|
},
|
|
438
387
|
);
|
|
439
388
|
}
|
|
440
|
-
rejectUnknownFields(
|
|
441
|
-
|
|
442
|
-
new Set(["mode", "provider", "geo", "session"]),
|
|
443
|
-
"proxy",
|
|
444
|
-
);
|
|
445
|
-
assertLiteralField(
|
|
446
|
-
proxy.mode,
|
|
447
|
-
"proxy.mode",
|
|
448
|
-
VALID_PROVIDER_PROXY_MODES,
|
|
449
|
-
config.id,
|
|
450
|
-
);
|
|
389
|
+
rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
|
|
390
|
+
assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
|
|
451
391
|
if (proxy.provider !== undefined) {
|
|
452
|
-
assertLiteralField(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
392
|
+
assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
393
|
+
}
|
|
394
|
+
if (proxy.providers !== undefined) {
|
|
395
|
+
if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
|
|
396
|
+
throw new ValidationError(
|
|
397
|
+
`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`,
|
|
398
|
+
{
|
|
399
|
+
fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
|
|
400
|
+
},
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
for (const vendor of proxy.providers) {
|
|
404
|
+
assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
405
|
+
}
|
|
458
406
|
}
|
|
459
407
|
if (proxy.geo !== undefined) {
|
|
460
|
-
if (
|
|
461
|
-
!proxy.geo ||
|
|
462
|
-
typeof proxy.geo !== "object" ||
|
|
463
|
-
Array.isArray(proxy.geo)
|
|
464
|
-
) {
|
|
408
|
+
if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
|
|
465
409
|
throw new ValidationError(
|
|
466
410
|
`Provider "${config.id}" has invalid proxy.geo: must be an object.`,
|
|
467
411
|
{
|
|
@@ -469,11 +413,7 @@ function validateProviderProxy(config: {
|
|
|
469
413
|
},
|
|
470
414
|
);
|
|
471
415
|
}
|
|
472
|
-
rejectUnknownFields(
|
|
473
|
-
proxy.geo,
|
|
474
|
-
new Set(["country", "subdivision", "city"]),
|
|
475
|
-
"proxy.geo",
|
|
476
|
-
);
|
|
416
|
+
rejectUnknownFields(proxy.geo, new Set(["country", "subdivision", "city"]), "proxy.geo");
|
|
477
417
|
if (proxy.geo.country !== undefined) {
|
|
478
418
|
assertIsoCountry(proxy.geo.country, "proxy.geo.country");
|
|
479
419
|
}
|
|
@@ -487,11 +427,7 @@ function validateProviderProxy(config: {
|
|
|
487
427
|
}
|
|
488
428
|
}
|
|
489
429
|
if (proxy.session !== undefined) {
|
|
490
|
-
if (
|
|
491
|
-
!proxy.session ||
|
|
492
|
-
typeof proxy.session !== "object" ||
|
|
493
|
-
Array.isArray(proxy.session)
|
|
494
|
-
) {
|
|
430
|
+
if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
|
|
495
431
|
throw new ValidationError(
|
|
496
432
|
`Provider "${config.id}" has invalid proxy.session: must be an object.`,
|
|
497
433
|
{
|
|
@@ -513,51 +449,75 @@ function validateProviderProxy(config: {
|
|
|
513
449
|
);
|
|
514
450
|
}
|
|
515
451
|
const lifetime = proxy.session.lifetimeMinutes;
|
|
516
|
-
if (
|
|
517
|
-
lifetime !== undefined &&
|
|
518
|
-
(!Number.isFinite(lifetime) || lifetime <= 0)
|
|
519
|
-
) {
|
|
452
|
+
if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
520
453
|
throw new ValidationError(
|
|
521
454
|
`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`,
|
|
522
455
|
);
|
|
523
456
|
}
|
|
524
457
|
const poolSize = proxy.session.poolSize;
|
|
525
|
-
if (
|
|
526
|
-
poolSize !== undefined &&
|
|
527
|
-
(!Number.isInteger(poolSize) || poolSize <= 0)
|
|
528
|
-
) {
|
|
458
|
+
if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
529
459
|
throw new ValidationError(
|
|
530
460
|
`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
|
|
531
461
|
);
|
|
532
462
|
}
|
|
533
463
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
464
|
+
// Every credentialed vendor in a required-mode chain must declare its
|
|
465
|
+
// provider secret(s) so a missing credential fails at build/validation time,
|
|
466
|
+
// not during a live outage. This covers the fallback legs too (not just the
|
|
467
|
+
// first vendor): a declared-but-uncredentialed nodemaven fallback would leave
|
|
468
|
+
// the chain silently down to a single vendor, reintroducing the SPOF the chain
|
|
469
|
+
// removes.
|
|
470
|
+
const vendorChain =
|
|
471
|
+
proxy.providers && proxy.providers.length > 0
|
|
472
|
+
? proxy.providers
|
|
473
|
+
: proxy.provider
|
|
474
|
+
? [proxy.provider]
|
|
475
|
+
: [];
|
|
476
|
+
if (proxy.mode === "required") {
|
|
477
|
+
for (const vendor of vendorChain) {
|
|
478
|
+
const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
|
|
479
|
+
if (!requiredSecrets) continue;
|
|
480
|
+
for (const secretName of requiredSecrets) {
|
|
481
|
+
// Match the canonical runtime gate (assertRequiredSecretsPresent /
|
|
482
|
+
// listMissingRequiredSecrets), which enforces only `required === true`
|
|
483
|
+
// declarations. A declaration that omits `required` (defaulting to
|
|
484
|
+
// optional) is skipped at runtime, so accepting it here would pass
|
|
485
|
+
// validation while leaving the credential unenforced until proxy
|
|
486
|
+
// resolution during a live request — the fail-open gap this check exists
|
|
487
|
+
// to close.
|
|
488
|
+
const declared = config.secrets?.some(
|
|
489
|
+
(secret) => secret.name === secretName && secret.required === true,
|
|
490
|
+
);
|
|
491
|
+
if (!declared) {
|
|
492
|
+
throw new ValidationError(
|
|
493
|
+
`Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`,
|
|
494
|
+
{
|
|
495
|
+
fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
|
|
496
|
+
},
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
546
500
|
}
|
|
547
501
|
}
|
|
502
|
+
// `decodo`/`custom` are deprecated vendor values (string-union members, so the
|
|
503
|
+
// @deprecated symbol gate can't catch them — warn at validation time instead).
|
|
504
|
+
const deprecatedVendors = vendorChain.filter(
|
|
505
|
+
(vendor) => vendor === "decodo" || vendor === "custom",
|
|
506
|
+
);
|
|
507
|
+
if (deprecatedVendors.length > 0) {
|
|
508
|
+
console.warn(
|
|
509
|
+
`[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.`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
548
512
|
}
|
|
549
513
|
|
|
550
|
-
function validateProviderStt(config: {
|
|
551
|
-
id: string;
|
|
552
|
-
stt?: ProviderSttConfig;
|
|
553
|
-
}): void {
|
|
514
|
+
function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): void {
|
|
554
515
|
const stt = config.stt;
|
|
555
516
|
if (stt === undefined) return;
|
|
556
517
|
if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
|
|
557
|
-
throw new ValidationError(
|
|
558
|
-
`
|
|
559
|
-
|
|
560
|
-
);
|
|
518
|
+
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
519
|
+
fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
|
|
520
|
+
});
|
|
561
521
|
}
|
|
562
522
|
rejectUnknownFields(stt, new Set(["mode"]), "stt");
|
|
563
523
|
assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
|
|
@@ -584,16 +544,9 @@ function validateOperationIds(
|
|
|
584
544
|
);
|
|
585
545
|
}
|
|
586
546
|
}
|
|
587
|
-
const OPERATION_CONTRACT_VERSION_REGEX =
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
/^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
591
|
-
const VALID_OPERATION_LIFECYCLES = [
|
|
592
|
-
"stable",
|
|
593
|
-
"beta",
|
|
594
|
-
"deprecated",
|
|
595
|
-
"removed",
|
|
596
|
-
] as const;
|
|
547
|
+
const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
548
|
+
const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
549
|
+
const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"] as const;
|
|
597
550
|
|
|
598
551
|
function assertNonEmptyString(
|
|
599
552
|
value: unknown,
|
|
@@ -624,10 +577,7 @@ function validateToolRouterMetadata(
|
|
|
624
577
|
},
|
|
625
578
|
);
|
|
626
579
|
}
|
|
627
|
-
if (
|
|
628
|
-
toolRouter.name !== undefined &&
|
|
629
|
-
!MCP_TOOL_NAME_REGEX.test(toolRouter.name)
|
|
630
|
-
) {
|
|
580
|
+
if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
631
581
|
throw new ValidationError(
|
|
632
582
|
`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`,
|
|
633
583
|
{
|
|
@@ -699,10 +649,7 @@ function validateOperationContracts(
|
|
|
699
649
|
providerId,
|
|
700
650
|
);
|
|
701
651
|
}
|
|
702
|
-
if (
|
|
703
|
-
contract.lifecycle === "deprecated" ||
|
|
704
|
-
contract.lifecycle === "removed"
|
|
705
|
-
) {
|
|
652
|
+
if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
|
|
706
653
|
if (!contract.deprecation || typeof contract.deprecation !== "object") {
|
|
707
654
|
throw new ValidationError(
|
|
708
655
|
`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`,
|
|
@@ -750,10 +697,7 @@ function validateOperationAnnotations(
|
|
|
750
697
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
|
|
751
698
|
},
|
|
752
699
|
);
|
|
753
|
-
if (
|
|
754
|
-
timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
|
|
755
|
-
timeoutMs > OPERATION_TIMEOUT_MS_MAX
|
|
756
|
-
)
|
|
700
|
+
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
757
701
|
throw new ValidationError(
|
|
758
702
|
`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`,
|
|
759
703
|
{
|
|
@@ -951,12 +895,7 @@ function validateOperationTransports(
|
|
|
951
895
|
},
|
|
952
896
|
);
|
|
953
897
|
}
|
|
954
|
-
assertLiteralField(
|
|
955
|
-
kind,
|
|
956
|
-
`${fieldPath}.kind`,
|
|
957
|
-
VALID_OPERATION_TRANSPORT_KINDS,
|
|
958
|
-
providerId,
|
|
959
|
-
);
|
|
898
|
+
assertLiteralField(kind, `${fieldPath}.kind`, VALID_OPERATION_TRANSPORT_KINDS, providerId);
|
|
960
899
|
|
|
961
900
|
switch (kind) {
|
|
962
901
|
case "json":
|
|
@@ -988,16 +927,9 @@ function validateOperationTransports(
|
|
|
988
927
|
STREAM_MAX_DURATION_MS_MAX,
|
|
989
928
|
"max duration",
|
|
990
929
|
);
|
|
991
|
-
assertPositiveBytes(
|
|
992
|
-
Reflect.get(transport, "maxEventBytes"),
|
|
993
|
-
`${fieldPath}.maxEventBytes`,
|
|
994
|
-
);
|
|
930
|
+
assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
|
|
995
931
|
const resumable = Reflect.get(transport, "resumable");
|
|
996
|
-
if (
|
|
997
|
-
resumable !== undefined &&
|
|
998
|
-
resumable !== false &&
|
|
999
|
-
resumable !== "last-event-id"
|
|
1000
|
-
) {
|
|
932
|
+
if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
|
|
1001
933
|
throw new ValidationError(
|
|
1002
934
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`,
|
|
1003
935
|
{
|
|
@@ -1017,12 +949,7 @@ function validateOperationTransports(
|
|
|
1017
949
|
rejectUnknownFields(transport, HTTP_STREAM_TRANSPORT_FIELDS, fieldPath);
|
|
1018
950
|
const contentType = Reflect.get(transport, "contentType");
|
|
1019
951
|
if (contentType !== undefined) {
|
|
1020
|
-
assertNonEmptyString(
|
|
1021
|
-
contentType,
|
|
1022
|
-
`${fieldPath}.contentType`,
|
|
1023
|
-
providerId,
|
|
1024
|
-
operationName,
|
|
1025
|
-
);
|
|
952
|
+
assertNonEmptyString(contentType, `${fieldPath}.contentType`, providerId, operationName);
|
|
1026
953
|
}
|
|
1027
954
|
assertStreamMs(
|
|
1028
955
|
Reflect.get(transport, "idleTimeoutMs"),
|
|
@@ -1038,10 +965,7 @@ function validateOperationTransports(
|
|
|
1038
965
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1039
966
|
"max duration",
|
|
1040
967
|
);
|
|
1041
|
-
assertPositiveBytes(
|
|
1042
|
-
Reflect.get(transport, "maxChunkBytes"),
|
|
1043
|
-
`${fieldPath}.maxChunkBytes`,
|
|
1044
|
-
);
|
|
968
|
+
assertPositiveBytes(Reflect.get(transport, "maxChunkBytes"), `${fieldPath}.maxChunkBytes`);
|
|
1045
969
|
break;
|
|
1046
970
|
}
|
|
1047
971
|
case "websocket": {
|
|
@@ -1066,10 +990,7 @@ function validateOperationTransports(
|
|
|
1066
990
|
);
|
|
1067
991
|
}
|
|
1068
992
|
for (const subprotocol of subprotocols) {
|
|
1069
|
-
if (
|
|
1070
|
-
typeof subprotocol !== "string" ||
|
|
1071
|
-
!WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)
|
|
1072
|
-
) {
|
|
993
|
+
if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
1073
994
|
throw new ValidationError(
|
|
1074
995
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`,
|
|
1075
996
|
{
|
|
@@ -1093,10 +1014,7 @@ function validateOperationTransports(
|
|
|
1093
1014
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1094
1015
|
"max duration",
|
|
1095
1016
|
);
|
|
1096
|
-
assertPositiveBytes(
|
|
1097
|
-
Reflect.get(transport, "maxFrameBytes"),
|
|
1098
|
-
`${fieldPath}.maxFrameBytes`,
|
|
1099
|
-
);
|
|
1017
|
+
assertPositiveBytes(Reflect.get(transport, "maxFrameBytes"), `${fieldPath}.maxFrameBytes`);
|
|
1100
1018
|
break;
|
|
1101
1019
|
}
|
|
1102
1020
|
}
|
|
@@ -1159,10 +1077,7 @@ function levenshtein(a: string, b: string): number {
|
|
|
1159
1077
|
return prev[n] ?? 0;
|
|
1160
1078
|
}
|
|
1161
1079
|
|
|
1162
|
-
function suggestField(
|
|
1163
|
-
unknown: string,
|
|
1164
|
-
candidates: ReadonlySet<string>,
|
|
1165
|
-
): string | undefined {
|
|
1080
|
+
function suggestField(unknown: string, candidates: ReadonlySet<string>): string | undefined {
|
|
1166
1081
|
let best: string | undefined;
|
|
1167
1082
|
let bestDist = 3;
|
|
1168
1083
|
for (const candidate of candidates) {
|
|
@@ -1175,11 +1090,7 @@ function suggestField(
|
|
|
1175
1090
|
return best;
|
|
1176
1091
|
}
|
|
1177
1092
|
|
|
1178
|
-
function rejectUnknownFields(
|
|
1179
|
-
value: object,
|
|
1180
|
-
allowed: ReadonlySet<string>,
|
|
1181
|
-
fieldPath: string,
|
|
1182
|
-
): void {
|
|
1093
|
+
function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldPath: string): void {
|
|
1183
1094
|
for (const key of Object.keys(value)) {
|
|
1184
1095
|
if (allowed.has(key)) continue;
|
|
1185
1096
|
const hint = suggestField(key, allowed);
|
|
@@ -1218,23 +1129,12 @@ function validateProviderHealthMonitor(
|
|
|
1218
1129
|
field: "healthMonitor" | "healthProbe" = "healthMonitor",
|
|
1219
1130
|
): void {
|
|
1220
1131
|
if (healthMonitor === undefined) return;
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
)
|
|
1226
|
-
throw new ValidationError(
|
|
1227
|
-
`Provider "${providerId}" has invalid ${field}: must be an object.`,
|
|
1228
|
-
{
|
|
1229
|
-
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
1230
|
-
},
|
|
1231
|
-
);
|
|
1132
|
+
if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
|
|
1133
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
|
|
1134
|
+
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
1135
|
+
});
|
|
1232
1136
|
const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
|
|
1233
|
-
rejectUnknownFields(
|
|
1234
|
-
healthMonitorRecord,
|
|
1235
|
-
PROVIDER_HEALTH_MONITOR_FIELDS,
|
|
1236
|
-
field,
|
|
1237
|
-
);
|
|
1137
|
+
rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
|
|
1238
1138
|
if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
|
|
1239
1139
|
assertBoundedIntegerMs(
|
|
1240
1140
|
healthMonitorRecord.defaultProbeTimeoutMs,
|
|
@@ -1302,11 +1202,7 @@ function validateProviderHealthMonitor(
|
|
|
1302
1202
|
|
|
1303
1203
|
const probeOverrides = healthMonitorRecord.probeOverrides;
|
|
1304
1204
|
if (probeOverrides !== undefined) {
|
|
1305
|
-
if (
|
|
1306
|
-
!probeOverrides ||
|
|
1307
|
-
typeof probeOverrides !== "object" ||
|
|
1308
|
-
Array.isArray(probeOverrides)
|
|
1309
|
-
)
|
|
1205
|
+
if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
|
|
1310
1206
|
throw new ValidationError(
|
|
1311
1207
|
`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`,
|
|
1312
1208
|
);
|
|
@@ -1372,14 +1268,8 @@ function validateHealthCheckCase(
|
|
|
1372
1268
|
): void {
|
|
1373
1269
|
const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
|
|
1374
1270
|
if (!caseValue || typeof caseValue !== "object" || Array.isArray(caseValue))
|
|
1375
|
-
throw new ValidationError(
|
|
1376
|
-
|
|
1377
|
-
);
|
|
1378
|
-
rejectUnknownFields(
|
|
1379
|
-
caseValue as Record<string, unknown>,
|
|
1380
|
-
HEALTH_CHECK_CASE_FIELDS,
|
|
1381
|
-
fieldPath,
|
|
1382
|
-
);
|
|
1271
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1272
|
+
rejectUnknownFields(caseValue as Record<string, unknown>, HEALTH_CHECK_CASE_FIELDS, fieldPath);
|
|
1383
1273
|
const c = caseValue as HealthCheckCase;
|
|
1384
1274
|
if (typeof c.name !== "string" || c.name.length === 0)
|
|
1385
1275
|
throw new ValidationError(
|
|
@@ -1407,15 +1297,11 @@ function validateHealthCheckCase(
|
|
|
1407
1297
|
`Provider "${providerId}" ${fieldPath}.degradedThresholdMs must be an integer degraded threshold in [${HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN}, ${HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX}] ms.`,
|
|
1408
1298
|
);
|
|
1409
1299
|
if (c.timeoutMs !== undefined) {
|
|
1410
|
-
assertBoundedIntegerMs(
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1416
|
-
label: "timeout",
|
|
1417
|
-
},
|
|
1418
|
-
);
|
|
1300
|
+
assertBoundedIntegerMs(c.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
|
|
1301
|
+
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1302
|
+
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1303
|
+
label: "timeout",
|
|
1304
|
+
});
|
|
1419
1305
|
}
|
|
1420
1306
|
if (
|
|
1421
1307
|
c.expectedStatus !== undefined &&
|
|
@@ -1431,21 +1317,11 @@ function validateHealthCheckCase(
|
|
|
1431
1317
|
);
|
|
1432
1318
|
}
|
|
1433
1319
|
|
|
1434
|
-
function validateHealthCheckSuite(
|
|
1435
|
-
providerId: string,
|
|
1436
|
-
operationName: string,
|
|
1437
|
-
suite: unknown,
|
|
1438
|
-
): void {
|
|
1320
|
+
function validateHealthCheckSuite(providerId: string, operationName: string, suite: unknown): void {
|
|
1439
1321
|
const fieldPath = `operations.${operationName}.healthCheck`;
|
|
1440
1322
|
if (!suite || typeof suite !== "object" || Array.isArray(suite))
|
|
1441
|
-
throw new ValidationError(
|
|
1442
|
-
|
|
1443
|
-
);
|
|
1444
|
-
rejectUnknownFields(
|
|
1445
|
-
suite as Record<string, unknown>,
|
|
1446
|
-
HEALTH_CHECK_SUITE_FIELDS,
|
|
1447
|
-
fieldPath,
|
|
1448
|
-
);
|
|
1323
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1324
|
+
rejectUnknownFields(suite as Record<string, unknown>, HEALTH_CHECK_SUITE_FIELDS, fieldPath);
|
|
1449
1325
|
const s = suite as HealthCheckSuite;
|
|
1450
1326
|
if (!isPositiveMsDurationString(s.interval))
|
|
1451
1327
|
throw new ValidationError(
|
|
@@ -1455,11 +1331,7 @@ function validateHealthCheckSuite(
|
|
|
1455
1331
|
},
|
|
1456
1332
|
);
|
|
1457
1333
|
if (s.schedule !== undefined) {
|
|
1458
|
-
if (
|
|
1459
|
-
!s.schedule ||
|
|
1460
|
-
typeof s.schedule !== "object" ||
|
|
1461
|
-
Array.isArray(s.schedule)
|
|
1462
|
-
) {
|
|
1334
|
+
if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
|
|
1463
1335
|
throw new ValidationError(
|
|
1464
1336
|
`Provider "${providerId}" ${fieldPath}.schedule must be an object.`,
|
|
1465
1337
|
);
|
|
@@ -1469,11 +1341,7 @@ function validateHealthCheckSuite(
|
|
|
1469
1341
|
`Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`,
|
|
1470
1342
|
);
|
|
1471
1343
|
}
|
|
1472
|
-
rejectUnknownFields(
|
|
1473
|
-
s.schedule,
|
|
1474
|
-
new Set(["randomize"]),
|
|
1475
|
-
`${fieldPath}.schedule`,
|
|
1476
|
-
);
|
|
1344
|
+
rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
|
|
1477
1345
|
const randomize = Reflect.get(s.schedule, "randomize");
|
|
1478
1346
|
if (randomize !== undefined) {
|
|
1479
1347
|
validateScheduleRandomization(
|
|
@@ -1484,15 +1352,11 @@ function validateHealthCheckSuite(
|
|
|
1484
1352
|
}
|
|
1485
1353
|
}
|
|
1486
1354
|
if (s.timeoutMs !== undefined) {
|
|
1487
|
-
assertBoundedIntegerMs(
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1493
|
-
label: "timeout",
|
|
1494
|
-
},
|
|
1495
|
-
);
|
|
1355
|
+
assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
|
|
1356
|
+
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1357
|
+
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1358
|
+
label: "timeout",
|
|
1359
|
+
});
|
|
1496
1360
|
}
|
|
1497
1361
|
if (s.degradedThresholdMs !== undefined) {
|
|
1498
1362
|
assertBoundedIntegerMs(
|
|
@@ -1505,10 +1369,7 @@ function validateHealthCheckSuite(
|
|
|
1505
1369
|
},
|
|
1506
1370
|
);
|
|
1507
1371
|
}
|
|
1508
|
-
if (
|
|
1509
|
-
s.requiresConnection !== undefined &&
|
|
1510
|
-
typeof s.requiresConnection !== "boolean"
|
|
1511
|
-
)
|
|
1372
|
+
if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
|
|
1512
1373
|
throw new ValidationError(
|
|
1513
1374
|
`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`,
|
|
1514
1375
|
);
|
|
@@ -1540,14 +1401,8 @@ function validateHealthCheckUnsupported(
|
|
|
1540
1401
|
unsupported: unknown,
|
|
1541
1402
|
): void {
|
|
1542
1403
|
const fieldPath = `operations.${operationName}.healthCheckUnsupported`;
|
|
1543
|
-
if (
|
|
1544
|
-
|
|
1545
|
-
typeof unsupported !== "object" ||
|
|
1546
|
-
Array.isArray(unsupported)
|
|
1547
|
-
)
|
|
1548
|
-
throw new ValidationError(
|
|
1549
|
-
`Provider "${providerId}" ${fieldPath} must be an object.`,
|
|
1550
|
-
);
|
|
1404
|
+
if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
|
|
1405
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1551
1406
|
rejectUnknownFields(
|
|
1552
1407
|
unsupported as Record<string, unknown>,
|
|
1553
1408
|
HEALTH_CHECK_UNSUPPORTED_FIELDS,
|
|
@@ -1581,12 +1436,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
|
|
|
1581
1436
|
"steps",
|
|
1582
1437
|
"run",
|
|
1583
1438
|
]);
|
|
1584
|
-
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
|
|
1585
|
-
"kind",
|
|
1586
|
-
"interval",
|
|
1587
|
-
"jitter",
|
|
1588
|
-
"randomize",
|
|
1589
|
-
]);
|
|
1439
|
+
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
|
|
1590
1440
|
const HEALTH_JOURNEY_STEP_FIELDS = new Set([
|
|
1591
1441
|
"id",
|
|
1592
1442
|
"description",
|
|
@@ -1605,10 +1455,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
|
|
|
1605
1455
|
"minManualInterval",
|
|
1606
1456
|
"publicRationale",
|
|
1607
1457
|
]);
|
|
1608
|
-
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
|
|
1609
|
-
"enabled",
|
|
1610
|
-
"reason",
|
|
1611
|
-
]);
|
|
1458
|
+
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
|
|
1612
1459
|
const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
|
|
1613
1460
|
"enabled",
|
|
1614
1461
|
"requiresAcknowledgement",
|
|
@@ -1628,32 +1475,18 @@ function validateHealthJourneyManualTrigger(
|
|
|
1628
1475
|
manualTrigger: unknown,
|
|
1629
1476
|
): void {
|
|
1630
1477
|
const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
|
|
1631
|
-
if (
|
|
1632
|
-
!manualTrigger ||
|
|
1633
|
-
typeof manualTrigger !== "object" ||
|
|
1634
|
-
Array.isArray(manualTrigger)
|
|
1635
|
-
) {
|
|
1478
|
+
if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
|
|
1636
1479
|
throw new ValidationError(
|
|
1637
1480
|
`Provider "${providerId}" ${fieldPath} must be an object when present.`,
|
|
1638
1481
|
);
|
|
1639
1482
|
}
|
|
1640
|
-
rejectUnknownFields(
|
|
1641
|
-
manualTrigger,
|
|
1642
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS,
|
|
1643
|
-
fieldPath,
|
|
1644
|
-
);
|
|
1483
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
|
|
1645
1484
|
const enabled = Reflect.get(manualTrigger, "enabled");
|
|
1646
1485
|
if (typeof enabled !== "boolean") {
|
|
1647
|
-
throw new ValidationError(
|
|
1648
|
-
`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`,
|
|
1649
|
-
);
|
|
1486
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`);
|
|
1650
1487
|
}
|
|
1651
1488
|
if (enabled === false) {
|
|
1652
|
-
rejectUnknownFields(
|
|
1653
|
-
manualTrigger,
|
|
1654
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS,
|
|
1655
|
-
fieldPath,
|
|
1656
|
-
);
|
|
1489
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS, fieldPath);
|
|
1657
1490
|
if (
|
|
1658
1491
|
Reflect.get(manualTrigger, "reason") !== undefined &&
|
|
1659
1492
|
(typeof Reflect.get(manualTrigger, "reason") !== "string" ||
|
|
@@ -1665,25 +1498,15 @@ function validateHealthJourneyManualTrigger(
|
|
|
1665
1498
|
}
|
|
1666
1499
|
return;
|
|
1667
1500
|
}
|
|
1668
|
-
rejectUnknownFields(
|
|
1669
|
-
|
|
1670
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS,
|
|
1671
|
-
fieldPath,
|
|
1672
|
-
);
|
|
1673
|
-
const requiresAcknowledgement = Reflect.get(
|
|
1674
|
-
manualTrigger,
|
|
1675
|
-
"requiresAcknowledgement",
|
|
1676
|
-
);
|
|
1501
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS, fieldPath);
|
|
1502
|
+
const requiresAcknowledgement = Reflect.get(manualTrigger, "requiresAcknowledgement");
|
|
1677
1503
|
if (typeof requiresAcknowledgement !== "boolean") {
|
|
1678
1504
|
throw new ValidationError(
|
|
1679
1505
|
`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`,
|
|
1680
1506
|
);
|
|
1681
1507
|
}
|
|
1682
1508
|
const risk = Reflect.get(manualTrigger, "risk");
|
|
1683
|
-
if (
|
|
1684
|
-
typeof risk !== "string" ||
|
|
1685
|
-
!HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)
|
|
1686
|
-
) {
|
|
1509
|
+
if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
1687
1510
|
throw new ValidationError(
|
|
1688
1511
|
`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`,
|
|
1689
1512
|
);
|
|
@@ -1694,10 +1517,7 @@ function validateHealthJourneyManualTrigger(
|
|
|
1694
1517
|
);
|
|
1695
1518
|
}
|
|
1696
1519
|
const minManualInterval = Reflect.get(manualTrigger, "minManualInterval");
|
|
1697
|
-
assertIsoDuration(
|
|
1698
|
-
minManualInterval,
|
|
1699
|
-
`Provider "${providerId}" ${fieldPath}.minManualInterval`,
|
|
1700
|
-
);
|
|
1520
|
+
assertIsoDuration(minManualInterval, `Provider "${providerId}" ${fieldPath}.minManualInterval`);
|
|
1701
1521
|
if (isoDurationMs(minManualInterval) <= 0) {
|
|
1702
1522
|
throw new ValidationError(
|
|
1703
1523
|
`Provider "${providerId}" ${fieldPath}.minManualInterval must be a positive duration.`,
|
|
@@ -1728,18 +1548,14 @@ const SMS_ORIGIN_FIELDS_BY_KIND: Record<string, ReadonlySet<string>> = {
|
|
|
1728
1548
|
e164: new Set(["kind", "value", "display"]),
|
|
1729
1549
|
nationalServiceCode: new Set(["kind", "country", "value", "display"]),
|
|
1730
1550
|
};
|
|
1731
|
-
const DURATION_RE =
|
|
1732
|
-
/^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
|
|
1551
|
+
const DURATION_RE = /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
|
|
1733
1552
|
const E164_RE = /^\+[1-9]\d{1,14}$/;
|
|
1734
1553
|
const ISO_COUNTRY_RE = /^[A-Z]{2}$/;
|
|
1735
1554
|
const NATIONAL_SERVICE_CODE_RE = /^[0-9]{2,15}$/;
|
|
1736
1555
|
const BCP47_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
1737
1556
|
const JOURNEY_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
1738
1557
|
|
|
1739
|
-
function assertIsoDuration(
|
|
1740
|
-
value: unknown,
|
|
1741
|
-
fieldPath: string,
|
|
1742
|
-
): asserts value is string {
|
|
1558
|
+
function assertIsoDuration(value: unknown, fieldPath: string): asserts value is string {
|
|
1743
1559
|
if (typeof value !== "string" || !DURATION_RE.test(value)) {
|
|
1744
1560
|
throw new ValidationError(
|
|
1745
1561
|
`${fieldPath} must be an ISO 8601 duration for example PT8H or PT2M30S.`,
|
|
@@ -1754,15 +1570,10 @@ function isoDurationMs(value: string): number {
|
|
|
1754
1570
|
const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
|
|
1755
1571
|
const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
|
|
1756
1572
|
const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
|
|
1757
|
-
return
|
|
1758
|
-
days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000
|
|
1759
|
-
);
|
|
1573
|
+
return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
|
|
1760
1574
|
}
|
|
1761
1575
|
|
|
1762
|
-
function scheduleRandomizationMs(
|
|
1763
|
-
randomize: unknown,
|
|
1764
|
-
fieldPath: string,
|
|
1765
|
-
): number {
|
|
1576
|
+
function scheduleRandomizationMs(randomize: unknown, fieldPath: string): number {
|
|
1766
1577
|
const mode = Reflect.get(randomize as object, "mode");
|
|
1767
1578
|
switch (mode) {
|
|
1768
1579
|
case "centered": {
|
|
@@ -1776,9 +1587,7 @@ function scheduleRandomizationMs(
|
|
|
1776
1587
|
return isoDurationMs(maxDelay);
|
|
1777
1588
|
}
|
|
1778
1589
|
default:
|
|
1779
|
-
throw new ValidationError(
|
|
1780
|
-
`${fieldPath}.mode must be "centered" or "delayed".`,
|
|
1781
|
-
);
|
|
1590
|
+
throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
|
|
1782
1591
|
}
|
|
1783
1592
|
}
|
|
1784
1593
|
|
|
@@ -1792,25 +1601,18 @@ function validateScheduleRandomization(
|
|
|
1792
1601
|
}
|
|
1793
1602
|
const mode = Reflect.get(randomize, "mode");
|
|
1794
1603
|
const allowedFields =
|
|
1795
|
-
mode === "centered"
|
|
1796
|
-
? new Set(["mode", "maxOffset"])
|
|
1797
|
-
: new Set(["mode", "maxDelay"]);
|
|
1604
|
+
mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
|
|
1798
1605
|
rejectUnknownFields(randomize, allowedFields, fieldPath);
|
|
1799
1606
|
const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
|
|
1800
1607
|
if (offsetMs <= 0) {
|
|
1801
1608
|
throw new ValidationError(`${fieldPath} duration must be positive.`);
|
|
1802
1609
|
}
|
|
1803
1610
|
if (offsetMs >= intervalMs) {
|
|
1804
|
-
throw new ValidationError(
|
|
1805
|
-
`${fieldPath} duration must be shorter than schedule interval.`,
|
|
1806
|
-
);
|
|
1611
|
+
throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
|
|
1807
1612
|
}
|
|
1808
1613
|
}
|
|
1809
1614
|
|
|
1810
|
-
function assertIsoCountry(
|
|
1811
|
-
value: unknown,
|
|
1812
|
-
fieldPath: string,
|
|
1813
|
-
): asserts value is string {
|
|
1615
|
+
function assertIsoCountry(value: unknown, fieldPath: string): asserts value is string {
|
|
1814
1616
|
if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
|
|
1815
1617
|
throw new ValidationError(
|
|
1816
1618
|
`${fieldPath} must be an ISO 3166-1 alpha-2 country code for example KR.`,
|
|
@@ -1833,9 +1635,7 @@ function normalizeIntervalDuration(input: string): string {
|
|
|
1833
1635
|
? durationMs / 3_600_000
|
|
1834
1636
|
: durationMs / 86_400_000;
|
|
1835
1637
|
if (!Number.isInteger(amount) || amount <= 0) {
|
|
1836
|
-
throw new ValidationError(
|
|
1837
|
-
`Journey schedule interval must be a positive duration.`,
|
|
1838
|
-
);
|
|
1638
|
+
throw new ValidationError(`Journey schedule interval must be a positive duration.`);
|
|
1839
1639
|
}
|
|
1840
1640
|
if (unit === "s") return `PT${amount}S`;
|
|
1841
1641
|
if (unit === "m") return `PT${amount}M`;
|
|
@@ -1894,11 +1694,7 @@ function countCapturingGroups(pattern: RegExp): number {
|
|
|
1894
1694
|
if (inCharacterClass || char !== "(") continue;
|
|
1895
1695
|
const next = source[i + 1];
|
|
1896
1696
|
if (next === "?" && source[i + 2] !== "<") continue;
|
|
1897
|
-
if (
|
|
1898
|
-
next === "?" &&
|
|
1899
|
-
source[i + 2] === "<" &&
|
|
1900
|
-
(source[i + 3] === "=" || source[i + 3] === "!")
|
|
1901
|
-
)
|
|
1697
|
+
if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1902
1698
|
continue;
|
|
1903
1699
|
count += 1;
|
|
1904
1700
|
}
|
|
@@ -1917,9 +1713,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1917
1713
|
}
|
|
1918
1714
|
const kind = Reflect.get(origin, "kind");
|
|
1919
1715
|
if (kind !== "e164" && kind !== "nationalServiceCode") {
|
|
1920
|
-
throw new ValidationError(
|
|
1921
|
-
`${fieldPath}.kind must be "e164" or "nationalServiceCode".`,
|
|
1922
|
-
);
|
|
1716
|
+
throw new ValidationError(`${fieldPath}.kind must be "e164" or "nationalServiceCode".`);
|
|
1923
1717
|
}
|
|
1924
1718
|
rejectUnknownFields(origin, SMS_ORIGIN_FIELDS_BY_KIND[kind], fieldPath);
|
|
1925
1719
|
if (kind === "e164") {
|
|
@@ -1946,9 +1740,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1946
1740
|
Reflect.get(origin, "display") !== undefined &&
|
|
1947
1741
|
typeof Reflect.get(origin, "display") !== "string"
|
|
1948
1742
|
) {
|
|
1949
|
-
throw new ValidationError(
|
|
1950
|
-
`${fieldPath}.display must be a string when present.`,
|
|
1951
|
-
);
|
|
1743
|
+
throw new ValidationError(`${fieldPath}.display must be a string when present.`);
|
|
1952
1744
|
}
|
|
1953
1745
|
}
|
|
1954
1746
|
|
|
@@ -1962,9 +1754,7 @@ function validateSmsOtpMatcher(
|
|
|
1962
1754
|
rejectUnknownFields(matcher, SMS_OTP_MATCHER_FIELDS, fieldPath);
|
|
1963
1755
|
const matcherId = Reflect.get(matcher, "id");
|
|
1964
1756
|
if (typeof matcherId !== "string" || !JOURNEY_ID_RE.test(matcherId)) {
|
|
1965
|
-
throw new ValidationError(
|
|
1966
|
-
`${fieldPath}.id must be a kebab-case identifier.`,
|
|
1967
|
-
);
|
|
1757
|
+
throw new ValidationError(`${fieldPath}.id must be a kebab-case identifier.`);
|
|
1968
1758
|
}
|
|
1969
1759
|
assertIsoCountry(Reflect.get(matcher, "country"), `${fieldPath}.country`);
|
|
1970
1760
|
if (
|
|
@@ -1972,24 +1762,18 @@ function validateSmsOtpMatcher(
|
|
|
1972
1762
|
(typeof Reflect.get(matcher, "locale") !== "string" ||
|
|
1973
1763
|
!BCP47_RE.test(Reflect.get(matcher, "locale")))
|
|
1974
1764
|
) {
|
|
1975
|
-
throw new ValidationError(
|
|
1976
|
-
`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`,
|
|
1977
|
-
);
|
|
1765
|
+
throw new ValidationError(`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`);
|
|
1978
1766
|
}
|
|
1979
1767
|
if (
|
|
1980
1768
|
Reflect.get(matcher, "phoneNumber") !== undefined &&
|
|
1981
1769
|
(typeof Reflect.get(matcher, "phoneNumber") !== "string" ||
|
|
1982
1770
|
!E164_RE.test(Reflect.get(matcher, "phoneNumber")))
|
|
1983
1771
|
) {
|
|
1984
|
-
throw new ValidationError(
|
|
1985
|
-
`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`,
|
|
1986
|
-
);
|
|
1772
|
+
throw new ValidationError(`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`);
|
|
1987
1773
|
}
|
|
1988
1774
|
const origins = Reflect.get(matcher, "origins");
|
|
1989
1775
|
if (!Array.isArray(origins) || origins.length === 0) {
|
|
1990
|
-
throw new ValidationError(
|
|
1991
|
-
`${fieldPath}.origins must be a non-empty array.`,
|
|
1992
|
-
);
|
|
1776
|
+
throw new ValidationError(`${fieldPath}.origins must be a non-empty array.`);
|
|
1993
1777
|
}
|
|
1994
1778
|
for (const [index, origin] of origins.entries()) {
|
|
1995
1779
|
validateSmsOrigin(origin, `${fieldPath}.origins[${index}]`);
|
|
@@ -2010,10 +1794,7 @@ function validateSmsOtpMatcher(
|
|
|
2010
1794
|
);
|
|
2011
1795
|
}
|
|
2012
1796
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
2013
|
-
if (
|
|
2014
|
-
countCapturingGroups(regex) !== 1 &&
|
|
2015
|
-
Reflect.get(code, "capture") === undefined
|
|
2016
|
-
) {
|
|
1797
|
+
if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
|
|
2017
1798
|
throw new ValidationError(
|
|
2018
1799
|
`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`,
|
|
2019
1800
|
);
|
|
@@ -2023,20 +1804,12 @@ function validateSmsOtpMatcher(
|
|
|
2023
1804
|
typeof Reflect.get(code, "capture") !== "string" &&
|
|
2024
1805
|
typeof Reflect.get(code, "capture") !== "number"
|
|
2025
1806
|
) {
|
|
2026
|
-
throw new ValidationError(
|
|
2027
|
-
`${fieldPath}.code.capture must be a string or number when present.`,
|
|
2028
|
-
);
|
|
1807
|
+
throw new ValidationError(`${fieldPath}.code.capture must be a string or number when present.`);
|
|
2029
1808
|
}
|
|
2030
1809
|
assertIsoDuration(Reflect.get(matcher, "maxAge"), `${fieldPath}.maxAge`);
|
|
2031
|
-
assertIsoDuration(
|
|
2032
|
-
Reflect.get(matcher, "waitTimeout"),
|
|
2033
|
-
`${fieldPath}.waitTimeout`,
|
|
2034
|
-
);
|
|
1810
|
+
assertIsoDuration(Reflect.get(matcher, "waitTimeout"), `${fieldPath}.waitTimeout`);
|
|
2035
1811
|
if (Reflect.get(matcher, "clockSkew") !== undefined)
|
|
2036
|
-
assertIsoDuration(
|
|
2037
|
-
Reflect.get(matcher, "clockSkew"),
|
|
2038
|
-
`${fieldPath}.clockSkew`,
|
|
2039
|
-
);
|
|
1812
|
+
assertIsoDuration(Reflect.get(matcher, "clockSkew"), `${fieldPath}.clockSkew`);
|
|
2040
1813
|
}
|
|
2041
1814
|
|
|
2042
1815
|
export function defineSmsOtpMatcher(
|
|
@@ -2068,9 +1841,7 @@ export function defineSmsOtpMatcher(
|
|
|
2068
1841
|
return matcher;
|
|
2069
1842
|
}
|
|
2070
1843
|
|
|
2071
|
-
export function defineHealthJourney(
|
|
2072
|
-
config: HealthJourneyDefinition,
|
|
2073
|
-
): HealthJourneyDefinition {
|
|
1844
|
+
export function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition {
|
|
2074
1845
|
return config;
|
|
2075
1846
|
}
|
|
2076
1847
|
|
|
@@ -2081,22 +1852,15 @@ function validateHealthJourneySchedule(
|
|
|
2081
1852
|
): void {
|
|
2082
1853
|
const fieldPath = `healthJourneys.${journeyId}.schedule`;
|
|
2083
1854
|
if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) {
|
|
2084
|
-
throw new ValidationError(
|
|
2085
|
-
`Provider "${providerId}" ${fieldPath} must be an object.`,
|
|
2086
|
-
);
|
|
1855
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
2087
1856
|
}
|
|
2088
1857
|
rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
|
|
2089
1858
|
if (Reflect.get(schedule, "kind") !== "interval")
|
|
2090
|
-
throw new ValidationError(
|
|
2091
|
-
`Provider "${providerId}" ${fieldPath}.kind must be "interval".`,
|
|
2092
|
-
);
|
|
1859
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
|
|
2093
1860
|
const interval = Reflect.get(schedule, "interval");
|
|
2094
1861
|
assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
|
|
2095
1862
|
const randomize = Reflect.get(schedule, "randomize");
|
|
2096
|
-
if (
|
|
2097
|
-
Reflect.get(schedule, "jitter") !== undefined &&
|
|
2098
|
-
randomize !== undefined
|
|
2099
|
-
) {
|
|
1863
|
+
if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
|
|
2100
1864
|
throw new ValidationError(
|
|
2101
1865
|
`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`,
|
|
2102
1866
|
);
|
|
@@ -2123,17 +1887,13 @@ function validateHealthJourneys(
|
|
|
2123
1887
|
const covered = new Set<string>();
|
|
2124
1888
|
if (healthJourneys === undefined) return covered;
|
|
2125
1889
|
if (!Array.isArray(healthJourneys)) {
|
|
2126
|
-
throw new ValidationError(
|
|
2127
|
-
`Provider "${providerId}" healthJourneys must be an array.`,
|
|
2128
|
-
);
|
|
1890
|
+
throw new ValidationError(`Provider "${providerId}" healthJourneys must be an array.`);
|
|
2129
1891
|
}
|
|
2130
1892
|
const journeyIds = new Set<string>();
|
|
2131
1893
|
for (const [index, journey] of healthJourneys.entries()) {
|
|
2132
1894
|
const prefix = `healthJourneys[${index}]`;
|
|
2133
1895
|
if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
|
|
2134
|
-
throw new ValidationError(
|
|
2135
|
-
`Provider "${providerId}" ${prefix} must be an object.`,
|
|
2136
|
-
);
|
|
1896
|
+
throw new ValidationError(`Provider "${providerId}" ${prefix} must be an object.`);
|
|
2137
1897
|
}
|
|
2138
1898
|
rejectUnknownFields(journey, HEALTH_JOURNEY_FIELDS, prefix);
|
|
2139
1899
|
if (typeof journey.id !== "string" || !JOURNEY_ID_RE.test(journey.id)) {
|
|
@@ -2147,10 +1907,7 @@ function validateHealthJourneys(
|
|
|
2147
1907
|
);
|
|
2148
1908
|
journeyIds.add(journey.id);
|
|
2149
1909
|
validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
|
|
2150
|
-
if (
|
|
2151
|
-
!Array.isArray(journey.coversOperations) ||
|
|
2152
|
-
journey.coversOperations.length === 0
|
|
2153
|
-
) {
|
|
1910
|
+
if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
|
|
2154
1911
|
throw new ValidationError(
|
|
2155
1912
|
`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`,
|
|
2156
1913
|
);
|
|
@@ -2185,10 +1942,7 @@ function validateHealthJourneys(
|
|
|
2185
1942
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers must be an array.`,
|
|
2186
1943
|
);
|
|
2187
1944
|
for (const [matcherIndex, matcher] of journey.smsMatchers.entries()) {
|
|
2188
|
-
validateSmsOtpMatcher(
|
|
2189
|
-
matcher,
|
|
2190
|
-
`healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`,
|
|
2191
|
-
);
|
|
1945
|
+
validateSmsOtpMatcher(matcher, `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`);
|
|
2192
1946
|
if (matcherIds.has(matcher.id))
|
|
2193
1947
|
throw new ValidationError(
|
|
2194
1948
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers has duplicate matcher id "${matcher.id}".`,
|
|
@@ -2199,9 +1953,7 @@ function validateHealthJourneys(
|
|
|
2199
1953
|
for (const [stepIndex, step] of journey.steps.entries()) {
|
|
2200
1954
|
const stepPath = `healthJourneys.${journey.id}.steps[${stepIndex}]`;
|
|
2201
1955
|
if (!step || typeof step !== "object" || Array.isArray(step))
|
|
2202
|
-
throw new ValidationError(
|
|
2203
|
-
`Provider "${providerId}" ${stepPath} must be an object.`,
|
|
2204
|
-
);
|
|
1956
|
+
throw new ValidationError(`Provider "${providerId}" ${stepPath} must be an object.`);
|
|
2205
1957
|
rejectUnknownFields(step, HEALTH_JOURNEY_STEP_FIELDS, stepPath);
|
|
2206
1958
|
if (typeof step.id !== "string" || !JOURNEY_ID_RE.test(step.id))
|
|
2207
1959
|
throw new ValidationError(
|
|
@@ -2211,20 +1963,13 @@ function validateHealthJourneys(
|
|
|
2211
1963
|
throw new ValidationError(
|
|
2212
1964
|
`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`,
|
|
2213
1965
|
);
|
|
2214
|
-
if (
|
|
2215
|
-
step.usesSmsMatcher !== undefined &&
|
|
2216
|
-
!matcherIds.has(step.usesSmsMatcher)
|
|
2217
|
-
)
|
|
1966
|
+
if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
|
|
2218
1967
|
throw new ValidationError(
|
|
2219
1968
|
`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`,
|
|
2220
1969
|
);
|
|
2221
1970
|
}
|
|
2222
1971
|
if (journey.manualTrigger !== undefined)
|
|
2223
|
-
validateHealthJourneyManualTrigger(
|
|
2224
|
-
providerId,
|
|
2225
|
-
journey.id,
|
|
2226
|
-
journey.manualTrigger,
|
|
2227
|
-
);
|
|
1972
|
+
validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
|
|
2228
1973
|
if (journey.timeout !== undefined)
|
|
2229
1974
|
assertIsoDuration(
|
|
2230
1975
|
journey.timeout,
|
|
@@ -2270,23 +2015,10 @@ function validateOperationHealthChecks(
|
|
|
2270
2015
|
fix: `Remove either operations.${operationName}.healthCheck or operations.${operationName}.healthCheckUnsupported.`,
|
|
2271
2016
|
},
|
|
2272
2017
|
);
|
|
2273
|
-
if (hasCheck)
|
|
2274
|
-
validateHealthCheckSuite(
|
|
2275
|
-
providerId,
|
|
2276
|
-
operationName,
|
|
2277
|
-
operation.healthCheck,
|
|
2278
|
-
);
|
|
2018
|
+
if (hasCheck) validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
|
|
2279
2019
|
if (hasUnsupported)
|
|
2280
|
-
validateHealthCheckUnsupported(
|
|
2281
|
-
|
|
2282
|
-
operationName,
|
|
2283
|
-
operation.healthCheckUnsupported,
|
|
2284
|
-
);
|
|
2285
|
-
if (
|
|
2286
|
-
!hasCheck &&
|
|
2287
|
-
!hasUnsupported &&
|
|
2288
|
-
!journeyCoveredOperations.has(operationName)
|
|
2289
|
-
)
|
|
2020
|
+
validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
|
|
2021
|
+
if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
|
|
2290
2022
|
throw new ValidationError(
|
|
2291
2023
|
`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`,
|
|
2292
2024
|
{
|
|
@@ -2341,6 +2073,19 @@ function validateOperationFixtures(
|
|
|
2341
2073
|
}
|
|
2342
2074
|
}
|
|
2343
2075
|
|
|
2076
|
+
/**
|
|
2077
|
+
* Shallow shape guard only: the `deployment` object is passed through
|
|
2078
|
+
* verbatim and deliberately not deep-validated by the SDK — the APIFuse
|
|
2079
|
+
* registry builder owns deployment validation and profile resolution.
|
|
2080
|
+
*/
|
|
2081
|
+
function validateProviderDeployment(providerId: string, deployment: unknown): void {
|
|
2082
|
+
if (deployment === undefined) return;
|
|
2083
|
+
if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
|
|
2084
|
+
throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
|
|
2085
|
+
fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2344
2089
|
export function defineProvider<
|
|
2345
2090
|
TOperations extends Record<string, ProviderOperation>,
|
|
2346
2091
|
TConfig extends ProviderConfig<TOperations>,
|
|
@@ -2353,12 +2098,9 @@ export function defineProvider<
|
|
|
2353
2098
|
fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
|
|
2354
2099
|
});
|
|
2355
2100
|
if (Object.keys(config.operations).length === 0)
|
|
2356
|
-
throw new ProviderError(
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
fix: "Add at least one operation to the operations object",
|
|
2360
|
-
},
|
|
2361
|
-
);
|
|
2101
|
+
throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
|
|
2102
|
+
fix: "Add at least one operation to the operations object",
|
|
2103
|
+
});
|
|
2362
2104
|
validateOperationIds(config.id, config.operations);
|
|
2363
2105
|
validateOperationAnnotations(config.id, config.operations);
|
|
2364
2106
|
validateOperationObservability(config.id, config.operations);
|
|
@@ -2370,11 +2112,7 @@ export function defineProvider<
|
|
|
2370
2112
|
config.operations,
|
|
2371
2113
|
config.healthJourneys,
|
|
2372
2114
|
);
|
|
2373
|
-
validateOperationHealthChecks(
|
|
2374
|
-
config.id,
|
|
2375
|
-
config.operations,
|
|
2376
|
-
journeyCoveredOperations,
|
|
2377
|
-
);
|
|
2115
|
+
validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
|
|
2378
2116
|
if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
|
|
2379
2117
|
throw new ValidationError(
|
|
2380
2118
|
`Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
|
|
@@ -2388,6 +2126,7 @@ export function defineProvider<
|
|
|
2388
2126
|
config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
|
|
2389
2127
|
);
|
|
2390
2128
|
validateOperationFixtures(config.id, config.operations);
|
|
2129
|
+
validateProviderDeployment(config.id, config.deployment);
|
|
2391
2130
|
validateProviderProxy(config);
|
|
2392
2131
|
validateProviderStt(config);
|
|
2393
2132
|
if (config.runtime === "browser" && !config.browser)
|
|
@@ -2406,6 +2145,9 @@ export function defineProvider<
|
|
|
2406
2145
|
id: config.id,
|
|
2407
2146
|
version: config.version,
|
|
2408
2147
|
runtime: config.runtime,
|
|
2148
|
+
// Verbatim passthrough: deployment validation and profile resolution
|
|
2149
|
+
// are owned by the APIFuse registry builder, not the SDK.
|
|
2150
|
+
deployment: config.deployment,
|
|
2409
2151
|
allowedHosts: config.allowedHosts,
|
|
2410
2152
|
stealth: config.stealth,
|
|
2411
2153
|
proxy: config.proxy,
|