@apifuse/provider-sdk 2.1.0-beta.1 → 2.1.0-beta.10

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 (212) hide show
  1. package/AUTHORING.md +208 -2
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +114 -10
  4. package/SUBMISSION.md +87 -0
  5. package/bin/apifuse-check.ts +86 -4
  6. package/bin/apifuse-dev.ts +87 -13
  7. package/bin/apifuse-pack-check.ts +80 -0
  8. package/bin/apifuse-pack-smoke.ts +303 -2
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +2538 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/ceremonies/index.d.ts +41 -0
  14. package/dist/ceremonies/index.js +490 -0
  15. package/dist/choice-token.d.ts +24 -0
  16. package/dist/choice-token.js +74 -0
  17. package/dist/cli/commands.d.ts +10 -0
  18. package/dist/cli/commands.js +80 -0
  19. package/dist/cli/create.d.ts +47 -0
  20. package/dist/cli/create.js +762 -0
  21. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  22. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  23. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  24. package/dist/cli/templates/provider/README.md.tpl +160 -0
  25. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  26. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  27. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  28. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  29. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  30. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  31. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  32. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  33. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  34. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  35. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  36. package/dist/config/loader.d.ts +107 -0
  37. package/dist/config/loader.js +935 -0
  38. package/dist/contract-json.d.ts +9 -0
  39. package/dist/contract-json.js +51 -0
  40. package/dist/contract-serialization.d.ts +4 -0
  41. package/dist/contract-serialization.js +78 -0
  42. package/dist/contract-types.d.ts +49 -0
  43. package/dist/contract-types.js +1 -0
  44. package/dist/contract.d.ts +6 -0
  45. package/dist/contract.js +155 -0
  46. package/dist/define.d.ts +97 -0
  47. package/dist/define.js +1320 -0
  48. package/dist/dev.d.ts +9 -0
  49. package/dist/dev.js +15 -0
  50. package/dist/errors.d.ts +59 -0
  51. package/dist/errors.js +97 -0
  52. package/dist/i18n/catalog.d.ts +29 -0
  53. package/dist/i18n/catalog.js +159 -0
  54. package/dist/i18n/index.d.ts +2 -0
  55. package/dist/i18n/index.js +2 -0
  56. package/dist/i18n/keys.d.ts +10 -0
  57. package/dist/i18n/keys.js +34 -0
  58. package/dist/index.d.ts +41 -0
  59. package/dist/index.js +37 -0
  60. package/dist/lint.d.ts +73 -0
  61. package/dist/lint.js +702 -0
  62. package/dist/observability.d.ts +5 -0
  63. package/dist/observability.js +39 -0
  64. package/dist/provider.d.ts +9 -0
  65. package/dist/provider.js +8 -0
  66. package/dist/public-schema-field-lint.d.ts +2 -0
  67. package/dist/public-schema-field-lint.js +158 -0
  68. package/dist/recipes/gov-api.d.ts +19 -0
  69. package/dist/recipes/gov-api.js +72 -0
  70. package/dist/recipes/rest-api.d.ts +21 -0
  71. package/dist/recipes/rest-api.js +115 -0
  72. package/dist/runtime/auth-flow.d.ts +14 -0
  73. package/dist/runtime/auth-flow.js +44 -0
  74. package/dist/runtime/browser.d.ts +25 -0
  75. package/dist/runtime/browser.js +1034 -0
  76. package/dist/runtime/cache.d.ts +10 -0
  77. package/dist/runtime/cache.js +372 -0
  78. package/dist/runtime/choice.d.ts +15 -0
  79. package/dist/runtime/choice.js +435 -0
  80. package/dist/runtime/credential.d.ts +8 -0
  81. package/dist/runtime/credential.js +61 -0
  82. package/dist/runtime/env.d.ts +2 -0
  83. package/dist/runtime/env.js +10 -0
  84. package/dist/runtime/executor.d.ts +16 -0
  85. package/dist/runtime/executor.js +51 -0
  86. package/dist/runtime/http.d.ts +8 -0
  87. package/dist/runtime/http.js +706 -0
  88. package/dist/runtime/insights.d.ts +9 -0
  89. package/dist/runtime/insights.js +324 -0
  90. package/dist/runtime/instrumentation.d.ts +8 -0
  91. package/dist/runtime/instrumentation.js +269 -0
  92. package/dist/runtime/key-derivation.d.ts +24 -0
  93. package/dist/runtime/key-derivation.js +73 -0
  94. package/dist/runtime/keyring.d.ts +25 -0
  95. package/dist/runtime/keyring.js +93 -0
  96. package/dist/runtime/namespace.d.ts +9 -0
  97. package/dist/runtime/namespace.js +19 -0
  98. package/dist/runtime/otlp.d.ts +39 -0
  99. package/dist/runtime/otlp.js +103 -0
  100. package/dist/runtime/perf.d.ts +12 -0
  101. package/dist/runtime/perf.js +52 -0
  102. package/dist/runtime/prevalidate.d.ts +12 -0
  103. package/dist/runtime/prevalidate.js +173 -0
  104. package/dist/runtime/provider.d.ts +2 -0
  105. package/dist/runtime/provider.js +11 -0
  106. package/dist/runtime/proxy-errors.d.ts +21 -0
  107. package/dist/runtime/proxy-errors.js +83 -0
  108. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  109. package/dist/runtime/proxy-telemetry.js +174 -0
  110. package/dist/runtime/redis.d.ts +17 -0
  111. package/dist/runtime/redis.js +82 -0
  112. package/dist/runtime/request-options.d.ts +3 -0
  113. package/dist/runtime/request-options.js +42 -0
  114. package/dist/runtime/state.d.ts +17 -0
  115. package/dist/runtime/state.js +344 -0
  116. package/dist/runtime/stealth.d.ts +18 -0
  117. package/dist/runtime/stealth.js +834 -0
  118. package/dist/runtime/stt.d.ts +22 -0
  119. package/dist/runtime/stt.js +480 -0
  120. package/dist/runtime/trace.d.ts +26 -0
  121. package/dist/runtime/trace.js +142 -0
  122. package/dist/runtime/waterfall.d.ts +12 -0
  123. package/dist/runtime/waterfall.js +147 -0
  124. package/dist/schema.d.ts +74 -0
  125. package/dist/schema.js +243 -0
  126. package/dist/serve.d.ts +1 -0
  127. package/dist/serve.js +1 -0
  128. package/dist/server/index.d.ts +3 -0
  129. package/dist/server/index.js +2 -0
  130. package/dist/server/serve.d.ts +64 -0
  131. package/dist/server/serve.js +1110 -0
  132. package/dist/server/types.d.ts +136 -0
  133. package/dist/server/types.js +86 -0
  134. package/dist/stealth/profiles.d.ts +4 -0
  135. package/dist/stealth/profiles.js +259 -0
  136. package/dist/stream.d.ts +44 -0
  137. package/dist/stream.js +151 -0
  138. package/dist/testing/helpers.d.ts +23 -0
  139. package/dist/testing/helpers.js +95 -0
  140. package/dist/testing/index.d.ts +2 -0
  141. package/dist/testing/index.js +2 -0
  142. package/dist/testing/run.d.ts +34 -0
  143. package/dist/testing/run.js +303 -0
  144. package/dist/types.d.ts +1326 -0
  145. package/dist/types.js +61 -0
  146. package/dist/utils/date.d.ts +6 -0
  147. package/dist/utils/date.js +101 -0
  148. package/dist/utils/parse.d.ts +16 -0
  149. package/dist/utils/parse.js +51 -0
  150. package/dist/utils/text.d.ts +4 -0
  151. package/dist/utils/text.js +14 -0
  152. package/dist/utils/transform.d.ts +8 -0
  153. package/dist/utils/transform.js +48 -0
  154. package/package.json +57 -30
  155. package/src/ceremonies/index.ts +8 -2
  156. package/src/choice-token.ts +165 -0
  157. package/src/cli/commands.ts +34 -11
  158. package/src/cli/create.ts +214 -52
  159. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  160. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  161. package/src/cli/templates/provider/README.md.tpl +120 -1
  162. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  163. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  164. package/src/cli/templates/provider/index.ts.tpl +5 -48
  165. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  166. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  167. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  168. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  169. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  170. package/src/cli/templates/provider/start.ts.tpl +1 -1
  171. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  172. package/src/config/loader.ts +1224 -9
  173. package/src/contract-json.ts +75 -0
  174. package/src/contract-serialization.ts +89 -0
  175. package/src/contract-types.ts +52 -0
  176. package/src/contract.ts +215 -0
  177. package/src/define.ts +1688 -48
  178. package/src/errors.ts +27 -0
  179. package/src/i18n/catalog.ts +277 -0
  180. package/src/i18n/index.ts +2 -0
  181. package/src/i18n/keys.ts +64 -0
  182. package/src/index.ts +174 -9
  183. package/src/lint.ts +547 -73
  184. package/src/observability.ts +41 -0
  185. package/src/provider.ts +104 -4
  186. package/src/public-schema-field-lint.ts +237 -0
  187. package/src/runtime/auth-flow.ts +7 -0
  188. package/src/runtime/browser.ts +762 -51
  189. package/src/runtime/cache.ts +528 -0
  190. package/src/runtime/choice.ts +760 -0
  191. package/src/runtime/executor.ts +32 -3
  192. package/src/runtime/http.ts +939 -195
  193. package/src/runtime/insights.ts +11 -11
  194. package/src/runtime/instrumentation.ts +12 -4
  195. package/src/runtime/key-derivation.ts +1 -1
  196. package/src/runtime/keyring.ts +4 -3
  197. package/src/runtime/proxy-errors.ts +132 -0
  198. package/src/runtime/proxy-telemetry.ts +253 -0
  199. package/src/runtime/redis.ts +116 -0
  200. package/src/runtime/request-options.ts +66 -0
  201. package/src/runtime/state.ts +563 -0
  202. package/src/runtime/stealth.ts +1159 -0
  203. package/src/runtime/stt.ts +629 -0
  204. package/src/runtime/trace.ts +1 -1
  205. package/src/schema.ts +363 -1
  206. package/src/server/serve.ts +1157 -75
  207. package/src/server/types.ts +37 -0
  208. package/src/stream.ts +210 -0
  209. package/src/testing/run.ts +31 -5
  210. package/src/types.ts +1107 -59
  211. package/src/runtime/tls.ts +0 -434
  212. package/src/types/playwright-stealth.d.ts +0 -9
