@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.4
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 +14 -0
- package/CHANGELOG.md +8 -0
- package/bin/apifuse-check.ts +18 -58
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- 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 +23 -61
- package/bin/apifuse-submit-check.ts +73 -30
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/index.d.ts +1 -1
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +1 -1
- package/dist/ceremonies/index.js +8 -18
- package/dist/config/loader.d.ts +2 -2
- package/dist/config/loader.js +19 -42
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +8 -1
- package/dist/define.js +57 -115
- 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 +43 -42
- package/dist/index.js +38 -37
- package/dist/lint.js +8 -15
- package/dist/provider.d.ts +11 -11
- package/dist/provider.js +9 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +1 -1
- package/dist/runtime/auth-flow.js +3 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +12 -34
- 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 +8 -3
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +5 -5
- 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 +5 -15
- 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 +3 -5
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +1 -1
- package/dist/runtime/proxy-telemetry.js +16 -48
- 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/state.d.ts +2 -2
- package/dist/runtime/state.js +3 -3
- package/dist/runtime/stealth.d.ts +2 -2
- package/dist/runtime/stealth.js +6 -6
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +3 -3
- package/dist/server/self-test.js +3 -3
- package/dist/server/serve.d.ts +3 -3
- package/dist/server/serve.js +49 -57
- package/dist/server/types.js +2 -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 +7 -14
- package/dist/types.d.ts +57 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +7 -4
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +24 -78
- package/src/config/loader.ts +58 -176
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +174 -495
- 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 +44 -42
- package/src/lint.ts +89 -159
- package/src/provider.ts +15 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +5 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +55 -129
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +15 -20
- package/src/runtime/http.ts +7 -7
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +19 -58
- 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 +17 -38
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +23 -72
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +4 -13
- package/src/runtime/state.ts +30 -109
- package/src/runtime/stealth.ts +8 -8
- 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 +7 -7
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +5 -5
- package/src/server/serve.ts +103 -252
- package/src/server/types.ts +6 -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 +19 -67
- package/src/types.ts +58 -0
- package/src/user-input.ts +118 -0
package/src/define.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ms from "ms";
|
|
2
2
|
|
|
3
|
-
import { ProviderError, ValidationError } from "./errors";
|
|
4
|
-
import { safeParseSchemaSync } from "./schema";
|
|
3
|
+
import { ProviderError, ValidationError } from "./errors.js";
|
|
4
|
+
import { safeParseSchemaSync } from "./schema.js";
|
|
5
5
|
import type {
|
|
6
6
|
AuthConfig,
|
|
7
7
|
BrowserEngine,
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
OperationWebSocketTransport,
|
|
23
23
|
ProviderAccessConfig,
|
|
24
24
|
ProviderDefinition,
|
|
25
|
+
ProviderDeploymentOverrides,
|
|
25
26
|
ProviderHealthMonitorConfig,
|
|
26
27
|
ProviderProxyConfig,
|
|
27
28
|
ProviderPublicProfile,
|
|
@@ -32,7 +33,7 @@ import type {
|
|
|
32
33
|
SchemaLike,
|
|
33
34
|
SmsOtpMatcherDefinition,
|
|
34
35
|
StealthPlatform,
|
|
35
|
-
} from "./types";
|
|
36
|
+
} from "./types.js";
|
|
36
37
|
import {
|
|
37
38
|
HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX,
|
|
38
39
|
HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN,
|
|
@@ -48,7 +49,7 @@ import {
|
|
|
48
49
|
STREAM_IDLE_TIMEOUT_MS_MIN,
|
|
49
50
|
STREAM_MAX_DURATION_MS_MAX,
|
|
50
51
|
STREAM_MAX_DURATION_MS_MIN,
|
|
51
|
-
} from "./types";
|
|
52
|
+
} from "./types.js";
|
|
52
53
|
|
|
53
54
|
type ProviderImplementationSourceAccess =
|
|
54
55
|
| "official_api"
|
|
@@ -74,23 +75,10 @@ interface ProviderImplementationProfile {
|
|
|
74
75
|
const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
|
|
75
76
|
const OPERATION_ID_REGEX = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/;
|
|
76
77
|
const VALID_RUNTIMES = ["standard", "shared", "browser"] as const;
|
|
77
|
-
const VALID_AUTH_MODES = [
|
|
78
|
-
"none",
|
|
79
|
-
"platform-managed",
|
|
80
|
-
"credentials",
|
|
81
|
-
"oauth2",
|
|
82
|
-
] as const;
|
|
78
|
+
const VALID_AUTH_MODES = ["none", "platform-managed", "credentials", "oauth2"] as const;
|
|
83
79
|
const VALID_PROVIDER_ACCESS_VISIBILITIES = ["public", "early_access"] as const;
|
|
84
|
-
const VALID_PROVIDER_PROXY_MODES = [
|
|
85
|
-
|
|
86
|
-
"optional",
|
|
87
|
-
"required",
|
|
88
|
-
] as const;
|
|
89
|
-
const VALID_PROVIDER_PROXY_PROVIDERS = [
|
|
90
|
-
"smartproxy",
|
|
91
|
-
"decodo",
|
|
92
|
-
"custom",
|
|
93
|
-
] as const;
|
|
80
|
+
const VALID_PROVIDER_PROXY_MODES = ["disabled", "optional", "required"] as const;
|
|
81
|
+
const VALID_PROVIDER_PROXY_PROVIDERS = ["smartproxy", "decodo", "custom"] as const;
|
|
94
82
|
const VALID_PROVIDER_PROXY_AFFINITIES = [
|
|
95
83
|
"request",
|
|
96
84
|
"operation",
|
|
@@ -101,23 +89,9 @@ const VALID_PROVIDER_STT_MODES = ["optional", "required"] as const;
|
|
|
101
89
|
const SMARTPROXY_APP_KEY_SECRET = "APIFUSE__PROXY__SMARTPROXY_APP_KEY";
|
|
102
90
|
const RESERVED_OPERATION_IDS = new Set(["auth", "health"]);
|
|
103
91
|
const MCP_TOOL_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_]{0,127}$/;
|
|
104
|
-
const VALID_OPERATION_RISK_CLASSES = [
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
"destructive",
|
|
108
|
-
"external-send",
|
|
109
|
-
] as const;
|
|
110
|
-
const VALID_OPERATION_APPROVAL_POLICIES = [
|
|
111
|
-
"never",
|
|
112
|
-
"risk-based",
|
|
113
|
-
"always",
|
|
114
|
-
] as const;
|
|
115
|
-
const VALID_OPERATION_TRANSPORT_KINDS = [
|
|
116
|
-
"json",
|
|
117
|
-
"sse",
|
|
118
|
-
"http-stream",
|
|
119
|
-
"websocket",
|
|
120
|
-
] as const;
|
|
92
|
+
const VALID_OPERATION_RISK_CLASSES = ["read", "write", "destructive", "external-send"] as const;
|
|
93
|
+
const VALID_OPERATION_APPROVAL_POLICIES = ["never", "risk-based", "always"] as const;
|
|
94
|
+
const VALID_OPERATION_TRANSPORT_KINDS = ["json", "sse", "http-stream", "websocket"] as const;
|
|
121
95
|
const SSE_EVENT_NAME_REGEX = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
122
96
|
const WEBSOCKET_SUBPROTOCOL_REGEX = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
123
97
|
|
|
@@ -135,18 +109,16 @@ function msDurationMs(value: string): number {
|
|
|
135
109
|
function parsePositiveMsDuration(value: string): number | undefined {
|
|
136
110
|
const trimmed = value.trim();
|
|
137
111
|
if (!MS_DURATION_PATTERN.test(trimmed)) return undefined;
|
|
138
|
-
const parsed = ms(
|
|
139
|
-
(trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue,
|
|
140
|
-
);
|
|
112
|
+
const parsed = ms((trimmed.startsWith("+") ? trimmed.slice(1) : trimmed) as ms.StringValue);
|
|
141
113
|
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
142
114
|
return parsed;
|
|
143
115
|
}
|
|
144
116
|
|
|
145
117
|
type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
|
|
146
|
-
type OperationConfig<
|
|
147
|
-
TInput
|
|
148
|
-
|
|
149
|
-
>
|
|
118
|
+
type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
119
|
+
OperationDefinition<TInput, TOutput>,
|
|
120
|
+
"handler"
|
|
121
|
+
> & {
|
|
150
122
|
handler(
|
|
151
123
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
152
124
|
input: InferSchemaOutput<TInput>,
|
|
@@ -154,59 +126,44 @@ type OperationConfig<
|
|
|
154
126
|
| OperationHandlerResult<InferSchemaOutput<TOutput>>
|
|
155
127
|
| Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
|
|
156
128
|
};
|
|
157
|
-
type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
|
|
164
|
-
: never;
|
|
165
|
-
};
|
|
166
|
-
type StreamOperationConfig<
|
|
167
|
-
TInput extends SchemaLike,
|
|
168
|
-
TOutput extends SchemaLike,
|
|
169
|
-
> =
|
|
129
|
+
type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
|
|
130
|
+
[K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
|
|
131
|
+
? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
|
|
132
|
+
: never;
|
|
133
|
+
};
|
|
134
|
+
type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
|
|
170
135
|
| SseOperationConfig<TInput, TOutput>
|
|
171
136
|
| HttpStreamOperationConfig<TInput, TOutput>
|
|
172
137
|
| WebSocketOperationConfig<TInput, TOutput>;
|
|
173
|
-
type SseOperationConfig<
|
|
174
|
-
TInput
|
|
175
|
-
|
|
176
|
-
>
|
|
138
|
+
type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
139
|
+
OperationConfig<TInput, TOutput>,
|
|
140
|
+
"handler" | "transport"
|
|
141
|
+
> & {
|
|
177
142
|
transport: OperationSseTransport;
|
|
178
143
|
handler(
|
|
179
144
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
180
145
|
input: InferSchemaOutput<TInput>,
|
|
181
|
-
):
|
|
182
|
-
| AsyncIterable<ProviderStreamEvent>
|
|
183
|
-
| Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
146
|
+
): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
|
|
184
147
|
};
|
|
185
|
-
type HttpStreamOperationConfig<
|
|
186
|
-
TInput
|
|
187
|
-
|
|
188
|
-
>
|
|
148
|
+
type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
149
|
+
OperationConfig<TInput, TOutput>,
|
|
150
|
+
"handler" | "transport"
|
|
151
|
+
> & {
|
|
189
152
|
transport: OperationHttpStreamTransport;
|
|
190
153
|
handler(
|
|
191
154
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
192
155
|
input: InferSchemaOutput<TInput>,
|
|
193
|
-
):
|
|
194
|
-
| Response
|
|
195
|
-
| ReadableStream<Uint8Array>
|
|
196
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
156
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
197
157
|
};
|
|
198
|
-
type WebSocketOperationConfig<
|
|
199
|
-
TInput
|
|
200
|
-
|
|
201
|
-
>
|
|
158
|
+
type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
|
|
159
|
+
OperationConfig<TInput, TOutput>,
|
|
160
|
+
"handler" | "transport"
|
|
161
|
+
> & {
|
|
202
162
|
transport: OperationWebSocketTransport;
|
|
203
163
|
handler(
|
|
204
164
|
ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
|
|
205
165
|
input: InferSchemaOutput<TInput>,
|
|
206
|
-
):
|
|
207
|
-
| Response
|
|
208
|
-
| ReadableStream<Uint8Array>
|
|
209
|
-
| Promise<Response | ReadableStream<Uint8Array>>;
|
|
166
|
+
): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
|
|
210
167
|
};
|
|
211
168
|
|
|
212
169
|
type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
@@ -221,12 +178,17 @@ type AuthStartNoInputGuard<TConfig> = TConfig extends {
|
|
|
221
178
|
: unknown
|
|
222
179
|
: unknown;
|
|
223
180
|
|
|
224
|
-
export interface ProviderConfig<
|
|
225
|
-
TOperations extends Record<string, ProviderOperation>,
|
|
226
|
-
> {
|
|
181
|
+
export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
|
|
227
182
|
id: string;
|
|
228
183
|
version: string;
|
|
229
184
|
runtime: "standard" | "shared" | "browser";
|
|
185
|
+
/**
|
|
186
|
+
* Optional deployment overrides, passed through verbatim onto the returned
|
|
187
|
+
* provider definition. The SDK types this field but does not deep-validate
|
|
188
|
+
* it — the APIFuse registry builder owns deployment validation and
|
|
189
|
+
* resolves omitted fields against the runtime deployment profiles.
|
|
190
|
+
*/
|
|
191
|
+
deployment?: ProviderDeploymentOverrides;
|
|
230
192
|
allowedHosts?: string[];
|
|
231
193
|
stealth?: {
|
|
232
194
|
profile: string;
|
|
@@ -270,35 +232,24 @@ export interface ProviderConfig<
|
|
|
270
232
|
}
|
|
271
233
|
|
|
272
234
|
/** Define one provider operation with schema-driven handler inference. */
|
|
273
|
-
export function defineOperation<
|
|
274
|
-
TInput extends SchemaLike,
|
|
275
|
-
TOutput extends SchemaLike,
|
|
276
|
-
>(
|
|
235
|
+
export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
277
236
|
operation: OperationConfig<TInput, TOutput>,
|
|
278
237
|
): OperationDefinition<TInput, TOutput> {
|
|
279
238
|
return operation;
|
|
280
239
|
}
|
|
281
240
|
|
|
282
241
|
/** Define a non-JSON provider operation with explicit transport metadata. */
|
|
283
|
-
export function defineStreamOperation<
|
|
284
|
-
TInput extends SchemaLike,
|
|
285
|
-
TOutput extends SchemaLike,
|
|
286
|
-
>(
|
|
242
|
+
export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
|
|
287
243
|
operation: StreamOperationConfig<TInput, TOutput>,
|
|
288
244
|
): OperationDefinition<TInput, TOutput> {
|
|
289
245
|
return operation;
|
|
290
246
|
}
|
|
291
247
|
|
|
292
|
-
function assertObjectConfig(
|
|
293
|
-
value: unknown,
|
|
294
|
-
): asserts value is Record<string, unknown> {
|
|
248
|
+
function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
|
|
295
249
|
if (!value || typeof value !== "object") {
|
|
296
|
-
throw new ProviderError(
|
|
297
|
-
"defineProvider
|
|
298
|
-
|
|
299
|
-
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
300
|
-
},
|
|
301
|
-
);
|
|
250
|
+
throw new ProviderError("defineProvider config must be an object. Offending field: config", {
|
|
251
|
+
fix: "Pass defineProvider({ id, version, runtime, meta, operations })",
|
|
252
|
+
});
|
|
302
253
|
}
|
|
303
254
|
}
|
|
304
255
|
function assertRequiredField(
|
|
@@ -336,25 +287,10 @@ function validateProviderShape(config: unknown): void {
|
|
|
336
287
|
assertRequiredField(config, "meta", String(config.id));
|
|
337
288
|
assertRequiredField(config, "operations", String(config.id));
|
|
338
289
|
if (typeof config.runtime === "string")
|
|
339
|
-
assertLiteralField(
|
|
340
|
-
config.runtime,
|
|
341
|
-
"runtime",
|
|
342
|
-
VALID_RUNTIMES,
|
|
343
|
-
String(config.id),
|
|
344
|
-
);
|
|
290
|
+
assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
|
|
345
291
|
const auth = config.auth;
|
|
346
|
-
if (
|
|
347
|
-
auth
|
|
348
|
-
typeof auth === "object" &&
|
|
349
|
-
"mode" in auth &&
|
|
350
|
-
typeof auth.mode === "string"
|
|
351
|
-
)
|
|
352
|
-
assertLiteralField(
|
|
353
|
-
auth.mode,
|
|
354
|
-
"auth.mode",
|
|
355
|
-
VALID_AUTH_MODES,
|
|
356
|
-
String(config.id),
|
|
357
|
-
);
|
|
292
|
+
if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
|
|
293
|
+
assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
|
|
358
294
|
if (auth && typeof auth === "object" && "exchange" in auth) {
|
|
359
295
|
throw new ProviderError(
|
|
360
296
|
`Provider "${String(config.id)}" auth.exchange is not part of the Provider SDK auth contract`,
|
|
@@ -390,9 +326,7 @@ function validateProviderShape(config: unknown): void {
|
|
|
390
326
|
},
|
|
391
327
|
);
|
|
392
328
|
}
|
|
393
|
-
const accessRecord: Record<string, unknown> = Object.fromEntries(
|
|
394
|
-
Object.entries(access),
|
|
395
|
-
);
|
|
329
|
+
const accessRecord: Record<string, unknown> = Object.fromEntries(Object.entries(access));
|
|
396
330
|
for (const key of Object.keys(accessRecord)) {
|
|
397
331
|
if (key !== "visibility") {
|
|
398
332
|
throw new ValidationError(`Unknown field "${key}" on access.`, {
|
|
@@ -437,31 +371,13 @@ function validateProviderProxy(config: {
|
|
|
437
371
|
},
|
|
438
372
|
);
|
|
439
373
|
}
|
|
440
|
-
rejectUnknownFields(
|
|
441
|
-
|
|
442
|
-
new Set(["mode", "provider", "geo", "session"]),
|
|
443
|
-
"proxy",
|
|
444
|
-
);
|
|
445
|
-
assertLiteralField(
|
|
446
|
-
proxy.mode,
|
|
447
|
-
"proxy.mode",
|
|
448
|
-
VALID_PROVIDER_PROXY_MODES,
|
|
449
|
-
config.id,
|
|
450
|
-
);
|
|
374
|
+
rejectUnknownFields(proxy, new Set(["mode", "provider", "geo", "session"]), "proxy");
|
|
375
|
+
assertLiteralField(proxy.mode, "proxy.mode", VALID_PROVIDER_PROXY_MODES, config.id);
|
|
451
376
|
if (proxy.provider !== undefined) {
|
|
452
|
-
assertLiteralField(
|
|
453
|
-
proxy.provider,
|
|
454
|
-
"proxy.provider",
|
|
455
|
-
VALID_PROVIDER_PROXY_PROVIDERS,
|
|
456
|
-
config.id,
|
|
457
|
-
);
|
|
377
|
+
assertLiteralField(proxy.provider, "proxy.provider", VALID_PROVIDER_PROXY_PROVIDERS, config.id);
|
|
458
378
|
}
|
|
459
379
|
if (proxy.geo !== undefined) {
|
|
460
|
-
if (
|
|
461
|
-
!proxy.geo ||
|
|
462
|
-
typeof proxy.geo !== "object" ||
|
|
463
|
-
Array.isArray(proxy.geo)
|
|
464
|
-
) {
|
|
380
|
+
if (!proxy.geo || typeof proxy.geo !== "object" || Array.isArray(proxy.geo)) {
|
|
465
381
|
throw new ValidationError(
|
|
466
382
|
`Provider "${config.id}" has invalid proxy.geo: must be an object.`,
|
|
467
383
|
{
|
|
@@ -469,11 +385,7 @@ function validateProviderProxy(config: {
|
|
|
469
385
|
},
|
|
470
386
|
);
|
|
471
387
|
}
|
|
472
|
-
rejectUnknownFields(
|
|
473
|
-
proxy.geo,
|
|
474
|
-
new Set(["country", "subdivision", "city"]),
|
|
475
|
-
"proxy.geo",
|
|
476
|
-
);
|
|
388
|
+
rejectUnknownFields(proxy.geo, new Set(["country", "subdivision", "city"]), "proxy.geo");
|
|
477
389
|
if (proxy.geo.country !== undefined) {
|
|
478
390
|
assertIsoCountry(proxy.geo.country, "proxy.geo.country");
|
|
479
391
|
}
|
|
@@ -487,11 +399,7 @@ function validateProviderProxy(config: {
|
|
|
487
399
|
}
|
|
488
400
|
}
|
|
489
401
|
if (proxy.session !== undefined) {
|
|
490
|
-
if (
|
|
491
|
-
!proxy.session ||
|
|
492
|
-
typeof proxy.session !== "object" ||
|
|
493
|
-
Array.isArray(proxy.session)
|
|
494
|
-
) {
|
|
402
|
+
if (!proxy.session || typeof proxy.session !== "object" || Array.isArray(proxy.session)) {
|
|
495
403
|
throw new ValidationError(
|
|
496
404
|
`Provider "${config.id}" has invalid proxy.session: must be an object.`,
|
|
497
405
|
{
|
|
@@ -513,19 +421,13 @@ function validateProviderProxy(config: {
|
|
|
513
421
|
);
|
|
514
422
|
}
|
|
515
423
|
const lifetime = proxy.session.lifetimeMinutes;
|
|
516
|
-
if (
|
|
517
|
-
lifetime !== undefined &&
|
|
518
|
-
(!Number.isFinite(lifetime) || lifetime <= 0)
|
|
519
|
-
) {
|
|
424
|
+
if (lifetime !== undefined && (!Number.isFinite(lifetime) || lifetime <= 0)) {
|
|
520
425
|
throw new ValidationError(
|
|
521
426
|
`Provider "${config.id}" has invalid proxy.session.lifetimeMinutes: must be a positive number of minutes.`,
|
|
522
427
|
);
|
|
523
428
|
}
|
|
524
429
|
const poolSize = proxy.session.poolSize;
|
|
525
|
-
if (
|
|
526
|
-
poolSize !== undefined &&
|
|
527
|
-
(!Number.isInteger(poolSize) || poolSize <= 0)
|
|
528
|
-
) {
|
|
430
|
+
if (poolSize !== undefined && (!Number.isInteger(poolSize) || poolSize <= 0)) {
|
|
529
431
|
throw new ValidationError(
|
|
530
432
|
`Provider "${config.id}" has invalid proxy.session.poolSize: must be a positive integer.`,
|
|
531
433
|
);
|
|
@@ -533,8 +435,7 @@ function validateProviderProxy(config: {
|
|
|
533
435
|
}
|
|
534
436
|
if (proxy.mode === "required" && proxy.provider === "smartproxy") {
|
|
535
437
|
const hasSmartproxySecret = config.secrets?.some(
|
|
536
|
-
(secret) =>
|
|
537
|
-
secret.name === SMARTPROXY_APP_KEY_SECRET && secret.required !== false,
|
|
438
|
+
(secret) => secret.name === SMARTPROXY_APP_KEY_SECRET && secret.required !== false,
|
|
538
439
|
);
|
|
539
440
|
if (!hasSmartproxySecret) {
|
|
540
441
|
throw new ValidationError(
|
|
@@ -547,17 +448,13 @@ function validateProviderProxy(config: {
|
|
|
547
448
|
}
|
|
548
449
|
}
|
|
549
450
|
|
|
550
|
-
function validateProviderStt(config: {
|
|
551
|
-
id: string;
|
|
552
|
-
stt?: ProviderSttConfig;
|
|
553
|
-
}): void {
|
|
451
|
+
function validateProviderStt(config: { id: string; stt?: ProviderSttConfig }): void {
|
|
554
452
|
const stt = config.stt;
|
|
555
453
|
if (stt === undefined) return;
|
|
556
454
|
if (!stt || typeof stt !== "object" || Array.isArray(stt)) {
|
|
557
|
-
throw new ValidationError(
|
|
558
|
-
`
|
|
559
|
-
|
|
560
|
-
);
|
|
455
|
+
throw new ValidationError(`Provider "${config.id}" has invalid stt: must be an object.`, {
|
|
456
|
+
fix: `Use stt: { mode: "required" } or stt: { mode: "optional" }.`,
|
|
457
|
+
});
|
|
561
458
|
}
|
|
562
459
|
rejectUnknownFields(stt, new Set(["mode"]), "stt");
|
|
563
460
|
assertLiteralField(stt.mode, "stt.mode", VALID_PROVIDER_STT_MODES, config.id);
|
|
@@ -584,16 +481,9 @@ function validateOperationIds(
|
|
|
584
481
|
);
|
|
585
482
|
}
|
|
586
483
|
}
|
|
587
|
-
const OPERATION_CONTRACT_VERSION_REGEX =
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
/^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
591
|
-
const VALID_OPERATION_LIFECYCLES = [
|
|
592
|
-
"stable",
|
|
593
|
-
"beta",
|
|
594
|
-
"deprecated",
|
|
595
|
-
"removed",
|
|
596
|
-
] as const;
|
|
484
|
+
const OPERATION_CONTRACT_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
485
|
+
const OPERATION_SENSITIVE_PATH_REGEX = /^(?:[A-Za-z0-9_$-]+|\*)(?:\.(?:[A-Za-z0-9_$-]+|\*))*$/;
|
|
486
|
+
const VALID_OPERATION_LIFECYCLES = ["stable", "beta", "deprecated", "removed"] as const;
|
|
597
487
|
|
|
598
488
|
function assertNonEmptyString(
|
|
599
489
|
value: unknown,
|
|
@@ -624,10 +514,7 @@ function validateToolRouterMetadata(
|
|
|
624
514
|
},
|
|
625
515
|
);
|
|
626
516
|
}
|
|
627
|
-
if (
|
|
628
|
-
toolRouter.name !== undefined &&
|
|
629
|
-
!MCP_TOOL_NAME_REGEX.test(toolRouter.name)
|
|
630
|
-
) {
|
|
517
|
+
if (toolRouter.name !== undefined && !MCP_TOOL_NAME_REGEX.test(toolRouter.name)) {
|
|
631
518
|
throw new ValidationError(
|
|
632
519
|
`Provider "${providerId}" operation "${operationName}" has invalid operations.${operationName}.toolRouter.name: expected an MCP-safe name.`,
|
|
633
520
|
{
|
|
@@ -699,10 +586,7 @@ function validateOperationContracts(
|
|
|
699
586
|
providerId,
|
|
700
587
|
);
|
|
701
588
|
}
|
|
702
|
-
if (
|
|
703
|
-
contract.lifecycle === "deprecated" ||
|
|
704
|
-
contract.lifecycle === "removed"
|
|
705
|
-
) {
|
|
589
|
+
if (contract.lifecycle === "deprecated" || contract.lifecycle === "removed") {
|
|
706
590
|
if (!contract.deprecation || typeof contract.deprecation !== "object") {
|
|
707
591
|
throw new ValidationError(
|
|
708
592
|
`Provider "${providerId}" operation "${operationName}" is ${contract.lifecycle} but lacks operations.${operationName}.contract.deprecation metadata.`,
|
|
@@ -750,10 +634,7 @@ function validateOperationAnnotations(
|
|
|
750
634
|
fix: `Set ${field} to an integer in [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] (milliseconds).`,
|
|
751
635
|
},
|
|
752
636
|
);
|
|
753
|
-
if (
|
|
754
|
-
timeoutMs < OPERATION_TIMEOUT_MS_MIN ||
|
|
755
|
-
timeoutMs > OPERATION_TIMEOUT_MS_MAX
|
|
756
|
-
)
|
|
637
|
+
if (timeoutMs < OPERATION_TIMEOUT_MS_MIN || timeoutMs > OPERATION_TIMEOUT_MS_MAX)
|
|
757
638
|
throw new ValidationError(
|
|
758
639
|
`Provider "${providerId}" has invalid ${field}: ${timeoutMs} is outside [${OPERATION_TIMEOUT_MS_MIN}, ${OPERATION_TIMEOUT_MS_MAX}] ms.`,
|
|
759
640
|
{
|
|
@@ -951,12 +832,7 @@ function validateOperationTransports(
|
|
|
951
832
|
},
|
|
952
833
|
);
|
|
953
834
|
}
|
|
954
|
-
assertLiteralField(
|
|
955
|
-
kind,
|
|
956
|
-
`${fieldPath}.kind`,
|
|
957
|
-
VALID_OPERATION_TRANSPORT_KINDS,
|
|
958
|
-
providerId,
|
|
959
|
-
);
|
|
835
|
+
assertLiteralField(kind, `${fieldPath}.kind`, VALID_OPERATION_TRANSPORT_KINDS, providerId);
|
|
960
836
|
|
|
961
837
|
switch (kind) {
|
|
962
838
|
case "json":
|
|
@@ -988,16 +864,9 @@ function validateOperationTransports(
|
|
|
988
864
|
STREAM_MAX_DURATION_MS_MAX,
|
|
989
865
|
"max duration",
|
|
990
866
|
);
|
|
991
|
-
assertPositiveBytes(
|
|
992
|
-
Reflect.get(transport, "maxEventBytes"),
|
|
993
|
-
`${fieldPath}.maxEventBytes`,
|
|
994
|
-
);
|
|
867
|
+
assertPositiveBytes(Reflect.get(transport, "maxEventBytes"), `${fieldPath}.maxEventBytes`);
|
|
995
868
|
const resumable = Reflect.get(transport, "resumable");
|
|
996
|
-
if (
|
|
997
|
-
resumable !== undefined &&
|
|
998
|
-
resumable !== false &&
|
|
999
|
-
resumable !== "last-event-id"
|
|
1000
|
-
) {
|
|
869
|
+
if (resumable !== undefined && resumable !== false && resumable !== "last-event-id") {
|
|
1001
870
|
throw new ValidationError(
|
|
1002
871
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.resumable: expected false or "last-event-id".`,
|
|
1003
872
|
{
|
|
@@ -1017,12 +886,7 @@ function validateOperationTransports(
|
|
|
1017
886
|
rejectUnknownFields(transport, HTTP_STREAM_TRANSPORT_FIELDS, fieldPath);
|
|
1018
887
|
const contentType = Reflect.get(transport, "contentType");
|
|
1019
888
|
if (contentType !== undefined) {
|
|
1020
|
-
assertNonEmptyString(
|
|
1021
|
-
contentType,
|
|
1022
|
-
`${fieldPath}.contentType`,
|
|
1023
|
-
providerId,
|
|
1024
|
-
operationName,
|
|
1025
|
-
);
|
|
889
|
+
assertNonEmptyString(contentType, `${fieldPath}.contentType`, providerId, operationName);
|
|
1026
890
|
}
|
|
1027
891
|
assertStreamMs(
|
|
1028
892
|
Reflect.get(transport, "idleTimeoutMs"),
|
|
@@ -1038,10 +902,7 @@ function validateOperationTransports(
|
|
|
1038
902
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1039
903
|
"max duration",
|
|
1040
904
|
);
|
|
1041
|
-
assertPositiveBytes(
|
|
1042
|
-
Reflect.get(transport, "maxChunkBytes"),
|
|
1043
|
-
`${fieldPath}.maxChunkBytes`,
|
|
1044
|
-
);
|
|
905
|
+
assertPositiveBytes(Reflect.get(transport, "maxChunkBytes"), `${fieldPath}.maxChunkBytes`);
|
|
1045
906
|
break;
|
|
1046
907
|
}
|
|
1047
908
|
case "websocket": {
|
|
@@ -1066,10 +927,7 @@ function validateOperationTransports(
|
|
|
1066
927
|
);
|
|
1067
928
|
}
|
|
1068
929
|
for (const subprotocol of subprotocols) {
|
|
1069
|
-
if (
|
|
1070
|
-
typeof subprotocol !== "string" ||
|
|
1071
|
-
!WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)
|
|
1072
|
-
) {
|
|
930
|
+
if (typeof subprotocol !== "string" || !WEBSOCKET_SUBPROTOCOL_REGEX.test(subprotocol)) {
|
|
1073
931
|
throw new ValidationError(
|
|
1074
932
|
`Provider "${providerId}" operation "${operationName}" has invalid ${fieldPath}.subprotocols: each subprotocol must be an RFC token string.`,
|
|
1075
933
|
{
|
|
@@ -1093,10 +951,7 @@ function validateOperationTransports(
|
|
|
1093
951
|
STREAM_MAX_DURATION_MS_MAX,
|
|
1094
952
|
"max duration",
|
|
1095
953
|
);
|
|
1096
|
-
assertPositiveBytes(
|
|
1097
|
-
Reflect.get(transport, "maxFrameBytes"),
|
|
1098
|
-
`${fieldPath}.maxFrameBytes`,
|
|
1099
|
-
);
|
|
954
|
+
assertPositiveBytes(Reflect.get(transport, "maxFrameBytes"), `${fieldPath}.maxFrameBytes`);
|
|
1100
955
|
break;
|
|
1101
956
|
}
|
|
1102
957
|
}
|
|
@@ -1159,10 +1014,7 @@ function levenshtein(a: string, b: string): number {
|
|
|
1159
1014
|
return prev[n] ?? 0;
|
|
1160
1015
|
}
|
|
1161
1016
|
|
|
1162
|
-
function suggestField(
|
|
1163
|
-
unknown: string,
|
|
1164
|
-
candidates: ReadonlySet<string>,
|
|
1165
|
-
): string | undefined {
|
|
1017
|
+
function suggestField(unknown: string, candidates: ReadonlySet<string>): string | undefined {
|
|
1166
1018
|
let best: string | undefined;
|
|
1167
1019
|
let bestDist = 3;
|
|
1168
1020
|
for (const candidate of candidates) {
|
|
@@ -1175,11 +1027,7 @@ function suggestField(
|
|
|
1175
1027
|
return best;
|
|
1176
1028
|
}
|
|
1177
1029
|
|
|
1178
|
-
function rejectUnknownFields(
|
|
1179
|
-
value: object,
|
|
1180
|
-
allowed: ReadonlySet<string>,
|
|
1181
|
-
fieldPath: string,
|
|
1182
|
-
): void {
|
|
1030
|
+
function rejectUnknownFields(value: object, allowed: ReadonlySet<string>, fieldPath: string): void {
|
|
1183
1031
|
for (const key of Object.keys(value)) {
|
|
1184
1032
|
if (allowed.has(key)) continue;
|
|
1185
1033
|
const hint = suggestField(key, allowed);
|
|
@@ -1218,23 +1066,12 @@ function validateProviderHealthMonitor(
|
|
|
1218
1066
|
field: "healthMonitor" | "healthProbe" = "healthMonitor",
|
|
1219
1067
|
): void {
|
|
1220
1068
|
if (healthMonitor === undefined) return;
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
)
|
|
1226
|
-
throw new ValidationError(
|
|
1227
|
-
`Provider "${providerId}" has invalid ${field}: must be an object.`,
|
|
1228
|
-
{
|
|
1229
|
-
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
1230
|
-
},
|
|
1231
|
-
);
|
|
1069
|
+
if (!healthMonitor || typeof healthMonitor !== "object" || Array.isArray(healthMonitor))
|
|
1070
|
+
throw new ValidationError(`Provider "${providerId}" has invalid ${field}: must be an object.`, {
|
|
1071
|
+
fix: `Set ${field} to { requiredSecrets?: string[]; serviceAccount?: string }`,
|
|
1072
|
+
});
|
|
1232
1073
|
const healthMonitorRecord = Object.fromEntries(Object.entries(healthMonitor));
|
|
1233
|
-
rejectUnknownFields(
|
|
1234
|
-
healthMonitorRecord,
|
|
1235
|
-
PROVIDER_HEALTH_MONITOR_FIELDS,
|
|
1236
|
-
field,
|
|
1237
|
-
);
|
|
1074
|
+
rejectUnknownFields(healthMonitorRecord, PROVIDER_HEALTH_MONITOR_FIELDS, field);
|
|
1238
1075
|
if (healthMonitorRecord.defaultProbeTimeoutMs !== undefined) {
|
|
1239
1076
|
assertBoundedIntegerMs(
|
|
1240
1077
|
healthMonitorRecord.defaultProbeTimeoutMs,
|
|
@@ -1302,11 +1139,7 @@ function validateProviderHealthMonitor(
|
|
|
1302
1139
|
|
|
1303
1140
|
const probeOverrides = healthMonitorRecord.probeOverrides;
|
|
1304
1141
|
if (probeOverrides !== undefined) {
|
|
1305
|
-
if (
|
|
1306
|
-
!probeOverrides ||
|
|
1307
|
-
typeof probeOverrides !== "object" ||
|
|
1308
|
-
Array.isArray(probeOverrides)
|
|
1309
|
-
)
|
|
1142
|
+
if (!probeOverrides || typeof probeOverrides !== "object" || Array.isArray(probeOverrides))
|
|
1310
1143
|
throw new ValidationError(
|
|
1311
1144
|
`Provider "${providerId}" has invalid ${field}.probeOverrides: must be an object keyed by probe id.`,
|
|
1312
1145
|
);
|
|
@@ -1372,14 +1205,8 @@ function validateHealthCheckCase(
|
|
|
1372
1205
|
): void {
|
|
1373
1206
|
const fieldPath = `operations.${operationName}.healthCheck.cases[${caseIndex}]`;
|
|
1374
1207
|
if (!caseValue || typeof caseValue !== "object" || Array.isArray(caseValue))
|
|
1375
|
-
throw new ValidationError(
|
|
1376
|
-
|
|
1377
|
-
);
|
|
1378
|
-
rejectUnknownFields(
|
|
1379
|
-
caseValue as Record<string, unknown>,
|
|
1380
|
-
HEALTH_CHECK_CASE_FIELDS,
|
|
1381
|
-
fieldPath,
|
|
1382
|
-
);
|
|
1208
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1209
|
+
rejectUnknownFields(caseValue as Record<string, unknown>, HEALTH_CHECK_CASE_FIELDS, fieldPath);
|
|
1383
1210
|
const c = caseValue as HealthCheckCase;
|
|
1384
1211
|
if (typeof c.name !== "string" || c.name.length === 0)
|
|
1385
1212
|
throw new ValidationError(
|
|
@@ -1407,15 +1234,11 @@ function validateHealthCheckCase(
|
|
|
1407
1234
|
`Provider "${providerId}" ${fieldPath}.degradedThresholdMs must be an integer degraded threshold in [${HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN}, ${HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX}] ms.`,
|
|
1408
1235
|
);
|
|
1409
1236
|
if (c.timeoutMs !== undefined) {
|
|
1410
|
-
assertBoundedIntegerMs(
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1416
|
-
label: "timeout",
|
|
1417
|
-
},
|
|
1418
|
-
);
|
|
1237
|
+
assertBoundedIntegerMs(c.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
|
|
1238
|
+
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1239
|
+
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1240
|
+
label: "timeout",
|
|
1241
|
+
});
|
|
1419
1242
|
}
|
|
1420
1243
|
if (
|
|
1421
1244
|
c.expectedStatus !== undefined &&
|
|
@@ -1431,21 +1254,11 @@ function validateHealthCheckCase(
|
|
|
1431
1254
|
);
|
|
1432
1255
|
}
|
|
1433
1256
|
|
|
1434
|
-
function validateHealthCheckSuite(
|
|
1435
|
-
providerId: string,
|
|
1436
|
-
operationName: string,
|
|
1437
|
-
suite: unknown,
|
|
1438
|
-
): void {
|
|
1257
|
+
function validateHealthCheckSuite(providerId: string, operationName: string, suite: unknown): void {
|
|
1439
1258
|
const fieldPath = `operations.${operationName}.healthCheck`;
|
|
1440
1259
|
if (!suite || typeof suite !== "object" || Array.isArray(suite))
|
|
1441
|
-
throw new ValidationError(
|
|
1442
|
-
|
|
1443
|
-
);
|
|
1444
|
-
rejectUnknownFields(
|
|
1445
|
-
suite as Record<string, unknown>,
|
|
1446
|
-
HEALTH_CHECK_SUITE_FIELDS,
|
|
1447
|
-
fieldPath,
|
|
1448
|
-
);
|
|
1260
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1261
|
+
rejectUnknownFields(suite as Record<string, unknown>, HEALTH_CHECK_SUITE_FIELDS, fieldPath);
|
|
1449
1262
|
const s = suite as HealthCheckSuite;
|
|
1450
1263
|
if (!isPositiveMsDurationString(s.interval))
|
|
1451
1264
|
throw new ValidationError(
|
|
@@ -1455,11 +1268,7 @@ function validateHealthCheckSuite(
|
|
|
1455
1268
|
},
|
|
1456
1269
|
);
|
|
1457
1270
|
if (s.schedule !== undefined) {
|
|
1458
|
-
if (
|
|
1459
|
-
!s.schedule ||
|
|
1460
|
-
typeof s.schedule !== "object" ||
|
|
1461
|
-
Array.isArray(s.schedule)
|
|
1462
|
-
) {
|
|
1271
|
+
if (!s.schedule || typeof s.schedule !== "object" || Array.isArray(s.schedule)) {
|
|
1463
1272
|
throw new ValidationError(
|
|
1464
1273
|
`Provider "${providerId}" ${fieldPath}.schedule must be an object.`,
|
|
1465
1274
|
);
|
|
@@ -1469,11 +1278,7 @@ function validateHealthCheckSuite(
|
|
|
1469
1278
|
`Provider "${providerId}" ${fieldPath}.schedule.jitter is not supported for operation healthCheck schedules. Use schedule.randomize instead.`,
|
|
1470
1279
|
);
|
|
1471
1280
|
}
|
|
1472
|
-
rejectUnknownFields(
|
|
1473
|
-
s.schedule,
|
|
1474
|
-
new Set(["randomize"]),
|
|
1475
|
-
`${fieldPath}.schedule`,
|
|
1476
|
-
);
|
|
1281
|
+
rejectUnknownFields(s.schedule, new Set(["randomize"]), `${fieldPath}.schedule`);
|
|
1477
1282
|
const randomize = Reflect.get(s.schedule, "randomize");
|
|
1478
1283
|
if (randomize !== undefined) {
|
|
1479
1284
|
validateScheduleRandomization(
|
|
@@ -1484,15 +1289,11 @@ function validateHealthCheckSuite(
|
|
|
1484
1289
|
}
|
|
1485
1290
|
}
|
|
1486
1291
|
if (s.timeoutMs !== undefined) {
|
|
1487
|
-
assertBoundedIntegerMs(
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1493
|
-
label: "timeout",
|
|
1494
|
-
},
|
|
1495
|
-
);
|
|
1292
|
+
assertBoundedIntegerMs(s.timeoutMs, `Provider "${providerId}" ${fieldPath}.timeoutMs`, {
|
|
1293
|
+
min: HEALTH_CHECK_TIMEOUT_MS_MIN,
|
|
1294
|
+
max: HEALTH_CHECK_TIMEOUT_MS_MAX,
|
|
1295
|
+
label: "timeout",
|
|
1296
|
+
});
|
|
1496
1297
|
}
|
|
1497
1298
|
if (s.degradedThresholdMs !== undefined) {
|
|
1498
1299
|
assertBoundedIntegerMs(
|
|
@@ -1505,10 +1306,7 @@ function validateHealthCheckSuite(
|
|
|
1505
1306
|
},
|
|
1506
1307
|
);
|
|
1507
1308
|
}
|
|
1508
|
-
if (
|
|
1509
|
-
s.requiresConnection !== undefined &&
|
|
1510
|
-
typeof s.requiresConnection !== "boolean"
|
|
1511
|
-
)
|
|
1309
|
+
if (s.requiresConnection !== undefined && typeof s.requiresConnection !== "boolean")
|
|
1512
1310
|
throw new ValidationError(
|
|
1513
1311
|
`Provider "${providerId}" ${fieldPath}.requiresConnection must be a boolean.`,
|
|
1514
1312
|
);
|
|
@@ -1540,14 +1338,8 @@ function validateHealthCheckUnsupported(
|
|
|
1540
1338
|
unsupported: unknown,
|
|
1541
1339
|
): void {
|
|
1542
1340
|
const fieldPath = `operations.${operationName}.healthCheckUnsupported`;
|
|
1543
|
-
if (
|
|
1544
|
-
|
|
1545
|
-
typeof unsupported !== "object" ||
|
|
1546
|
-
Array.isArray(unsupported)
|
|
1547
|
-
)
|
|
1548
|
-
throw new ValidationError(
|
|
1549
|
-
`Provider "${providerId}" ${fieldPath} must be an object.`,
|
|
1550
|
-
);
|
|
1341
|
+
if (!unsupported || typeof unsupported !== "object" || Array.isArray(unsupported))
|
|
1342
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
1551
1343
|
rejectUnknownFields(
|
|
1552
1344
|
unsupported as Record<string, unknown>,
|
|
1553
1345
|
HEALTH_CHECK_UNSUPPORTED_FIELDS,
|
|
@@ -1581,12 +1373,7 @@ const HEALTH_JOURNEY_FIELDS = new Set([
|
|
|
1581
1373
|
"steps",
|
|
1582
1374
|
"run",
|
|
1583
1375
|
]);
|
|
1584
|
-
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set([
|
|
1585
|
-
"kind",
|
|
1586
|
-
"interval",
|
|
1587
|
-
"jitter",
|
|
1588
|
-
"randomize",
|
|
1589
|
-
]);
|
|
1376
|
+
const HEALTH_JOURNEY_SCHEDULE_FIELDS = new Set(["kind", "interval", "jitter", "randomize"]);
|
|
1590
1377
|
const HEALTH_JOURNEY_STEP_FIELDS = new Set([
|
|
1591
1378
|
"id",
|
|
1592
1379
|
"description",
|
|
@@ -1605,10 +1392,7 @@ const HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS = new Set([
|
|
|
1605
1392
|
"minManualInterval",
|
|
1606
1393
|
"publicRationale",
|
|
1607
1394
|
]);
|
|
1608
|
-
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set([
|
|
1609
|
-
"enabled",
|
|
1610
|
-
"reason",
|
|
1611
|
-
]);
|
|
1395
|
+
const HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS = new Set(["enabled", "reason"]);
|
|
1612
1396
|
const HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS = new Set([
|
|
1613
1397
|
"enabled",
|
|
1614
1398
|
"requiresAcknowledgement",
|
|
@@ -1628,32 +1412,18 @@ function validateHealthJourneyManualTrigger(
|
|
|
1628
1412
|
manualTrigger: unknown,
|
|
1629
1413
|
): void {
|
|
1630
1414
|
const fieldPath = `healthJourneys.${journeyId}.manualTrigger`;
|
|
1631
|
-
if (
|
|
1632
|
-
!manualTrigger ||
|
|
1633
|
-
typeof manualTrigger !== "object" ||
|
|
1634
|
-
Array.isArray(manualTrigger)
|
|
1635
|
-
) {
|
|
1415
|
+
if (!manualTrigger || typeof manualTrigger !== "object" || Array.isArray(manualTrigger)) {
|
|
1636
1416
|
throw new ValidationError(
|
|
1637
1417
|
`Provider "${providerId}" ${fieldPath} must be an object when present.`,
|
|
1638
1418
|
);
|
|
1639
1419
|
}
|
|
1640
|
-
rejectUnknownFields(
|
|
1641
|
-
manualTrigger,
|
|
1642
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS,
|
|
1643
|
-
fieldPath,
|
|
1644
|
-
);
|
|
1420
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_FIELDS, fieldPath);
|
|
1645
1421
|
const enabled = Reflect.get(manualTrigger, "enabled");
|
|
1646
1422
|
if (typeof enabled !== "boolean") {
|
|
1647
|
-
throw new ValidationError(
|
|
1648
|
-
`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`,
|
|
1649
|
-
);
|
|
1423
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.enabled must be a boolean.`);
|
|
1650
1424
|
}
|
|
1651
1425
|
if (enabled === false) {
|
|
1652
|
-
rejectUnknownFields(
|
|
1653
|
-
manualTrigger,
|
|
1654
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS,
|
|
1655
|
-
fieldPath,
|
|
1656
|
-
);
|
|
1426
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_DISABLED_FIELDS, fieldPath);
|
|
1657
1427
|
if (
|
|
1658
1428
|
Reflect.get(manualTrigger, "reason") !== undefined &&
|
|
1659
1429
|
(typeof Reflect.get(manualTrigger, "reason") !== "string" ||
|
|
@@ -1665,25 +1435,15 @@ function validateHealthJourneyManualTrigger(
|
|
|
1665
1435
|
}
|
|
1666
1436
|
return;
|
|
1667
1437
|
}
|
|
1668
|
-
rejectUnknownFields(
|
|
1669
|
-
|
|
1670
|
-
HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS,
|
|
1671
|
-
fieldPath,
|
|
1672
|
-
);
|
|
1673
|
-
const requiresAcknowledgement = Reflect.get(
|
|
1674
|
-
manualTrigger,
|
|
1675
|
-
"requiresAcknowledgement",
|
|
1676
|
-
);
|
|
1438
|
+
rejectUnknownFields(manualTrigger, HEALTH_JOURNEY_MANUAL_TRIGGER_ENABLED_FIELDS, fieldPath);
|
|
1439
|
+
const requiresAcknowledgement = Reflect.get(manualTrigger, "requiresAcknowledgement");
|
|
1677
1440
|
if (typeof requiresAcknowledgement !== "boolean") {
|
|
1678
1441
|
throw new ValidationError(
|
|
1679
1442
|
`Provider "${providerId}" ${fieldPath}.requiresAcknowledgement must be a boolean.`,
|
|
1680
1443
|
);
|
|
1681
1444
|
}
|
|
1682
1445
|
const risk = Reflect.get(manualTrigger, "risk");
|
|
1683
|
-
if (
|
|
1684
|
-
typeof risk !== "string" ||
|
|
1685
|
-
!HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)
|
|
1686
|
-
) {
|
|
1446
|
+
if (typeof risk !== "string" || !HEALTH_JOURNEY_MANUAL_TRIGGER_RISKS.has(risk)) {
|
|
1687
1447
|
throw new ValidationError(
|
|
1688
1448
|
`Provider "${providerId}" ${fieldPath}.risk must be one of read_only, writes_external_state, or sms_or_payment.`,
|
|
1689
1449
|
);
|
|
@@ -1694,10 +1454,7 @@ function validateHealthJourneyManualTrigger(
|
|
|
1694
1454
|
);
|
|
1695
1455
|
}
|
|
1696
1456
|
const minManualInterval = Reflect.get(manualTrigger, "minManualInterval");
|
|
1697
|
-
assertIsoDuration(
|
|
1698
|
-
minManualInterval,
|
|
1699
|
-
`Provider "${providerId}" ${fieldPath}.minManualInterval`,
|
|
1700
|
-
);
|
|
1457
|
+
assertIsoDuration(minManualInterval, `Provider "${providerId}" ${fieldPath}.minManualInterval`);
|
|
1701
1458
|
if (isoDurationMs(minManualInterval) <= 0) {
|
|
1702
1459
|
throw new ValidationError(
|
|
1703
1460
|
`Provider "${providerId}" ${fieldPath}.minManualInterval must be a positive duration.`,
|
|
@@ -1728,18 +1485,14 @@ const SMS_ORIGIN_FIELDS_BY_KIND: Record<string, ReadonlySet<string>> = {
|
|
|
1728
1485
|
e164: new Set(["kind", "value", "display"]),
|
|
1729
1486
|
nationalServiceCode: new Set(["kind", "country", "value", "display"]),
|
|
1730
1487
|
};
|
|
1731
|
-
const DURATION_RE =
|
|
1732
|
-
/^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
|
|
1488
|
+
const DURATION_RE = /^P(?=\d|T\d)(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;
|
|
1733
1489
|
const E164_RE = /^\+[1-9]\d{1,14}$/;
|
|
1734
1490
|
const ISO_COUNTRY_RE = /^[A-Z]{2}$/;
|
|
1735
1491
|
const NATIONAL_SERVICE_CODE_RE = /^[0-9]{2,15}$/;
|
|
1736
1492
|
const BCP47_RE = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
1737
1493
|
const JOURNEY_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
1738
1494
|
|
|
1739
|
-
function assertIsoDuration(
|
|
1740
|
-
value: unknown,
|
|
1741
|
-
fieldPath: string,
|
|
1742
|
-
): asserts value is string {
|
|
1495
|
+
function assertIsoDuration(value: unknown, fieldPath: string): asserts value is string {
|
|
1743
1496
|
if (typeof value !== "string" || !DURATION_RE.test(value)) {
|
|
1744
1497
|
throw new ValidationError(
|
|
1745
1498
|
`${fieldPath} must be an ISO 8601 duration for example PT8H or PT2M30S.`,
|
|
@@ -1754,15 +1507,10 @@ function isoDurationMs(value: string): number {
|
|
|
1754
1507
|
const hours = Number(/(\d+)H/.exec(value)?.[1] ?? 0);
|
|
1755
1508
|
const minutes = Number(/(\d+)M/.exec(value)?.[1] ?? 0);
|
|
1756
1509
|
const seconds = Number(/(\d+(?:\.\d+)?)S/.exec(value)?.[1] ?? 0);
|
|
1757
|
-
return
|
|
1758
|
-
days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000
|
|
1759
|
-
);
|
|
1510
|
+
return days * 86_400_000 + hours * 3_600_000 + minutes * 60_000 + seconds * 1_000;
|
|
1760
1511
|
}
|
|
1761
1512
|
|
|
1762
|
-
function scheduleRandomizationMs(
|
|
1763
|
-
randomize: unknown,
|
|
1764
|
-
fieldPath: string,
|
|
1765
|
-
): number {
|
|
1513
|
+
function scheduleRandomizationMs(randomize: unknown, fieldPath: string): number {
|
|
1766
1514
|
const mode = Reflect.get(randomize as object, "mode");
|
|
1767
1515
|
switch (mode) {
|
|
1768
1516
|
case "centered": {
|
|
@@ -1776,9 +1524,7 @@ function scheduleRandomizationMs(
|
|
|
1776
1524
|
return isoDurationMs(maxDelay);
|
|
1777
1525
|
}
|
|
1778
1526
|
default:
|
|
1779
|
-
throw new ValidationError(
|
|
1780
|
-
`${fieldPath}.mode must be "centered" or "delayed".`,
|
|
1781
|
-
);
|
|
1527
|
+
throw new ValidationError(`${fieldPath}.mode must be "centered" or "delayed".`);
|
|
1782
1528
|
}
|
|
1783
1529
|
}
|
|
1784
1530
|
|
|
@@ -1792,25 +1538,18 @@ function validateScheduleRandomization(
|
|
|
1792
1538
|
}
|
|
1793
1539
|
const mode = Reflect.get(randomize, "mode");
|
|
1794
1540
|
const allowedFields =
|
|
1795
|
-
mode === "centered"
|
|
1796
|
-
? new Set(["mode", "maxOffset"])
|
|
1797
|
-
: new Set(["mode", "maxDelay"]);
|
|
1541
|
+
mode === "centered" ? new Set(["mode", "maxOffset"]) : new Set(["mode", "maxDelay"]);
|
|
1798
1542
|
rejectUnknownFields(randomize, allowedFields, fieldPath);
|
|
1799
1543
|
const offsetMs = scheduleRandomizationMs(randomize, fieldPath);
|
|
1800
1544
|
if (offsetMs <= 0) {
|
|
1801
1545
|
throw new ValidationError(`${fieldPath} duration must be positive.`);
|
|
1802
1546
|
}
|
|
1803
1547
|
if (offsetMs >= intervalMs) {
|
|
1804
|
-
throw new ValidationError(
|
|
1805
|
-
`${fieldPath} duration must be shorter than schedule interval.`,
|
|
1806
|
-
);
|
|
1548
|
+
throw new ValidationError(`${fieldPath} duration must be shorter than schedule interval.`);
|
|
1807
1549
|
}
|
|
1808
1550
|
}
|
|
1809
1551
|
|
|
1810
|
-
function assertIsoCountry(
|
|
1811
|
-
value: unknown,
|
|
1812
|
-
fieldPath: string,
|
|
1813
|
-
): asserts value is string {
|
|
1552
|
+
function assertIsoCountry(value: unknown, fieldPath: string): asserts value is string {
|
|
1814
1553
|
if (typeof value !== "string" || !ISO_COUNTRY_RE.test(value)) {
|
|
1815
1554
|
throw new ValidationError(
|
|
1816
1555
|
`${fieldPath} must be an ISO 3166-1 alpha-2 country code for example KR.`,
|
|
@@ -1833,9 +1572,7 @@ function normalizeIntervalDuration(input: string): string {
|
|
|
1833
1572
|
? durationMs / 3_600_000
|
|
1834
1573
|
: durationMs / 86_400_000;
|
|
1835
1574
|
if (!Number.isInteger(amount) || amount <= 0) {
|
|
1836
|
-
throw new ValidationError(
|
|
1837
|
-
`Journey schedule interval must be a positive duration.`,
|
|
1838
|
-
);
|
|
1575
|
+
throw new ValidationError(`Journey schedule interval must be a positive duration.`);
|
|
1839
1576
|
}
|
|
1840
1577
|
if (unit === "s") return `PT${amount}S`;
|
|
1841
1578
|
if (unit === "m") return `PT${amount}M`;
|
|
@@ -1894,11 +1631,7 @@ function countCapturingGroups(pattern: RegExp): number {
|
|
|
1894
1631
|
if (inCharacterClass || char !== "(") continue;
|
|
1895
1632
|
const next = source[i + 1];
|
|
1896
1633
|
if (next === "?" && source[i + 2] !== "<") continue;
|
|
1897
|
-
if (
|
|
1898
|
-
next === "?" &&
|
|
1899
|
-
source[i + 2] === "<" &&
|
|
1900
|
-
(source[i + 3] === "=" || source[i + 3] === "!")
|
|
1901
|
-
)
|
|
1634
|
+
if (next === "?" && source[i + 2] === "<" && (source[i + 3] === "=" || source[i + 3] === "!"))
|
|
1902
1635
|
continue;
|
|
1903
1636
|
count += 1;
|
|
1904
1637
|
}
|
|
@@ -1917,9 +1650,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1917
1650
|
}
|
|
1918
1651
|
const kind = Reflect.get(origin, "kind");
|
|
1919
1652
|
if (kind !== "e164" && kind !== "nationalServiceCode") {
|
|
1920
|
-
throw new ValidationError(
|
|
1921
|
-
`${fieldPath}.kind must be "e164" or "nationalServiceCode".`,
|
|
1922
|
-
);
|
|
1653
|
+
throw new ValidationError(`${fieldPath}.kind must be "e164" or "nationalServiceCode".`);
|
|
1923
1654
|
}
|
|
1924
1655
|
rejectUnknownFields(origin, SMS_ORIGIN_FIELDS_BY_KIND[kind], fieldPath);
|
|
1925
1656
|
if (kind === "e164") {
|
|
@@ -1946,9 +1677,7 @@ function validateSmsOrigin(origin: unknown, fieldPath: string): void {
|
|
|
1946
1677
|
Reflect.get(origin, "display") !== undefined &&
|
|
1947
1678
|
typeof Reflect.get(origin, "display") !== "string"
|
|
1948
1679
|
) {
|
|
1949
|
-
throw new ValidationError(
|
|
1950
|
-
`${fieldPath}.display must be a string when present.`,
|
|
1951
|
-
);
|
|
1680
|
+
throw new ValidationError(`${fieldPath}.display must be a string when present.`);
|
|
1952
1681
|
}
|
|
1953
1682
|
}
|
|
1954
1683
|
|
|
@@ -1962,9 +1691,7 @@ function validateSmsOtpMatcher(
|
|
|
1962
1691
|
rejectUnknownFields(matcher, SMS_OTP_MATCHER_FIELDS, fieldPath);
|
|
1963
1692
|
const matcherId = Reflect.get(matcher, "id");
|
|
1964
1693
|
if (typeof matcherId !== "string" || !JOURNEY_ID_RE.test(matcherId)) {
|
|
1965
|
-
throw new ValidationError(
|
|
1966
|
-
`${fieldPath}.id must be a kebab-case identifier.`,
|
|
1967
|
-
);
|
|
1694
|
+
throw new ValidationError(`${fieldPath}.id must be a kebab-case identifier.`);
|
|
1968
1695
|
}
|
|
1969
1696
|
assertIsoCountry(Reflect.get(matcher, "country"), `${fieldPath}.country`);
|
|
1970
1697
|
if (
|
|
@@ -1972,24 +1699,18 @@ function validateSmsOtpMatcher(
|
|
|
1972
1699
|
(typeof Reflect.get(matcher, "locale") !== "string" ||
|
|
1973
1700
|
!BCP47_RE.test(Reflect.get(matcher, "locale")))
|
|
1974
1701
|
) {
|
|
1975
|
-
throw new ValidationError(
|
|
1976
|
-
`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`,
|
|
1977
|
-
);
|
|
1702
|
+
throw new ValidationError(`${fieldPath}.locale must be a BCP 47 locale for example ko-KR.`);
|
|
1978
1703
|
}
|
|
1979
1704
|
if (
|
|
1980
1705
|
Reflect.get(matcher, "phoneNumber") !== undefined &&
|
|
1981
1706
|
(typeof Reflect.get(matcher, "phoneNumber") !== "string" ||
|
|
1982
1707
|
!E164_RE.test(Reflect.get(matcher, "phoneNumber")))
|
|
1983
1708
|
) {
|
|
1984
|
-
throw new ValidationError(
|
|
1985
|
-
`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`,
|
|
1986
|
-
);
|
|
1709
|
+
throw new ValidationError(`${fieldPath}.phoneNumber must be an ITU-T E.164 number.`);
|
|
1987
1710
|
}
|
|
1988
1711
|
const origins = Reflect.get(matcher, "origins");
|
|
1989
1712
|
if (!Array.isArray(origins) || origins.length === 0) {
|
|
1990
|
-
throw new ValidationError(
|
|
1991
|
-
`${fieldPath}.origins must be a non-empty array.`,
|
|
1992
|
-
);
|
|
1713
|
+
throw new ValidationError(`${fieldPath}.origins must be a non-empty array.`);
|
|
1993
1714
|
}
|
|
1994
1715
|
for (const [index, origin] of origins.entries()) {
|
|
1995
1716
|
validateSmsOrigin(origin, `${fieldPath}.origins[${index}]`);
|
|
@@ -2010,10 +1731,7 @@ function validateSmsOtpMatcher(
|
|
|
2010
1731
|
);
|
|
2011
1732
|
}
|
|
2012
1733
|
const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern);
|
|
2013
|
-
if (
|
|
2014
|
-
countCapturingGroups(regex) !== 1 &&
|
|
2015
|
-
Reflect.get(code, "capture") === undefined
|
|
2016
|
-
) {
|
|
1734
|
+
if (countCapturingGroups(regex) !== 1 && Reflect.get(code, "capture") === undefined) {
|
|
2017
1735
|
throw new ValidationError(
|
|
2018
1736
|
`${fieldPath}.code.pattern must contain exactly one OTP capture or declare code.capture.`,
|
|
2019
1737
|
);
|
|
@@ -2023,20 +1741,12 @@ function validateSmsOtpMatcher(
|
|
|
2023
1741
|
typeof Reflect.get(code, "capture") !== "string" &&
|
|
2024
1742
|
typeof Reflect.get(code, "capture") !== "number"
|
|
2025
1743
|
) {
|
|
2026
|
-
throw new ValidationError(
|
|
2027
|
-
`${fieldPath}.code.capture must be a string or number when present.`,
|
|
2028
|
-
);
|
|
1744
|
+
throw new ValidationError(`${fieldPath}.code.capture must be a string or number when present.`);
|
|
2029
1745
|
}
|
|
2030
1746
|
assertIsoDuration(Reflect.get(matcher, "maxAge"), `${fieldPath}.maxAge`);
|
|
2031
|
-
assertIsoDuration(
|
|
2032
|
-
Reflect.get(matcher, "waitTimeout"),
|
|
2033
|
-
`${fieldPath}.waitTimeout`,
|
|
2034
|
-
);
|
|
1747
|
+
assertIsoDuration(Reflect.get(matcher, "waitTimeout"), `${fieldPath}.waitTimeout`);
|
|
2035
1748
|
if (Reflect.get(matcher, "clockSkew") !== undefined)
|
|
2036
|
-
assertIsoDuration(
|
|
2037
|
-
Reflect.get(matcher, "clockSkew"),
|
|
2038
|
-
`${fieldPath}.clockSkew`,
|
|
2039
|
-
);
|
|
1749
|
+
assertIsoDuration(Reflect.get(matcher, "clockSkew"), `${fieldPath}.clockSkew`);
|
|
2040
1750
|
}
|
|
2041
1751
|
|
|
2042
1752
|
export function defineSmsOtpMatcher(
|
|
@@ -2068,9 +1778,7 @@ export function defineSmsOtpMatcher(
|
|
|
2068
1778
|
return matcher;
|
|
2069
1779
|
}
|
|
2070
1780
|
|
|
2071
|
-
export function defineHealthJourney(
|
|
2072
|
-
config: HealthJourneyDefinition,
|
|
2073
|
-
): HealthJourneyDefinition {
|
|
1781
|
+
export function defineHealthJourney(config: HealthJourneyDefinition): HealthJourneyDefinition {
|
|
2074
1782
|
return config;
|
|
2075
1783
|
}
|
|
2076
1784
|
|
|
@@ -2081,22 +1789,15 @@ function validateHealthJourneySchedule(
|
|
|
2081
1789
|
): void {
|
|
2082
1790
|
const fieldPath = `healthJourneys.${journeyId}.schedule`;
|
|
2083
1791
|
if (!schedule || typeof schedule !== "object" || Array.isArray(schedule)) {
|
|
2084
|
-
throw new ValidationError(
|
|
2085
|
-
`Provider "${providerId}" ${fieldPath} must be an object.`,
|
|
2086
|
-
);
|
|
1792
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath} must be an object.`);
|
|
2087
1793
|
}
|
|
2088
1794
|
rejectUnknownFields(schedule, HEALTH_JOURNEY_SCHEDULE_FIELDS, fieldPath);
|
|
2089
1795
|
if (Reflect.get(schedule, "kind") !== "interval")
|
|
2090
|
-
throw new ValidationError(
|
|
2091
|
-
`Provider "${providerId}" ${fieldPath}.kind must be "interval".`,
|
|
2092
|
-
);
|
|
1796
|
+
throw new ValidationError(`Provider "${providerId}" ${fieldPath}.kind must be "interval".`);
|
|
2093
1797
|
const interval = Reflect.get(schedule, "interval");
|
|
2094
1798
|
assertIsoDuration(interval, `Provider "${providerId}" ${fieldPath}.interval`);
|
|
2095
1799
|
const randomize = Reflect.get(schedule, "randomize");
|
|
2096
|
-
if (
|
|
2097
|
-
Reflect.get(schedule, "jitter") !== undefined &&
|
|
2098
|
-
randomize !== undefined
|
|
2099
|
-
) {
|
|
1800
|
+
if (Reflect.get(schedule, "jitter") !== undefined && randomize !== undefined) {
|
|
2100
1801
|
throw new ValidationError(
|
|
2101
1802
|
`Provider "${providerId}" ${fieldPath} cannot define both jitter and randomize.`,
|
|
2102
1803
|
);
|
|
@@ -2123,17 +1824,13 @@ function validateHealthJourneys(
|
|
|
2123
1824
|
const covered = new Set<string>();
|
|
2124
1825
|
if (healthJourneys === undefined) return covered;
|
|
2125
1826
|
if (!Array.isArray(healthJourneys)) {
|
|
2126
|
-
throw new ValidationError(
|
|
2127
|
-
`Provider "${providerId}" healthJourneys must be an array.`,
|
|
2128
|
-
);
|
|
1827
|
+
throw new ValidationError(`Provider "${providerId}" healthJourneys must be an array.`);
|
|
2129
1828
|
}
|
|
2130
1829
|
const journeyIds = new Set<string>();
|
|
2131
1830
|
for (const [index, journey] of healthJourneys.entries()) {
|
|
2132
1831
|
const prefix = `healthJourneys[${index}]`;
|
|
2133
1832
|
if (!journey || typeof journey !== "object" || Array.isArray(journey)) {
|
|
2134
|
-
throw new ValidationError(
|
|
2135
|
-
`Provider "${providerId}" ${prefix} must be an object.`,
|
|
2136
|
-
);
|
|
1833
|
+
throw new ValidationError(`Provider "${providerId}" ${prefix} must be an object.`);
|
|
2137
1834
|
}
|
|
2138
1835
|
rejectUnknownFields(journey, HEALTH_JOURNEY_FIELDS, prefix);
|
|
2139
1836
|
if (typeof journey.id !== "string" || !JOURNEY_ID_RE.test(journey.id)) {
|
|
@@ -2147,10 +1844,7 @@ function validateHealthJourneys(
|
|
|
2147
1844
|
);
|
|
2148
1845
|
journeyIds.add(journey.id);
|
|
2149
1846
|
validateHealthJourneySchedule(providerId, journey.id, journey.schedule);
|
|
2150
|
-
if (
|
|
2151
|
-
!Array.isArray(journey.coversOperations) ||
|
|
2152
|
-
journey.coversOperations.length === 0
|
|
2153
|
-
) {
|
|
1847
|
+
if (!Array.isArray(journey.coversOperations) || journey.coversOperations.length === 0) {
|
|
2154
1848
|
throw new ValidationError(
|
|
2155
1849
|
`Provider "${providerId}" healthJourneys.${journey.id}.coversOperations must be a non-empty array.`,
|
|
2156
1850
|
);
|
|
@@ -2185,10 +1879,7 @@ function validateHealthJourneys(
|
|
|
2185
1879
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers must be an array.`,
|
|
2186
1880
|
);
|
|
2187
1881
|
for (const [matcherIndex, matcher] of journey.smsMatchers.entries()) {
|
|
2188
|
-
validateSmsOtpMatcher(
|
|
2189
|
-
matcher,
|
|
2190
|
-
`healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`,
|
|
2191
|
-
);
|
|
1882
|
+
validateSmsOtpMatcher(matcher, `healthJourneys.${journey.id}.smsMatchers[${matcherIndex}]`);
|
|
2192
1883
|
if (matcherIds.has(matcher.id))
|
|
2193
1884
|
throw new ValidationError(
|
|
2194
1885
|
`Provider "${providerId}" healthJourneys.${journey.id}.smsMatchers has duplicate matcher id "${matcher.id}".`,
|
|
@@ -2199,9 +1890,7 @@ function validateHealthJourneys(
|
|
|
2199
1890
|
for (const [stepIndex, step] of journey.steps.entries()) {
|
|
2200
1891
|
const stepPath = `healthJourneys.${journey.id}.steps[${stepIndex}]`;
|
|
2201
1892
|
if (!step || typeof step !== "object" || Array.isArray(step))
|
|
2202
|
-
throw new ValidationError(
|
|
2203
|
-
`Provider "${providerId}" ${stepPath} must be an object.`,
|
|
2204
|
-
);
|
|
1893
|
+
throw new ValidationError(`Provider "${providerId}" ${stepPath} must be an object.`);
|
|
2205
1894
|
rejectUnknownFields(step, HEALTH_JOURNEY_STEP_FIELDS, stepPath);
|
|
2206
1895
|
if (typeof step.id !== "string" || !JOURNEY_ID_RE.test(step.id))
|
|
2207
1896
|
throw new ValidationError(
|
|
@@ -2211,20 +1900,13 @@ function validateHealthJourneys(
|
|
|
2211
1900
|
throw new ValidationError(
|
|
2212
1901
|
`Provider "${providerId}" ${stepPath}.operationId references unknown operation "${step.operationId}".`,
|
|
2213
1902
|
);
|
|
2214
|
-
if (
|
|
2215
|
-
step.usesSmsMatcher !== undefined &&
|
|
2216
|
-
!matcherIds.has(step.usesSmsMatcher)
|
|
2217
|
-
)
|
|
1903
|
+
if (step.usesSmsMatcher !== undefined && !matcherIds.has(step.usesSmsMatcher))
|
|
2218
1904
|
throw new ValidationError(
|
|
2219
1905
|
`Provider "${providerId}" ${stepPath}.usesSmsMatcher references unknown matcher "${step.usesSmsMatcher}".`,
|
|
2220
1906
|
);
|
|
2221
1907
|
}
|
|
2222
1908
|
if (journey.manualTrigger !== undefined)
|
|
2223
|
-
validateHealthJourneyManualTrigger(
|
|
2224
|
-
providerId,
|
|
2225
|
-
journey.id,
|
|
2226
|
-
journey.manualTrigger,
|
|
2227
|
-
);
|
|
1909
|
+
validateHealthJourneyManualTrigger(providerId, journey.id, journey.manualTrigger);
|
|
2228
1910
|
if (journey.timeout !== undefined)
|
|
2229
1911
|
assertIsoDuration(
|
|
2230
1912
|
journey.timeout,
|
|
@@ -2270,23 +1952,10 @@ function validateOperationHealthChecks(
|
|
|
2270
1952
|
fix: `Remove either operations.${operationName}.healthCheck or operations.${operationName}.healthCheckUnsupported.`,
|
|
2271
1953
|
},
|
|
2272
1954
|
);
|
|
2273
|
-
if (hasCheck)
|
|
2274
|
-
validateHealthCheckSuite(
|
|
2275
|
-
providerId,
|
|
2276
|
-
operationName,
|
|
2277
|
-
operation.healthCheck,
|
|
2278
|
-
);
|
|
1955
|
+
if (hasCheck) validateHealthCheckSuite(providerId, operationName, operation.healthCheck);
|
|
2279
1956
|
if (hasUnsupported)
|
|
2280
|
-
validateHealthCheckUnsupported(
|
|
2281
|
-
|
|
2282
|
-
operationName,
|
|
2283
|
-
operation.healthCheckUnsupported,
|
|
2284
|
-
);
|
|
2285
|
-
if (
|
|
2286
|
-
!hasCheck &&
|
|
2287
|
-
!hasUnsupported &&
|
|
2288
|
-
!journeyCoveredOperations.has(operationName)
|
|
2289
|
-
)
|
|
1957
|
+
validateHealthCheckUnsupported(providerId, operationName, operation.healthCheckUnsupported);
|
|
1958
|
+
if (!hasCheck && !hasUnsupported && !journeyCoveredOperations.has(operationName))
|
|
2290
1959
|
throw new ValidationError(
|
|
2291
1960
|
`Provider "${providerId}" operation "${operationName}" declares neither healthCheck nor healthCheckUnsupported.`,
|
|
2292
1961
|
{
|
|
@@ -2341,6 +2010,19 @@ function validateOperationFixtures(
|
|
|
2341
2010
|
}
|
|
2342
2011
|
}
|
|
2343
2012
|
|
|
2013
|
+
/**
|
|
2014
|
+
* Shallow shape guard only: the `deployment` object is passed through
|
|
2015
|
+
* verbatim and deliberately not deep-validated by the SDK — the APIFuse
|
|
2016
|
+
* registry builder owns deployment validation and profile resolution.
|
|
2017
|
+
*/
|
|
2018
|
+
function validateProviderDeployment(providerId: string, deployment: unknown): void {
|
|
2019
|
+
if (deployment === undefined) return;
|
|
2020
|
+
if (!deployment || typeof deployment !== "object" || Array.isArray(deployment))
|
|
2021
|
+
throw new ProviderError(`Provider "${providerId}" deployment must be an object when present`, {
|
|
2022
|
+
fix: 'Pass deployment: { runtime: "shared" | "dedicated" | "browser", ... } or remove the field',
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2344
2026
|
export function defineProvider<
|
|
2345
2027
|
TOperations extends Record<string, ProviderOperation>,
|
|
2346
2028
|
TConfig extends ProviderConfig<TOperations>,
|
|
@@ -2353,12 +2035,9 @@ export function defineProvider<
|
|
|
2353
2035
|
fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
|
|
2354
2036
|
});
|
|
2355
2037
|
if (Object.keys(config.operations).length === 0)
|
|
2356
|
-
throw new ProviderError(
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
fix: "Add at least one operation to the operations object",
|
|
2360
|
-
},
|
|
2361
|
-
);
|
|
2038
|
+
throw new ProviderError(`Provider "${config.id}" must define at least one operation`, {
|
|
2039
|
+
fix: "Add at least one operation to the operations object",
|
|
2040
|
+
});
|
|
2362
2041
|
validateOperationIds(config.id, config.operations);
|
|
2363
2042
|
validateOperationAnnotations(config.id, config.operations);
|
|
2364
2043
|
validateOperationObservability(config.id, config.operations);
|
|
@@ -2370,11 +2049,7 @@ export function defineProvider<
|
|
|
2370
2049
|
config.operations,
|
|
2371
2050
|
config.healthJourneys,
|
|
2372
2051
|
);
|
|
2373
|
-
validateOperationHealthChecks(
|
|
2374
|
-
config.id,
|
|
2375
|
-
config.operations,
|
|
2376
|
-
journeyCoveredOperations,
|
|
2377
|
-
);
|
|
2052
|
+
validateOperationHealthChecks(config.id, config.operations, journeyCoveredOperations);
|
|
2378
2053
|
if (config.healthMonitor !== undefined && config.healthProbe !== undefined)
|
|
2379
2054
|
throw new ValidationError(
|
|
2380
2055
|
`Provider "${config.id}" declares both healthMonitor and healthProbe. They are aliases; declare exactly one.`,
|
|
@@ -2388,6 +2063,7 @@ export function defineProvider<
|
|
|
2388
2063
|
config.healthProbe !== undefined ? "healthProbe" : "healthMonitor",
|
|
2389
2064
|
);
|
|
2390
2065
|
validateOperationFixtures(config.id, config.operations);
|
|
2066
|
+
validateProviderDeployment(config.id, config.deployment);
|
|
2391
2067
|
validateProviderProxy(config);
|
|
2392
2068
|
validateProviderStt(config);
|
|
2393
2069
|
if (config.runtime === "browser" && !config.browser)
|
|
@@ -2406,6 +2082,9 @@ export function defineProvider<
|
|
|
2406
2082
|
id: config.id,
|
|
2407
2083
|
version: config.version,
|
|
2408
2084
|
runtime: config.runtime,
|
|
2085
|
+
// Verbatim passthrough: deployment validation and profile resolution
|
|
2086
|
+
// are owned by the APIFuse registry builder, not the SDK.
|
|
2087
|
+
deployment: config.deployment,
|
|
2409
2088
|
allowedHosts: config.allowedHosts,
|
|
2410
2089
|
stealth: config.stealth,
|
|
2411
2090
|
proxy: config.proxy,
|