@apifuse/provider-sdk 2.1.0-beta.2 → 2.1.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.
Files changed (60) hide show
  1. package/AUTHORING.md +172 -8
  2. package/CHANGELOG.md +15 -1
  3. package/README.md +29 -15
  4. package/SUBMISSION.md +86 -0
  5. package/bin/apifuse-dev.ts +12 -5
  6. package/bin/apifuse-pack-check.ts +17 -2
  7. package/bin/apifuse-pack-smoke.ts +133 -6
  8. package/bin/apifuse-perf.ts +19 -15
  9. package/bin/apifuse-record.ts +41 -53
  10. package/bin/apifuse-submit-check.ts +1052 -0
  11. package/bin/apifuse.ts +1 -1
  12. package/package.json +19 -9
  13. package/src/choice-token.ts +164 -0
  14. package/src/cli/commands.ts +24 -3
  15. package/src/cli/create.ts +166 -51
  16. package/src/cli/templates/provider/README.md.tpl +66 -7
  17. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  18. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  19. package/src/cli/templates/provider/index.ts.tpl +5 -47
  20. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  21. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  22. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  23. package/src/cli/templates/provider/operations/ping.ts.tpl +23 -0
  24. package/src/cli/templates/provider/schemas/ping.ts.tpl +16 -0
  25. package/src/cli/templates/provider/start.ts.tpl +1 -1
  26. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  27. package/src/config/loader.ts +1206 -9
  28. package/src/define.ts +1648 -43
  29. package/src/errors.ts +12 -0
  30. package/src/i18n/catalog.ts +121 -0
  31. package/src/i18n/index.ts +2 -0
  32. package/src/i18n/keys.ts +64 -0
  33. package/src/index.ts +152 -8
  34. package/src/lint.ts +297 -42
  35. package/src/observability.ts +41 -0
  36. package/src/provider.ts +60 -3
  37. package/src/public-schema-field-lint.ts +237 -0
  38. package/src/runtime/auth-flow.ts +7 -0
  39. package/src/runtime/browser.ts +77 -21
  40. package/src/runtime/cache.ts +582 -0
  41. package/src/runtime/executor.ts +13 -1
  42. package/src/runtime/http.ts +939 -195
  43. package/src/runtime/insights.ts +11 -11
  44. package/src/runtime/instrumentation.ts +12 -4
  45. package/src/runtime/key-derivation.ts +1 -1
  46. package/src/runtime/keyring.ts +4 -3
  47. package/src/runtime/proxy-errors.ts +132 -0
  48. package/src/runtime/proxy-telemetry.ts +253 -0
  49. package/src/runtime/request-options.ts +66 -0
  50. package/src/runtime/state.ts +76 -0
  51. package/src/runtime/stealth.ts +1145 -0
  52. package/src/runtime/stt.ts +629 -0
  53. package/src/schema.ts +363 -1
  54. package/src/server/serve.ts +827 -60
  55. package/src/server/types.ts +35 -0
  56. package/src/stream.ts +210 -0
  57. package/src/testing/run.ts +17 -4
  58. package/src/types.ts +889 -50
  59. package/src/runtime/tls.ts +0 -434
  60. package/src/types/playwright-stealth.d.ts +0 -9