@@ -1,13 +1,20 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { execFileSync, spawnSync } from "node:child_process";
3
+ import {
4
+ type ChildProcess,
5
+ execFileSync,
6
+ spawn,
7
+ spawnSync,
8
+ } from "node:child_process";
4
9
  import {
5
10
  existsSync,
6
11
  mkdirSync,
7
12
  mkdtempSync,
13
+ readFileSync,
8
14
  rmSync,
9
15
  writeFileSync,
10
16
  } from "node:fs";
17
+ import { createServer } from "node:net";
11
18
  import { tmpdir } from "node:os";
12
19
  import { join, resolve } from "node:path";
13
20
  import { z } from "zod";
@@ -17,18 +24,34 @@ const PACK_RESULT_SCHEMA = z.array(
17
24
  filename: z.string(),
18
25
  }),
19
26
  );
27
+ const HEALTH_RESPONSE_SCHEMA = z.object({
28
+ status: z.string(),
29
+ provider: z.string(),
30
+ version: z.string().optional(),
31
+ });
32
+ const PING_RESPONSE_SCHEMA = z.object({
33
+ data: z
34
+ .object({
35
+ ok: z.boolean(),
36
+ message: z.string(),
37
+ })
38
+ .optional(),
39
+ error: z.unknown().optional(),
40
+ });
20
41
 
