@apifuse/provider-sdk 2.0.0-beta.1 → 2.1.0-beta.0
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 +102 -0
- package/CHANGELOG.md +14 -0
- package/README.md +100 -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 +47 -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 +28 -9
- package/src/ceremonies/index.ts +747 -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 +28 -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 +54 -0
- package/src/cli/templates/provider/start.ts.tpl +5 -0
- package/src/composite.ts +43 -0
- package/src/define.ts +527 -41
- package/src/dev.ts +2 -6
- package/src/errors.ts +42 -0
- package/src/index.ts +50 -38
- package/src/lint.ts +574 -0
- package/src/provider.ts +14 -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 +10 -2
- 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 +20 -5
- 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 +610 -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 +364 -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,
|
|
@@ -21,10 +21,21 @@ export type TlsClientOptions = ProxyResolutionOptions & {
|
|
|
21
21
|
warn?: (message: string) => void;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
const REMOVED_CHROME_PROFILE_NAMES = new Set([
|
|
25
|
+
"chrome-120",
|
|
26
|
+
"chrome-124",
|
|
27
|
+
"chrome-129",
|
|
28
|
+
"chrome-130",
|
|
29
|
+
"chrome-131",
|
|
30
|
+
"chrome-133",
|
|
31
|
+
"chrome-144",
|
|
32
|
+
"chrome-146-psk",
|
|
33
|
+
"chrome-131-psk",
|
|
34
|
+
"chrome-130-psk",
|
|
35
|
+
"edge-131",
|
|
36
|
+
]);
|
|
37
|
+
|
|
24
38
|
const TLS_PROFILE_MAP: Record<string, string> = {
|
|
25
|
-
"chrome-131": "chrome_131",
|
|
26
|
-
"chrome-133": "chrome_133",
|
|
27
|
-
"chrome-144": "chrome_144",
|
|
28
39
|
"chrome-146": "chrome_146",
|
|
29
40
|
"firefox-132": "firefox_132",
|
|
30
41
|
"firefox-133": "firefox_133",
|
|
@@ -99,6 +110,10 @@ class CookieJarImpl implements CookieJar {
|
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
function resolveIdentifier(profileName: string): string {
|
|
113
|
+
if (REMOVED_CHROME_PROFILE_NAMES.has(profileName)) {
|
|
114
|
+
throw new SDKError(`Unknown stealth profile: ${profileName}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
102
117
|
try {
|
|
103
118
|
const profile = getStealthProfile(profileName);
|
|
104
119
|
if (profile.tlsClientIdentifier) {
|
|
@@ -348,7 +363,7 @@ function createSessionFetcher(
|
|
|
348
363
|
|
|
349
364
|
return normalizeResponse(response);
|
|
350
365
|
} catch (error) {
|
|
351
|
-
if (error instanceof TransportError) {
|
|
366
|
+
if (error instanceof SDKError || error instanceof TransportError) {
|
|
352
367
|
throw error;
|
|
353
368
|
}
|
|
354
369
|
|
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
|
+
}
|