@apifuse/provider-sdk 2.0.0-beta.1 → 2.1.0-beta.1
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 +93 -0
- package/CHANGELOG.md +21 -0
- package/README.md +133 -28
- package/bin/apifuse-check.ts +78 -71
- package/bin/apifuse-create.ts +12 -0
- package/bin/apifuse-dev.ts +24 -61
- package/bin/apifuse-pack-check.ts +87 -0
- package/bin/apifuse-pack-smoke.ts +122 -0
- package/bin/apifuse-perf.ts +33 -32
- package/bin/apifuse-record.ts +17 -7
- package/bin/apifuse-test.ts +6 -4
- package/bin/apifuse.ts +36 -35
- package/package.json +29 -9
- package/src/ceremonies/index.ts +768 -0
- package/src/cli/commands.ts +87 -0
- package/src/cli/create.ts +845 -0
- package/src/cli/templates/provider/Dockerfile.tpl +7 -0
- package/src/cli/templates/provider/README.md.tpl +41 -0
- package/src/cli/templates/provider/dev.ts.tpl +5 -0
- package/src/cli/templates/provider/index.test.ts.tpl +13 -0
- package/src/cli/templates/provider/index.ts.tpl +58 -0
- package/src/cli/templates/provider/start.ts.tpl +5 -0
- package/src/config/loader.ts +61 -1
- package/src/define.ts +565 -41
- package/src/dev.ts +2 -6
- package/src/errors.ts +42 -0
- package/src/index.ts +44 -38
- package/src/lint.ts +574 -0
- package/src/provider.ts +13 -0
- package/src/runtime/auth-flow.ts +67 -0
- package/src/runtime/credential.ts +95 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +13 -14
- package/src/runtime/http.ts +36 -12
- package/src/runtime/insights.ts +3 -3
- package/src/runtime/key-derivation.ts +122 -0
- package/src/runtime/keyring.ts +148 -0
- package/src/runtime/namespace.ts +33 -0
- package/src/runtime/prevalidate.ts +252 -0
- package/src/runtime/tls.ts +41 -17
- package/src/runtime/waterfall.ts +0 -1
- package/src/schema.ts +77 -0
- package/src/serve.ts +1 -664
- package/src/server/index.ts +22 -0
- package/src/server/serve.ts +624 -0
- package/src/server/types.ts +78 -0
- package/src/stealth/profiles.ts +10 -93
- package/src/testing/run.ts +391 -32
- package/src/types.ts +390 -41
- package/bin/apifuse-init.ts +0 -387
- package/src/__tests__/auth.test.ts +0 -396
- package/src/__tests__/browser-auth.test.ts +0 -180
- package/src/__tests__/browser.test.ts +0 -632
- package/src/__tests__/define.test.ts +0 -225
- package/src/__tests__/errors.test.ts +0 -69
- package/src/__tests__/executor.test.ts +0 -214
- package/src/__tests__/http.test.ts +0 -238
- package/src/__tests__/insights.test.ts +0 -210
- package/src/__tests__/instrumentation.test.ts +0 -290
- package/src/__tests__/otlp.test.ts +0 -141
- package/src/__tests__/perf.test.ts +0 -60
- package/src/__tests__/providers-yaml.test.ts +0 -135
- package/src/__tests__/proxy.test.ts +0 -359
- package/src/__tests__/recipes.test.ts +0 -36
- package/src/__tests__/serve.test.ts +0 -233
- package/src/__tests__/session.test.ts +0 -231
- package/src/__tests__/state.test.ts +0 -100
- package/src/__tests__/stealth.test.ts +0 -57
- package/src/__tests__/testing.test.ts +0 -97
- package/src/__tests__/tls.test.ts +0 -345
- package/src/__tests__/types.test.ts +0 -142
- package/src/__tests__/utils.test.ts +0 -62
- package/src/__tests__/waterfall.test.ts +0 -270
- package/src/config/providers-yaml.ts +0 -370
- package/src/index.test.ts +0 -1
- package/src/protocol.ts +0 -183
- package/src/runtime/auth.ts +0 -245
- package/src/runtime/session.ts +0 -573
- package/src/runtime/state.ts +0 -124
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import { RE2 } from "re2-wasm";
|
|
3
|
+
|
|
4
|
+
export interface PrevalidateResult {
|
|
5
|
+
valid: boolean;
|
|
6
|
+
errors?: Array<{ path: string; message: string }>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type JsonSchema = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 500;
|
|
12
|
+
|
|
13
|
+
function now(): number {
|
|
14
|
+
return Date.now();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cloneWithoutPatterns(value: unknown): unknown {
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
return value.map((entry) => cloneWithoutPatterns(entry));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!value || typeof value !== "object") {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cloned: Record<string, unknown> = {};
|
|
27
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
28
|
+
if (key === "pattern") {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
cloned[key] = cloneWithoutPatterns(entry);
|
|
32
|
+
}
|
|
33
|
+
return cloned;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildAjv(): Ajv {
|
|
37
|
+
return new Ajv({ allErrors: true, strict: true, strictSchema: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createTimeoutGuard(timeoutMs: number): () => void {
|
|
41
|
+
const startedAt = now();
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
if (now() - startedAt > timeoutMs) {
|
|
45
|
+
throw new Error("prevalidation_timeout");
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatInstancePath(path: string): string {
|
|
51
|
+
return path.length > 0 ? path : "$";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function appendPath(basePath: string, segment: string): string {
|
|
55
|
+
if (segment.startsWith("[")) {
|
|
56
|
+
return `${basePath}${segment}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return basePath === "$" ? `${basePath}.${segment}` : `${basePath}.${segment}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
63
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function collectPatternErrors(
|
|
67
|
+
schema: unknown,
|
|
68
|
+
data: unknown,
|
|
69
|
+
path: string,
|
|
70
|
+
guard: () => void,
|
|
71
|
+
errors: Array<{ path: string; message: string }>,
|
|
72
|
+
): void {
|
|
73
|
+
guard();
|
|
74
|
+
|
|
75
|
+
if (!isRecord(schema)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof schema.pattern === "string" && typeof data === "string") {
|
|
80
|
+
try {
|
|
81
|
+
const regex = new RE2(schema.pattern, "u");
|
|
82
|
+
if (!regex.test(data)) {
|
|
83
|
+
errors.push({
|
|
84
|
+
path,
|
|
85
|
+
message: `must match pattern ${schema.pattern}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const message =
|
|
90
|
+
error instanceof Error ? error.message : "Invalid RE2 pattern";
|
|
91
|
+
errors.push({ path, message });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (schema.$ref !== undefined) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(schema.allOf)) {
|
|
100
|
+
for (const entry of schema.allOf) {
|
|
101
|
+
collectPatternErrors(entry, data, path, guard, errors);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(schema.anyOf)) {
|
|
106
|
+
for (const entry of schema.anyOf) {
|
|
107
|
+
collectPatternErrors(entry, data, path, guard, errors);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (Array.isArray(schema.oneOf)) {
|
|
112
|
+
for (const entry of schema.oneOf) {
|
|
113
|
+
collectPatternErrors(entry, data, path, guard, errors);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isRecord(schema.not)) {
|
|
118
|
+
collectPatternErrors(schema.not, data, path, guard, errors);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (isRecord(schema.if)) {
|
|
122
|
+
collectPatternErrors(schema.if, data, path, guard, errors);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isRecord(schema.then)) {
|
|
126
|
+
collectPatternErrors(schema.then, data, path, guard, errors);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (isRecord(schema.else)) {
|
|
130
|
+
collectPatternErrors(schema.else, data, path, guard, errors);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (Array.isArray(data) && schema.items !== undefined) {
|
|
134
|
+
for (const [index, item] of data.entries()) {
|
|
135
|
+
collectPatternErrors(
|
|
136
|
+
schema.items,
|
|
137
|
+
item,
|
|
138
|
+
appendPath(path, `[${index}]`),
|
|
139
|
+
guard,
|
|
140
|
+
errors,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!isRecord(data)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (isRecord(schema.properties)) {
|
|
150
|
+
for (const [key, childSchema] of Object.entries(schema.properties)) {
|
|
151
|
+
if (key in data) {
|
|
152
|
+
collectPatternErrors(
|
|
153
|
+
childSchema,
|
|
154
|
+
data[key],
|
|
155
|
+
appendPath(path, key),
|
|
156
|
+
guard,
|
|
157
|
+
errors,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (isRecord(schema.patternProperties)) {
|
|
164
|
+
for (const [pattern, childSchema] of Object.entries(
|
|
165
|
+
schema.patternProperties,
|
|
166
|
+
)) {
|
|
167
|
+
const keyPattern = new RE2(pattern, "u");
|
|
168
|
+
for (const [key, value] of Object.entries(data)) {
|
|
169
|
+
guard();
|
|
170
|
+
if (keyPattern.test(key)) {
|
|
171
|
+
collectPatternErrors(
|
|
172
|
+
childSchema,
|
|
173
|
+
value,
|
|
174
|
+
appendPath(path, key),
|
|
175
|
+
guard,
|
|
176
|
+
errors,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (schema.additionalProperties && isRecord(schema.additionalProperties)) {
|
|
184
|
+
const declaredKeys = isRecord(schema.properties)
|
|
185
|
+
? new Set(Object.keys(schema.properties))
|
|
186
|
+
: new Set<string>();
|
|
187
|
+
|
|
188
|
+
for (const [key, value] of Object.entries(data)) {
|
|
189
|
+
if (!declaredKeys.has(key)) {
|
|
190
|
+
collectPatternErrors(
|
|
191
|
+
schema.additionalProperties,
|
|
192
|
+
value,
|
|
193
|
+
appendPath(path, key),
|
|
194
|
+
guard,
|
|
195
|
+
errors,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function prevalidate(
|
|
203
|
+
schema: JsonSchema,
|
|
204
|
+
data: unknown,
|
|
205
|
+
options: { timeoutMs?: number } = {},
|
|
206
|
+
): PrevalidateResult {
|
|
207
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
208
|
+
const guard = createTimeoutGuard(timeoutMs);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
guard();
|
|
212
|
+
const ajv = buildAjv();
|
|
213
|
+
const strippedSchema = cloneWithoutPatterns(schema);
|
|
214
|
+
if (!strippedSchema || typeof strippedSchema !== "object") {
|
|
215
|
+
return {
|
|
216
|
+
valid: false,
|
|
217
|
+
errors: [{ path: "$", message: "Invalid schema" }],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const validate = ajv.compile(strippedSchema);
|
|
221
|
+
const schemaValid = validate(data);
|
|
222
|
+
const errors =
|
|
223
|
+
validate.errors?.map((error) => ({
|
|
224
|
+
path: formatInstancePath(error.instancePath),
|
|
225
|
+
message: error.message ?? "Invalid value",
|
|
226
|
+
})) ?? [];
|
|
227
|
+
|
|
228
|
+
collectPatternErrors(schema, data, "$", guard, errors);
|
|
229
|
+
|
|
230
|
+
if (!schemaValid || errors.length > 0) {
|
|
231
|
+
return { valid: false, errors };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { valid: true };
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error instanceof Error && error.message === "prevalidation_timeout") {
|
|
237
|
+
return {
|
|
238
|
+
valid: false,
|
|
239
|
+
errors: [
|
|
240
|
+
{
|
|
241
|
+
path: "$",
|
|
242
|
+
message: `Prevalidation timed out after ${timeoutMs}ms`,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const message =
|
|
249
|
+
error instanceof Error ? error.message : "Prevalidation failed";
|
|
250
|
+
return { valid: false, errors: [{ path: "$", message }] };
|
|
251
|
+
}
|
|
252
|
+
}
|
package/src/runtime/tls.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ModuleClient, SessionClient } from "tlsclientwrapper";
|
|
|
2
2
|
|
|
3
3
|
import type { ProxyResolutionOptions } from "../config/loader";
|
|
4
4
|
import { resolveProxyConfig } from "../config/loader";
|
|
5
|
-
import { TransportError } from "../errors";
|
|
5
|
+
import { SDKError, TransportError } from "../errors";
|
|
6
6
|
import { getStealthProfile } from "../stealth/profiles";
|
|
7
7
|
import type {
|
|
8
8
|
CookieJar,
|
|
@@ -19,12 +19,29 @@ const MISSING_PROXY_WARNING =
|
|
|
19
19
|
|
|
20
20
|
export type TlsClientOptions = ProxyResolutionOptions & {
|
|
21
21
|
warn?: (message: string) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Proxy-only TLS transport overrides. Use only for upstream proxy products
|
|
24
|
+
* that terminate CONNECT with a private CA instead of tunneling the origin
|
|
25
|
+
* certificate chain.
|
|
26
|
+
*/
|
|
27
|
+
proxyTls?: { insecureSkipVerify?: boolean };
|
|
22
28
|
};
|
|
23
29
|
|
|
30
|
+
const REMOVED_CHROME_PROFILE_NAMES = new Set([
|
|
31
|
+
"chrome-120",
|
|
32
|
+
"chrome-124",
|
|
33
|
+
"chrome-129",
|
|
34
|
+
"chrome-130",
|
|
35
|
+
"chrome-131",
|
|
36
|
+
"chrome-133",
|
|
37
|
+
"chrome-144",
|
|
38
|
+
"chrome-146-psk",
|
|
39
|
+
"chrome-131-psk",
|
|
40
|
+
"chrome-130-psk",
|
|
41
|
+
"edge-131",
|
|
42
|
+
]);
|
|
43
|
+
|
|
24
44
|
const TLS_PROFILE_MAP: Record<string, string> = {
|
|
25
|
-
"chrome-131": "chrome_131",
|
|
26
|
-
"chrome-133": "chrome_133",
|
|
27
|
-
"chrome-144": "chrome_144",
|
|
28
45
|
"chrome-146": "chrome_146",
|
|
29
46
|
"firefox-132": "firefox_132",
|
|
30
47
|
"firefox-133": "firefox_133",
|
|
@@ -99,6 +116,10 @@ class CookieJarImpl implements CookieJar {
|
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
function resolveIdentifier(profileName: string): string {
|
|
119
|
+
if (REMOVED_CHROME_PROFILE_NAMES.has(profileName)) {
|
|
120
|
+
throw new SDKError(`Unknown stealth profile: ${profileName}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
102
123
|
try {
|
|
103
124
|
const profile = getStealthProfile(profileName);
|
|
104
125
|
if (profile.tlsClientIdentifier) {
|
|
@@ -172,14 +193,6 @@ export function normalizeResponse(
|
|
|
172
193
|
};
|
|
173
194
|
}
|
|
174
195
|
|
|
175
|
-
function getErrorMessage(error: unknown): string {
|
|
176
|
-
if (error instanceof Error) {
|
|
177
|
-
return error.toString();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return String(error);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
196
|
function normalizeBody(body: TlsFetchOptions["body"]): string | null {
|
|
184
197
|
if (body === undefined) {
|
|
185
198
|
return null;
|
|
@@ -259,6 +272,7 @@ function createSessionFetcher(
|
|
|
259
272
|
let sessionClient: SessionClient | null = null;
|
|
260
273
|
let activeProxy: string | undefined;
|
|
261
274
|
let activeTlsIdentifier: string | undefined;
|
|
275
|
+
let activeInsecureSkipVerify = false;
|
|
262
276
|
let hasWarnedMissingProxy = false;
|
|
263
277
|
const warn = clientOptions.warn ?? console.warn;
|
|
264
278
|
|
|
@@ -290,19 +304,22 @@ function createSessionFetcher(
|
|
|
290
304
|
sessionClient = null;
|
|
291
305
|
activeProxy = undefined;
|
|
292
306
|
activeTlsIdentifier = undefined;
|
|
307
|
+
activeInsecureSkipVerify = false;
|
|
293
308
|
}
|
|
294
309
|
|
|
295
310
|
function getSessionClient(
|
|
296
311
|
profile?: string,
|
|
297
312
|
proxy?: string,
|
|
298
313
|
ja3?: string,
|
|
314
|
+
insecureSkipVerify = false,
|
|
299
315
|
): SessionClient {
|
|
300
316
|
const tlsIdentifier = ja3 ?? resolveIdentifier(profile ?? defaultProfile);
|
|
301
317
|
|
|
302
318
|
if (
|
|
303
319
|
!sessionClient ||
|
|
304
320
|
activeProxy !== proxy ||
|
|
305
|
-
activeTlsIdentifier !== tlsIdentifier
|
|
321
|
+
activeTlsIdentifier !== tlsIdentifier ||
|
|
322
|
+
activeInsecureSkipVerify !== insecureSkipVerify
|
|
306
323
|
) {
|
|
307
324
|
if (sessionClient) {
|
|
308
325
|
closeCurrentSession();
|
|
@@ -311,10 +328,12 @@ function createSessionFetcher(
|
|
|
311
328
|
sessionClient = new SessionClient(moduleClient, {
|
|
312
329
|
tlsClientIdentifier: tlsIdentifier,
|
|
313
330
|
...(proxy ? { proxyUrl: proxy } : {}),
|
|
331
|
+
...(insecureSkipVerify ? { insecureSkipVerify: true } : {}),
|
|
314
332
|
timeoutSeconds: 30,
|
|
315
333
|
} as ConstructorParameters<typeof SessionClient>[1]);
|
|
316
334
|
activeProxy = proxy;
|
|
317
335
|
activeTlsIdentifier = tlsIdentifier;
|
|
336
|
+
activeInsecureSkipVerify = insecureSkipVerify;
|
|
318
337
|
}
|
|
319
338
|
|
|
320
339
|
return sessionClient;
|
|
@@ -323,10 +342,15 @@ function createSessionFetcher(
|
|
|
323
342
|
return {
|
|
324
343
|
async fetch(url, options = {}) {
|
|
325
344
|
const proxy = resolveRequestProxy(options);
|
|
345
|
+
const insecureSkipVerify = Boolean(
|
|
346
|
+
options.tls?.insecureSkipVerify ??
|
|
347
|
+
(proxy && clientOptions.proxyTls?.insecureSkipVerify),
|
|
348
|
+
);
|
|
326
349
|
const session = getSessionClient(
|
|
327
350
|
options.profile,
|
|
328
351
|
proxy,
|
|
329
352
|
options.tls?.ja3,
|
|
353
|
+
insecureSkipVerify,
|
|
330
354
|
);
|
|
331
355
|
const requestUrl = resolveUrl(baseUrl, url);
|
|
332
356
|
|
|
@@ -337,9 +361,9 @@ function createSessionFetcher(
|
|
|
337
361
|
proxy,
|
|
338
362
|
});
|
|
339
363
|
|
|
340
|
-
if (response.status >= 400) {
|
|
364
|
+
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
341
365
|
throw new TransportError(
|
|
342
|
-
`
|
|
366
|
+
`Upstream request failed with status ${response.status}`,
|
|
343
367
|
{
|
|
344
368
|
status: response.status,
|
|
345
369
|
},
|
|
@@ -348,11 +372,11 @@ function createSessionFetcher(
|
|
|
348
372
|
|
|
349
373
|
return normalizeResponse(response);
|
|
350
374
|
} catch (error) {
|
|
351
|
-
if (error instanceof TransportError) {
|
|
375
|
+
if (error instanceof SDKError || error instanceof TransportError) {
|
|
352
376
|
throw error;
|
|
353
377
|
}
|
|
354
378
|
|
|
355
|
-
throw new TransportError(
|
|
379
|
+
throw new TransportError("Network error", {
|
|
356
380
|
status: 0,
|
|
357
381
|
cause: error instanceof Error ? error : undefined,
|
|
358
382
|
});
|
package/src/runtime/waterfall.ts
CHANGED
|
@@ -102,7 +102,6 @@ function renderChildren(
|
|
|
102
102
|
}
|
|
103
103
|
const isLast = i === children.length - 1;
|
|
104
104
|
const provider = isLast ? "└─" : "├─";
|
|
105
|
-
const _childPrefix = isLast ? " " : "│ ";
|
|
106
105
|
|
|
107
106
|
const duration = formatDuration(node.span.duration_ms);
|
|
108
107
|
const bar = renderBar(
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ValidationError } from "./errors";
|
|
2
|
+
import type { InferSchemaOutput, SchemaLike, StandardSchemaV1 } from "./types";
|
|
3
|
+
|
|
4
|
+
export type SchemaValidationResult<TSchema extends SchemaLike> =
|
|
5
|
+
| { success: true; data: InferSchemaOutput<TSchema> }
|
|
6
|
+
| { success: false; error: unknown };
|
|
7
|
+
|
|
8
|
+
type UnknownSchemaValidationResult =
|
|
9
|
+
| { success: true; data: unknown }
|
|
10
|
+
| { success: false; error: unknown };
|
|
11
|
+
|
|
12
|
+
function isFailureResult<Output>(
|
|
13
|
+
result: StandardSchemaV1.Result<Output>,
|
|
14
|
+
): result is StandardSchemaV1.FailureResult {
|
|
15
|
+
return "issues" in result;
|
|
16
|
+
}
|
|
17
|
+
function isPromiseResult<Output>(
|
|
18
|
+
result:
|
|
19
|
+
| StandardSchemaV1.Result<Output>
|
|
20
|
+
| Promise<StandardSchemaV1.Result<Output>>,
|
|
21
|
+
): result is Promise<StandardSchemaV1.Result<Output>> {
|
|
22
|
+
return result instanceof Promise;
|
|
23
|
+
}
|
|
24
|
+
function formatStandardSchemaIssues(
|
|
25
|
+
issues: readonly StandardSchemaV1.Issue[],
|
|
26
|
+
): string {
|
|
27
|
+
return issues.map((issue) => issue.message).join("; ");
|
|
28
|
+
}
|
|
29
|
+
export function parseSchema<TSchema extends SchemaLike>(
|
|
30
|
+
schema: TSchema,
|
|
31
|
+
value: unknown,
|
|
32
|
+
fieldPath: string,
|
|
33
|
+
): Promise<InferSchemaOutput<TSchema>>;
|
|
34
|
+
export async function parseSchema(
|
|
35
|
+
schema: SchemaLike,
|
|
36
|
+
value: unknown,
|
|
37
|
+
fieldPath: string,
|
|
38
|
+
): Promise<unknown> {
|
|
39
|
+
if ("parse" in schema && typeof schema.parse === "function")
|
|
40
|
+
return schema.parse(value);
|
|
41
|
+
const result = schema["~standard"].validate(value);
|
|
42
|
+
const resolved = isPromiseResult(result) ? await result : result;
|
|
43
|
+
if (isFailureResult(resolved))
|
|
44
|
+
throw new ValidationError(
|
|
45
|
+
`Schema validation failed for ${fieldPath}: ${formatStandardSchemaIssues(resolved.issues)}`,
|
|
46
|
+
{ zodError: resolved.issues },
|
|
47
|
+
);
|
|
48
|
+
return resolved.value;
|
|
49
|
+
}
|
|
50
|
+
export function safeParseSchemaSync<TSchema extends SchemaLike>(
|
|
51
|
+
schema: TSchema,
|
|
52
|
+
value: unknown,
|
|
53
|
+
fieldPath: string,
|
|
54
|
+
): SchemaValidationResult<TSchema>;
|
|
55
|
+
export function safeParseSchemaSync(
|
|
56
|
+
schema: SchemaLike,
|
|
57
|
+
value: unknown,
|
|
58
|
+
fieldPath: string,
|
|
59
|
+
): UnknownSchemaValidationResult {
|
|
60
|
+
if ("safeParse" in schema && typeof schema.safeParse === "function")
|
|
61
|
+
return schema.safeParse(value);
|
|
62
|
+
try {
|
|
63
|
+
const result = schema["~standard"].validate(value);
|
|
64
|
+
if (isPromiseResult(result))
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
error: new ValidationError(
|
|
68
|
+
`Schema validation for ${fieldPath} returned a Promise. defineProvider fixture validation requires synchronous Standard Schema validation.`,
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
if (isFailureResult(result))
|
|
72
|
+
return { success: false, error: result.issues };
|
|
73
|
+
return { success: true, data: result.value };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { success: false, error };
|
|
76
|
+
}
|
|
77
|
+
}
|