21
- const KEEP_TEMP = process.env.APIFUSE_PACK_SMOKE_KEEP_TEMP === "1";
42
+ const KEEP_TEMP = process.env.APIFUSE__PACK_SMOKE__KEEP_TEMP === "1";
22
43
 
23
44
  const tempRoot = mkdtempSync(
24
45
  join(tmpdir(), "apifuse-provider-sdk-pack-smoke-"),
25
46
  );
26
47
  const packDir = join(tempRoot, "pack");
27
48
  const consumerDir = join(tempRoot, "consumer");
49
+ const externalWorkspaceDir = join(tempRoot, "external-workspace");
28
50
 
29
51
  try {
30
52
  mkdirSync(packDir, { recursive: true });
31
53
  mkdirSync(consumerDir, { recursive: true });
54
+ mkdirSync(join(externalWorkspaceDir, "providers"), { recursive: true });
32
55
 
33
56
  const packed = packSdk(packDir);
34
57
  const tarballPath = resolve(packDir, packed.filename);
@@ -72,7 +95,15 @@ try {
72
95
 
73
96
  const generatedProviderDir = join(consumerDir, "dx-smoke");
74
97
  run("bun", ["run", "check"], generatedProviderDir);
98
+ run("bun", ["run", "submit-check"], generatedProviderDir);
75
99
  run("bun", ["run", "test"], generatedProviderDir);
100
+ assertGeneratedReadme(generatedProviderDir);
101
+ await smokeGeneratedDevServer(generatedProviderDir);
102
+ assertExternalWorkspaceTopology(
103
+ cliBin,
104
+ externalWorkspaceDir,
105
+ tarballSpecifier,
106
+ );
76
107
 
77
108
  console.log(
78
109
  `Provider SDK packed-artifact smoke passed: ${tarballPath} -> ${generatedProviderDir}`,
@@ -85,6 +116,103 @@ try {
85
116
  }
86
117
  }
87
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
+
88
216
  function packSdk(destination: string): { filename: string } {
89
217
  const raw = execFileSync(
90
218
  "npm",
@@ -120,3 +248,176 @@ function run(command: string, args: string[], cwd: string): void {
120
248
  );
121
249
  }
122
250
  }
251
+
252
+ function assertGeneratedReadme(providerDir: string): void {
253
+ const readme = readFileSync(join(providerDir, "README.md"), "utf8");
254
+ if (!readme.includes('"requestId":"req_local_ping"')) {
255
+ throw new Error(
256
+ "Generated README is missing requestId in local smoke docs.",
257
+ );
258
+ }
259
+ if (readme.includes('"connection":null')) {
260
+ throw new Error(
261
+ "Generated README must not document connection:null for no-auth local smoke.",
262
+ );
263
+ }
264
+ if (!readme.includes("bunx playwright install chromium")) {
265
+ throw new Error(
266
+ "Generated README is missing browser runtime troubleshooting guidance.",
267
+ );
268
+ }
269
+ if (!readme.includes("impit")) {
270
+ throw new Error(
271
+ "Generated README is missing impit stealth runtime guidance.",
272
+ );
273
+ }
274
+ if (!readme.includes("bun run submit-check")) {
275
+ throw new Error(
276
+ "Generated README must document the submit-check pre-submission workflow.",
277
+ );
278
+ }
279
+ if (!readme.includes("bun run record -- --operation <operation>")) {
280
+ throw new Error(
281
+ "Generated README must document fixture recording through the generated record script.",
282
+ );
283
+ }
284
+ }
285
+
286
+ async function smokeGeneratedDevServer(providerDir: string): Promise<void> {
287
+ const port = await getAvailablePort();
288
+ const server = spawn("bun", ["run", "dev"], {
289
+ cwd: providerDir,
290
+ env: { ...process.env, APIFUSE__RUNTIME__PORT: String(port) },
291
+ detached: process.platform !== "win32",
292
+ stdio: ["ignore", "pipe", "pipe"],
293
+ });
294
+ let output = "";
295
+ server.stdout?.on("data", (chunk) => {
296
+ output += chunk.toString();
297
+ });
298
+ server.stderr?.on("data", (chunk) => {
299
+ output += chunk.toString();
300
+ });
301
+
302
+ try {
303
+ const baseUrl = `http://127.0.0.1:${port}`;
304
+ await waitForHttp(`${baseUrl}/health`, server, () => output);
305
+
306
+ const health = await fetchJson(`${baseUrl}/health`, HEALTH_RESPONSE_SCHEMA);
307
+ if (health.status !== "ok" || health.provider !== "dx-smoke") {
308
+ throw new Error(`Unexpected /health payload: ${JSON.stringify(health)}`);
309
+ }
310
+
311
+ const response = await fetch(`${baseUrl}/v1/ping`, {
312
+ method: "POST",
313
+ headers: { "content-type": "application/json" },
314
+ body: JSON.stringify({
315
+ requestId: "req_pack_smoke_ping",
316
+ input: { value: "hello" },
317
+ headers: {},
318
+ }),
319
+ });
320
+ const payload = PING_RESPONSE_SCHEMA.parse(await response.json());
321
+
322
+ if (!response.ok || payload.data?.ok !== true) {
323
+ throw new Error(
324
+ `Unexpected /v1/ping response (${response.status}): ${JSON.stringify(payload)}`,
325
+ );
326
+ }
327
+ } finally {
328
+ await stopServer(server);
329
+ }
330
+ }
331
+
332
+ async function getAvailablePort(): Promise<number> {
333
+ return await new Promise((resolvePromise, rejectPromise) => {
334
+ const server = createServer();
335
+ server.once("error", rejectPromise);
336
+ server.listen(0, "127.0.0.1", () => {
337
+ const address = server.address();
338
+ server.close((error) => {
339
+ if (error) {
340
+ rejectPromise(error);
341
+ return;
342
+ }
343
+ if (!address || typeof address === "string") {
344
+ rejectPromise(new Error("Could not allocate a local TCP port."));
345
+ return;
346
+ }
347
+ resolvePromise(address.port);
348
+ });
349
+ });
350
+ });
351
+ }
352
+
353
+ async function fetchJson<T>(url: string, schema: z.ZodType<T>): Promise<T> {
354
+ const response = await fetch(url);
355
+ if (!response.ok) {
356
+ throw new Error(`${url} returned ${response.status}`);
357
+ }
358
+ return schema.parse(await response.json());
359
+ }
360
+
361
+ async function waitForHttp(
362
+ url: string,
363
+ server: ChildProcess,
364
+ getOutput: () => string,
365
+ ): Promise<void> {
366
+ const deadline = Date.now() + 10_000;
367
+ let lastError: unknown;
368
+
369
+ while (Date.now() < deadline) {
370
+ if (server.exitCode !== null) {
371
+ throw new Error(
372
+ `Dev server exited early with code ${server.exitCode}\n${getOutput()}`,
373
+ );
374
+ }
375
+
376
+ try {
377
+ await fetchJson(url, HEALTH_RESPONSE_SCHEMA);
378
+ return;
379
+ } catch (error) {
380
+ lastError = error;
381
+ await new Promise((resolve) => setTimeout(resolve, 200));
382
+ }
383
+ }
384
+
385
+ throw new Error(
386
+ `Timed out waiting for ${url}: ${lastError instanceof Error ? lastError.message : String(lastError)}\n${getOutput()}`,
387
+ );
388
+ }
389
+
390
+ async function stopServer(server: ChildProcess): Promise<void> {
391
+ if (server.exitCode !== null) {
392
+ return;
393
+ }
394
+ killProcessTree(server, "SIGTERM");
395
+ await new Promise<void>((resolvePromise) => {
396
+ const timeout = setTimeout(() => {
397
+ if (server.exitCode === null) {
398
+ killProcessTree(server, "SIGKILL");
399
+ }
400
+ resolvePromise();
401
+ }, 2_000);
402
+ server.once("exit", () => {
403
+ clearTimeout(timeout);
404
+ resolvePromise();
405
+ });
406
+ });
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,11 @@ import { pathToFileURL } from "node:url";
8
8
 
9
9
  import {
10
10
  type ApiFuseConfig,
11
+ createBypassProviderCache,
11
12
  createHttpClient,
12
- createTlsClient,
13
+ createProviderChoiceContext,
14
+ createStealthClient,
15
+ createSttClientFromEnv,
13
16
  executeOperation,
14
17
  getProviderBaseUrl,
15
18
  type HttpClient,
@@ -18,8 +21,8 @@ import {
18
21
  type ProviderDefinition,
19
22
  ProviderError,
20
23
  type Span,
21
- type TlsClient,
22
- type TlsResponse,
24
+ type StealthClient,
25
+ type StealthResponse,
23
26
  wrapWithInstrumentation,
24
27
  } from "../src";
25
28
  import {
@@ -27,6 +30,7 @@ import {
27
30
  groupSpansByName,
28
31
  type PerfStats,
29
32
  } from "../src/runtime/perf";
33
+ import { createMemoryProviderRuntimeState } from "../src/runtime/state";
30
34
  import {
31
35
  createTraceContext,
32
36
  resolveTraceContextOptions,
@@ -41,6 +45,7 @@ type CliArgs = {
41
45
  exportPath?: string;
42
46
  flame: boolean;
43
47
  operation: string;
48
+ params?: string;
44
49
  runs: number;
45
50
  warmup: number;
46
51
  };
@@ -79,6 +84,21 @@ const DEFAULT_RUNS = 10;
79
84
  const DEFAULT_WARMUP = 2;
80
85
  const DEFAULT_CONCURRENCY = 1;
81
86
  const BAR_WIDTH = 20;
87
+ const HELP_TEXT = `Usage: apifuse perf <provider-path> --operation <operation> [options]
88
+
89
+ Options:
90
+ --operation, -o <name> operation to profile (required)
91
+ --params, -p <json> JSON input template; falls back to fixtures.request or {}
92
+ --runs, -n <number> number of runs (default: 10)
93
+ --warmup <number> warmup runs (default: 2)
94
+ --concurrency, -c <n> concurrent requests (default: 1)
95
+ --compare-proxy run with proxy on/off and compare
96
+ --export <path> export results to JSON file
97
+ --flame generate flamegraph SVG
98
+ --help, -h show this help
99
+
100
+ Example:
101
+ apifuse perf providers/korea-air-quality --operation realtime --params '{"stationName":"jongno"}' --runs 5`;
82
102
 
83
103
  export async function main() {
84
104
  try {
@@ -92,7 +112,11 @@ export async function main() {
92
112
  const inputSchema = getOperationSchema(provider, operation, "input");
93
113
  const outputSchema = getOperationSchema(provider, operation, "output");
94
114
  const fixtureReplay = await loadFixtureReplay(providerDirectory);
95
- const inputTemplate = resolveInputTemplate(provider, inputSchema);
115
+ const inputTemplate = resolveInputTemplate(
116
+ provider,
117
+ inputSchema,
118
+ args.params,
119
+ );
96
120
 
97
121
  const directSuite = await runProfileSuite({
98
122
  args,
@@ -182,6 +206,7 @@ function parseArgs(argv: string[]): CliArgs {
182
206
  let compareProxy = false;
183
207
  let exportPath: string | undefined;
184
208
  let flame = false;
209
+ let params: string | undefined;
185
210
 
186
211
  for (let index = 0; index < argv.length; index += 1) {
187
212
  const arg = argv[index];
@@ -199,6 +224,11 @@ function parseArgs(argv: string[]): CliArgs {
199
224
  continue;
200
225
  }
201
226
 
227
+ if (arg === "--help" || arg === "-h") {
228
+ console.log(HELP_TEXT);
229
+ process.exit(0);
230
+ }
231
+
202
232
  if (arg === "--compare-proxy") {
203
233
  compareProxy = true;
204
234
  continue;
@@ -220,6 +250,22 @@ function parseArgs(argv: string[]): CliArgs {
220
250
  continue;
221
251
  }
222
252
 
253
+ if (arg === "--params" || arg === "-p") {
254
+ params = requireArgValue(argv, index, arg);
255
+ index += 1;
256
+ continue;
257
+ }
258
+
259
+ if (arg.startsWith("--params=")) {
260
+ params = arg.slice("--params=".length);
261
+ continue;
262
+ }
263
+
264
+ if (arg.startsWith("-p=")) {
265
+ params = arg.slice("-p=".length);
266
+ continue;
267
+ }
268
+
223
269
  if (arg.startsWith("-o=")) {
224
270
  operation = arg.slice("-o=".length);
225
271
  continue;
@@ -292,20 +338,7 @@ function parseArgs(argv: string[]): CliArgs {
292
338
  }
293
339
 
294
340
  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
- );
341
+ throw new Error(HELP_TEXT);
309
342
  }
310
343
 
311
344
  return {
@@ -315,6 +348,7 @@ function parseArgs(argv: string[]): CliArgs {
315
348
  exportPath,
316
349
  flame,
317
350
  operation,
351
+ params,
318
352
  runs,
319
353
  warmup,
320
354
  };
@@ -409,7 +443,18 @@ function isSchema(value: unknown): value is { parse(input: unknown): unknown } {
409
443
  function resolveInputTemplate(
410
444
  provider: ProviderDefinition,
411
445
  inputSchema: { parse(input: unknown): unknown },
446
+ params: string | undefined,
412
447
  ): unknown {
448
+ if (params !== undefined) {
449
+ try {
450
+ return inputSchema.parse(JSON.parse(params));
451
+ } catch (error) {
452
+ throw new Error(
453
+ `Failed to parse --params JSON or validate input: ${error instanceof Error ? error.message : String(error)}`,
454
+ );
455
+ }
456
+ }
457
+
413
458
  const firstOp = Object.values(provider.operations)[0];
414
459
  if (firstOp?.fixtures?.request !== undefined) {
415
460
  return firstOp.fixtures.request;
@@ -440,12 +485,12 @@ async function loadFixtureReplay(
440
485
  }
441
486
 
442
487
  function assertProxyConfigured(config: ApiFuseConfig): void {
443
- if (config.proxy?.url || process.env.APIFUSE_PROXY_URL) {
488
+ if (config.proxy?.url || process.env.APIFUSE__PROXY__URL) {
444
489
  return;
445
490
  }
446
491
 
447
492
  throw new Error(
448
- "--compare-proxy requires a proxy URL in apifuse.config.ts or APIFUSE_PROXY_URL.",
493
+ "--compare-proxy requires a proxy URL in apifuse.config.ts or APIFUSE__PROXY__URL.",
449
494
  );
450
495
  }
451
496
 
@@ -637,30 +682,44 @@ function createBaseContext(options: {
637
682
  apifuseConfig,
638
683
  upstream,
639
684
  });
640
- const tls =
685
+ const stealth =
641
686
  options.forceFixtureReplay && options.fixtureReplay
642
- ? createFixtureTlsClient(options.fixtureReplay.rawText)
643
- : createTlsClient(getProviderBaseUrl(options.provider), {
687
+ ? createFixtureStealthClient(options.fixtureReplay.rawText)
688
+ : createStealthClient(getProviderBaseUrl(options.provider), {
644
689
  apifuseConfig,
645
690
  upstream,
646
691
  });
647
692
 
693
+ const env = {
694
+ get: (key: string) => process.env[key],
695
+ };
696
+ const credential = {
697
+ mode: "none" as const,
698
+ get: () => undefined,
699
+ getAll: () => ({}),
700
+ getAccessToken: () => undefined,
701
+ getScopes: () => [],
702
+ };
703
+ const state = createMemoryProviderRuntimeState();
648
704
  return {
649
- env: {
650
- get: (key: string) => process.env[key],
651
- },
652
- credential: {
653
- mode: "none",
654
- get: () => undefined,
655
- getAll: () => ({}),
656
- getAccessToken: () => undefined,
657
- getScopes: () => [],
658
- },
705
+ env,
706
+ credential,
707
+ request: { headers: {} },
659
708
  http,
660
- tls,
709
+ cache: createBypassProviderCache({ providerId: options.provider.id }),
710
+ state,
711
+ stealth,
661
712
  browser: createBrowserStub(),
662
713
  trace: options.traceContext,
663
714
  auth: createAuthStub(),
715
+ stt: createSttClientFromEnv(options.provider.stt),
716
+ choice: createProviderChoiceContext({
717
+ providerId: options.provider.id,
718
+ env,
719
+ request: { headers: {} },
720
+ credential,
721
+ state,
722
+ }),
664
723
  };
665
724
  }
666
725
 
@@ -695,16 +754,25 @@ function createFixtureResponse(raw: unknown) {
695
754
  };
696
755
  }
697
756
 
698
- function createFixtureTlsClient(rawText: string): TlsClient {
699
- const createResponse = async (): Promise<TlsResponse> => ({
700
- status: 200,
701
- ok: true,
702
- headers: { "content-type": "application/json" },
703
- rawHeaders: [["content-type", "application/json"]],
704
- body: rawText,
705
- cookies: { get: () => undefined, getAll: () => ({}), toString: () => "" },
706
- json: async <T>() => JSON.parse(rawText) as T,
707
- });
757
+ function createFixtureStealthClient(rawText: string): StealthClient {
758
+ const createResponse = async (): Promise<StealthResponse> => {
759
+ const bodyBytes = new TextEncoder().encode(rawText);
760
+ return {
761
+ status: 200,
762
+ ok: true,
763
+ headers: { "content-type": "application/json" },
764
+ rawHeaders: [["content-type", "application/json"]],
765
+ body: rawText,
766
+ cookies: { get: () => undefined, getAll: () => ({}), toString: () => "" },
767
+ json: async <T>() => JSON.parse(rawText) as T,
768
+ arrayBuffer: async () =>
769
+ bodyBytes.buffer.slice(
770
+ bodyBytes.byteOffset,
771
+ bodyBytes.byteOffset + bodyBytes.byteLength,
772
+ ) as ArrayBuffer,
773
+ bytes: async () => new Uint8Array(bodyBytes),
774
+ };
775
+ };
708
776
 
709
777
  return {
710
778
  fetch: async () => createResponse(),
@@ -720,6 +788,7 @@ function createFixtureTlsClient(rawText: string): TlsClient {
720
788
  function createBrowserStub(): BrowserClient {
721
789
  return {
722
790
  engine: "playwright-stealth",
791
+ async close() {},
723
792
  async newPage() {
724
793
  throw new ProviderError(
725
794
  "Browser runtime is not supported by apifuse perf yet.",
@@ -728,6 +797,30 @@ function createBrowserStub(): BrowserClient {
728
797
  },
729
798
  );
730
799
  },
800
+ async rawPage() {
801
+ throw new ProviderError(
802
+ "Browser runtime is not supported by apifuse perf yet.",
803
+ {
804
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
805
+ },
806
+ );
807
+ },
808
+ async withIsolatedContext() {
809
+ throw new ProviderError(
810
+ "Browser runtime is not supported by apifuse perf yet.",
811
+ {
812
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
813
+ },
814
+ );
815
+ },
816
+ async solveChallenge() {
817
+ throw new ProviderError(
818
+ "Browser runtime is not supported by apifuse perf yet.",
819
+ {
820
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
821
+ },
822
+ );
823
+ },
731
824
  };
732
825
  }
733
826
 
@@ -738,7 +831,7 @@ function buildInsights(
738
831
  ): string[] {
739
832
  const insights: string[] = [];
740
833
  const allSpans = runs.flatMap((run) => run.spans);
741
- const tlsSpans = allSpans.filter((span) => span.name === "tls.fetch");
834
+ const stealthSpans = allSpans.filter((span) => span.name === "stealth.fetch");
742
835
  const dnsSpans = allSpans.filter((span) => span.name === "dns");
743
836
  const transform = breakdown.find(
744
837
  (entry) => entry.name === "transformResponse",
@@ -746,7 +839,7 @@ function buildInsights(
746
839
  const responseSizes = allSpans
747
840
  .map((span) => span.attributes.response_size)
748
841
  .filter((value): value is number => typeof value === "number");
749
- const reuseFlags = tlsSpans
842
+ const reuseFlags = stealthSpans
750
843
  .map((span) => span.attributes.connection_reused)
751
844
  .filter((value): value is boolean => typeof value === "boolean");
752
845
 
@@ -756,8 +849,8 @@ function buildInsights(
756
849
  );
757
850
  insights.push(
758
851
  reusePercent >= 80
759
- ? `✓ TLS connection reuse: ${reusePercent}% (good)`
760
- : `⚠ TLS connection reuse: ${reusePercent}% — consider session reuse`,
852
+ ? `✓ Stealth connection reuse: ${reusePercent}% (good)`
853
+ : `⚠ Stealth connection reuse: ${reusePercent}% — consider session reuse`,
761
854
  );
762
855
  }
763
856
 
@@ -1091,7 +1184,7 @@ function cloneValue<T>(value: T): T {
1091
1184
 
1092
1185
  function handleCliError(error: unknown): never {
1093
1186
  const message = error instanceof Error ? error.message : String(error);
1094
- console.error(message);
1187
+ console.error(`[apifuse perf] ${message}`);
1095
1188
  process.exit(1);
1096
1189
  }
1097
1190