@apifuse/provider-sdk 2.1.0-beta.3 → 2.1.0-beta.5

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 (63) hide show
  1. package/AUTHORING.md +187 -8
  2. package/CHANGELOG.md +13 -1
  3. package/README.md +40 -18
  4. package/SUBMISSION.md +4 -4
  5. package/bin/apifuse-dev.ts +12 -5
  6. package/bin/apifuse-pack-check.ts +9 -2
  7. package/bin/apifuse-pack-smoke.ts +127 -6
  8. package/bin/apifuse-perf.ts +76 -31
  9. package/bin/apifuse-record.ts +148 -94
  10. package/bin/apifuse-submit-check.ts +243 -7
  11. package/bin/apifuse.ts +1 -1
  12. package/package.json +17 -8
  13. package/src/choice-token.ts +164 -0
  14. package/src/cli/commands.ts +4 -7
  15. package/src/cli/create.ts +180 -51
  16. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  17. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  18. package/src/cli/templates/provider/README.md.tpl +42 -7
  19. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  20. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  21. package/src/cli/templates/provider/index.ts.tpl +5 -47
  22. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  23. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  24. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  25. package/src/cli/templates/provider/operations/ping.ts.tpl +23 -0
  26. package/src/cli/templates/provider/schemas/ping.ts.tpl +16 -0
  27. package/src/cli/templates/provider/start.ts.tpl +1 -1
  28. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  29. package/src/config/loader.ts +1206 -9
  30. package/src/define.ts +1620 -106
  31. package/src/errors.ts +12 -0
  32. package/src/i18n/catalog.ts +121 -0
  33. package/src/i18n/index.ts +2 -0
  34. package/src/i18n/keys.ts +64 -0
  35. package/src/index.ts +149 -8
  36. package/src/lint.ts +306 -51
  37. package/src/observability.ts +41 -0
  38. package/src/provider.ts +60 -3
  39. package/src/public-schema-field-lint.ts +237 -0
  40. package/src/runtime/auth-flow.ts +7 -0
  41. package/src/runtime/browser.ts +77 -21
  42. package/src/runtime/cache.ts +582 -0
  43. package/src/runtime/executor.ts +13 -1
  44. package/src/runtime/http.ts +939 -195
  45. package/src/runtime/insights.ts +11 -11
  46. package/src/runtime/instrumentation.ts +12 -4
  47. package/src/runtime/key-derivation.ts +1 -1
  48. package/src/runtime/keyring.ts +4 -3
  49. package/src/runtime/proxy-errors.ts +132 -0
  50. package/src/runtime/proxy-telemetry.ts +253 -0
  51. package/src/runtime/request-options.ts +66 -0
  52. package/src/runtime/state.ts +76 -0
  53. package/src/runtime/stealth.ts +1145 -0
  54. package/src/runtime/stt.ts +629 -0
  55. package/src/runtime/trace.ts +1 -1
  56. package/src/schema.ts +363 -1
  57. package/src/server/serve.ts +816 -58
  58. package/src/server/types.ts +35 -0
  59. package/src/stream.ts +210 -0
  60. package/src/testing/run.ts +17 -4
  61. package/src/types.ts +876 -53
  62. package/src/runtime/tls.ts +0 -434
  63. package/src/types/playwright-stealth.d.ts +0 -9
@@ -39,17 +39,19 @@ const PING_RESPONSE_SCHEMA = z.object({
39
39
  error: z.unknown().optional(),
40
40
  });
41
41
 
42
- const KEEP_TEMP = process.env.APIFUSE_PACK_SMOKE_KEEP_TEMP === "1";
42
+ const KEEP_TEMP = process.env.APIFUSE__PACK_SMOKE__KEEP_TEMP === "1";
43
43
 
44
44
  const tempRoot = mkdtempSync(
45
45
  join(tmpdir(), "apifuse-provider-sdk-pack-smoke-"),
46
46
  );
47
47
  const packDir = join(tempRoot, "pack");
48
48
  const consumerDir = join(tempRoot, "consumer");
49
+ const externalWorkspaceDir = join(tempRoot, "external-workspace");
49
50
 
