@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.
Files changed (79) hide show
  1. package/AUTHORING.md +93 -0
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +133 -28
  4. package/bin/apifuse-check.ts +78 -71
  5. package/bin/apifuse-create.ts +12 -0
  6. package/bin/apifuse-dev.ts +24 -61
  7. package/bin/apifuse-pack-check.ts +87 -0
  8. package/bin/apifuse-pack-smoke.ts +122 -0
  9. package/bin/apifuse-perf.ts +33 -32
  10. package/bin/apifuse-record.ts +17 -7
  11. package/bin/apifuse-test.ts +6 -4
  12. package/bin/apifuse.ts +36 -35
  13. package/package.json +29 -9
  14. package/src/ceremonies/index.ts +768 -0
  15. package/src/cli/commands.ts +87 -0
  16. package/src/cli/create.ts +845 -0
  17. package/src/cli/templates/provider/Dockerfile.tpl +7 -0
  18. package/src/cli/templates/provider/README.md.tpl +41 -0
  19. package/src/cli/templates/provider/dev.ts.tpl +5 -0
  20. package/src/cli/templates/provider/index.test.ts.tpl +13 -0
  21. package/src/cli/templates/provider/index.ts.tpl +58 -0
  22. package/src/cli/templates/provider/start.ts.tpl +5 -0
  23. package/src/config/loader.ts +61 -1
  24. package/src/define.ts +565 -41
  25. package/src/dev.ts +2 -6
  26. package/src/errors.ts +42 -0
  27. package/src/index.ts +44 -38
  28. package/src/lint.ts +574 -0
  29. package/src/provider.ts +13 -0
  30. package/src/runtime/auth-flow.ts +67 -0
  31. package/src/runtime/credential.ts +95 -0
  32. package/src/runtime/env.ts +13 -0
  33. package/src/runtime/executor.ts +13 -14
  34. package/src/runtime/http.ts +36 -12
  35. package/src/runtime/insights.ts +3 -3
  36. package/src/runtime/key-derivation.ts +122 -0
  37. package/src/runtime/keyring.ts +148 -0
  38. package/src/runtime/namespace.ts +33 -0
  39. package/src/runtime/prevalidate.ts +252 -0
  40. package/src/runtime/tls.ts +41 -17
  41. package/src/runtime/waterfall.ts +0 -1
  42. package/src/schema.ts +77 -0
  43. package/src/serve.ts +1 -664
  44. package/src/server/index.ts +22 -0
  45. package/src/server/serve.ts +624 -0
  46. package/src/server/types.ts +78 -0
  47. package/src/stealth/profiles.ts +10 -93
  48. package/src/testing/run.ts +391 -32
  49. package/src/types.ts +390 -41
  50. package/bin/apifuse-init.ts +0 -387
  51. package/src/__tests__/auth.test.ts +0 -396
  52. package/src/__tests__/browser-auth.test.ts +0 -180
  53. package/src/__tests__/browser.test.ts +0 -632
  54. package/src/__tests__/define.test.ts +0 -225
  55. package/src/__tests__/errors.test.ts +0 -69
  56. package/src/__tests__/executor.test.ts +0 -214
  57. package/src/__tests__/http.test.ts +0 -238
  58. package/src/__tests__/insights.test.ts +0 -210
  59. package/src/__tests__/instrumentation.test.ts +0 -290
  60. package/src/__tests__/otlp.test.ts +0 -141
  61. package/src/__tests__/perf.test.ts +0 -60
  62. package/src/__tests__/providers-yaml.test.ts +0 -135
  63. package/src/__tests__/proxy.test.ts +0 -359
  64. package/src/__tests__/recipes.test.ts +0 -36
  65. package/src/__tests__/serve.test.ts +0 -233
  66. package/src/__tests__/session.test.ts +0 -231
  67. package/src/__tests__/state.test.ts +0 -100
  68. package/src/__tests__/stealth.test.ts +0 -57
  69. package/src/__tests__/testing.test.ts +0 -97
  70. package/src/__tests__/tls.test.ts +0 -345
  71. package/src/__tests__/types.test.ts +0 -142
  72. package/src/__tests__/utils.test.ts +0 -62
  73. package/src/__tests__/waterfall.test.ts +0 -270
  74. package/src/config/providers-yaml.ts +0 -370
  75. package/src/index.test.ts +0 -1
  76. package/src/protocol.ts +0 -183
  77. package/src/runtime/auth.ts +0 -245
  78. package/src/runtime/session.ts +0 -573
  79. 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
+ }
@@ -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
- `HTTP ${response.status}: ${response.body || "Request failed"}`,
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(`Network error: ${getErrorMessage(error)}`, {
379
+ throw new TransportError("Network error", {
356
380
  status: 0,
357
381
  cause: error instanceof Error ? error : undefined,
358
382
  });
@@ -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
+ }