@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/dist/define.js
CHANGED
|
@@ -1,26 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import ms from "ms";
|
|
2
|
+
import { ProviderError, ValidationError } from "./errors.js";
|
|
3
|
+
import { safeParseSchemaSync } from "./schema.js";
|
|
4
|
+
import { HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX, HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN, HEALTH_CHECK_TIMEOUT_MS_MAX, HEALTH_CHECK_TIMEOUT_MS_MIN, OPERATION_TIMEOUT_MS_MAX, OPERATION_TIMEOUT_MS_MIN, STREAM_CHUNK_BYTES_MAX, STREAM_CHUNK_BYTES_MIN, STREAM_HEARTBEAT_MS_MAX, STREAM_HEARTBEAT_MS_MIN, STREAM_IDLE_TIMEOUT_MS_MAX, STREAM_IDLE_TIMEOUT_MS_MIN, STREAM_MAX_DURATION_MS_MAX, STREAM_MAX_DURATION_MS_MIN, } from "./types.js";
|
|
4
5
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
5
6
|
const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
6
7
|
const VALID_RUNTIMES = ["standard", "shared", "browser"];
|
|
7
|
-
const VALID_AUTH_MODES = [
|
|
8
|
-
"none",
|
|
9
|
-
"platform-managed",
|
|
10
|
-
"credentials",
|
|
11
|
-
"oauth2",
|
|
12
|
-
];
|
|
8
|
+
const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"];
|
|
13
9
|
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"];
|
|
14
|
-
const VALID_PROVIDER_PROXY_MODES = [
|
|
15
|
-
|
|
16
|
-
"optional",
|
|
17
|
-
"required",
|
|
18
|
-
];
|
|
19
|
-
const VALID_PROVIDER_PROXY_PROVIDERS = [
|
|
20
|
-
"smartproxy",
|
|
21
|
-
"decodo",
|
|
22
|
-
"custom",
|
|
23
|
-
];
|
|
10
|
+
const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"];
|
|
11
|
+
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "nodemaven", "decodo", "custom"];
|
|
24
12
|
const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
25
13
|
"request",
|
|
26
14
|
"operation",
|
|
@@ -29,72 +17,44 @@ const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
|
29
17
|
];
|
|
30
18
|
const VALID_PROVIDER_STT_MODES = ["optional", "required"];
|
|
31
19
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
20
|
+
const NODEMAVEN_USERNAME_SECRET = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
21
|
+
const NODEMAVEN_PASSWORD_SECRET = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
22
|
+
// Per-vendor provider-declared credential secrets. A required-mode chain must
|
|
23
|
+
// declare every secret of every credentialed vendor it names, so a missing
|
|
24
|
+
// credential fails at build/validation time rather than during a live outage: a
|
|
25
|
+
// declared-but-uncredentialed fallback leg is a silently dead SPOF, which is
|
|
26
|
+
// exactly the failure class the multi-vendor chain exists to remove. Vendors
|
|
27
|
+
// absent from this map (e.g. `custom`/`decodo`, whose credentials come from the
|
|
28
|
+
// `APIFUSE__PROXY__URL` bring-your-own escape hatch, not provider secrets) impose
|
|
29
|
+
// no declaration requirement.
|
|
30
|
+
const VENDOR_REQUIRED_SECRETS = {
|
|
31
|
+
smartproxy: [SMARTPROXY_APP_KEY_SECRET],
|
|
32
|
+
nodemaven: [NODEMAVEN_USERNAME_SECRET, NODEMAVEN_PASSWORD_SECRET],
|
|
33
|
+
};
|
|
32
34
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
33
35
|
const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
|
|
34
|
-
const VALID_OPERATION_RISK_CLASSES = [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"destructive",
|
|
38
|
-
"external-send",
|
|
39
|
-
];
|
|
40
|
-
const VALID_OPERATION_APPROVAL_POLICIES = [
|
|
41
|
-
"never",
|
|
42
|
-
"risk-based",
|
|
43
|
-
"always",
|
|
44
|
-
];
|
|
45
|
-
const VALID_OPERATION_TRANSPORT_KINDS = [
|
|
46
|
-
"json",
|
|
47
|
-
"sse",
|
|
48
|
-
"http-stream",
|
|
49
|
-
"websocket",
|
|
50
|
-
];
|
|
36
|
+
const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"];
|
|
37
|
+
const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"];
|
|
38
|
+
const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"];
|
|
51
39
|
const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
52
40
|
const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
53
|
-
const MS_DURATION_UNITS = new Set([
|
|
54
|
-
"years",
|
|
55
|
-
"year",
|
|
56
|
-
"yrs",
|
|
57
|
-
"yr",
|
|
58
|
-
"y",
|
|
59
|
-
"weeks",
|
|
60
|
-
"week",
|
|
61
|
-
"w",
|
|
62
|
-
"days",
|
|
63
|
-
"day",
|
|
64
|
-
"d",
|
|
65
|
-
"hours",
|
|
66
|
-
"hour",
|
|
67
|
-
"hrs",
|
|
68
|
-
"hr",
|
|
69
|
-
"h",
|
|
70
|
-
"minutes",
|
|
71
|
-
"minute",
|
|
72
|
-
"mins",
|
|
73
|
-
"min",
|
|
74
|
-
"m",
|
|
75
|
-
"seconds",
|
|
76
|
-
"second",
|
|
77
|
-
"secs",
|
|
78
|
-
"sec",
|
|
79
|
-
"s",
|
|
80
|
-
"milliseconds",
|
|
81
|
-
"millisecond",
|
|
82
|
-
"msecs",
|
|
83
|
-
"msec",
|
|
84
|
-
"ms",
|
|
85
|
-
]);
|
|
86
41
|
const MS_DURATION_PATTERN = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*([a-zA-Z]+)?$/;
|
|
87
42
|
function isPositiveMsDurationString(value) {
|
|
88
43
|
if (typeof value !== "string")
|
|
89
44
|
return false;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
45
|
+
return parsePositiveMsDuration(value) !== undefined;
|
|
46
|
+
}
|
|
47
|
+
function msDurationMs(value) {
|
|
48
|
+
return parsePositiveMsDuration(value) ?? 0;
|
|
49
|
+
}
|
|
50
|
+
function parsePositiveMsDuration(value) {
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
if (!MS_DURATION_PATTERN.test(trimmed))
|
|
53
|
+
return undefined;
|
|
54
|
+
const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed));
|
|
55
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
56
|
+
return undefined;
|
|
57
|
+
return parsed;
|
|
98
58
|
}
|
|
99
59
|
/** Define one provider operation with schema-driven handler inference. */
|
|
100
60
|
export function defineOperation(operation) {
|
|
@@ -136,11 +96,13 @@ function validateProviderShape(config) {
|
|
|
136
96
|
if (typeof config.runtime === "string")
|
|
137
97
|
assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
|
|
138
98
|
const auth = config.auth;
|
|
139
|
-
if (auth &&
|
|
140
|
-
typeof auth === "object" &&
|
|
141
|
-
"mode" in auth &&
|
|
142
|
-
typeof auth.mode === "string")
|
|
99
|
+
if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
|
|
143
100
|
assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
|
|
101
|
+
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
102
|
+
throw new ProviderError(`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`, {
|
|
103
|
+
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.",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
144
106
|
if (auth &&
|
|
145
107
|
typeof auth === "object" &&
|
|
146
108
|
"flow" in auth &&
|
|
@@ -189,15 +151,23 @@ function validateProviderProxy(config) {
|
|
|
189
151
|
fix: `Use proxy: { mode: "required", provider: "smartproxy", geo: { country: "KR" }, session: { affinity: "connection", lifetimeMinutes: 30 } }`,
|
|
190
152
|
});
|
|
191
153
|
}
|
|
192
|
-
rejectUnknownFields(proxy, new Set(["mode", "provider", "geo", "session"]), "proxy");
|
|
154
|
+
rejectUnknownFields(proxy, new Set(["mode", "provider", "providers", "geo", "session"]), "proxy");
|
|
193
155
|
assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
|
|
194
156
|
if (proxy.provider !== undefined) {
|
|
195
157
|
assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
196
158
|
}
|
|
159
|
+
if (proxy.providers !== undefined) {
|
|
160
|
+
if (!Array.isArray(proxy.providers) || proxy.providers.length === 0) {
|
|
161
|
+
throw new ValidationError(`Provider "${config.id}" has invalid proxy.providers: must be a non-empty array of proxy vendors.`, {
|
|
162
|
+
fix: `Use proxy.providers: ["smartproxy", "nodemaven"] to declare an ordered fallback chain.`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
for (const vendor of proxy.providers) {
|
|
166
|
+
assertLiteralField(vendor, "proxy.providers[]", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
197
169
|
if (proxy.geo !== undefined) {
|
|
198
|
-
if (!proxy.geo ||
|
|
199
|
-
typeof proxy.geo !== "object" ||
|
|
200
|
-
Array.isArray(proxy.geo)) {
|
|
170
|
+
if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
|
|
201
171
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.geo: must be an object.`, {
|
|
202
172
|
fix: `Use proxy.geo: { country: "KR" } with ISO alpha-2 country codes.`,
|
|
203
173
|
});
|
|
@@ -214,9 +184,7 @@ function validateProviderProxy(config) {
|
|
|
214
184
|
}
|
|
215
185
|
}
|
|
216
186
|
if (proxy.session !== undefined) {
|
|
217
|
-
if (!proxy.session ||
|
|
218
|
-
typeof proxy.session !== "object" ||
|
|
219
|
-
Array.isArray(proxy.session)) {
|
|
187
|
+
if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
|
|
220
188
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session: must be an object.`, {
|
|
221
189
|
fix: `Use proxy.session: { affinity: "connection", lifetimeMinutes: 30 }.`,
|
|
222
190
|
});
|
|
@@ -226,31 +194,62 @@ function validateProviderProxy(config) {
|
|
|
226
194
|
assertLiteralField(proxy.session.affinity, "proxy.session.affinity", VALID_PROVIDER_PROXY_AFFINITIES, config.id);
|
|
227
195
|
}
|
|
228
196
|
const lifetime = proxy.session.lifetimeMinutes;
|
|
229
|
-
if (lifetime !== undefined &&
|
|
230
|
-
(!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
197
|
+
if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
231
198
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`);
|
|
232
199
|
}
|
|
233
200
|
const poolSize = proxy.session.poolSize;
|
|
234
|
-
if (poolSize !== undefined &&
|
|
235
|
-
(!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
201
|
+
if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
236
202
|
throw new ValidationError(`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`);
|
|
237
203
|
}
|
|
238
204
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
205
|
+
// Every credentialed vendor in a required-mode chain must declare its
|
|
206
|
+
// provider secret(s) so a missing credential fails at build/validation time,
|
|
207
|
+
// not during a live outage. This covers the fallback legs too (not just the
|
|
208
|
+
// first vendor): a declared-but-uncredentialed nodemaven fallback would leave
|
|
209
|
+
// the chain silently down to a single vendor, reintroducing the SPOF the chain
|
|
210
|
+
// removes.
|
|
211
|
+
const vendorChain = proxy.providers && proxy.providers.length > 0
|
|
212
|
+
? proxy.providers
|
|
213
|
+
: proxy.provider
|
|
214
|
+
? [proxy.provider]
|
|
215
|
+
: [];
|
|
216
|
+
if (proxy.mode === "required") {
|
|
217
|
+
for (const vendor of vendorChain) {
|
|
218
|
+
const requiredSecrets = VENDOR_REQUIRED_SECRETS[vendor];
|
|
219
|
+
if (!requiredSecrets)
|
|
220
|
+
continue;
|
|
221
|
+
for (const secretName of requiredSecrets) {
|
|
222
|
+
// Match the canonical runtime gate (assertRequiredSecretsPresent /
|
|
223
|
+
// listMissingRequiredSecrets), which enforces only `required === true`
|
|
224
|
+
// declarations. A declaration that omits `required` (defaulting to
|
|
225
|
+
// optional) is skipped at runtime, so accepting it here would pass
|
|
226
|
+
// validation while leaving the credential unenforced until proxy
|
|
227
|
+
// resolution during a live request — the fail-open gap this check exists
|
|
228
|
+
// to close.
|
|
229
|
+
const declared = config.secrets?.some((secret) => secret.name === secretName && secret.required === true);
|
|
230
|
+
if (!declared) {
|
|
231
|
+
throw new ValidationError(`Provider "${config.id}" requires ${vendor} egress but does not declare ${secretName}.`, {
|
|
232
|
+
fix: `Add secrets: [{ name: "${secretName}", required: true }] to the provider (every vendor in a required proxy chain must declare its credential secrets).`,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
245
236
|
}
|
|
246
237
|
}
|
|
238
|
+
// `decodo`/`custom` are deprecated vendor values (string-union members, so the
|
|
239
|
+
// @deprecated symbol gate can't catch them — warn at validation time instead).
|
|
240
|
+
const deprecatedVendors = vendorChain.filter((vendor) => vendor === "decodo" || vendor === "custom");
|
|
241
|
+
if (deprecatedVendors.length > 0) {
|
|
242
|
+
console.warn(`[provider-sdk] Provider "${config.id}" uses deprecated proxy vendor(s): ${deprecatedVendors.join(", ")}. Use "smartproxy"/"nodemaven", or the APIFUSE__PROXY__URL bring-your-own escape hatch.`);
|
|
243
|
+
}
|
|
247
244
|
}
|
|
248
245
|
function validateProviderStt(config) {
|
|
249
246
|
const stt = config.stt;
|
|
250
247
|
if (stt === undefined)
|
|
251
248
|
return;
|
|
252
249
|
if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
|
|
253
|
-
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
250
|
+
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
251
|
+
fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
|
|
252
|
+
});
|
|
254
253
|
}
|
|
255
254
|
rejectUnknownFields(stt, new Set(["mode"]), "stt");
|
|
256
255
|
assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
|
|
@@ -269,12 +268,7 @@ function validateOperationIds(providerId, operations) {
|
|
|
269
268
|
}
|
|
270
269
|
const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
271
270
|
const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
272
|
-
const VALID_OPERATION_LIFECYCLES = [
|
|
273
|
-
"stable",
|
|
274
|
-
"beta",
|
|
275
|
-
"deprecated",
|
|
276
|
-
"removed",
|
|
277
|
-
];
|
|
271
|
+
const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"];
|
|
278
272
|
function assertNonEmptyString(value, field, providerId, operationName) {
|
|
279
273
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
280
274
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${field}: must be a non-empty string.`, { fix: `Set ${field} to a non-empty customer-facing value.` });
|
|
@@ -290,8 +284,7 @@ function validateToolRouterMetadata(providerId, operations) {
|
|
|
290
284
|
fix: `Remove operations.${operationName}.toolRouter or provide MCP-safe metadata.`,
|
|
291
285
|
});
|
|
292
286
|
}
|
|
293
|
-
if (toolRouter.name !== undefined &&
|
|
294
|
-
!MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
287
|
+
if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
295
288
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`, {
|
|
296
289
|
fix: `Use letters, numbers, and underscores only, starting with a letter, for example "${providerId.replace(/[^A-Za-z0-9]+/g, "_")}__${operationName.replace(/[^A-Za-z0-9]+/g, "_")}".`,
|
|
297
290
|
});
|
|
@@ -329,8 +322,7 @@ function validateOperationContracts(providerId, operations) {
|
|
|
329
322
|
if (contract.lifecycle !== undefined) {
|
|
330
323
|
assertLiteralField(contract.lifecycle, `operations.${operationName}.contract.lifecycle`, VALID_OPERATION_LIFECYCLES, providerId);
|
|
331
324
|
}
|
|
332
|
-
if (contract.lifecycle === "deprecated" ||
|
|
333
|
-
contract.lifecycle === "removed") {
|
|
325
|
+
if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
|
|
334
326
|
if (!contract.deprecation || typeof contract.deprecation !== "object") {
|
|
335
327
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`, {
|
|
336
328
|
fix: `Add announcedAt, removalAfter, and migrationGuide to operations.${operationName}.contract.deprecation.`,
|
|
@@ -355,8 +347,7 @@ function validateOperationAnnotations(providerId, operations) {
|
|
|
355
347
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an integer number of milliseconds.`, {
|
|
356
348
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
|
|
357
349
|
});
|
|
358
|
-
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
|
|
359
|
-
timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
350
|
+
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
360
351
|
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`, {
|
|
361
352
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms (the upper bound stays below the gateway/ALB ceiling).`,
|
|
362
353
|
});
|
|
@@ -500,9 +491,7 @@ function validateOperationTransports(providerId, operations) {
|
|
|
500
491
|
assertStreamMs(maxDurationMs, `${fieldPath}.maxDurationMs`, STREAM_MAX_DURATION_MS_MIN, STREAM_MAX_DURATION_MS_MAX, "max duration");
|
|
501
492
|
assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
|
|
502
493
|
const resumable = Reflect.get(transport, "resumable");
|
|
503
|
-
if (resumable !== undefined &&
|
|
504
|
-
resumable !== false &&
|
|
505
|
-
resumable !== "last-event-id") {
|
|
494
|
+
if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
|
|
506
495
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`, {
|
|
507
496
|
fix: `Use ${fieldPath}.resumable: "last-event-id" for SSE Last-Event-ID resume support, or false to disable resume.`,
|
|
508
497
|
});
|
|
@@ -537,8 +526,7 @@ function validateOperationTransports(providerId, operations) {
|
|
|
537
526
|
});
|
|
538
527
|
}
|
|
539
528
|
for (const subprotocol of subprotocols) {
|
|
540
|
-
if (typeof subprotocol !== "string" ||
|
|
541
|
-
!WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
529
|
+
if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
542
530
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`, {
|
|
543
531
|
fix: `Use values such as "apifuse.v1" without spaces or separators that are invalid for Sec-WebSocket-Protocol.`,
|
|
544
532
|
});
|
|
@@ -555,6 +543,7 @@ function validateOperationTransports(providerId, operations) {
|
|
|
555
543
|
}
|
|
556
544
|
const HEALTH_CHECK_SUITE_FIELDS = new Set([
|
|
557
545
|
"interval",
|
|
546
|
+
"schedule",
|
|
558
547
|
"timeoutMs",
|
|
559
548
|
"degradedThresholdMs",
|
|
560
549
|
"cases",
|
|
@@ -642,26 +631,24 @@ function assertBoundedIntegerMs(value, fieldPath, options) {
|
|
|
642
631
|
});
|
|
643
632
|
}
|
|
644
633
|
}
|
|
645
|
-
function validateProviderHealthMonitor(providerId, healthMonitor) {
|
|
634
|
+
function validateProviderHealthMonitor(providerId, healthMonitor, field = "healthMonitor") {
|
|
646
635
|
if (healthMonitor === undefined)
|
|
647
636
|
return;
|
|
648
|
-
if (!healthMonitor ||
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor: must be an object.`, {
|
|
652
|
-
fix: `Set healthMonitor to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
637
|
+
if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
|
|
638
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
|
|
639
|
+
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
653
640
|
});
|
|
654
641
|
const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
|
|
655
|
-
rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS,
|
|
642
|
+
rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
|
|
656
643
|
if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
|
|
657
|
-
assertBoundedIntegerMs(healthMonitorRecord.defaultProbeTimeoutMs, `Provider "${providerId}"
|
|
644
|
+
assertBoundedIntegerMs(healthMonitorRecord.defaultProbeTimeoutMs, `Provider "${providerId}" ${field}.defaultProbeTimeoutMs`, {
|
|
658
645
|
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
659
646
|
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
660
647
|
label: "timeout",
|
|
661
648
|
});
|
|
662
649
|
}
|
|
663
650
|
if (healthMonitorRecord.defaultDegradedThresholdMs !== undefined) {
|
|
664
|
-
assertBoundedIntegerMs(healthMonitorRecord.defaultDegradedThresholdMs, `Provider "${providerId}"
|
|
651
|
+
assertBoundedIntegerMs(healthMonitorRecord.defaultDegradedThresholdMs, `Provider "${providerId}" ${field}.defaultDegradedThresholdMs`, {
|
|
665
652
|
min: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
|
|
666
653
|
max: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
667
654
|
label: "degraded threshold",
|
|
@@ -670,10 +657,10 @@ function validateProviderHealthMonitor(providerId, healthMonitor) {
|
|
|
670
657
|
const requiredSecrets = healthMonitorRecord.requiredSecrets;
|
|
671
658
|
if (requiredSecrets !== undefined) {
|
|
672
659
|
if (!Array.isArray(requiredSecrets))
|
|
673
|
-
throw new ValidationError(`Provider "${providerId}" has invalid
|
|
660
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.requiredSecrets: must be string[].`);
|
|
674
661
|
for (const [index, secret] of requiredSecrets.entries()) {
|
|
675
662
|
if (typeof secret !== "string" || secret.length === 0)
|
|
676
|
-
throw new ValidationError(`Provider "${providerId}" has invalid
|
|
663
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.requiredSecrets[${index}]: must be a non-empty string.`);
|
|
677
664
|
}
|
|
678
665
|
}
|
|
679
666
|
const credentialInputs = healthMonitorRecord.credentialInputs;
|
|
@@ -681,45 +668,43 @@ function validateProviderHealthMonitor(providerId, healthMonitor) {
|
|
|
681
668
|
if (!credentialInputs ||
|
|
682
669
|
typeof credentialInputs !== "object" ||
|
|
683
670
|
Array.isArray(credentialInputs)) {
|
|
684
|
-
throw new ValidationError(`Provider "${providerId}" has invalid
|
|
671
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.credentialInputs: must be an object mapping auth input fields to env var names.`);
|
|
685
672
|
}
|
|
686
|
-
for (const [
|
|
687
|
-
if (
|
|
688
|
-
throw new ValidationError(`Provider "${providerId}" has invalid
|
|
673
|
+
for (const [inputField, envVar] of Object.entries(credentialInputs)) {
|
|
674
|
+
if (inputField.trim().length === 0) {
|
|
675
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.credentialInputs key: must be a non-empty auth input field.`);
|
|
689
676
|
}
|
|
690
677
|
if (typeof envVar !== "string" || envVar.trim().length === 0) {
|
|
691
|
-
throw new ValidationError(`Provider "${providerId}" has invalid
|
|
678
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.credentialInputs.${inputField}: must be a non-empty env var name.`);
|
|
692
679
|
}
|
|
693
680
|
if (Array.isArray(requiredSecrets) && !requiredSecrets.includes(envVar)) {
|
|
694
|
-
throw new ValidationError(`Provider "${providerId}"
|
|
681
|
+
throw new ValidationError(`Provider "${providerId}" ${field}.credentialInputs.${inputField} references ${envVar}, which must also be listed in ${field}.requiredSecrets.`);
|
|
695
682
|
}
|
|
696
683
|
}
|
|
697
684
|
}
|
|
698
685
|
const probeOverrides = healthMonitorRecord.probeOverrides;
|
|
699
686
|
if (probeOverrides !== undefined) {
|
|
700
|
-
if (!probeOverrides ||
|
|
701
|
-
|
|
702
|
-
Array.isArray(probeOverrides))
|
|
703
|
-
throw new ValidationError(`Provider "${providerId}" has invalid healthMonitor.probeOverrides: must be an object keyed by probe id.`);
|
|
687
|
+
if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
|
|
688
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`);
|
|
704
689
|
for (const [probeId, override] of Object.entries(probeOverrides)) {
|
|
705
690
|
if (probeId.length === 0)
|
|
706
|
-
throw new ValidationError(`Provider "${providerId}" has invalid
|
|
691
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides key: must be a non-empty probe id.`);
|
|
707
692
|
if (!override || typeof override !== "object" || Array.isArray(override))
|
|
708
|
-
throw new ValidationError(`Provider "${providerId}" has invalid
|
|
693
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides["${probeId}"]: must be an object.`);
|
|
709
694
|
const overrideRecord = Object.fromEntries(Object.entries(override));
|
|
710
|
-
rejectUnknownFields(overrideRecord, PROVIDER_HEALTH_MONITOR_PROBE_OVERRIDE_FIELDS,
|
|
695
|
+
rejectUnknownFields(overrideRecord, PROVIDER_HEALTH_MONITOR_PROBE_OVERRIDE_FIELDS, `${field}.probeOverrides["${probeId}"]`);
|
|
711
696
|
const interval = overrideRecord.interval;
|
|
712
697
|
if (interval !== undefined && !isPositiveMsDurationString(interval))
|
|
713
|
-
throw new ValidationError(`Provider "${providerId}" has invalid
|
|
698
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.probeOverrides["${probeId}"].interval: must be a positive ms-style duration string such as 30s, 5m, 8h, or 1 day.`);
|
|
714
699
|
if (overrideRecord.timeoutMs !== undefined) {
|
|
715
|
-
assertBoundedIntegerMs(overrideRecord.timeoutMs, `Provider "${providerId}"
|
|
700
|
+
assertBoundedIntegerMs(overrideRecord.timeoutMs, `Provider "${providerId}" ${field}.probeOverrides["${probeId}"].timeoutMs`, {
|
|
716
701
|
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
717
702
|
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
718
703
|
label: "timeout",
|
|
719
704
|
});
|
|
720
705
|
}
|
|
721
706
|
if (overrideRecord.degradedThresholdMs !== undefined) {
|
|
722
|
-
assertBoundedIntegerMs(overrideRecord.degradedThresholdMs, `Provider "${providerId}"
|
|
707
|
+
assertBoundedIntegerMs(overrideRecord.degradedThresholdMs, `Provider "${providerId}" ${field}.probeOverrides["${probeId}"].degradedThresholdMs`, {
|
|
723
708
|
min: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
|
|
724
709
|
max: HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
725
710
|
label: "degraded threshold",
|
|
@@ -730,7 +715,7 @@ function validateProviderHealthMonitor(providerId, healthMonitor) {
|
|
|
730
715
|
const serviceAccount = healthMonitorRecord.serviceAccount;
|
|
731
716
|
if (serviceAccount !== undefined &&
|
|
732
717
|
(typeof serviceAccount !== "string" || serviceAccount.length === 0))
|
|
733
|
-
throw new ValidationError(`Provider "${providerId}" has invalid
|
|
718
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}.serviceAccount: must be a non-empty string.`);
|
|
734
719
|
}
|
|
735
720
|
function validateHealthCheckCase(providerId, operationName, caseValue, caseIndex) {
|
|
736
721
|
const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
|
|
@@ -776,6 +761,19 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
776
761
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.interval must be a positive ms-style duration string such as 30s, 5m, 8h, or 1 day.`, {
|
|
777
762
|
fix: `Set ${fieldPath}.interval to a positive ms-style duration string.`,
|
|
778
763
|
});
|
|
764
|
+
if (s.schedule !== undefined) {
|
|
765
|
+
if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
|
|
766
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.schedule must be an object.`);
|
|
767
|
+
}
|
|
768
|
+
if (Reflect.get(s.schedule, "jitter") !== undefined) {
|
|
769
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`);
|
|
770
|
+
}
|
|
771
|
+
rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
|
|
772
|
+
const randomize = Reflect.get(s.schedule, "randomize");
|
|
773
|
+
if (randomize !== undefined) {
|
|
774
|
+
validateScheduleRandomization(randomize, `Provider "${providerId}" ${fieldPath}.schedule.randomize`, msDurationMs(s.interval));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
779
777
|
if (s.timeoutMs !== undefined) {
|
|
780
778
|
assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
|
|
781
779
|
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
@@ -790,8 +788,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
790
788
|
label: "degraded threshold",
|
|
791
789
|
});
|
|
792
790
|
}
|
|
793
|
-
if (s.requiresConnection !== undefined &&
|
|
794
|
-
typeof s.requiresConnection !== "boolean")
|
|
791
|
+
if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
|
|
795
792
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`);
|
|
796
793
|
if (!Array.isArray(s.cases) || s.cases.length === 0)
|
|
797
794
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.cases must be a non-empty array.`, {
|
|
@@ -810,9 +807,7 @@ function validateHealthCheckSuite(providerId, operationName, suite) {
|
|
|
810
807
|
}
|
|
811
808
|
function validateHealthCheckUnsupported(providerId, operationName, unsupported) {
|
|
812
809
|
const fieldPath = `operations.${operationName}.healthCheckUnsupported`;
|
|
813
|
-
if (!unsupported ||
|
|
814
|
-
typeof unsupported !== "object" ||
|
|
815
|
-
Array.isArray(unsupported))
|
|
810
|
+
if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
|
|
816
811
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
817
812
|
rejectUnknownFields(unsupported, HEALTH_CHECK_UNSUPPORTED_FIELDS, fieldPath);
|
|
818
813
|
const u = unsupported;
|
|
@@ -837,7 +832,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
|
|
|
837
832
|
"steps",
|
|
838
833
|
"run",
|
|
839
834
|
]);
|
|
840
|
-
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter"]);
|
|
835
|
+
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
|
|
841
836
|
const HEALTH_JOURNEY_STEP_FIELDS = new Set([
|
|
842
837
|
"id",
|
|
843
838
|
"description",
|
|
@@ -855,10 +850,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
|
|
|
855
850
|
"minManualInterval",
|
|
856
851
|
"publicRationale",
|
|
857
852
|
]);
|
|
858
|
-
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
|
|
859
|
-
"enabled",
|
|
860
|
-
"reason",
|
|
861
|
-
]);
|
|
853
|
+
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
|
|
862
854
|
const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
|
|
863
855
|
"enabled",
|
|
864
856
|
"requiresAcknowledgement",
|
|
@@ -873,9 +865,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS = new Set([
|
|
|
873
865
|
]);
|
|
874
866
|
function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger) {
|
|
875
867
|
const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
|
|
876
|
-
if (!manualTrigger ||
|
|
877
|
-
typeof manualTrigger !== "object" ||
|
|
878
|
-
Array.isArray(manualTrigger)) {
|
|
868
|
+
if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
|
|
879
869
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object when present.`);
|
|
880
870
|
}
|
|
881
871
|
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
|
|
@@ -898,8 +888,7 @@ function validateHealthJourneyManualTrigger(providerId, journeyId, manualTrigger
|
|
|
898
888
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`);
|
|
899
889
|
}
|
|
900
890
|
const risk = Reflect.get(manualTrigger, "risk");
|
|
901
|
-
if (typeof risk !== "string" ||
|
|
902
|
-
!HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
891
|
+
if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
903
892
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`);
|
|
904
893
|
}
|
|
905
894
|
if (risk !== "read_only" && requiresAcknowledgement !== true) {
|
|
@@ -951,7 +940,39 @@ function isoDurationMs(value) {
|
|
|
951
940
|
const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
|
|
952
941
|
const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
|
|
953
942
|
const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
|
|
954
|
-
return
|
|
943
|
+
return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
|
|
944
|
+
}
|
|
945
|
+
function scheduleRandomizationMs(randomize, fieldPath) {
|
|
946
|
+
const mode = Reflect.get(randomize, "mode");
|
|
947
|
+
switch (mode) {
|
|
948
|
+
case "centered": {
|
|
949
|
+
const maxOffset = Reflect.get(randomize, "maxOffset");
|
|
950
|
+
assertIsoDuration(maxOffset, `${fieldPath}.maxOffset`);
|
|
951
|
+
return isoDurationMs(maxOffset);
|
|
952
|
+
}
|
|
953
|
+
case "delayed": {
|
|
954
|
+
const maxDelay = Reflect.get(randomize, "maxDelay");
|
|
955
|
+
assertIsoDuration(maxDelay, `${fieldPath}.maxDelay`);
|
|
956
|
+
return isoDurationMs(maxDelay);
|
|
957
|
+
}
|
|
958
|
+
default:
|
|
959
|
+
throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
function validateScheduleRandomization(randomize, fieldPath, intervalMs) {
|
|
963
|
+
if (!randomize || typeof randomize !== "object" || Array.isArray(randomize)) {
|
|
964
|
+
throw new ValidationError(`${fieldPath} must be an object.`);
|
|
965
|
+
}
|
|
966
|
+
const mode = Reflect.get(randomize, "mode");
|
|
967
|
+
const allowedFields = mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
|
|
968
|
+
rejectUnknownFields(randomize, allowedFields, fieldPath);
|
|
969
|
+
const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
|
|
970
|
+
if (offsetMs <= 0) {
|
|
971
|
+
throw new ValidationError(`${fieldPath} duration must be positive.`);
|
|
972
|
+
}
|
|
973
|
+
if (offsetMs >= intervalMs) {
|
|
974
|
+
throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
|
|
975
|
+
}
|
|
955
976
|
}
|
|
956
977
|
function assertIsoCountry(value, fieldPath) {
|
|
957
978
|
if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
|
|
@@ -962,11 +983,18 @@ function normalizeIntervalDuration(input) {
|
|
|
962
983
|
const trimmed = input.trim();
|
|
963
984
|
const shorthand = /^(\d+)(s|m|h|d)$/i.exec(trimmed);
|
|
964
985
|
if (shorthand) {
|
|
965
|
-
const
|
|
986
|
+
const durationMs = msDurationMs(trimmed);
|
|
987
|
+
const unit = shorthand[2]?.toLowerCase();
|
|
988
|
+
const amount = unit === "s"
|
|
989
|
+
? durationMs / 1_000
|
|
990
|
+
: unit === "m"
|
|
991
|
+
? durationMs / 60_000
|
|
992
|
+
: unit === "h"
|
|
993
|
+
? durationMs / 3_600_000
|
|
994
|
+
: durationMs / 86_400_000;
|
|
966
995
|
if (!Number.isInteger(amount) || amount <= 0) {
|
|
967
996
|
throw new ValidationError(`Journey schedule interval must be a positive duration.`);
|
|
968
997
|
}
|
|
969
|
-
const unit = shorthand[2]?.toLowerCase();
|
|
970
998
|
if (unit === "s")
|
|
971
999
|
return `PT${amount}S`;
|
|
972
1000
|
if (unit === "m")
|
|
@@ -980,15 +1008,27 @@ function normalizeIntervalDuration(input) {
|
|
|
980
1008
|
return trimmed;
|
|
981
1009
|
}
|
|
982
1010
|
export function every(interval, options = {}) {
|
|
1011
|
+
if (options.jitter !== undefined && options.randomize !== undefined) {
|
|
1012
|
+
throw new ValidationError(`Schedule cannot define both jitter and randomize. Use randomize instead.`);
|
|
1013
|
+
}
|
|
983
1014
|
const schedule = {
|
|
984
1015
|
kind: "interval",
|
|
985
1016
|
interval: normalizeIntervalDuration(interval),
|
|
986
1017
|
};
|
|
1018
|
+
if (options.randomize !== undefined) {
|
|
1019
|
+
schedule.randomize = options.randomize;
|
|
1020
|
+
}
|
|
987
1021
|
if (options.jitter !== undefined) {
|
|
988
1022
|
schedule.jitter = normalizeIntervalDuration(options.jitter);
|
|
989
1023
|
}
|
|
990
1024
|
return schedule;
|
|
991
1025
|
}
|
|
1026
|
+
export function centered(maxOffset) {
|
|
1027
|
+
return { mode: "centered", maxOffset: normalizeIntervalDuration(maxOffset) };
|
|
1028
|
+
}
|
|
1029
|
+
export function delayed(maxDelay) {
|
|
1030
|
+
return { mode: "delayed", maxDelay: normalizeIntervalDuration(maxDelay) };
|
|
1031
|
+
}
|
|
992
1032
|
function countCapturingGroups(pattern) {
|
|
993
1033
|
let count = 0;
|
|
994
1034
|
const source = pattern.source;
|
|
@@ -1010,9 +1050,7 @@ function countCapturingGroups(pattern) {
|
|
|
1010
1050
|
const next = source[i + 1];
|
|
1011
1051
|
if (next === "?" && source[i + 2] !== "<")
|
|
1012
1052
|
continue;
|
|
1013
|
-
if (next === "?" &&
|
|
1014
|
-
source[i + 2] === "<" &&
|
|
1015
|
-
(source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1053
|
+
if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1016
1054
|
continue;
|
|
1017
1055
|
count += 1;
|
|
1018
1056
|
}
|
|
@@ -1090,8 +1128,7 @@ function validateSmsOtpMatcher(matcher, fieldPath) {
|
|
|
1090
1128
|
throw new ValidationError(`${fieldPath}.code.pattern must be a RegExp or pattern source string.`);
|
|
1091
1129
|
}
|
|
1092
1130
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
1093
|
-
if (countCapturingGroups(regex) !== 1 &&
|
|
1094
|
-
Reflect.get(code, "capture") === undefined) {
|
|
1131
|
+
if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
|
|
1095
1132
|
throw new ValidationError(`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`);
|
|
1096
1133
|
}
|
|
1097
1134
|
if (Reflect.get(code, "capture") !== undefined &&
|
|
@@ -1140,9 +1177,17 @@ function validateHealthJourneySchedule(providerId, journeyId, schedule) {
|
|
|
1140
1177
|
rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
|
|
1141
1178
|
if (Reflect.get(schedule, "kind") !== "interval")
|
|
1142
1179
|
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
|
|
1143
|
-
|
|
1180
|
+
const interval = Reflect.get(schedule, "interval");
|
|
1181
|
+
assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
|
|
1182
|
+
const randomize = Reflect.get(schedule, "randomize");
|
|
1183
|
+
if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
|
|
1184
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`);
|
|
1185
|
+
}
|
|
1144
1186
|
if (Reflect.get(schedule, "jitter") !== undefined)
|
|
1145
1187
|
assertIsoDuration(Reflect.get(schedule, "jitter"), `Provider "${providerId}" ${fieldPath}.jitter`);
|
|
1188
|
+
if (randomize !== undefined) {
|
|
1189
|
+
validateScheduleRandomization(randomize, `Provider "${providerId}" ${fieldPath}.randomize`, isoDurationMs(interval));
|
|
1190
|
+
}
|
|
1146
1191
|
}
|
|
1147
1192
|
function validateHealthJourneys(providerId, operations, healthJourneys) {
|
|
1148
1193
|
const covered = new Set();
|
|
@@ -1165,8 +1210,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
|
|
|
1165
1210
|
throw new ValidationError(`Provider "${providerId}" has duplicate health journey id "${journey.id}".`);
|
|
1166
1211
|
journeyIds.add(journey.id);
|
|
1167
1212
|
validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
|
|
1168
|
-
if (!Array.isArray(journey.coversOperations) ||
|
|
1169
|
-
journey.coversOperations.length === 0) {
|
|
1213
|
+
if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
|
|
1170
1214
|
throw new ValidationError(`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`);
|
|
1171
1215
|
}
|
|
1172
1216
|
for (const operationId of journey.coversOperations) {
|
|
@@ -1204,8 +1248,7 @@ function validateHealthJourneys(providerId, operations, healthJourneys) {
|
|
|
1204
1248
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.id must be a kebab-case identifier.`);
|
|
1205
1249
|
if (step.operationId !== undefined && !operations[step.operationId])
|
|
1206
1250
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`);
|
|
1207
|
-
if (step.usesSmsMatcher !== undefined &&
|
|
1208
|
-
!matcherIds.has(step.usesSmsMatcher))
|
|
1251
|
+
if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
|
|
1209
1252
|
throw new ValidationError(`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`);
|
|
1210
1253
|
}
|
|
1211
1254
|
if (journey.manualTrigger !== undefined)
|
|
@@ -1239,9 +1282,7 @@ function validateOperationHealthChecks(providerId, operations, journeyCoveredOpe
|
|
|
1239
1282
|
validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
|
|
1240
1283
|
if (hasUnsupported)
|
|
1241
1284
|
validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
|
|
1242
|
-
if (!hasCheck &&
|
|
1243
|
-
!hasUnsupported &&
|
|
1244
|
-
!journeyCoveredOperations.has(operationName))
|
|
1285
|
+
if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
|
|
1245
1286
|
throw new ValidationError(`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`, {
|
|
1246
1287
|
fix: `Add \`healthCheck: { interval, cases: [...] }\` or \`healthCheckUnsupported: { reason: "..." }\` to operations.${operationName}.`,
|
|
1247
1288
|
});
|
|
@@ -1271,6 +1312,19 @@ function validateOperationFixtures(providerId, operations) {
|
|
|
1271
1312
|
}
|
|
1272
1313
|
}
|
|
1273
1314
|
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Shallow shape guard only: the `deployment` object is passed through
|
|
1317
|
+
* verbatim and deliberately not deep-validated by the SDK — the APIFuse
|
|
1318
|
+
* registry builder owns deployment validation and profile resolution.
|
|
1319
|
+
*/
|
|
1320
|
+
function validateProviderDeployment(providerId, deployment) {
|
|
1321
|
+
if (deployment === undefined)
|
|
1322
|
+
return;
|
|
1323
|
+
if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
|
|
1324
|
+
throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
|
|
1325
|
+
fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1274
1328
|
export function defineProvider(config) {
|
|
1275
1329
|
validateProviderShape(config);
|
|
1276
1330
|
if (!CONNECTOR_ID_REGEX.test(config.id))
|
|
@@ -1278,7 +1332,9 @@ export function defineProvider(config) {
|
|
|
1278
1332
|
fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
|
|
1279
1333
|
});
|
|
1280
1334
|
if (Object.keys(config.operations).length === 0)
|
|
1281
|
-
throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
|
|
1335
|
+
throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
|
|
1336
|
+
fix: "Add at least one operation to the operations object",
|
|
1337
|
+
});
|
|
1282
1338
|
validateOperationIds(config.id, config.operations);
|
|
1283
1339
|
validateOperationAnnotations(config.id, config.operations);
|
|
1284
1340
|
validateOperationObservability(config.id, config.operations);
|
|
@@ -1287,8 +1343,13 @@ export function defineProvider(config) {
|
|
|
1287
1343
|
validateToolRouterMetadata(config.id, config.operations);
|
|
1288
1344
|
const journeyCoveredOperations = validateHealthJourneys(config.id, config.operations, config.healthJourneys);
|
|
1289
1345
|
validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
|
|
1290
|
-
|
|
1346
|
+
if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
|
|
1347
|
+
throw new ValidationError(`Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`, {
|
|
1348
|
+
fix: "Keep healthProbe (the new name) and delete the healthMonitor block.",
|
|
1349
|
+
});
|
|
1350
|
+
validateProviderHealthMonitor(config.id, config.healthProbe ?? config.healthMonitor, config.healthProbe !== undefined ? "healthProbe" : "healthMonitor");
|
|
1291
1351
|
validateOperationFixtures(config.id, config.operations);
|
|
1352
|
+
validateProviderDeployment(config.id, config.deployment);
|
|
1292
1353
|
validateProviderProxy(config);
|
|
1293
1354
|
validateProviderStt(config);
|
|
1294
1355
|
if (config.runtime === "browser" && !config.browser)
|
|
@@ -1301,6 +1362,9 @@ export function defineProvider(config) {
|
|
|
1301
1362
|
id: config.id,
|
|
1302
1363
|
version: config.version,
|
|
1303
1364
|
runtime: config.runtime,
|
|
1365
|
+
// Verbatim passthrough: deployment validation and profile resolution
|
|
1366
|
+
// are owned by the APIFuse registry builder, not the SDK.
|
|
1367
|
+
deployment: config.deployment,
|
|
1304
1368
|
allowedHosts: config.allowedHosts,
|
|
1305
1369
|
stealth: config.stealth,
|
|
1306
1370
|
proxy: config.proxy,
|
|
@@ -1314,7 +1378,10 @@ export function defineProvider(config) {
|
|
|
1314
1378
|
context: config.context,
|
|
1315
1379
|
meta: config.meta,
|
|
1316
1380
|
operations: config.operations,
|
|
1317
|
-
healthMonitor:
|
|
1381
|
+
// Transitional healthMonitor → healthProbe alias: mirror whichever field
|
|
1382
|
+
// was declared onto both so old and new consumers keep working.
|
|
1383
|
+
healthMonitor: config.healthMonitor ?? config.healthProbe,
|
|
1384
|
+
healthProbe: config.healthProbe ?? config.healthMonitor,
|
|
1318
1385
|
healthJourneys: config.healthJourneys,
|
|
1319
1386
|
};
|
|
1320
1387
|
}
|