@@ -0,0 +1,237 @@
1
+ import type { ZodType } from "zod";
2
+
3
+ import type { LintDiagnostic } from "./lint";
4
+
5
+ type SchemaLike = ZodType & {
6
+ def?: Record<string, unknown>;
7
+ _def?: Record<string, unknown>;
8
+ shape?: Record<string, SchemaLike> | (() => Record<string, SchemaLike>);
9
+ element?: SchemaLike;
10
+ items?: SchemaLike[];
11
+ options?: SchemaLike[] | Set<SchemaLike> | Map<string, SchemaLike>;
12
+ innerType?: SchemaLike;
13
+ sourceType?: () => SchemaLike;
14
+ unwrap?: () => SchemaLike;
15
+ in?: SchemaLike;
16
+ out?: SchemaLike;
17
+ left?: SchemaLike;
18
+ right?: SchemaLike;
19
+ };
20
+
21
+ const ESTABLISHED_APIFUSE_PROTOCOL_FIELDS = new Set(["externalRef"]);
22
+
23
+ const UPSTREAM_FIELD_REPLACEMENTS = new Map<string, string>([
24
+ ["display", "limit"],
25
+ ["start", "offset"],
26
+ ["sort", "sort_by"],
27
+ ["lprice", "lowest_price"],
28
+ ["hprice", "highest_price"],
29
+ ["mallName", "mall_name"],
30
+ ["productId", "product_id"],
31
+ ["productType", "product_type_code"],
32
+ ["lastBuildDate", "upstream_generated_at"],
33
+ ["meta", "summary"],
34
+ ]);
35
+
36
+ function isSchema(value: unknown): value is SchemaLike {
37
+ return (
38
+ !!value &&
39
+ typeof value === "object" &&
40
+ "safeParse" in value &&
41
+ typeof value.safeParse === "function"
42
+ );
43
+ }
44
+
45
+ function getSchemaDef(schema: SchemaLike): Record<string, unknown> {
46
+ const def = schema.def ?? schema._def;
47
+ return def && typeof def === "object" ? def : {};
48
+ }
49
+
50
+ function isSchemaRecord(value: unknown): value is Record<string, SchemaLike> {
51
+ if (!value || typeof value !== "object") {
52
+ return false;
53
+ }
54
+ return Object.values(value).every(isSchema);
55
+ }
56
+
57
+ function getObjectShape(schema: SchemaLike): Record<string, SchemaLike> {
58
+ const rawShape =
59
+ typeof schema.shape === "function" ? schema.shape() : schema.shape;
60
+ if (isSchemaRecord(rawShape)) {
61
+ return rawShape;
62
+ }
63
+
64
+ const defShape = getSchemaDef(schema).shape;
65
+ if (typeof defShape === "function") {
66
+ const resolved = defShape();
67
+ return isSchemaRecord(resolved) ? resolved : {};
68
+ }
69
+ return isSchemaRecord(defShape) ? defShape : {};
70
+ }
71
+
72
+ function appendSchemaChildren(
73
+ children: SchemaLike[],
74
+ value: unknown,
75
+ ): SchemaLike[] {
76
+ if (isSchema(value)) {
77
+ children.push(value);
78
+ return children;
79
+ }
80
+ if (Array.isArray(value)) {
81
+ children.push(...value.filter(isSchema));
82
+ return children;
83
+ }
84
+ if (value instanceof Set) {
85
+ children.push(...Array.from(value).filter(isSchema));
86
+ return children;
87
+ }
88
+ if (value instanceof Map) {
89
+ children.push(...Array.from(value.values()).filter(isSchema));
90
+ return children;
91
+ }
92
+ return children;
93
+ }
94
+
95
+ function safeSourceType(schema: SchemaLike): SchemaLike | undefined {
96
+ try {
97
+ return schema.sourceType?.();
98
+ } catch {
99
+ return undefined;
100
+ }
101
+ }
102
+
103
+ function safeUnwrap(schema: SchemaLike): SchemaLike | undefined {
104
+ try {
105
+ return schema.unwrap?.();
106
+ } catch {
107
+ return undefined;
108
+ }
109
+ }
110
+
111
+ function getTransparentChildSchemas(schema: SchemaLike): SchemaLike[] {
112
+ const def = getSchemaDef(schema);
113
+ const children: SchemaLike[] = [];
114
+ for (const value of [
115
+ schema.element,
116
+ schema.items,
117
+ schema.options,
118
+ schema.innerType,
119
+ safeSourceType(schema),
120
+ safeUnwrap(schema),
121
+ schema.in,
122
+ schema.out,
123
+ schema.left,
124
+ schema.right,
125
+ def.schema,
126
+ def.innerType,
127
+ def.type,
128
+ def.valueType,
129
+ def.item,
130
+ def.items,
131
+ def.rest,
132
+ def.catchall,
133
+ def.option,
134
+ def.options,
135
+ def.pipe,
136
+ def.payload,
137
+ def.sourceType,
138
+ def.left,
139
+ def.right,
140
+ ]) {
141
+ appendSchemaChildren(children, value);
142
+ }
143
+ return children;
144
+ }
145
+
146
+ function recommendedReplacement(fieldName: string): string | undefined {
147
+ if (/^category\d+$/.test(fieldName)) {
148
+ return "category_path";
149
+ }
150
+ if (UPSTREAM_FIELD_REPLACEMENTS.has(fieldName)) {
151
+ return UPSTREAM_FIELD_REPLACEMENTS.get(fieldName);
152
+ }
153
+ if (/[a-z][A-Z]/.test(fieldName)) {
154
+ return fieldName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
155
+ }
156
+ return undefined;
157
+ }
158
+
159
+ function collectPublicSchemaFieldDiagnostics(
160
+ providerId: string,
161
+ operationId: string,
162
+ schema: unknown,
163
+ basePath: string,
164
+ seen = new Set<SchemaLike>(),
165
+ ): LintDiagnostic[] {
166
+ if (!isSchema(schema) || seen.has(schema)) {
167
+ return [];
168
+ }
169
+
170
+ seen.add(schema);
171
+ const diagnostics: LintDiagnostic[] = [];
172
+ for (const [fieldName, child] of Object.entries(getObjectShape(schema))) {
173
+ const fieldPath = `${basePath}.${fieldName}`;
174
+ const replacement = ESTABLISHED_APIFUSE_PROTOCOL_FIELDS.has(fieldName)
175
+ ? undefined
176
+ : recommendedReplacement(fieldName);
177
+ if (replacement) {
178
+ diagnostics.push({
179
+ rule: "public-schema-upstream-field",
180
+ level: "error",
181
+ field: fieldPath,
182
+ message: `Provider "${providerId}" operation "${operationId}" public schema field "${fieldPath}" uses upstream-shaped field "${fieldName}"; use APIFuse field "${replacement}" instead.`,
183
+ });
184
+ }
185
+ diagnostics.push(
186
+ ...collectPublicSchemaFieldDiagnostics(
187
+ providerId,
188
+ operationId,
189
+ child,
190
+ fieldPath,
191
+ seen,
192
+ ),
193
+ );
194
+ }
195
+
196
+ for (const child of getTransparentChildSchemas(schema)) {
197
+ const childPath = schema.element === child ? `${basePath}[]` : basePath;
198
+ diagnostics.push(
199
+ ...collectPublicSchemaFieldDiagnostics(
200
+ providerId,
201
+ operationId,
202
+ child,
203
+ childPath,
204
+ seen,
205
+ ),
206
+ );
207
+ }
208
+
209
+ return diagnostics;
210
+ }
211
+
212
+ export function lintPublicSchemaFieldNames(
213
+ providerId: string | undefined,
214
+ operationId: string,
215
+ input: unknown,
216
+ output: unknown,
217
+ enforce: boolean,
218
+ ): LintDiagnostic[] {
219
+ if (!providerId || !enforce) {
220
+ return [];
221
+ }
222
+
223
+ return [
224
+ ...collectPublicSchemaFieldDiagnostics(
225
+ providerId,
226
+ operationId,
227
+ input,
228
+ "input",
229
+ ),
230
+ ...collectPublicSchemaFieldDiagnostics(
231
+ providerId,
232
+ operationId,
233
+ output,
234
+ "output",
235
+ ),
236
+ ];
237
+ }
@@ -4,7 +4,10 @@ import type {
4
4
  EnvContext,
5
5
  FlowContext,
6
6
  HttpClient,
7
+ StealthClient,
8
+ SttContext,
7
9
  } from "../types";