50
51
  try {
51
52
  mkdirSync(packDir, { recursive: true });
52
53
  mkdirSync(consumerDir, { recursive: true });
54
+ mkdirSync(join(externalWorkspaceDir, "providers"), { recursive: true });
53
55
 
54
56
  const packed = packSdk(packDir);
55
57
  const tarballPath = resolve(packDir, packed.filename);
@@ -97,6 +99,11 @@ try {
97
99
  run("bun", ["run", "test"], generatedProviderDir);
98
100
  assertGeneratedReadme(generatedProviderDir);
99
101
  await smokeGeneratedDevServer(generatedProviderDir);
102
+ assertExternalWorkspaceTopology(
103
+ cliBin,
104
+ externalWorkspaceDir,
105
+ tarballSpecifier,
106
+ );
100
107
 
101
108
  console.log(
102
109
  `Provider SDK packed-artifact smoke passed: ${tarballPath} -> ${generatedProviderDir}`,
@@ -109,6 +116,103 @@ try {
109
116
  }
110
117
  }
111
118
 
119
+ function assertExternalWorkspaceTopology(
120
+ cliBin: string,
121
+ externalWorkspaceDir: string,
122
+ tarballSpecifier: string,
123
+ ): void {
124
+ writeFileSync(
125
+ join(externalWorkspaceDir, "package.json"),
126
+ `${JSON.stringify(
127
+ {
128
+ private: true,
129
+ type: "module",
130
+ workspaces: ["providers/*"],
131
+ },
132
+ null,
133
+ 2,
134
+ )}\n`,
135
+ );
136
+
137
+ run(
138
+ "bun",
139
+ [
140
+ cliBin,
141
+ "create",
142
+ "external-workspace-smoke",
143
+ "--yes",
144
+ "--json",
145
+ "--sdk-specifier",
146
+ tarballSpecifier,
147
+ ],
148
+ externalWorkspaceDir,
149
+ );
150
+
151
+ const generatedProviderDir = join(
152
+ externalWorkspaceDir,
153
+ "external-workspace-smoke",
154
+ );
155
+ const forbiddenProviderDir = join(
156
+ externalWorkspaceDir,
157
+ "providers",
158
+ "external-workspace-smoke",
159
+ );
160
+ if (!existsSync(generatedProviderDir)) {
161
+ throw new Error(
162
+ "Public create must generate a one-provider repository at <name>/ even when providers/ exists.",
163
+ );
164
+ }
165
+ if (existsSync(forbiddenProviderDir)) {
166
+ throw new Error(
167
+ "Public create must not generate providers/<name>/ in external bounty workspaces.",
168
+ );
169
+ }
170
+
171
+ const packageJson = JSON.parse(
172
+ readFileSync(join(generatedProviderDir, "package.json"), "utf8"),
173
+ );
174
+ const sdkDependency = packageJson?.dependencies?.["@apifuse/provider-sdk"];
175
+ if (sdkDependency !== tarballSpecifier) {
176
+ throw new Error(
177
+ `Expected generated provider to depend on packed SDK ${tarballSpecifier}, got ${sdkDependency}`,
178
+ );
179
+ }
180
+ if (JSON.stringify(packageJson).includes("workspace:")) {
181
+ throw new Error(
182
+ "External bounty workspace scaffold must not contain workspace: dependencies.",
183
+ );
184
+ }
185
+
186
+ run("bun", ["install"], generatedProviderDir);
187
+ run("bun", ["run", "check"], generatedProviderDir);
188
+ run("bun", ["run", "submit-check"], generatedProviderDir);
189
+ run("bun", ["run", "test"], generatedProviderDir);
190
+
191
+ const monorepoAttempt = spawnSync(
192
+ "bun",
193
+ [cliBin, "create", "bad-monorepo-smoke", "--preset", "monorepo", "--yes"],
194
+ {
195
+ cwd: externalWorkspaceDir,
196
+ env: { ...process.env, APIFUSE__SDK__SPECIFIER: tarballSpecifier },
197
+ encoding: "utf8",
198
+ stdio: ["ignore", "pipe", "pipe"],
199
+ },
200
+ );
201
+ if (monorepoAttempt.status === 0) {
202
+ throw new Error(
203
+ "--preset monorepo must reject outside the private APIFuse monorepo.",
204
+ );
205
+ }
206
+ const rejectionOutput = `${monorepoAttempt.stdout}\n${monorepoAttempt.stderr}`;
207
+ if (
208
+ !rejectionOutput.includes(
209
+ "Monorepo preset is internal to the APIFuse repository",
210
+ )
211
+ ) {
212
+ throw new Error(`Unexpected monorepo rejection output: ${rejectionOutput}`);
213
+ }
214
+ }
215
+
112
216
  function packSdk(destination: string): { filename: string } {
113
217
  const raw = execFileSync(
114
218
  "npm",
@@ -162,9 +266,9 @@ function assertGeneratedReadme(providerDir: string): void {
162
266
  "Generated README is missing browser runtime troubleshooting guidance.",
163
267
  );
164
268
  }
165
- if (!readme.includes("bun pm untrusted")) {
269
+ if (!readme.includes("impit")) {
166
270
  throw new Error(
167
- "Generated README is missing Bun trusted-dependency troubleshooting guidance.",
271
+ "Generated README is missing impit stealth runtime guidance.",
168
272
  );
169
273
  }
170
274
  if (!readme.includes("bun run submit-check")) {
@@ -183,7 +287,8 @@ async function smokeGeneratedDevServer(providerDir: string): Promise<void> {
183
287
  const port = await getAvailablePort();
184
288
  const server = spawn("bun", ["run", "dev"], {
185
289
  cwd: providerDir,
186
- env: { ...process.env, PORT: String(port) },
290
+ env: { ...process.env, APIFUSE__RUNTIME__PORT: String(port) },
291
+ detached: process.platform !== "win32",
187
292
  stdio: ["ignore", "pipe", "pipe"],
188
293
  });
189
294
  let output = "";
@@ -286,11 +391,11 @@ async function stopServer(server: ChildProcess): Promise<void> {
286
391
  if (server.exitCode !== null) {
287
392
  return;
288
393
  }
289
- server.kill("SIGTERM");
394
+ killProcessTree(server, "SIGTERM");
290
395
  await new Promise<void>((resolvePromise) => {
291
396
  const timeout = setTimeout(() => {
292
397
  if (server.exitCode === null) {
293
- server.kill("SIGKILL");
398
+ killProcessTree(server, "SIGKILL");
294
399
  }
295
400
  resolvePromise();
296
401
  }, 2_000);
@@ -300,3 +405,19 @@ async function stopServer(server: ChildProcess): Promise<void> {
300
405
  });
301
406
  });
302
407
  }
408
+
409
+ function killProcessTree(server: ChildProcess, signal: NodeJS.Signals): void {
410
+ if (server.pid === undefined) {
411
+ return;
412
+ }
413
+
414
+ try {
415
+ if (process.platform === "win32") {
416
+ server.kill(signal);
417
+ return;
418
+ }
419
+ process.kill(-server.pid, signal);
420
+ } catch {
421
+ server.kill(signal);
422
+ }
423
+ }
@@ -8,8 +8,10 @@ import { pathToFileURL } from "node:url";
8
8
 
9
9
  import {
10
10
  type ApiFuseConfig,
11
+ createBypassProviderCache,
11
12
  createHttpClient,
12
- createTlsClient,
13
+ createStealthClient,
14
+ createSttClientFromEnv,
13
15
  executeOperation,
14
16
  getProviderBaseUrl,
15
17
  type HttpClient,
@@ -18,8 +20,8 @@ import {
18
20
  type ProviderDefinition,
19
21
  ProviderError,
20
22
  type Span,
21
- type TlsClient,
22
- type TlsResponse,
23
+ type StealthClient,
24
+ type StealthResponse,
23
25
  wrapWithInstrumentation,
24
26
  } from "../src";
25
27
  import {
@@ -41,6 +43,7 @@ type CliArgs = {
41
43
  exportPath?: string;
42
44
  flame: boolean;
43
45
  operation: string;
46
+ params?: string;
44
47
  runs: number;
45
48
  warmup: number;
46
49
  };
@@ -79,6 +82,21 @@ const DEFAULT_RUNS = 10;
79
82
  const DEFAULT_WARMUP = 2;
80
83
  const DEFAULT_CONCURRENCY = 1;
81
84
  const BAR_WIDTH = 20;
85
+ const HELP_TEXT = `Usage: apifuse perf <provider-path> --operation <operation> [options]
86
+
87
+ Options:
88
+ --operation, -o <name> operation to profile (required)
89
+ --params, -p <json> JSON input template; falls back to fixtures.request or {}
90
+ --runs, -n <number> number of runs (default: 10)
91
+ --warmup <number> warmup runs (default: 2)
92
+ --concurrency, -c <n> concurrent requests (default: 1)
93
+ --compare-proxy run with proxy on/off and compare
94
+ --export <path> export results to JSON file
95
+ --flame generate flamegraph SVG
96
+ --help, -h show this help
97
+
98
+ Example:
99
+ apifuse perf providers/airkorea --operation realtime --params '{"stationName":"jongno"}' --runs 5`;
82
100
 
83
101
  export async function main() {
84
102
  try {
@@ -92,7 +110,11 @@ export async function main() {
92
110
  const inputSchema = getOperationSchema(provider, operation, "input");
93
111
  const outputSchema = getOperationSchema(provider, operation, "output");
94
112
  const fixtureReplay = await loadFixtureReplay(providerDirectory);
95
- const inputTemplate = resolveInputTemplate(provider, inputSchema);
113
+ const inputTemplate = resolveInputTemplate(
114
+ provider,
115
+ inputSchema,
116
+ args.params,
117
+ );
96
118
 
97
119
  const directSuite = await runProfileSuite({
98
120
  args,
@@ -182,6 +204,7 @@ function parseArgs(argv: string[]): CliArgs {
182
204
  let compareProxy = false;
183
205
  let exportPath: string | undefined;
184
206
  let flame = false;
207
+ let params: string | undefined;
185
208
 
186
209
  for (let index = 0; index < argv.length; index += 1) {
187
210
  const arg = argv[index];
@@ -199,6 +222,11 @@ function parseArgs(argv: string[]): CliArgs {
199
222
  continue;
200
223
  }
201
224
 
225
+ if (arg === "--help" || arg === "-h") {
226
+ console.log(HELP_TEXT);
227
+ process.exit(0);
228
+ }
229
+
202
230
  if (arg === "--compare-proxy") {
203
231
  compareProxy = true;
204
232
  continue;
@@ -220,6 +248,22 @@ function parseArgs(argv: string[]): CliArgs {
220
248
  continue;
221
249
  }
222
250
 
251
+ if (arg === "--params" || arg === "-p") {
252
+ params = requireArgValue(argv, index, arg);
253
+ index += 1;
254
+ continue;
255
+ }
256
+
257
+ if (arg.startsWith("--params=")) {
258
+ params = arg.slice("--params=".length);
259
+ continue;
260
+ }
261
+
262
+ if (arg.startsWith("-p=")) {
263
+ params = arg.slice("-p=".length);
264
+ continue;
265
+ }
266
+
223
267
  if (arg.startsWith("-o=")) {
224
268
  operation = arg.slice("-o=".length);
225
269
  continue;
@@ -292,20 +336,7 @@ function parseArgs(argv: string[]): CliArgs {
292
336
  }
293
337
 
294
338
  if (!providerPath || !operation) {
295
- throw new Error(
296
- [
297
- "Usage: apifuse perf <provider-path> [options]",
298
- "",
299
- "Options:",
300
- " --operation, -o <name> operation to profile (required)",
301
- " --runs, -n <number> number of runs (default: 10)",
302
- " --warmup <number> warmup runs (default: 2)",
303
- " --concurrency, -c <n> concurrent requests (default: 1)",
304
- " --compare-proxy run with proxy on/off and compare",
305
- " --export <path> export results to JSON file",
306
- " --flame generate flamegraph SVG",
307
- ].join("\n"),
308
- );
339
+ throw new Error(HELP_TEXT);
309
340
  }
310
341
 
311
342
  return {
@@ -315,6 +346,7 @@ function parseArgs(argv: string[]): CliArgs {
315
346
  exportPath,
316
347
  flame,
317
348
  operation,
349
+ params,
318
350
  runs,
319
351
  warmup,
320
352
  };
@@ -409,7 +441,18 @@ function isSchema(value: unknown): value is { parse(input: unknown): unknown } {
409
441
  function resolveInputTemplate(
410
442
  provider: ProviderDefinition,
411
443
  inputSchema: { parse(input: unknown): unknown },
444
+ params: string | undefined,
412
445
  ): unknown {
446
+ if (params !== undefined) {
447
+ try {
448
+ return inputSchema.parse(JSON.parse(params));
449
+ } catch (error) {
450
+ throw new Error(
451
+ `Failed to parse --params JSON or validate input: ${error instanceof Error ? error.message : String(error)}`,
452
+ );
453
+ }
454
+ }
455
+
413
456
  const firstOp = Object.values(provider.operations)[0];
414
457
  if (firstOp?.fixtures?.request !== undefined) {
415
458
  return firstOp.fixtures.request;
@@ -440,12 +483,12 @@ async function loadFixtureReplay(
440
483
  }
441
484
 
442
485
  function assertProxyConfigured(config: ApiFuseConfig): void {
443
- if (config.proxy?.url || process.env.APIFUSE_PROXY_URL) {
486
+ if (config.proxy?.url || process.env.APIFUSE__PROXY__URL) {
444
487
  return;
445
488
  }
446
489
 
447
490
  throw new Error(
448
- "--compare-proxy requires a proxy URL in apifuse.config.ts or APIFUSE_PROXY_URL.",
491
+ "--compare-proxy requires a proxy URL in apifuse.config.ts or APIFUSE__PROXY__URL.",
449
492
  );
450
493
  }
451
494
 
@@ -637,10 +680,10 @@ function createBaseContext(options: {
637
680
  apifuseConfig,
638
681
  upstream,
639
682
  });
640
- const tls =
683
+ const stealth =
641
684
  options.forceFixtureReplay && options.fixtureReplay
642
- ? createFixtureTlsClient(options.fixtureReplay.rawText)
643
- : createTlsClient(getProviderBaseUrl(options.provider), {
685
+ ? createFixtureStealthClient(options.fixtureReplay.rawText)
686
+ : createStealthClient(getProviderBaseUrl(options.provider), {
644
687
  apifuseConfig,
645
688
  upstream,
646
689
  });
@@ -657,10 +700,12 @@ function createBaseContext(options: {
657
700
  getScopes: () => [],
658
701
  },
659
702
  http,
660
- tls,
703
+ cache: createBypassProviderCache({ providerId: options.provider.id }),
704
+ stealth,
661
705
  browser: createBrowserStub(),
662
706
  trace: options.traceContext,
663
707
  auth: createAuthStub(),
708
+ stt: createSttClientFromEnv(options.provider.stt),
664
709
  };
665
710
  }
666
711
 
@@ -695,8 +740,8 @@ function createFixtureResponse(raw: unknown) {
695
740
  };
696
741
  }
697
742
 
698
- function createFixtureTlsClient(rawText: string): TlsClient {
699
- const createResponse = async (): Promise<TlsResponse> => ({
743
+ function createFixtureStealthClient(rawText: string): StealthClient {
744
+ const createResponse = async (): Promise<StealthResponse> => ({
700
745
  status: 200,
701
746
  ok: true,
702
747
  headers: { "content-type": "application/json" },
@@ -738,7 +783,7 @@ function buildInsights(
738
783
  ): string[] {
739
784
  const insights: string[] = [];
740
785
  const allSpans = runs.flatMap((run) => run.spans);
741
- const tlsSpans = allSpans.filter((span) => span.name === "tls.fetch");
786
+ const stealthSpans = allSpans.filter((span) => span.name === "stealth.fetch");
742
787
  const dnsSpans = allSpans.filter((span) => span.name === "dns");
743
788
  const transform = breakdown.find(
744
789
  (entry) => entry.name === "transformResponse",
@@ -746,7 +791,7 @@ function buildInsights(
746
791
  const responseSizes = allSpans
747
792
  .map((span) => span.attributes.response_size)
748
793
  .filter((value): value is number => typeof value === "number");
749
- const reuseFlags = tlsSpans
794
+ const reuseFlags = stealthSpans
750
795
  .map((span) => span.attributes.connection_reused)
751
796
  .filter((value): value is boolean => typeof value === "boolean");
752
797
 
@@ -756,8 +801,8 @@ function buildInsights(
756
801
  );
757
802
  insights.push(
758
803
  reusePercent >= 80
759
- ? `✓ TLS connection reuse: ${reusePercent}% (good)`
760
- : `⚠ TLS connection reuse: ${reusePercent}% — consider session reuse`,
804
+ ? `✓ Stealth connection reuse: ${reusePercent}% (good)`
805
+ : `⚠ Stealth connection reuse: ${reusePercent}% — consider session reuse`,
761
806
  );
762
807
  }
763
808
 
@@ -1091,7 +1136,7 @@ function cloneValue<T>(value: T): T {
1091
1136
 
1092
1137
  function handleCliError(error: unknown): never {
1093
1138
  const message = error instanceof Error ? error.message : String(error);
1094
- console.error(message);
1139
+ console.error(`[apifuse perf] ${message}`);
1095
1140
  process.exit(1);
1096
1141
  }
1097
1142