@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.10
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 +240 -0
- package/CHANGELOG.md +93 -0
- package/README.md +26 -10
- package/SUBMISSION.md +11 -12
- 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 +36 -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 +2177 -353
- 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/auth-turn.v1.schema.json +89 -0
- package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/dist/auth-turn/fixtures/valid/abort.json +8 -0
- package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
- package/dist/auth-turn/fixtures/valid/complete.json +13 -0
- package/dist/auth-turn/fixtures/valid/form.json +14 -0
- package/dist/auth-turn/fixtures/valid/message.json +13 -0
- package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/dist/auth-turn/fixtures/valid/pending.json +5 -0
- package/dist/auth-turn/fixtures/valid/poll.json +9 -0
- package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
- package/dist/auth-turn/fixtures/valid/retry.json +8 -0
- package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/dist/auth-turn/index.d.ts +195 -0
- package/dist/auth-turn/index.js +133 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +427 -0
- package/dist/ceremonies/index.d.ts +1 -1
- package/dist/ceremonies/index.js +14 -48
- 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 +47 -33
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/dist/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/dist/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/dist/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/dist/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +96 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +163 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- 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 +5 -6
- package/dist/define.d.ts +13 -1
- package/dist/define.js +245 -178
- 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 -41
- package/dist/index.js +39 -36
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +35 -15
- package/dist/provider.d.ts +11 -9
- package/dist/provider.js +9 -8
- 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 +4 -2
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +214 -25
- 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 +160 -344
- 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 +7 -4
- package/dist/runtime/stealth.js +257 -215
- 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 -3
- package/dist/server/index.js +6 -2
- package/dist/server/self-test-input-tokens.d.ts +1 -0
- package/dist/server/self-test-input-tokens.js +37 -0
- package/dist/server/self-test-redaction.d.ts +20 -0
- package/dist/server/self-test-redaction.js +70 -0
- package/dist/server/self-test-token.d.ts +30 -0
- package/dist/server/self-test-token.js +50 -0
- package/dist/server/self-test.d.ts +199 -0
- package/dist/server/self-test.js +1113 -0
- package/dist/server/serve.d.ts +14 -3
- package/dist/server/serve.js +135 -64
- package/dist/server/types.d.ts +10 -9
- package/dist/server/types.js +3 -7
- 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 +12 -15
- package/dist/types.d.ts +237 -1
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/auth-turn.v1.schema.json +89 -0
- package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/src/auth-turn/fixtures/valid/abort.json +8 -0
- package/src/auth-turn/fixtures/valid/challenge.json +17 -0
- package/src/auth-turn/fixtures/valid/complete.json +13 -0
- package/src/auth-turn/fixtures/valid/form.json +14 -0
- package/src/auth-turn/fixtures/valid/message.json +13 -0
- package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/src/auth-turn/fixtures/valid/pending.json +5 -0
- package/src/auth-turn/fixtures/valid/poll.json +9 -0
- package/src/auth-turn/fixtures/valid/redirect.json +16 -0
- package/src/auth-turn/fixtures/valid/retry.json +8 -0
- package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/src/auth-turn/index.ts +177 -0
- package/src/auth.ts +728 -0
- package/src/ceremonies/index.ts +33 -121
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +69 -99
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/src/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/src/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/src/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/src/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl +13 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +96 -0
- package/src/cli/templates/provider/README.md.tpl +7 -4
- 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 +13 -28
- package/src/define.ts +397 -528
- 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 +64 -41
- package/src/lint.ts +122 -159
- package/src/provider.ts +40 -9
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +6 -6
- package/src/runtime/browser.ts +320 -151
- 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 +216 -539
- 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 +331 -369
- 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 +44 -3
- package/src/server/self-test-input-tokens.ts +46 -0
- package/src/server/self-test-redaction.ts +97 -0
- package/src/server/self-test-token.ts +70 -0
- package/src/server/self-test.ts +1450 -0
- package/src/server/serve.ts +206 -216
- package/src/server/types.ts +7 -19
- 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 +24 -64
- package/src/types.ts +274 -1
- package/src/user-input.ts +118 -0
package/src/define.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import ms from "ms";
|
|
2
|
+
|
|
3
|
+
import { ProviderError, ValidationError } from "./errors.js";
|
|
4
|
+
import { safeParseSchemaSync } from "./schema.js";
|
|
3
5
|
import type {
|
|
4
6
|
AuthConfig,
|
|
5
7
|
BrowserEngine,
|
|
@@ -10,6 +12,7 @@ import type {
|
|
|
10
12
|
HealthCheckUnsupported,
|
|
11
13
|
HealthJourneyDefinition,
|
|
12
14
|
HealthJourneySchedule,
|
|
15
|
+
HealthScheduleRandomization,
|
|
13
16
|
InferSchemaOutput,
|
|
14
17
|
OperationDefinition,
|
|
15
18
|
OperationHandlerResult,
|
|
@@ -19,8 +22,10 @@ import type {
|
|
|
19
22
|
OperationWebSocketTransport,
|
|
20
23
|
ProviderAccessConfig,
|
|
21
24
|
ProviderDefinition,
|
|
25
|
+
ProviderDeploymentOverrides,
|
|
22
26
|
ProviderHealthMonitorConfig,
|
|
23
27
|
ProviderProxyConfig,
|
|
28
|
+
ProviderProxyProvider,
|
|
24
29
|
ProviderPublicProfile,
|
|
25
30
|
ProviderReviewed,
|
|
26
31
|
ProviderSecretDeclaration,
|
|
@@ -29,7 +34,7 @@ import type {
|
|
|
29
34
|
SchemaLike,
|
|
30
35
|
SmsOtpMatcherDefinition,
|
|
31
36
|
StealthPlatform,
|
|
32
|
-
} from "./types";
|
|
37
|
+
} from "./types.js";
|
|
33
38
|
import {
|
|
34
39
|
HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
35
40
|
HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
|
|
@@ -45,7 +50,7 @@ import {
|
|
|
45
50
|
STREAM_IDLE_TIMEOUT_MS_MIN,
|
|
46
51
|
STREAM_MAX_DURATION_MS_MAX,
|
|
47
52
|
STREAM_MAX_DURATION_MS_MIN,
|
|
48
|
-
} from "./types";
|
|
53
|
+
} from "./types.js";
|
|
49
54
|
|
|
50
55
|
type ProviderImplementationSourceAccess =
|
|
51
56
|
| "official_api"
|
|
@@ -71,23 +76,10 @@ interface ProviderImplementationProfile {
|
|
|
71
76
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
72
77
|
const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
73
78
|
const VALID_RUNTIMES = ["standard", "shared", "browser"] as const;
|
|
74
|
-
const VALID_AUTH_MODES = [
|
|
75
|
-
"none",
|
|
76
|
-
"platform-managed",
|
|
77
|
-
"credentials",
|
|
78
|
-
"oauth2",
|
|
79
|
-
] as const;
|
|
79
|
+
const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"] as const;
|
|
80
80
|
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
|
|
81
|
-
const VALID_PROVIDER_PROXY_MODES = [
|
|
82
|
-
|
|
83
|
-
"optional",
|
|
84
|
-
"required",
|
|
85
|
-
] as const;
|
|
86
|
-
const VALID_PROVIDER_PROXY_PROVIDERS = [
|
|
87
|
-
"smartproxy",
|
|
88
|
-
"decodo",
|
|
89
|
-
"custom",
|
|
90
|
-
] 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;
|
|
91
83
|
const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
92
84
|
"request",
|
|
93
85
|
"operation",
|
|
@@ -96,78 +88,52 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
|
96
88
|
] as const;
|
|
97
89
|
const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
|
|
98
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
|
+
};
|
|
99
105
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
100
106
|
const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
|
|
101
|
-
const VALID_OPERATION_RISK_CLASSES = [
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
"destructive",
|
|
105
|
-
"external-send",
|
|
106
|
-
] as const;
|
|
107
|
-
const VALID_OPERATION_APPROVAL_POLICIES = [
|
|
108
|
-
"never",
|
|
109
|
-
"risk-based",
|
|
110
|
-
"always",
|
|
111
|
-
] as const;
|
|
112
|
-
const VALID_OPERATION_TRANSPORT_KINDS = [
|
|
113
|
-
"json",
|
|
114
|
-
"sse",
|
|
115
|
-
"http-stream",
|
|
116
|
-
"websocket",
|
|
117
|
-
] 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;
|
|
118
110
|
const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
119
111
|
const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
120
112
|
|
|
121
|
-
const MS_DURATION_UNITS = new Set([
|
|
122
|
-
"years",
|
|
123
|
-
"year",
|
|
124
|
-
"yrs",
|
|
125
|
-
"yr",
|
|
126
|
-
"y",
|
|
127
|
-
"weeks",
|
|
128
|
-
"week",
|
|
129
|
-
"w",
|
|
130
|
-
"days",
|
|
131
|
-
"day",
|
|
132
|
-
"d",
|
|
133
|
-
"hours",
|
|
134
|
-
"hour",
|
|
135
|
-
"hrs",
|
|
136
|
-
"hr",
|
|
137
|
-
"h",
|
|
138
|
-
"minutes",
|
|
139
|
-
"minute",
|
|
140
|
-
"mins",
|
|
141
|
-
"min",
|
|
142
|
-
"m",
|
|
143
|
-
"seconds",
|
|
144
|
-
"second",
|
|
145
|
-
"secs",
|
|
146
|
-
"sec",
|
|
147
|
-
"s",
|
|
148
|
-
"milliseconds",
|
|
149
|
-
"millisecond",
|
|
150
|
-
"msecs",
|
|
151
|
-
"msec",
|
|
152
|
-
"ms",
|
|
153
|
-
]);
|
|
154
113
|
const MS_DURATION_PATTERN = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*([a-zA-Z]+)?$/;
|
|
155
114
|
|
|
156
115
|
function isPositiveMsDurationString(value: unknown): value is string {
|
|
157
116
|
if (typeof value !== "string") return false;
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
117
|
+
return parsePositiveMsDuration(value) !== undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function msDurationMs(value: string): number {
|
|
121
|
+
return parsePositiveMsDuration(value) ?? 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parsePositiveMsDuration(value: string): number | undefined {
|
|
125
|
+
const trimmed = value.trim();
|
|
126
|
+
if (!MS_DURATION_PATTERN.test(trimmed)) return undefined;
|
|
127
|
+
const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue);
|
|
128
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
129
|
+
return parsed;
|
|
164
130
|
}
|
|
165
131
|
|
|
166
132
|
type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
|
|
167
|
-
type OperationConfig<
|
|
168
|
-
TInput
|
|
169
|
-
|
|
170
|
-
>
|
|
133
|
+
type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
134
|
+
OperationDefinition<TInput, TOutput>,
|
|
135
|
+
"handler"
|
|
136
|
+
> & {
|
|
171
137
|
handler(
|
|
172
138
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
173
139
|
input: InferSchemaOutput<TInput>,
|
|
@@ -175,59 +141,44 @@ type OperationConfig<
|
|
|
175
141
|
| OperationHandlerResult<InferSchemaOutput<TOutput>>
|
|
176
142
|
| Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
|
|
177
143
|
};
|
|
178
|
-
type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> =
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
|
|
185
|
-
: never;
|
|
186
|
-
};
|
|
187
|
-
type StreamOperationConfig<
|
|
188
|
-
TInput extends SchemaLike,
|
|
189
|
-
TOutput extends SchemaLike,
|
|
190
|
-
> =
|
|
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> =
|
|
191
150
|
| SseOperationConfig<TInput, TOutput>
|
|
192
151
|
| HttpStreamOperationConfig<TInput, TOutput>
|
|
193
152
|
| WebSocketOperationConfig<TInput, TOutput>;
|
|
194
|
-
type SseOperationConfig<
|
|
195
|
-
TInput
|
|
196
|
-
|
|
197
|
-
>
|
|
153
|
+
type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
154
|
+
OperationConfig<TInput, TOutput>,
|
|
155
|
+
"handler" | "transport"
|
|
156
|
+
> & {
|
|
198
157
|
transport: OperationSseTransport;
|
|
199
158
|
handler(
|
|
200
159
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
201
160
|
input: InferSchemaOutput<TInput>,
|
|
202
|
-
):
|
|
203
|
-
| AsyncIterable<ProviderStreamEvent>
|
|
204
|
-
| Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
161
|
+
): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
205
162
|
};
|
|
206
|
-
type HttpStreamOperationConfig<
|
|
207
|
-
TInput
|
|
208
|
-
|
|
209
|
-
>
|
|
163
|
+
type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
164
|
+
OperationConfig<TInput, TOutput>,
|
|
165
|
+
"handler" | "transport"
|
|
166
|
+
> & {
|
|
210
167
|
transport: OperationHttpStreamTransport;
|
|
211
168
|
handler(
|
|
212
169
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
213
170
|
input: InferSchemaOutput<TInput>,
|
|
214
|
-
):
|
|
215
|
-
| Response
|
|
216
|
-
| ReadableStream<Uint8Array>
|
|
217
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
171
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
218
172
|
};
|
|
219
|
-
type WebSocketOperationConfig<
|
|
220
|
-
TInput
|
|
221
|
-
|
|
222
|
-
>
|
|
173
|
+
type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
174
|
+
OperationConfig<TInput, TOutput>,
|
|
175
|
+
"handler" | "transport"
|
|
176
|
+
> & {
|
|
223
177
|
transport: OperationWebSocketTransport;
|
|
224
178
|
handler(
|
|
225
179
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
226
180
|
input: InferSchemaOutput<TInput>,
|
|
227
|
-
):
|
|
228
|
-
| Response
|
|
229
|
-
| ReadableStream<Uint8Array>
|
|
230
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
181
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
231
182
|
};
|
|
232
183
|
|
|
233
184
|
type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
@@ -242,12 +193,17 @@ type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
|
242
193
|
: unknown
|
|
243
194
|
: unknown;
|
|
244
195
|
|
|
245
|
-
export interface ProviderConfig<
|
|
246
|
-
TOperations extends Record<string, ProviderOperation>,
|
|
247
|
-
> {
|
|
196
|
+
export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
|
|
248
197
|
id: string;
|
|
249
198
|
version: string;
|
|
250
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;
|
|
251
207
|
allowedHosts?: string[];
|
|
252
208
|
stealth?: {
|
|
253
209
|
profile: string;
|
|
@@ -285,39 +241,30 @@ export interface ProviderConfig<
|
|
|
285
241
|
};
|
|
286
242
|
operations: OperationMapConfig<TOperations>;
|
|
287
243
|
healthMonitor?: ProviderHealthMonitorConfig;
|
|
244
|
+
/** New name for `healthMonitor` (transitional alias); declaring both is a ValidationError. */
|
|
245
|
+
healthProbe?: ProviderHealthMonitorConfig;
|
|
288
246
|
healthJourneys?: readonly HealthJourneyDefinition[];
|
|
289
247
|
}
|
|
290
248
|
|
|
291
249
|
/** Define one provider operation with schema-driven handler inference. */
|
|
292
|
-
export function defineOperation<
|
|
293
|
-
TInput extends SchemaLike,
|
|
294
|
-
TOutput extends SchemaLike,
|
|
295
|
-
>(
|
|
250
|
+
export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
296
251
|
operation: OperationConfig<TInput, TOutput>,
|
|
297
252
|
): OperationDefinition<TInput, TOutput> {
|
|
298
253
|
return operation;
|
|
299
254
|
}
|
|
300
255
|
|
|
301
256
|
/** Define a non-JSON provider operation with explicit transport metadata. */
|
|
302
|
-
export function defineStreamOperation<
|
|
303
|
-
TInput extends SchemaLike,
|
|
304
|
-
TOutput extends SchemaLike,
|
|
305
|
-
>(
|
|
257
|
+
export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
306
258
|
operation: StreamOperationConfig<TInput, TOutput>,
|
|
307
259
|
): OperationDefinition<TInput, TOutput> {
|
|
308
260
|
return operation;
|
|
309
261
|
}
|
|
310
262
|
|
|
311
|
-
function assertObjectConfig(
|
|
312
|
-
value: unknown,
|
|
313
|
-
): asserts value is Record<string, unknown> {
|
|
263
|
+
function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
|
|
314
264
|
if (!value || typeof value !== "object") {
|
|
315
|
-
throw new ProviderError(
|
|
316
|
-
"defineProvider
|
|
317
|
-
|
|
318
|
-
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
319
|
-
},
|
|
320
|
-
);
|
|
265
|
+
throw new ProviderError("defineProvider config must be an object. Offending field: config", {
|
|
266
|
+
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
267
|
+
});
|
|
321
268
|
}
|
|
322
269
|
}
|
|
323
270
|
function assertRequiredField(
|
|
@@ -355,25 +302,18 @@ function validateProviderShape(config: unknown): void {
|
|
|
355
302
|
assertRequiredField(config, "meta", String(config.id));
|
|
356
303
|
assertRequiredField(config, "operations", String(config.id));
|
|
357
304
|
if (typeof config.runtime === "string")
|
|
358
|
-
assertLiteralField(
|
|
359
|
-
config.runtime,
|
|
360
|
-
"runtime",
|
|
361
|
-
VALID_RUNTIMES,
|
|
362
|
-
String(config.id),
|
|
363
|
-
);
|
|
305
|
+
assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
|
|
364
306
|
const auth = config.auth;
|
|
365
|
-
if (
|
|
366
|
-
auth
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
"auth.mode",
|
|
374
|
-
VALID_AUTH_MODES,
|
|
375
|
-
String(config.id),
|
|
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));
|
|
309
|
+
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
310
|
+
throw new ProviderError(
|
|
311
|
+
`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
|
|
312
|
+
{
|
|
313
|
+
fix: "Use the single canonical auth interface: auth.flow. Gateway calls auth.flow.start/continue/poll/abort/refresh only and persists complete turn data.credential as-is, so put login/token/session exchange inside auth.flow.continue.",
|
|
314
|
+
},
|
|
376
315
|
);
|
|
316
|
+
}
|
|
377
317
|
if (
|
|
378
318
|
auth &&
|
|
379
319
|
typeof auth === "object" &&
|
|
@@ -401,9 +341,7 @@ function validateProviderShape(config: unknown): void {
|
|
|
401
341
|
},
|
|
402
342
|
);
|
|
403
343
|
}
|
|
404
|
-
const accessRecord: Record<string, unknown> = Object.fromEntries(
|
|
405
|
-
Object.entries(access),
|
|
406
|
-
);
|
|
344
|
+
const accessRecord: Record<string, unknown> = Object.fromEntries(Object.entries(access));
|
|
407
345
|
for (const key of Object.keys(accessRecord)) {
|
|
408
346
|
if (key !== "visibility") {
|
|
409
347
|
throw new ValidationError(`Unknown field "${key}" on access.`, {
|
|
@@ -448,31 +386,26 @@ function validateProviderProxy(config: {
|
|
|
448
386
|
},
|
|
449
387
|
);
|
|
450
388
|
}
|
|
451
|
-
rejectUnknownFields(
|
|
452
|
-
|
|
453
|
-
new Set(["mode", "provider", "geo", "session"]),
|
|
454
|
-
"proxy",
|
|
455
|
-
);
|
|
456
|
-
assertLiteralField(
|
|
457
|
-
proxy.mode,
|
|
458
|
-
"proxy.mode",
|
|
459
|
-
VALID_PROVIDER_PROXY_MODES,
|
|
460
|
-
config.id,
|
|
461
|
-
);
|
|
389
|
+
rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
|
|
390
|
+
assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
|
|
462
391
|
if (proxy.provider !== undefined) {
|
|
463
|
-
assertLiteralField(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
+
}
|
|
469
406
|
}
|
|
470
407
|
if (proxy.geo !== undefined) {
|
|
471
|
-
if (
|
|
472
|
-
!proxy.geo ||
|
|
473
|
-
typeof proxy.geo !== "object" ||
|
|
474
|
-
Array.isArray(proxy.geo)
|
|
475
|
-
) {
|
|
408
|
+
if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
|
|
476
409
|
throw new ValidationError(
|
|
477
410
|
`Provider "${config.id}" has invalid proxy.geo: must be an object.`,
|
|
478
411
|
{
|
|
@@ -480,11 +413,7 @@ function validateProviderProxy(config: {
|
|
|
480
413
|
},
|
|
481
414
|
);
|
|
482
415
|
}
|
|
483
|
-
rejectUnknownFields(
|
|
484
|
-
proxy.geo,
|
|
485
|
-
new Set(["country", "subdivision", "city"]),
|
|
486
|
-
"proxy.geo",
|
|
487
|
-
);
|
|
416
|
+
rejectUnknownFields(proxy.geo, new Set(["country", "subdivision", "city"]), "proxy.geo");
|
|
488
417
|
if (proxy.geo.country !== undefined) {
|
|
489
418
|
assertIsoCountry(proxy.geo.country, "proxy.geo.country");
|
|
490
419
|
}
|
|
@@ -498,11 +427,7 @@ function validateProviderProxy(config: {
|
|
|
498
427
|
}
|
|
499
428
|
}
|
|
500
429
|
if (proxy.session !== undefined) {
|
|
501
|
-
if (
|
|
502
|
-
!proxy.session ||
|
|
503
|
-
typeof proxy.session !== "object" ||
|
|
504
|
-
Array.isArray(proxy.session)
|
|
505
|
-
) {
|
|
430
|
+
if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
|
|
506
431
|
throw new ValidationError(
|
|
507
432
|
`Provider "${config.id}" has invalid proxy.session: must be an object.`,
|
|
508
433
|
{
|
|
@@ -524,51 +449,75 @@ function validateProviderProxy(config: {
|
|
|
524
449
|
);
|
|
525
450
|
}
|
|
526
451
|
const lifetime = proxy.session.lifetimeMinutes;
|
|
527
|
-
if (
|
|
528
|
-
lifetime !== undefined &&
|
|
529
|
-
(!Number.isFinite(lifetime) || lifetime <= 0)
|
|
530
|
-
) {
|
|
452
|
+
if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
531
453
|
throw new ValidationError(
|
|
532
454
|
`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`,
|
|
533
455
|
);
|
|
534
456
|
}
|
|
535
457
|
const poolSize = proxy.session.poolSize;
|
|
536
|
-
if (
|
|
537
|
-
poolSize !== undefined &&
|
|
538
|
-
(!Number.isInteger(poolSize) || poolSize <= 0)
|
|
539
|
-
) {
|
|
458
|
+
if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
540
459
|
throw new ValidationError(
|
|
541
460
|
`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
|
|
542
461
|
);
|
|
543
462
|
}
|
|
544
463
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
+
}
|
|
557
500
|
}
|
|
558
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
|
+
}
|
|
559
512
|
}
|
|
560
513
|
|
|
561
|
-
function validateProviderStt(config: {
|
|
562
|
-
id: string;
|
|
563
|
-
stt?: ProviderSttConfig;
|
|
564
|
-
}): void {
|
|
514
|
+
function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): void {
|
|
565
515
|
const stt = config.stt;
|
|
566
516
|
if (stt === undefined) return;
|
|
567
517
|
if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
|
|
568
|
-
throw new ValidationError(
|
|
569
|
-
`
|
|
570
|
-
|
|
571
|
-
);
|
|
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
|
+
});
|
|
572
521
|
}
|
|
573
522
|
rejectUnknownFields(stt, new Set(["mode"]), "stt");
|
|
574
523
|
assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
|
|
@@ -595,16 +544,9 @@ function validateOperationIds(
|
|
|
595
544
|
);
|
|
596
545
|
}
|
|
597
546
|
}
|
|
598
|
-
const OPERATION_CONTRACT_VERSION_REGEX =
|
|
599
|
-
|
|
600
|
-
const
|
|
601
|
-
/^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
602
|
-
const VALID_OPERATION_LIFECYCLES = [
|
|
603
|
-
"stable",
|
|
604
|
-
"beta",
|
|
605
|
-
"deprecated",
|
|
606
|
-
"removed",
|
|
607
|
-
] 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;
|
|
608
550
|
|
|
609
551
|
function assertNonEmptyString(
|
|
610
552
|
value: unknown,
|
|
@@ -635,10 +577,7 @@ function validateToolRouterMetadata(
|
|
|
635
577
|
},
|
|
636
578
|
);
|
|
637
579
|
}
|
|
638
|
-
if (
|
|
639
|
-
toolRouter.name !== undefined &&
|
|
640
|
-
!MCP_TOOL_NAME_REGEX.test(toolRouter.name)
|
|
641
|
-
) {
|
|
580
|
+
if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
642
581
|
throw new ValidationError(
|
|
643
582
|
`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`,
|
|
644
583
|
{
|
|
@@ -710,10 +649,7 @@ function validateOperationContracts(
|
|
|
710
649
|
providerId,
|
|
711
650
|
);
|
|
712
651
|
}
|
|
713
|
-
if (
|
|
714
|
-
contract.lifecycle === "deprecated" ||
|
|
715
|
-
contract.lifecycle === "removed"
|
|
716
|
-
) {
|
|
652
|
+
if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
|
|
717
653
|
if (!contract.deprecation || typeof contract.deprecation !== "object") {
|
|
718
654
|
throw new ValidationError(
|
|
719
655
|
`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`,
|
|
@@ -761,10 +697,7 @@ function validateOperationAnnotations(
|
|
|
761
697
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
|
|
762
698
|
},
|
|
763
699
|
);
|
|
764
|
-
if (
|
|
765
|
-
timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
|
|
766
|
-
timeoutMs > OPERATION_TIMEOUT_MS_MAX
|
|
767
|
-
)
|
|
700
|
+
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
768
701
|
throw new ValidationError(
|
|
769
702
|
`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`,
|
|
770
703
|
{
|
|
@@ -962,12 +895,7 @@ function validateOperationTransports(
|
|
|
962
895
|
},
|
|
963
896
|
);
|
|
964
897
|
}
|
|
965
|
-
assertLiteralField(
|
|
966
|
-
kind,
|
|
967
|
-
`${fieldPath}.kind`,
|
|
968
|
-
VALID_OPERATION_TRANSPORT_KINDS,
|
|
969
|
-
providerId,
|
|
970
|
-
);
|
|
898
|
+
assertLiteralField(kind, `${fieldPath}.kind`, VALID_OPERATION_TRANSPORT_KINDS, providerId);
|
|
971
899
|
|
|
972
900
|
switch (kind) {
|
|
973
901
|
case "json":
|
|
@@ -999,16 +927,9 @@ function validateOperationTransports(
|
|
|
999
927
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1000
928
|
"max duration",
|
|
1001
929
|
);
|
|
1002
|
-
assertPositiveBytes(
|
|
1003
|
-
Reflect.get(transport, "maxEventBytes"),
|
|
1004
|
-
`${fieldPath}.maxEventBytes`,
|
|
1005
|
-
);
|
|
930
|
+
assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
|
|
1006
931
|
const resumable = Reflect.get(transport, "resumable");
|
|
1007
|
-
if (
|
|
1008
|
-
resumable !== undefined &&
|
|
1009
|
-
resumable !== false &&
|
|
1010
|
-
resumable !== "last-event-id"
|
|
1011
|
-
) {
|
|
932
|
+
if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
|
|
1012
933
|
throw new ValidationError(
|
|
1013
934
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`,
|
|
1014
935
|
{
|
|
@@ -1028,12 +949,7 @@ function validateOperationTransports(
|
|
|
1028
949
|
rejectUnknownFields(transport, HTTP_STREAM_TRANSPORT_FIELDS, fieldPath);
|
|
1029
950
|
const contentType = Reflect.get(transport, "contentType");
|
|
1030
951
|
if (contentType !== undefined) {
|
|
1031
|
-
assertNonEmptyString(
|
|
1032
|
-
contentType,
|
|
1033
|
-
`${fieldPath}.contentType`,
|
|
1034
|
-
providerId,
|
|
1035
|
-
operationName,
|
|
1036
|
-
);
|
|
952
|
+
assertNonEmptyString(contentType, `${fieldPath}.contentType`, providerId, operationName);
|
|
1037
953
|
}
|
|
1038
954
|
assertStreamMs(
|
|
1039
955
|
Reflect.get(transport, "idleTimeoutMs"),
|
|
@@ -1049,10 +965,7 @@ function validateOperationTransports(
|
|
|
1049
965
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1050
966
|
"max duration",
|
|
1051
967
|
);
|
|
1052
|
-
assertPositiveBytes(
|
|
1053
|
-
Reflect.get(transport, "maxChunkBytes"),
|
|
1054
|
-
`${fieldPath}.maxChunkBytes`,
|
|
1055
|
-
);
|
|
968
|
+
assertPositiveBytes(Reflect.get(transport, "maxChunkBytes"), `${fieldPath}.maxChunkBytes`);
|
|
1056
969
|
break;
|
|
1057
970
|
}
|
|
1058
971
|
case "websocket": {
|
|
@@ -1077,10 +990,7 @@ function validateOperationTransports(
|
|
|
1077
990
|
);
|
|
1078
991
|
}
|
|
1079
992
|
for (const subprotocol of subprotocols) {
|
|
1080
|
-
if (
|
|
1081
|
-
typeof subprotocol !== "string" ||
|
|
1082
|
-
!WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)
|
|
1083
|
-
) {
|
|
993
|
+
if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
1084
994
|
throw new ValidationError(
|
|
1085
995
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`,
|
|
1086
996
|
{
|
|
@@ -1104,10 +1014,7 @@ function validateOperationTransports(
|
|
|
1104
1014
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1105
1015
|
"max duration",
|
|
1106
1016
|
);
|
|
1107
|
-
assertPositiveBytes(
|
|
1108
|
-
Reflect.get(transport, "maxFrameBytes"),
|
|
1109
|
-
`${fieldPath}.maxFrameBytes`,
|
|
1110
|
-
);
|
|
1017
|
+
assertPositiveBytes(Reflect.get(transport, "maxFrameBytes"), `${fieldPath}.maxFrameBytes`);
|
|
1111
1018
|
break;
|
|
1112
1019
|
}
|
|
1113
1020
|
}
|
|
@@ -1116,6 +1023,7 @@ function validateOperationTransports(
|
|
|
1116
1023
|
|
|
1117
1024
|
const HEALTH_CHECK_SUITE_FIELDS = new Set([
|
|
1118
1025
|
"interval",
|
|
1026
|
+
"schedule",
|
|
1119
1027
|
"timeoutMs",
|
|
1120
1028
|
"degradedThresholdMs",
|
|
1121
1029
|
"cases",
|
|
@@ -1169,10 +1077,7 @@ function levenshtein(a: string, b: string): number {
|
|
|
1169
1077
|
return prev[n] ?? 0;
|
|
1170
1078
|
}
|
|
1171
1079
|
|
|
1172
|
-
function suggestField(
|
|
1173
|
-
unknown: string,
|
|
1174
|
-
candidates: ReadonlySet<string>,
|
|
1175
|
-
): string | undefined {
|
|
1080
|
+
function suggestField(unknown: string, candidates: ReadonlySet<string>): string | undefined {
|
|
1176
1081
|
let best: string | undefined;
|
|
1177
1082
|
let bestDist = 3;
|
|
1178
1083
|
for (const candidate of candidates) {
|
|
@@ -1185,11 +1090,7 @@ function suggestField(
|
|
|
1185
1090
|
return best;
|
|
1186
1091
|
}
|
|
1187
1092
|
|
|
1188
|
-
function rejectUnknownFields(
|
|
1189
|
-
value: object,
|
|
1190
|
-
allowed: ReadonlySet<string>,
|
|
1191
|
-
fieldPath: string,
|
|
1192
|
-
): void {
|
|
1093
|
+
function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldPath: string): void {
|
|
1193
1094
|
for (const key of Object.keys(value)) {
|
|
1194
1095
|
if (allowed.has(key)) continue;
|
|
1195
1096
|
const hint = suggestField(key, allowed);
|
|
@@ -1225,29 +1126,19 @@ function assertBoundedIntegerMs(
|
|
|
1225
1126
|
function validateProviderHealthMonitor(
|
|
1226
1127
|
providerId: string,
|
|
1227
1128
|
healthMonitor: unknown,
|
|
1129
|
+
field: "healthMonitor" | "healthProbe" = "healthMonitor",
|
|
1228
1130
|
): void {
|
|
1229
1131
|
if (healthMonitor === undefined) return;
|
|
1230
|
-
if (
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
)
|
|
1235
|
-
throw new ValidationError(
|
|
1236
|
-
`Provider "${providerId}" has invalid healthMonitor: must be an object.`,
|
|
1237
|
-
{
|
|
1238
|
-
fix: `Set healthMonitor to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
1239
|
-
},
|
|
1240
|
-
);
|
|
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
|
+
});
|
|
1241
1136
|
const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
|
|
1242
|
-
rejectUnknownFields(
|
|
1243
|
-
healthMonitorRecord,
|
|
1244
|
-
PROVIDER_HEALTH_MONITOR_FIELDS,
|
|
1245
|
-
"healthMonitor",
|
|
1246
|
-
);
|
|
1137
|
+
rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
|
|
1247
1138
|
if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
|
|
1248
1139
|
assertBoundedIntegerMs(
|
|
1249
1140
|
healthMonitorRecord.defaultProbeTimeoutMs,
|
|
1250
|
-
`Provider "${providerId}"
|
|
1141
|
+
`Provider "${providerId}" ${field}.defaultProbeTimeoutMs`,
|
|
1251
1142
|
{
|
|
1252
1143
|
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1253
1144
|
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
@@ -1258,7 +1149,7 @@ function validateProviderHealthMonitor(
|
|
|
1258
1149
|
if (healthMonitorRecord.defaultDegradedThresholdMs !== undefined) {
|
|
1259
1150
|
assertBoundedIntegerMs(
|
|
1260
1151
|
healthMonitorRecord.defaultDegradedThresholdMs,
|
|
1261
|
-
`Provider "${providerId}"
|
|
1152
|
+
`Provider "${providerId}" ${field}.defaultDegradedThresholdMs`,
|
|
1262
1153
|
{
|
|
1263
1154
|
min: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
|
|
1264
1155
|
max: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
@@ -1270,12 +1161,12 @@ function validateProviderHealthMonitor(
|
|
|
1270
1161
|
if (requiredSecrets !== undefined) {
|
|
1271
1162
|
if (!Array.isArray(requiredSecrets))
|
|
1272
1163
|
throw new ValidationError(
|
|
1273
|
-
`Provider "${providerId}" has invalid
|
|
1164
|
+
`Provider "${providerId}" has invalid ${field}.requiredSecrets: must be string[].`,
|
|
1274
1165
|
);
|
|
1275
1166
|
for (const [index, secret] of requiredSecrets.entries()) {
|
|
1276
1167
|
if (typeof secret !== "string" || secret.length === 0)
|
|
1277
1168
|
throw new ValidationError(
|
|
1278
|
-
`Provider "${providerId}" has invalid
|
|
1169
|
+
`Provider "${providerId}" has invalid ${field}.requiredSecrets[${index}]: must be a non-empty string.`,
|
|
1279
1170
|
);
|
|
1280
1171
|
}
|
|
1281
1172
|
}
|
|
@@ -1287,23 +1178,23 @@ function validateProviderHealthMonitor(
|
|
|
1287
1178
|
Array.isArray(credentialInputs)
|
|
1288
1179
|
) {
|
|
1289
1180
|
throw new ValidationError(
|
|
1290
|
-
`Provider "${providerId}" has invalid
|
|
1181
|
+
`Provider "${providerId}" has invalid ${field}.credentialInputs: must be an object mapping auth input fields to env var names.`,
|
|
1291
1182
|
);
|
|
1292
1183
|
}
|
|
1293
|
-
for (const [
|
|
1294
|
-
if (
|
|
1184
|
+
for (const [inputField, envVar] of Object.entries(credentialInputs)) {
|
|
1185
|
+
if (inputField.trim().length === 0) {
|
|
1295
1186
|
throw new ValidationError(
|
|
1296
|
-
`Provider "${providerId}" has invalid
|
|
1187
|
+
`Provider "${providerId}" has invalid ${field}.credentialInputs key: must be a non-empty auth input field.`,
|
|
1297
1188
|
);
|
|
1298
1189
|
}
|
|
1299
1190
|
if (typeof envVar !== "string" || envVar.trim().length === 0) {
|
|
1300
1191
|
throw new ValidationError(
|
|
1301
|
-
`Provider "${providerId}" has invalid
|
|
1192
|
+
`Provider "${providerId}" has invalid ${field}.credentialInputs.${inputField}: must be a non-empty env var name.`,
|
|
1302
1193
|
);
|
|
1303
1194
|
}
|
|
1304
1195
|
if (Array.isArray(requiredSecrets) && !requiredSecrets.includes(envVar)) {
|
|
1305
1196
|
throw new ValidationError(
|
|
1306
|
-
`Provider "${providerId}"
|
|
1197
|
+
`Provider "${providerId}" ${field}.credentialInputs.${inputField} references ${envVar}, which must also be listed in ${field}.requiredSecrets.`,
|
|
1307
1198
|
);
|
|
1308
1199
|
}
|
|
1309
1200
|
}
|
|
@@ -1311,38 +1202,34 @@ function validateProviderHealthMonitor(
|
|
|
1311
1202
|
|
|
1312
1203
|
const probeOverrides = healthMonitorRecord.probeOverrides;
|
|
1313
1204
|
if (probeOverrides !== undefined) {
|
|
1314
|
-
if (
|
|
1315
|
-
!probeOverrides ||
|
|
1316
|
-
typeof probeOverrides !== "object" ||
|
|
1317
|
-
Array.isArray(probeOverrides)
|
|
1318
|
-
)
|
|
1205
|
+
if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
|
|
1319
1206
|
throw new ValidationError(
|
|
1320
|
-
`Provider "${providerId}" has invalid
|
|
1207
|
+
`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`,
|
|
1321
1208
|
);
|
|
1322
1209
|
for (const [probeId, override] of Object.entries(probeOverrides)) {
|
|
1323
1210
|
if (probeId.length === 0)
|
|
1324
1211
|
throw new ValidationError(
|
|
1325
|
-
`Provider "${providerId}" has invalid
|
|
1212
|
+
`Provider "${providerId}" has invalid ${field}.probeOverrides key: must be a non-empty probe id.`,
|
|
1326
1213
|
);
|
|
1327
1214
|
if (!override || typeof override !== "object" || Array.isArray(override))
|
|
1328
1215
|
throw new ValidationError(
|
|
1329
|
-
`Provider "${providerId}" has invalid
|
|
1216
|
+
`Provider "${providerId}" has invalid ${field}.probeOverrides["${probeId}"]: must be an object.`,
|
|
1330
1217
|
);
|
|
1331
1218
|
const overrideRecord = Object.fromEntries(Object.entries(override));
|
|
1332
1219
|
rejectUnknownFields(
|
|
1333
1220
|
overrideRecord,
|
|
1334
1221
|
PROVIDER_HEALTH_MONITOR_PROBE_OVERRIDE_FIELDS,
|
|
1335
|
-
|
|
1222
|
+
`${field}.probeOverrides["${probeId}"]`,
|
|
1336
1223
|
);
|
|
1337
1224
|
const interval = overrideRecord.interval;
|
|
1338
1225
|
if (interval !== undefined && !isPositiveMsDurationString(interval))
|
|
1339
1226
|
throw new ValidationError(
|
|
1340
|
-
`Provider "${providerId}" has invalid
|
|
1227
|
+
`Provider "${providerId}" has invalid ${field}.probeOverrides["${probeId}"].interval: must be a positive ms-style duration string such as 30s, 5m, 8h, or 1 day.`,
|
|
1341
1228
|
);
|
|
1342
1229
|
if (overrideRecord.timeoutMs !== undefined) {
|
|
1343
1230
|
assertBoundedIntegerMs(
|
|
1344
1231
|
overrideRecord.timeoutMs,
|
|
1345
|
-
`Provider "${providerId}"
|
|
1232
|
+
`Provider "${providerId}" ${field}.probeOverrides["${probeId}"].timeoutMs`,
|
|
1346
1233
|
{
|
|
1347
1234
|
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1348
1235
|
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
@@ -1353,7 +1240,7 @@ function validateProviderHealthMonitor(
|
|
|
1353
1240
|
if (overrideRecord.degradedThresholdMs !== undefined) {
|
|
1354
1241
|
assertBoundedIntegerMs(
|
|
1355
1242
|
overrideRecord.degradedThresholdMs,
|
|
1356
|
-
`Provider "${providerId}"
|
|
1243
|
+
`Provider "${providerId}" ${field}.probeOverrides["${probeId}"].degradedThresholdMs`,
|
|
1357
1244
|
{
|
|
1358
1245
|
min: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
|
|
1359
1246
|
max: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
@@ -1369,7 +1256,7 @@ function validateProviderHealthMonitor(
|
|
|
1369
1256
|
(typeof serviceAccount !== "string" || serviceAccount.length === 0)
|
|
1370
1257
|
)
|
|
1371
1258
|
throw new ValidationError(
|
|
1372
|
-
`Provider "${providerId}" has invalid
|
|
1259
|
+
`Provider "${providerId}" has invalid ${field}.serviceAccount: must be a non-empty string.`,
|
|
1373
1260
|
);
|
|
1374
1261
|
}
|
|
1375
1262
|
|
|
@@ -1381,14 +1268,8 @@ function validateHealthCheckCase(
|
|
|
1381
1268
|
): void {
|
|
1382
1269
|
const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
|
|
1383
1270
|
if (!caseValue || typeof caseValue !== "object" || Array.isArray(caseValue))
|
|
1384
|
-
throw new ValidationError(
|
|
1385
|
-
|
|
1386
|
-
);
|
|
1387
|
-
rejectUnknownFields(
|
|
1388
|
-
caseValue as Record<string, unknown>,
|
|
1389
|
-
HEALTH_CHECK_CASE_FIELDS,
|
|
1390
|
-
fieldPath,
|
|
1391
|
-
);
|
|
1271
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1272
|
+
rejectUnknownFields(caseValue as Record<string, unknown>, HEALTH_CHECK_CASE_FIELDS, fieldPath);
|
|
1392
1273
|
const c = caseValue as HealthCheckCase;
|
|
1393
1274
|
if (typeof c.name !== "string" || c.name.length === 0)
|
|
1394
1275
|
throw new ValidationError(
|
|
@@ -1416,15 +1297,11 @@ function validateHealthCheckCase(
|
|
|
1416
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.`,
|
|
1417
1298
|
);
|
|
1418
1299
|
if (c.timeoutMs !== undefined) {
|
|
1419
|
-
assertBoundedIntegerMs(
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1425
|
-
label: "timeout",
|
|
1426
|
-
},
|
|
1427
|
-
);
|
|
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
|
+
});
|
|
1428
1305
|
}
|
|
1429
1306
|
if (
|
|
1430
1307
|
c.expectedStatus !== undefined &&
|
|
@@ -1440,21 +1317,11 @@ function validateHealthCheckCase(
|
|
|
1440
1317
|
);
|
|
1441
1318
|
}
|
|
1442
1319
|
|
|
1443
|
-
function validateHealthCheckSuite(
|
|
1444
|
-
providerId: string,
|
|
1445
|
-
operationName: string,
|
|
1446
|
-
suite: unknown,
|
|
1447
|
-
): void {
|
|
1320
|
+
function validateHealthCheckSuite(providerId: string, operationName: string, suite: unknown): void {
|
|
1448
1321
|
const fieldPath = `operations.${operationName}.healthCheck`;
|
|
1449
1322
|
if (!suite || typeof suite !== "object" || Array.isArray(suite))
|
|
1450
|
-
throw new ValidationError(
|
|
1451
|
-
|
|
1452
|
-
);
|
|
1453
|
-
rejectUnknownFields(
|
|
1454
|
-
suite as Record<string, unknown>,
|
|
1455
|
-
HEALTH_CHECK_SUITE_FIELDS,
|
|
1456
|
-
fieldPath,
|
|
1457
|
-
);
|
|
1323
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1324
|
+
rejectUnknownFields(suite as Record<string, unknown>, HEALTH_CHECK_SUITE_FIELDS, fieldPath);
|
|
1458
1325
|
const s = suite as HealthCheckSuite;
|
|
1459
1326
|
if (!isPositiveMsDurationString(s.interval))
|
|
1460
1327
|
throw new ValidationError(
|
|
@@ -1463,16 +1330,33 @@ function validateHealthCheckSuite(
|
|
|
1463
1330
|
fix: `Set ${fieldPath}.interval to a positive ms-style duration string.`,
|
|
1464
1331
|
},
|
|
1465
1332
|
);
|
|
1333
|
+
if (s.schedule !== undefined) {
|
|
1334
|
+
if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
|
|
1335
|
+
throw new ValidationError(
|
|
1336
|
+
`Provider "${providerId}" ${fieldPath}.schedule must be an object.`,
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
if (Reflect.get(s.schedule, "jitter") !== undefined) {
|
|
1340
|
+
throw new ValidationError(
|
|
1341
|
+
`Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`,
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
|
|
1345
|
+
const randomize = Reflect.get(s.schedule, "randomize");
|
|
1346
|
+
if (randomize !== undefined) {
|
|
1347
|
+
validateScheduleRandomization(
|
|
1348
|
+
randomize,
|
|
1349
|
+
`Provider "${providerId}" ${fieldPath}.schedule.randomize`,
|
|
1350
|
+
msDurationMs(s.interval),
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1466
1354
|
if (s.timeoutMs !== undefined) {
|
|
1467
|
-
assertBoundedIntegerMs(
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1473
|
-
label: "timeout",
|
|
1474
|
-
},
|
|
1475
|
-
);
|
|
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
|
+
});
|
|
1476
1360
|
}
|
|
1477
1361
|
if (s.degradedThresholdMs !== undefined) {
|
|
1478
1362
|
assertBoundedIntegerMs(
|
|
@@ -1485,10 +1369,7 @@ function validateHealthCheckSuite(
|
|
|
1485
1369
|
},
|
|
1486
1370
|
);
|
|
1487
1371
|
}
|
|
1488
|
-
if (
|
|
1489
|
-
s.requiresConnection !== undefined &&
|
|
1490
|
-
typeof s.requiresConnection !== "boolean"
|
|
1491
|
-
)
|
|
1372
|
+
if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
|
|
1492
1373
|
throw new ValidationError(
|
|
1493
1374
|
`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`,
|
|
1494
1375
|
);
|
|
@@ -1520,14 +1401,8 @@ function validateHealthCheckUnsupported(
|
|
|
1520
1401
|
unsupported: unknown,
|
|
1521
1402
|
): void {
|
|
1522
1403
|
const fieldPath = `operations.${operationName}.healthCheckUnsupported`;
|
|
1523
|
-
if (
|
|
1524
|
-
|
|
1525
|
-
typeof unsupported !== "object" ||
|
|
1526
|
-
Array.isArray(unsupported)
|
|
1527
|
-
)
|
|
1528
|
-
throw new ValidationError(
|
|
1529
|
-
`Provider "${providerId}" ${fieldPath} must be an object.`,
|
|
1530
|
-
);
|
|
1404
|
+
if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
|
|
1405
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1531
1406
|
rejectUnknownFields(
|
|
1532
1407
|
unsupported as Record<string, unknown>,
|
|
1533
1408
|
HEALTH_CHECK_UNSUPPORTED_FIELDS,
|
|
@@ -1561,7 +1436,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
|
|
|
1561
1436
|
"steps",
|
|
1562
1437
|
"run",
|
|
1563
1438
|
]);
|
|
1564
|
-
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter"]);
|
|
1439
|
+
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
|
|
1565
1440
|
const HEALTH_JOURNEY_STEP_FIELDS = new Set([
|
|
1566
1441
|
"id",
|
|
1567
1442
|
"description",
|
|
@@ -1580,10 +1455,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
|
|
|
1580
1455
|
"minManualInterval",
|
|
1581
1456
|
"publicRationale",
|
|
1582
1457
|
]);
|
|
1583
|
-
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
|
|
1584
|
-
"enabled",
|
|
1585
|
-
"reason",
|
|
1586
|
-
]);
|
|
1458
|
+
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
|
|
1587
1459
|
const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
|
|
1588
1460
|
"enabled",
|
|
1589
1461
|
"requiresAcknowledgement",
|
|
@@ -1603,32 +1475,18 @@ function validateHealthJourneyManualTrigger(
|
|
|
1603
1475
|
manualTrigger: unknown,
|
|
1604
1476
|
): void {
|
|
1605
1477
|
const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
|
|
1606
|
-
if (
|
|
1607
|
-
!manualTrigger ||
|
|
1608
|
-
typeof manualTrigger !== "object" ||
|
|
1609
|
-
Array.isArray(manualTrigger)
|
|
1610
|
-
) {
|
|
1478
|
+
if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
|
|
1611
1479
|
throw new ValidationError(
|
|
1612
1480
|
`Provider "${providerId}" ${fieldPath} must be an object when present.`,
|
|
1613
1481
|
);
|
|
1614
1482
|
}
|
|
1615
|
-
rejectUnknownFields(
|
|
1616
|
-
manualTrigger,
|
|
1617
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS,
|
|
1618
|
-
fieldPath,
|
|
1619
|
-
);
|
|
1483
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
|
|
1620
1484
|
const enabled = Reflect.get(manualTrigger, "enabled");
|
|
1621
1485
|
if (typeof enabled !== "boolean") {
|
|
1622
|
-
throw new ValidationError(
|
|
1623
|
-
`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`,
|
|
1624
|
-
);
|
|
1486
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`);
|
|
1625
1487
|
}
|
|
1626
1488
|
if (enabled === false) {
|
|
1627
|
-
rejectUnknownFields(
|
|
1628
|
-
manualTrigger,
|
|
1629
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS,
|
|
1630
|
-
fieldPath,
|
|
1631
|
-
);
|
|
1489
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS, fieldPath);
|
|
1632
1490
|
if (
|
|
1633
1491
|
Reflect.get(manualTrigger, "reason") !== undefined &&
|
|
1634
1492
|
(typeof Reflect.get(manualTrigger, "reason") !== "string" ||
|
|
@@ -1640,25 +1498,15 @@ function validateHealthJourneyManualTrigger(
|
|
|
1640
1498
|
}
|
|
1641
1499
|
return;
|
|
1642
1500
|
}
|
|
1643
|
-
rejectUnknownFields(
|
|
1644
|
-
|
|
1645
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS,
|
|
1646
|
-
fieldPath,
|
|
1647
|
-
);
|
|
1648
|
-
const requiresAcknowledgement = Reflect.get(
|
|
1649
|
-
manualTrigger,
|
|
1650
|
-
"requiresAcknowledgement",
|
|
1651
|
-
);
|
|
1501
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS, fieldPath);
|
|
1502
|
+
const requiresAcknowledgement = Reflect.get(manualTrigger, "requiresAcknowledgement");
|
|
1652
1503
|
if (typeof requiresAcknowledgement !== "boolean") {
|
|
1653
1504
|
throw new ValidationError(
|
|
1654
1505
|
`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`,
|
|
1655
1506
|
);
|
|
1656
1507
|
}
|
|
1657
1508
|
const risk = Reflect.get(manualTrigger, "risk");
|
|
1658
|
-
if (
|
|
1659
|
-
typeof risk !== "string" ||
|
|
1660
|
-
!HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)
|
|
1661
|
-
) {
|
|
1509
|
+
if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
1662
1510
|
throw new ValidationError(
|
|
1663
1511
|
`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`,
|
|
1664
1512
|
);
|
|
@@ -1669,10 +1517,7 @@ function validateHealthJourneyManualTrigger(
|
|
|
1669
1517
|
);
|
|
1670
1518
|
}
|
|
1671
1519
|
const minManualInterval = Reflect.get(manualTrigger, "minManualInterval");
|
|
1672
|
-
assertIsoDuration(
|
|
1673
|
-
minManualInterval,
|
|
1674
|
-
`Provider "${providerId}" ${fieldPath}.minManualInterval`,
|
|
1675
|
-
);
|
|
1520
|
+
assertIsoDuration(minManualInterval, `Provider "${providerId}" ${fieldPath}.minManualInterval`);
|
|
1676
1521
|
if (isoDurationMs(minManualInterval) <= 0) {
|
|
1677
1522
|
throw new ValidationError(
|
|
1678
1523
|
`Provider "${providerId}" ${fieldPath}.minManualInterval must be a positive duration.`,
|
|
@@ -1703,18 +1548,14 @@ const SMS_ORIGIN_FIELDS_BY_KIND: Record<string, ReadonlySet<string>> = {
|
|
|
1703
1548
|
e164: new Set(["kind", "value", "display"]),
|
|
1704
1549
|
nationalServiceCode: new Set(["kind", "country", "value", "display"]),
|
|
1705
1550
|
};
|
|
1706
|
-
const DURATION_RE =
|
|
1707
|
-
/^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)?)?$/;
|
|
1708
1552
|
const E164_RE = /^\+[1-9]\d{1,14}$/;
|
|
1709
1553
|
const ISO_COUNTRY_RE = /^[A-Z]{2}$/;
|
|
1710
1554
|
const NATIONAL_SERVICE_CODE_RE = /^[0-9]{2,15}$/;
|
|
1711
1555
|
const BCP47_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
1712
1556
|
const JOURNEY_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
1713
1557
|
|
|
1714
|
-
function assertIsoDuration(
|
|
1715
|
-
value: unknown,
|
|
1716
|
-
fieldPath: string,
|
|
1717
|
-
): asserts value is string {
|
|
1558
|
+
function assertIsoDuration(value: unknown, fieldPath: string): asserts value is string {
|
|
1718
1559
|
if (typeof value !== "string" || !DURATION_RE.test(value)) {
|
|
1719
1560
|
throw new ValidationError(
|
|
1720
1561
|
`${fieldPath} must be an ISO 8601 duration for example PT8H or PT2M30S.`,
|
|
@@ -1729,15 +1570,49 @@ function isoDurationMs(value: string): number {
|
|
|
1729
1570
|
const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
|
|
1730
1571
|
const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
|
|
1731
1572
|
const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
|
|
1732
|
-
return
|
|
1733
|
-
days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000
|
|
1734
|
-
);
|
|
1573
|
+
return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
|
|
1735
1574
|
}
|
|
1736
1575
|
|
|
1737
|
-
function
|
|
1738
|
-
|
|
1576
|
+
function scheduleRandomizationMs(randomize: unknown, fieldPath: string): number {
|
|
1577
|
+
const mode = Reflect.get(randomize as object, "mode");
|
|
1578
|
+
switch (mode) {
|
|
1579
|
+
case "centered": {
|
|
1580
|
+
const maxOffset = Reflect.get(randomize as object, "maxOffset");
|
|
1581
|
+
assertIsoDuration(maxOffset, `${fieldPath}.maxOffset`);
|
|
1582
|
+
return isoDurationMs(maxOffset);
|
|
1583
|
+
}
|
|
1584
|
+
case "delayed": {
|
|
1585
|
+
const maxDelay = Reflect.get(randomize as object, "maxDelay");
|
|
1586
|
+
assertIsoDuration(maxDelay, `${fieldPath}.maxDelay`);
|
|
1587
|
+
return isoDurationMs(maxDelay);
|
|
1588
|
+
}
|
|
1589
|
+
default:
|
|
1590
|
+
throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function validateScheduleRandomization(
|
|
1595
|
+
randomize: unknown,
|
|
1739
1596
|
fieldPath: string,
|
|
1740
|
-
|
|
1597
|
+
intervalMs: number,
|
|
1598
|
+
): void {
|
|
1599
|
+
if (!randomize || typeof randomize !== "object" || Array.isArray(randomize)) {
|
|
1600
|
+
throw new ValidationError(`${fieldPath} must be an object.`);
|
|
1601
|
+
}
|
|
1602
|
+
const mode = Reflect.get(randomize, "mode");
|
|
1603
|
+
const allowedFields =
|
|
1604
|
+
mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
|
|
1605
|
+
rejectUnknownFields(randomize, allowedFields, fieldPath);
|
|
1606
|
+
const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
|
|
1607
|
+
if (offsetMs <= 0) {
|
|
1608
|
+
throw new ValidationError(`${fieldPath} duration must be positive.`);
|
|
1609
|
+
}
|
|
1610
|
+
if (offsetMs >= intervalMs) {
|
|
1611
|
+
throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function assertIsoCountry(value: unknown, fieldPath: string): asserts value is string {
|
|
1741
1616
|
if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
|
|
1742
1617
|
throw new ValidationError(
|
|
1743
1618
|
`${fieldPath} must be an ISO 3166-1 alpha-2 country code for example KR.`,
|
|
@@ -1749,13 +1624,19 @@ function normalizeIntervalDuration(input: string): string {
|
|
|
1749
1624
|
const trimmed = input.trim();
|
|
1750
1625
|
const shorthand = /^(\d+)(s|m|h|d)$/i.exec(trimmed);
|
|
1751
1626
|
if (shorthand) {
|
|
1752
|
-
const
|
|
1627
|
+
const durationMs = msDurationMs(trimmed);
|
|
1628
|
+
const unit = shorthand[2]?.toLowerCase();
|
|
1629
|
+
const amount =
|
|
1630
|
+
unit === "s"
|
|
1631
|
+
? durationMs / 1_000
|
|
1632
|
+
: unit === "m"
|
|
1633
|
+
? durationMs / 60_000
|
|
1634
|
+
: unit === "h"
|
|
1635
|
+
? durationMs / 3_600_000
|
|
1636
|
+
: durationMs / 86_400_000;
|
|
1753
1637
|
if (!Number.isInteger(amount) || amount <= 0) {
|
|
1754
|
-
throw new ValidationError(
|
|
1755
|
-
`Journey schedule interval must be a positive duration.`,
|
|
1756
|
-
);
|
|
1638
|
+
throw new ValidationError(`Journey schedule interval must be a positive duration.`);
|
|
1757
1639
|
}
|
|
1758
|
-
const unit = shorthand[2]?.toLowerCase();
|
|
1759
1640
|
if (unit === "s") return `PT${amount}S`;
|
|
1760
1641
|
if (unit === "m") return `PT${amount}M`;
|
|
1761
1642
|
if (unit === "h") return `PT${amount}H`;
|
|
@@ -1767,18 +1648,34 @@ function normalizeIntervalDuration(input: string): string {
|
|
|
1767
1648
|
|
|
1768
1649
|
export function every(
|
|
1769
1650
|
interval: string,
|
|
1770
|
-
options: { jitter?: string } = {},
|
|
1651
|
+
options: { jitter?: string; randomize?: HealthScheduleRandomization } = {},
|
|
1771
1652
|
): HealthJourneySchedule {
|
|
1653
|
+
if (options.jitter !== undefined && options.randomize !== undefined) {
|
|
1654
|
+
throw new ValidationError(
|
|
1655
|
+
`Schedule cannot define both jitter and randomize. Use randomize instead.`,
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1772
1658
|
const schedule: HealthJourneySchedule = {
|
|
1773
1659
|
kind: "interval",
|
|
1774
1660
|
interval: normalizeIntervalDuration(interval),
|
|
1775
1661
|
};
|
|
1662
|
+
if (options.randomize !== undefined) {
|
|
1663
|
+
schedule.randomize = options.randomize;
|
|
1664
|
+
}
|
|
1776
1665
|
if (options.jitter !== undefined) {
|
|
1777
1666
|
schedule.jitter = normalizeIntervalDuration(options.jitter);
|
|
1778
1667
|
}
|
|
1779
1668
|
return schedule;
|
|
1780
1669
|
}
|
|
1781
1670
|
|
|
1671
|
+
export function centered(maxOffset: string): HealthScheduleRandomization {
|
|
1672
|
+
return { mode: "centered", maxOffset: normalizeIntervalDuration(maxOffset) };
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
export function delayed(maxDelay: string): HealthScheduleRandomization {
|
|
1676
|
+
return { mode: "delayed", maxDelay: normalizeIntervalDuration(maxDelay) };
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1782
1679
|
function countCapturingGroups(pattern: RegExp): number {
|
|
1783
1680
|
let count = 0;
|
|
1784
1681
|
const source = pattern.source;
|
|
@@ -1797,11 +1694,7 @@ function countCapturingGroups(pattern: RegExp): number {
|
|
|
1797
1694
|
if (inCharacterClass || char !== "(") continue;
|
|
1798
1695
|
const next = source[i + 1];
|
|
1799
1696
|
if (next === "?" && source[i + 2] !== "<") continue;
|
|
1800
|
-
if (
|
|
1801
|
-
next === "?" &&
|
|
1802
|
-
source[i + 2] === "<" &&
|
|
1803
|
-
(source[i + 3] === "=" || source[i + 3] === "!")
|
|
1804
|
-
)
|
|
1697
|
+
if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1805
1698
|
continue;
|
|
1806
1699
|
count += 1;
|
|
1807
1700
|
}
|
|
@@ -1820,9 +1713,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1820
1713
|
}
|
|
1821
1714
|
const kind = Reflect.get(origin, "kind");
|
|
1822
1715
|
if (kind !== "e164" && kind !== "nationalServiceCode") {
|
|
1823
|
-
throw new ValidationError(
|
|
1824
|
-
`${fieldPath}.kind must be "e164" or "nationalServiceCode".`,
|
|
1825
|
-
);
|
|
1716
|
+
throw new ValidationError(`${fieldPath}.kind must be "e164" or "nationalServiceCode".`);
|
|
1826
1717
|
}
|
|
1827
1718
|
rejectUnknownFields(origin, SMS_ORIGIN_FIELDS_BY_KIND[kind], fieldPath);
|
|
1828
1719
|
if (kind === "e164") {
|
|
@@ -1849,9 +1740,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1849
1740
|
Reflect.get(origin, "display") !== undefined &&
|
|
1850
1741
|
typeof Reflect.get(origin, "display") !== "string"
|
|
1851
1742
|
) {
|
|
1852
|
-
throw new ValidationError(
|
|
1853
|
-
`${fieldPath}.display must be a string when present.`,
|
|
1854
|
-
);
|
|
1743
|
+
throw new ValidationError(`${fieldPath}.display must be a string when present.`);
|
|
1855
1744
|
}
|
|
1856
1745
|
}
|
|
1857
1746
|
|
|
@@ -1865,9 +1754,7 @@ function validateSmsOtpMatcher(
|
|
|
1865
1754
|
rejectUnknownFields(matcher, SMS_OTP_MATCHER_FIELDS, fieldPath);
|
|
1866
1755
|
const matcherId = Reflect.get(matcher, "id");
|
|
1867
1756
|
if (typeof matcherId !== "string" || !JOURNEY_ID_RE.test(matcherId)) {
|
|
1868
|
-
throw new ValidationError(
|
|
1869
|
-
`${fieldPath}.id must be a kebab-case identifier.`,
|
|
1870
|
-
);
|
|
1757
|
+
throw new ValidationError(`${fieldPath}.id must be a kebab-case identifier.`);
|
|
1871
1758
|
}
|
|
1872
1759
|
assertIsoCountry(Reflect.get(matcher, "country"), `${fieldPath}.country`);
|
|
1873
1760
|
if (
|
|
@@ -1875,24 +1762,18 @@ function validateSmsOtpMatcher(
|
|
|
1875
1762
|
(typeof Reflect.get(matcher, "locale") !== "string" ||
|
|
1876
1763
|
!BCP47_RE.test(Reflect.get(matcher, "locale")))
|
|
1877
1764
|
) {
|
|
1878
|
-
throw new ValidationError(
|
|
1879
|
-
`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`,
|
|
1880
|
-
);
|
|
1765
|
+
throw new ValidationError(`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`);
|
|
1881
1766
|
}
|
|
1882
1767
|
if (
|
|
1883
1768
|
Reflect.get(matcher, "phoneNumber") !== undefined &&
|
|
1884
1769
|
(typeof Reflect.get(matcher, "phoneNumber") !== "string" ||
|
|
1885
1770
|
!E164_RE.test(Reflect.get(matcher, "phoneNumber")))
|
|
1886
1771
|
) {
|
|
1887
|
-
throw new ValidationError(
|
|
1888
|
-
`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`,
|
|
1889
|
-
);
|
|
1772
|
+
throw new ValidationError(`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`);
|
|
1890
1773
|
}
|
|
1891
1774
|
const origins = Reflect.get(matcher, "origins");
|
|
1892
1775
|
if (!Array.isArray(origins) || origins.length === 0) {
|
|
1893
|
-
throw new ValidationError(
|
|
1894
|
-
`${fieldPath}.origins must be a non-empty array.`,
|
|
1895
|
-
);
|
|
1776
|
+
throw new ValidationError(`${fieldPath}.origins must be a non-empty array.`);
|
|
1896
1777
|
}
|
|
1897
1778
|
for (const [index, origin] of origins.entries()) {
|
|
1898
1779
|
validateSmsOrigin(origin, `${fieldPath}.origins[${index}]`);
|
|
@@ -1913,10 +1794,7 @@ function validateSmsOtpMatcher(
|
|
|
1913
1794
|
);
|
|
1914
1795
|
}
|
|
1915
1796
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
1916
|
-
if (
|
|
1917
|
-
countCapturingGroups(regex) !== 1 &&
|
|
1918
|
-
Reflect.get(code, "capture") === undefined
|
|
1919
|
-
) {
|
|
1797
|
+
if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
|
|
1920
1798
|
throw new ValidationError(
|
|
1921
1799
|
`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`,
|
|
1922
1800
|
);
|
|
@@ -1926,20 +1804,12 @@ function validateSmsOtpMatcher(
|
|
|
1926
1804
|
typeof Reflect.get(code, "capture") !== "string" &&
|
|
1927
1805
|
typeof Reflect.get(code, "capture") !== "number"
|
|
1928
1806
|
) {
|
|
1929
|
-
throw new ValidationError(
|
|
1930
|
-
`${fieldPath}.code.capture must be a string or number when present.`,
|
|
1931
|
-
);
|
|
1807
|
+
throw new ValidationError(`${fieldPath}.code.capture must be a string or number when present.`);
|
|
1932
1808
|
}
|
|
1933
1809
|
assertIsoDuration(Reflect.get(matcher, "maxAge"), `${fieldPath}.maxAge`);
|
|
1934
|
-
assertIsoDuration(
|
|
1935
|
-
Reflect.get(matcher, "waitTimeout"),
|
|
1936
|
-
`${fieldPath}.waitTimeout`,
|
|
1937
|
-
);
|
|
1810
|
+
assertIsoDuration(Reflect.get(matcher, "waitTimeout"), `${fieldPath}.waitTimeout`);
|
|
1938
1811
|
if (Reflect.get(matcher, "clockSkew") !== undefined)
|
|
1939
|
-
assertIsoDuration(
|
|
1940
|
-
Reflect.get(matcher, "clockSkew"),
|
|
1941
|
-
`${fieldPath}.clockSkew`,
|
|
1942
|
-
);
|
|
1812
|
+
assertIsoDuration(Reflect.get(matcher, "clockSkew"), `${fieldPath}.clockSkew`);
|
|
1943
1813
|
}
|
|
1944
1814
|
|
|
1945
1815
|
export function defineSmsOtpMatcher(
|
|
@@ -1971,9 +1841,7 @@ export function defineSmsOtpMatcher(
|
|
|
1971
1841
|
return matcher;
|
|
1972
1842
|
}
|
|
1973
1843
|
|
|
1974
|
-
export function defineHealthJourney(
|
|
1975
|
-
config: HealthJourneyDefinition,
|
|
1976
|
-
): HealthJourneyDefinition {
|
|
1844
|
+
export function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition {
|
|
1977
1845
|
return config;
|
|
1978
1846
|
}
|
|
1979
1847
|
|
|
@@ -1984,24 +1852,31 @@ function validateHealthJourneySchedule(
|
|
|
1984
1852
|
): void {
|
|
1985
1853
|
const fieldPath = `healthJourneys.${journeyId}.schedule`;
|
|
1986
1854
|
if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) {
|
|
1987
|
-
throw new ValidationError(
|
|
1988
|
-
`Provider "${providerId}" ${fieldPath} must be an object.`,
|
|
1989
|
-
);
|
|
1855
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1990
1856
|
}
|
|
1991
1857
|
rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
|
|
1992
1858
|
if (Reflect.get(schedule, "kind") !== "interval")
|
|
1859
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
|
|
1860
|
+
const interval = Reflect.get(schedule, "interval");
|
|
1861
|
+
assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
|
|
1862
|
+
const randomize = Reflect.get(schedule, "randomize");
|
|
1863
|
+
if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
|
|
1993
1864
|
throw new ValidationError(
|
|
1994
|
-
`Provider "${providerId}" ${fieldPath}
|
|
1865
|
+
`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`,
|
|
1995
1866
|
);
|
|
1996
|
-
|
|
1997
|
-
Reflect.get(schedule, "interval"),
|
|
1998
|
-
`Provider "${providerId}" ${fieldPath}.interval`,
|
|
1999
|
-
);
|
|
1867
|
+
}
|
|
2000
1868
|
if (Reflect.get(schedule, "jitter") !== undefined)
|
|
2001
1869
|
assertIsoDuration(
|
|
2002
1870
|
Reflect.get(schedule, "jitter"),
|
|
2003
1871
|
`Provider "${providerId}" ${fieldPath}.jitter`,
|
|
2004
1872
|
);
|
|
1873
|
+
if (randomize !== undefined) {
|
|
1874
|
+
validateScheduleRandomization(
|
|
1875
|
+
randomize,
|
|
1876
|
+
`Provider "${providerId}" ${fieldPath}.randomize`,
|
|
1877
|
+
isoDurationMs(interval),
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
2005
1880
|
}
|
|
2006
1881
|
|
|
2007
1882
|
function validateHealthJourneys(
|
|
@@ -2012,17 +1887,13 @@ function validateHealthJourneys(
|
|
|
2012
1887
|
const covered = new Set<string>();
|
|
2013
1888
|
if (healthJourneys === undefined) return covered;
|
|
2014
1889
|
if (!Array.isArray(healthJourneys)) {
|
|
2015
|
-
throw new ValidationError(
|
|
2016
|
-
`Provider "${providerId}" healthJourneys must be an array.`,
|
|
2017
|
-
);
|
|
1890
|
+
throw new ValidationError(`Provider "${providerId}" healthJourneys must be an array.`);
|
|
2018
1891
|
}
|
|
2019
1892
|
const journeyIds = new Set<string>();
|
|
2020
1893
|
for (const [index, journey] of healthJourneys.entries()) {
|
|
2021
1894
|
const prefix = `healthJourneys[${index}]`;
|
|
2022
1895
|
if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
|
|
2023
|
-
throw new ValidationError(
|
|
2024
|
-
`Provider "${providerId}" ${prefix} must be an object.`,
|
|
2025
|
-
);
|
|
1896
|
+
throw new ValidationError(`Provider "${providerId}" ${prefix} must be an object.`);
|
|
2026
1897
|
}
|
|
2027
1898
|
rejectUnknownFields(journey, HEALTH_JOURNEY_FIELDS, prefix);
|
|
2028
1899
|
if (typeof journey.id !== "string" || !JOURNEY_ID_RE.test(journey.id)) {
|
|
@@ -2036,10 +1907,7 @@ function validateHealthJourneys(
|
|
|
2036
1907
|
);
|
|
2037
1908
|
journeyIds.add(journey.id);
|
|
2038
1909
|
validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
|
|
2039
|
-
if (
|
|
2040
|
-
!Array.isArray(journey.coversOperations) ||
|
|
2041
|
-
journey.coversOperations.length === 0
|
|
2042
|
-
) {
|
|
1910
|
+
if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
|
|
2043
1911
|
throw new ValidationError(
|
|
2044
1912
|
`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`,
|
|
2045
1913
|
);
|
|
@@ -2074,10 +1942,7 @@ function validateHealthJourneys(
|
|
|
2074
1942
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers must be an array.`,
|
|
2075
1943
|
);
|
|
2076
1944
|
for (const [matcherIndex, matcher] of journey.smsMatchers.entries()) {
|
|
2077
|
-
validateSmsOtpMatcher(
|
|
2078
|
-
matcher,
|
|
2079
|
-
`healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`,
|
|
2080
|
-
);
|
|
1945
|
+
validateSmsOtpMatcher(matcher, `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`);
|
|
2081
1946
|
if (matcherIds.has(matcher.id))
|
|
2082
1947
|
throw new ValidationError(
|
|
2083
1948
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers has duplicate matcher id "${matcher.id}".`,
|
|
@@ -2088,9 +1953,7 @@ function validateHealthJourneys(
|
|
|
2088
1953
|
for (const [stepIndex, step] of journey.steps.entries()) {
|
|
2089
1954
|
const stepPath = `healthJourneys.${journey.id}.steps[${stepIndex}]`;
|
|
2090
1955
|
if (!step || typeof step !== "object" || Array.isArray(step))
|
|
2091
|
-
throw new ValidationError(
|
|
2092
|
-
`Provider "${providerId}" ${stepPath} must be an object.`,
|
|
2093
|
-
);
|
|
1956
|
+
throw new ValidationError(`Provider "${providerId}" ${stepPath} must be an object.`);
|
|
2094
1957
|
rejectUnknownFields(step, HEALTH_JOURNEY_STEP_FIELDS, stepPath);
|
|
2095
1958
|
if (typeof step.id !== "string" || !JOURNEY_ID_RE.test(step.id))
|
|
2096
1959
|
throw new ValidationError(
|
|
@@ -2100,20 +1963,13 @@ function validateHealthJourneys(
|
|
|
2100
1963
|
throw new ValidationError(
|
|
2101
1964
|
`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`,
|
|
2102
1965
|
);
|
|
2103
|
-
if (
|
|
2104
|
-
step.usesSmsMatcher !== undefined &&
|
|
2105
|
-
!matcherIds.has(step.usesSmsMatcher)
|
|
2106
|
-
)
|
|
1966
|
+
if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
|
|
2107
1967
|
throw new ValidationError(
|
|
2108
1968
|
`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`,
|
|
2109
1969
|
);
|
|
2110
1970
|
}
|
|
2111
1971
|
if (journey.manualTrigger !== undefined)
|
|
2112
|
-
validateHealthJourneyManualTrigger(
|
|
2113
|
-
providerId,
|
|
2114
|
-
journey.id,
|
|
2115
|
-
journey.manualTrigger,
|
|
2116
|
-
);
|
|
1972
|
+
validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
|
|
2117
1973
|
if (journey.timeout !== undefined)
|
|
2118
1974
|
assertIsoDuration(
|
|
2119
1975
|
journey.timeout,
|
|
@@ -2159,23 +2015,10 @@ function validateOperationHealthChecks(
|
|
|
2159
2015
|
fix: `Remove either operations.${operationName}.healthCheck or operations.${operationName}.healthCheckUnsupported.`,
|
|
2160
2016
|
},
|
|
2161
2017
|
);
|
|
2162
|
-
if (hasCheck)
|
|
2163
|
-
validateHealthCheckSuite(
|
|
2164
|
-
providerId,
|
|
2165
|
-
operationName,
|
|
2166
|
-
operation.healthCheck,
|
|
2167
|
-
);
|
|
2018
|
+
if (hasCheck) validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
|
|
2168
2019
|
if (hasUnsupported)
|
|
2169
|
-
validateHealthCheckUnsupported(
|
|
2170
|
-
|
|
2171
|
-
operationName,
|
|
2172
|
-
operation.healthCheckUnsupported,
|
|
2173
|
-
);
|
|
2174
|
-
if (
|
|
2175
|
-
!hasCheck &&
|
|
2176
|
-
!hasUnsupported &&
|
|
2177
|
-
!journeyCoveredOperations.has(operationName)
|
|
2178
|
-
)
|
|
2020
|
+
validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
|
|
2021
|
+
if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
|
|
2179
2022
|
throw new ValidationError(
|
|
2180
2023
|
`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`,
|
|
2181
2024
|
{
|
|
@@ -2230,6 +2073,19 @@ function validateOperationFixtures(
|
|
|
2230
2073
|
}
|
|
2231
2074
|
}
|
|
2232
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
|
+
|
|
2233
2089
|
export function defineProvider<
|
|
2234
2090
|
TOperations extends Record<string, ProviderOperation>,
|
|
2235
2091
|
TConfig extends ProviderConfig<TOperations>,
|
|
@@ -2242,10 +2098,9 @@ export function defineProvider<
|
|
|
2242
2098
|
fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
|
|
2243
2099
|
});
|
|
2244
2100
|
if (Object.keys(config.operations).length === 0)
|
|
2245
|
-
throw new ProviderError(
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
);
|
|
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
|
+
});
|
|
2249
2104
|
validateOperationIds(config.id, config.operations);
|
|
2250
2105
|
validateOperationAnnotations(config.id, config.operations);
|
|
2251
2106
|
validateOperationObservability(config.id, config.operations);
|
|
@@ -2257,13 +2112,21 @@ export function defineProvider<
|
|
|
2257
2112
|
config.operations,
|
|
2258
2113
|
config.healthJourneys,
|
|
2259
2114
|
);
|
|
2260
|
-
validateOperationHealthChecks(
|
|
2115
|
+
validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
|
|
2116
|
+
if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
|
|
2117
|
+
throw new ValidationError(
|
|
2118
|
+
`Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
|
|
2119
|
+
{
|
|
2120
|
+
fix: "Keep healthProbe (the new name) and delete the healthMonitor block.",
|
|
2121
|
+
},
|
|
2122
|
+
);
|
|
2123
|
+
validateProviderHealthMonitor(
|
|
2261
2124
|
config.id,
|
|
2262
|
-
config.
|
|
2263
|
-
|
|
2125
|
+
config.healthProbe ?? config.healthMonitor,
|
|
2126
|
+
config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
|
|
2264
2127
|
);
|
|
2265
|
-
validateProviderHealthMonitor(config.id, config.healthMonitor);
|
|
2266
2128
|
validateOperationFixtures(config.id, config.operations);
|
|
2129
|
+
validateProviderDeployment(config.id, config.deployment);
|
|
2267
2130
|
validateProviderProxy(config);
|
|
2268
2131
|
validateProviderStt(config);
|
|
2269
2132
|
if (config.runtime === "browser" && !config.browser)
|
|
@@ -2282,6 +2145,9 @@ export function defineProvider<
|
|
|
2282
2145
|
id: config.id,
|
|
2283
2146
|
version: config.version,
|
|
2284
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,
|
|
2285
2151
|
allowedHosts: config.allowedHosts,
|
|
2286
2152
|
stealth: config.stealth,
|
|
2287
2153
|
proxy: config.proxy,
|
|
@@ -2295,7 +2161,10 @@ export function defineProvider<
|
|
|
2295
2161
|
context: config.context,
|
|
2296
2162
|
meta: config.meta,
|
|
2297
2163
|
operations: config.operations,
|
|
2298
|
-
healthMonitor:
|
|
2164
|
+
// Transitional healthMonitor → healthProbe alias: mirror whichever field
|
|
2165
|
+
// was declared onto both so old and new consumers keep working.
|
|
2166
|
+
healthMonitor: config.healthMonitor ?? config.healthProbe,
|
|
2167
|
+
healthProbe: config.healthProbe ?? config.healthMonitor,
|
|
2299
2168
|
healthJourneys: config.healthJourneys,
|
|
2300
2169
|
};
|
|
2301
2170
|
}
|