10
+ import { createUnsupportedSttClient } from "./stt";
8
11
 
9
12
  function normalizeAllowedKeys(allowedKeys: string[]): Set<string> {
10
13
  return new Set(allowedKeys.filter((key) => key.trim().length > 0));
@@ -47,6 +50,7 @@ export function createScratchpad(
47
50
 
48
51
  export function createFlowContext(options: {
49
52
  http: HttpClient;
53
+ stealth: StealthClient;
50
54
  env: EnvContext;
51
55
  tenantId: string;
52
56
  providerId: string;
@@ -54,6 +58,7 @@ export function createFlowContext(options: {
54
58
  externalRef?: string;
55
59
  allowedKeys: string[];
56
60
  initialContext?: Record<string, unknown>;
61
+ stt?: SttContext;
57
62
  }): FlowContext {
58
63
  return {
59
64
  connectionId: options.connectionId,
@@ -61,7 +66,9 @@ export function createFlowContext(options: {
61
66
  tenantId: options.tenantId,
62
67
  providerId: options.providerId,
63
68
  http: options.http,
69
+ stealth: options.stealth,
64
70
  env: options.env,
65
71
  context: createScratchpad(options.allowedKeys, options.initialContext),
72
+ stt: options.stt ?? createUnsupportedSttClient(),
66
73
  };
67
74
  }
@@ -13,10 +13,14 @@ const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
13
13
  const SELECTOR_POLL_INTERVAL_MS = 100;
14
14
 
15
15
  type PlaywrightModule = typeof import("playwright");
16
- type PlaywrightStealthModule = {
17
- stealth(page: unknown): Promise<void>;
16
+ type PlaywrightExtraModule = {
17
+ chromium: PlaywrightModule["chromium"] & { use(plugin: unknown): unknown };
18
18
  };
19
19
 
20
+ type StealthPluginFactory = (options?: {
21
+ enabledEvasions?: Set<string>;
22
+ }) => unknown;
23
+
20
24
  type PoolAcquireResponse = {
21
25
  pageId: string;
22
26
  wsEndpoint: string;
@@ -65,7 +69,7 @@ type SupportedBrowserClient = BrowserClientContract & {
65
69
  };
66
70
 
67
71
  function getDefaultCdpPoolUrl(env = process.env): string | undefined {
68
- return env.CDP_POOL_URL ?? env.APIFUSE_CDP_POOL_URL;
72
+ return env.APIFUSE__CDP_POOL__URL;
69
73
  }
70
74
 
71
75
  async function importOptionalModule<T extends object>(
@@ -89,11 +93,16 @@ function unwrapModuleDefault<T extends object>(module: T): T {
89
93
  }
90
94
 
91
95
  function isModuleNotFoundError(error: unknown): boolean {
96
+ if (!(error instanceof Error)) {
97
+ return false;
98
+ }
99
+
100
+ const code = "code" in error ? error.code : undefined;
92
101
  return (
93
- error instanceof Error &&
94
- ("code" in error
95
- ? (error as Error & { code?: string }).code === "MODULE_NOT_FOUND"
96
- : error.message.includes("Cannot find module"))
102
+ code === "MODULE_NOT_FOUND" ||
103
+ code === "ERR_MODULE_NOT_FOUND" ||
104
+ error.message.includes("Cannot find module") ||
105
+ error.message.includes("Cannot find package")
97
106
  );
98
107
  }
99
108
 
@@ -120,7 +129,7 @@ function toLaunchOptions(options: BrowserClientOptions): LaunchOptions {
120
129
 
121
130
  async function loadPlaywright(): Promise<PlaywrightModule> {
122
131
  try {
123
- require("playwright");
132
+ await importOptionalModule<PlaywrightModule>("playwright");
124
133
  } catch (error) {
125
134
  if (isModuleNotFoundError(error)) {
126
135
  throw new ProviderError("Playwright is not installed", {
@@ -148,16 +157,31 @@ async function loadPlaywright(): Promise<PlaywrightModule> {
148
157
  }
149
158
  }
150
159
 
151
- async function loadPlaywrightStealth(): Promise<PlaywrightStealthModule> {
160
+ const playwrightExtraStealthLaunchers = new WeakSet<object>();
161
+
162
+ async function loadPlaywrightExtra(): Promise<PlaywrightExtraModule> {
163
+ try {
164
+ require("playwright");
165
+ } catch (error) {
166
+ if (isModuleNotFoundError(error)) {
167
+ throw new ProviderError("Playwright is not installed", {
168
+ cause: error instanceof Error ? error : undefined,
169
+ fix: "Run: bun add playwright",
170
+ });
171
+ }
172
+
173
+ throw error;
174
+ }
175
+
152
176
  try {
153
177
  return unwrapModuleDefault(
154
- await importOptionalModule<PlaywrightStealthModule>("playwright-stealth"),
178
+ await importOptionalModule<PlaywrightExtraModule>("playwright-extra"),
155
179
  );
156
180
  } catch (error) {
157
181
  if (isModuleNotFoundError(error)) {
158
- throw new ProviderError("playwright-stealth is not installed", {
182
+ throw new ProviderError("playwright-extra is not installed", {
159
183
  cause: error instanceof Error ? error : undefined,
160
- fix: "Run: bun add playwright-stealth",
184
+ fix: "Run: bun add playwright-extra puppeteer-extra-plugin-stealth",
161
185
  });
162
186
  }
163
187
 
@@ -165,6 +189,45 @@ async function loadPlaywrightStealth(): Promise<PlaywrightStealthModule> {
165
189
  }
166
190
  }
167
191
 
192
+ async function loadStealthPluginFactory(): Promise<StealthPluginFactory> {
193
+ try {
194
+ return unwrapModuleDefault(
195
+ await importOptionalModule<StealthPluginFactory>(
196
+ "puppeteer-extra-plugin-stealth",
197
+ ),
198
+ );
199
+ } catch (error) {
200
+ if (isModuleNotFoundError(error)) {
201
+ throw new ProviderError(
202
+ "puppeteer-extra-plugin-stealth is not installed",
203
+ {
204
+ cause: error instanceof Error ? error : undefined,
205
+ fix: "Run: bun add playwright-extra puppeteer-extra-plugin-stealth",
206
+ },
207
+ );
208
+ }
209
+
210
+ throw error;
211
+ }
212
+ }
213
+
214
+ async function loadChromiumLauncher(
215
+ options: BrowserClientOptions,
216
+ ): Promise<PlaywrightModule["chromium"]> {
217
+ if (!(options.stealth ?? true)) {
218
+ return (await loadPlaywright()).chromium;
219
+ }
220
+
221
+ const playwrightExtra = await loadPlaywrightExtra();
222
+ if (!playwrightExtraStealthLaunchers.has(playwrightExtra.chromium)) {
223
+ const createStealthPlugin = await loadStealthPluginFactory();
224
+ playwrightExtra.chromium.use(createStealthPlugin());
225
+ playwrightExtraStealthLaunchers.add(playwrightExtra.chromium);
226
+ }
227
+
228
+ return playwrightExtra.chromium;
229
+ }
230
+
168
231
  async function loadNodriver(): Promise<void> {
169
232
  try {
170
233
  await importOptionalModule("nodriver");
@@ -256,10 +319,8 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
256
319
  return this.browser;
257
320
  }
258
321
 
259
- const playwright = await loadPlaywright();
260
- this.browser = await playwright.chromium.launch(
261
- toLaunchOptions(this.options),
262
- );
322
+ const chromium = await loadChromiumLauncher(this.options);
323
+ this.browser = await chromium.launch(toLaunchOptions(this.options));
263
324
  return this.browser;
264
325
  }
265
326
 
@@ -267,11 +328,6 @@ class PlaywrightBrowserClient implements SupportedBrowserClient {
267
328
  const browser = await this.ensureBrowser();
268
329
  const page = await browser.newPage();
269
330
 
270
- if (this.options.stealth ?? true) {
271
- const { stealth } = await loadPlaywrightStealth();
272
- await stealth(page);
273
- }
274
-
275
331
  return new PlaywrightBrowserPage(page);
276
332
  }
277
333