@apifuse/provider-sdk 2.2.0-beta.1 → 2.2.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 (203) hide show
  1. package/AUTHORING.md +106 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +5 -1
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +44 -59
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +27 -52
  8. package/bin/apifuse-pack-check.ts +14 -0
  9. package/bin/apifuse-pack-smoke.ts +22 -81
  10. package/bin/apifuse-pack-types.ts +266 -0
  11. package/bin/apifuse-perf.ts +45 -127
  12. package/bin/apifuse-record.ts +53 -70
  13. package/bin/apifuse-submit-check.ts +510 -45
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml-semantics.ts +204 -0
  18. package/bin/submit-check-xml.ts +134 -0
  19. package/dist/auth-turn/index.d.ts +3 -3
  20. package/dist/auth.d.ts +2 -2
  21. package/dist/auth.js +9 -18
  22. package/dist/ceremonies/index.d.ts +1 -1
  23. package/dist/ceremonies/index.js +8 -18
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/config/loader.d.ts +149 -8
  32. package/dist/config/loader.js +378 -89
  33. package/dist/contract-serialization.d.ts +2 -2
  34. package/dist/contract-serialization.js +3 -6
  35. package/dist/contract-types.d.ts +2 -2
  36. package/dist/contract.d.ts +3 -3
  37. package/dist/contract.js +4 -6
  38. package/dist/define.d.ts +8 -1
  39. package/dist/define.js +119 -122
  40. package/dist/dev.d.ts +1 -1
  41. package/dist/dev.js +1 -1
  42. package/dist/errors.d.ts +4 -1
  43. package/dist/errors.js +48 -0
  44. package/dist/i18n/catalog.d.ts +2 -2
  45. package/dist/i18n/catalog.js +4 -10
  46. package/dist/i18n/index.d.ts +2 -2
  47. package/dist/i18n/index.js +2 -2
  48. package/dist/i18n/keys.d.ts +2 -2
  49. package/dist/index.d.ts +44 -42
  50. package/dist/index.js +39 -37
  51. package/dist/lint.js +8 -15
  52. package/dist/provider.d.ts +11 -11
  53. package/dist/provider.js +9 -9
  54. package/dist/public-schema-field-lint.d.ts +1 -1
  55. package/dist/recipes/gov-api.js +1 -1
  56. package/dist/runtime/auth-flow.d.ts +1 -1
  57. package/dist/runtime/auth-flow.js +3 -3
  58. package/dist/runtime/browser.d.ts +1 -1
  59. package/dist/runtime/browser.js +15 -29
  60. package/dist/runtime/cache.d.ts +1 -1
  61. package/dist/runtime/cache.js +4 -8
  62. package/dist/runtime/choice.d.ts +1 -1
  63. package/dist/runtime/choice.js +31 -35
  64. package/dist/runtime/credential.d.ts +1 -1
  65. package/dist/runtime/credential.js +1 -1
  66. package/dist/runtime/env.d.ts +1 -1
  67. package/dist/runtime/executor.d.ts +1 -1
  68. package/dist/runtime/executor.js +15 -3
  69. package/dist/runtime/http.d.ts +2 -2
  70. package/dist/runtime/http.js +144 -348
  71. package/dist/runtime/insights.d.ts +1 -1
  72. package/dist/runtime/insights.js +6 -13
  73. package/dist/runtime/instrumentation.d.ts +2 -2
  74. package/dist/runtime/instrumentation.js +56 -19
  75. package/dist/runtime/keyring.js +1 -1
  76. package/dist/runtime/namespace.js +1 -1
  77. package/dist/runtime/otlp.d.ts +1 -1
  78. package/dist/runtime/perf.d.ts +1 -1
  79. package/dist/runtime/provider.d.ts +1 -1
  80. package/dist/runtime/provider.js +1 -2
  81. package/dist/runtime/proxy-errors.d.ts +1 -1
  82. package/dist/runtime/proxy-errors.js +9 -7
  83. package/dist/runtime/proxy-nodemaven.d.ts +35 -0
  84. package/dist/runtime/proxy-nodemaven.js +128 -0
  85. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  86. package/dist/runtime/proxy-retry-policy.js +326 -0
  87. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  88. package/dist/runtime/proxy-telemetry.js +55 -52
  89. package/dist/runtime/redis.d.ts +1 -1
  90. package/dist/runtime/redis.js +2 -4
  91. package/dist/runtime/request-options.d.ts +1 -1
  92. package/dist/runtime/secrets.d.ts +27 -0
  93. package/dist/runtime/secrets.js +51 -0
  94. package/dist/runtime/state.d.ts +2 -2
  95. package/dist/runtime/state.js +15 -4
  96. package/dist/runtime/stealth.d.ts +2 -2
  97. package/dist/runtime/stealth.js +108 -212
  98. package/dist/runtime/stt.d.ts +1 -1
  99. package/dist/runtime/stt.js +11 -15
  100. package/dist/runtime/trace.d.ts +2 -2
  101. package/dist/runtime/trace.js +2 -4
  102. package/dist/runtime/waterfall.d.ts +1 -1
  103. package/dist/schema.d.ts +1 -1
  104. package/dist/schema.js +7 -15
  105. package/dist/serve.d.ts +1 -1
  106. package/dist/serve.js +1 -1
  107. package/dist/server/index.d.ts +7 -7
  108. package/dist/server/index.js +6 -6
  109. package/dist/server/self-test-redaction.d.ts +1 -1
  110. package/dist/server/self-test-redaction.js +1 -1
  111. package/dist/server/self-test.d.ts +104 -3
  112. package/dist/server/self-test.js +673 -115
  113. package/dist/server/serve.d.ts +8 -3
  114. package/dist/server/serve.js +105 -59
  115. package/dist/server/types.d.ts +10 -9
  116. package/dist/server/types.js +3 -7
  117. package/dist/stealth/profiles.d.ts +1 -1
  118. package/dist/stealth/profiles.js +5 -14
  119. package/dist/stream.d.ts +1 -1
  120. package/dist/testing/index.d.ts +2 -2
  121. package/dist/testing/index.js +2 -2
  122. package/dist/testing/run.d.ts +1 -1
  123. package/dist/testing/run.js +7 -14
  124. package/dist/types.d.ts +87 -1
  125. package/dist/user-input.d.ts +30 -0
  126. package/dist/user-input.js +66 -0
  127. package/package.json +9 -5
  128. package/src/auth-turn/index.ts +1 -1
  129. package/src/auth.ts +28 -86
  130. package/src/ceremonies/index.ts +24 -78
  131. package/src/cli/commands.ts +10 -0
  132. package/src/cli/create.ts +42 -35
  133. package/src/cli/prompt-assets.ts +865 -0
  134. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  135. package/src/config/loader.ts +543 -208
  136. package/src/contract-serialization.ts +5 -11
  137. package/src/contract-types.ts +2 -2
  138. package/src/contract.ts +12 -28
  139. package/src/define.ts +247 -505
  140. package/src/dev.ts +4 -9
  141. package/src/errors.ts +58 -5
  142. package/src/i18n/catalog.ts +10 -32
  143. package/src/i18n/index.ts +2 -2
  144. package/src/i18n/keys.ts +5 -11
  145. package/src/index.ts +49 -42
  146. package/src/lint.ts +89 -159
  147. package/src/provider.ts +15 -11
  148. package/src/public-schema-field-lint.ts +7 -33
  149. package/src/recipes/gov-api.ts +2 -5
  150. package/src/runtime/auth-flow.ts +5 -7
  151. package/src/runtime/browser.ts +64 -187
  152. package/src/runtime/cache.ts +20 -67
  153. package/src/runtime/choice.ts +79 -132
  154. package/src/runtime/credential.ts +2 -2
  155. package/src/runtime/env.ts +1 -1
  156. package/src/runtime/executor.ts +23 -20
  157. package/src/runtime/http.ts +195 -559
  158. package/src/runtime/insights.ts +15 -53
  159. package/src/runtime/instrumentation.ts +78 -65
  160. package/src/runtime/keyring.ts +7 -19
  161. package/src/runtime/namespace.ts +2 -7
  162. package/src/runtime/otlp.ts +12 -23
  163. package/src/runtime/perf.ts +1 -1
  164. package/src/runtime/provider.ts +4 -9
  165. package/src/runtime/proxy-errors.ts +29 -42
  166. package/src/runtime/proxy-nodemaven.ts +178 -0
  167. package/src/runtime/proxy-retry-policy.ts +469 -0
  168. package/src/runtime/proxy-telemetry.ts +79 -77
  169. package/src/runtime/redis.ts +4 -12
  170. package/src/runtime/request-options.ts +4 -13
  171. package/src/runtime/secrets.ts +64 -0
  172. package/src/runtime/state.ts +41 -110
  173. package/src/runtime/stealth.ts +162 -377
  174. package/src/runtime/stt.ts +38 -94
  175. package/src/runtime/trace.ts +14 -44
  176. package/src/runtime/waterfall.ts +5 -18
  177. package/src/schema.ts +23 -84
  178. package/src/serve.ts +1 -1
  179. package/src/server/index.ts +12 -7
  180. package/src/server/self-test-redaction.ts +2 -2
  181. package/src/server/self-test.ts +857 -132
  182. package/src/server/serve.ts +172 -245
  183. package/src/server/types.ts +7 -19
  184. package/src/stealth/profiles.ts +10 -26
  185. package/src/stream.ts +8 -19
  186. package/src/testing/index.ts +2 -2
  187. package/src/testing/run.ts +19 -67
  188. package/src/types.ts +88 -1
  189. package/src/user-input.ts +118 -0
  190. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  191. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  192. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  193. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  194. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  195. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  196. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  197. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  198. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  199. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  200. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  201. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  202. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  203. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -24,19 +24,12 @@ import {
24
24
  type StealthClient,
25
25
  type StealthResponse,
26
26
  wrapWithInstrumentation,
27
- } from "../src";
28
- import {
29
- computeStats,
30
- groupSpansByName,
31
- type PerfStats,
32
- } from "../src/runtime/perf";
33
- import { createMemoryProviderRuntimeState } from "../src/runtime/state";
34
- import {
35
- createTraceContext,
36
- resolveTraceContextOptions,
37
- } from "../src/runtime/trace";
38
- import { renderWaterfall } from "../src/runtime/waterfall";
39
- import type { BrowserClient } from "../src/types";
27
+ } from "../src/index.js";
28
+ import { computeStats, groupSpansByName, type PerfStats } from "../src/runtime/perf.js";
29
+ import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
30
+ import { createTraceContext, resolveTraceContextOptions } from "../src/runtime/trace.js";
31
+ import { renderWaterfall } from "../src/runtime/waterfall.js";
32
+ import type { BrowserClient } from "../src/types.js";
40
33
 
41
34
  type CliArgs = {
42
35
  compareProxy: boolean;
@@ -112,11 +105,7 @@ export async function main() {
112
105
  const inputSchema = getOperationSchema(provider, operation, "input");
113
106
  const outputSchema = getOperationSchema(provider, operation, "output");
114
107
  const fixtureReplay = await loadFixtureReplay(providerDirectory);
115
- const inputTemplate = resolveInputTemplate(
116
- provider,
117
- inputSchema,
118
- args.params,
119
- );
108
+ const inputTemplate = resolveInputTemplate(provider, inputSchema, args.params);
120
109
 
121
110
  const directSuite = await runProfileSuite({
122
111
  args,
@@ -168,9 +157,7 @@ export async function main() {
168
157
  label: `${providerId}/${args.operation}`,
169
158
  proxyEnabled: false,
170
159
  stats: directSuite.stats,
171
- mode: directSuite.runs.some((run) => run.mode === "fixture")
172
- ? "fixture"
173
- : "live",
160
+ mode: directSuite.runs.some((run) => run.mode === "fixture") ? "fixture" : "live",
174
161
  });
175
162
  console.log(`Flamegraph: ${flamePath}`);
176
163
  }
@@ -294,27 +281,18 @@ function parseArgs(argv: string[]): CliArgs {
294
281
  }
295
282
 
296
283
  if (arg.startsWith("--warmup=")) {
297
- warmup = parseNonNegativeInteger(
298
- arg.slice("--warmup=".length),
299
- "--warmup",
300
- );
284
+ warmup = parseNonNegativeInteger(arg.slice("--warmup=".length), "--warmup");
301
285
  continue;
302
286
  }
303
287
 
304
288
  if (arg === "--concurrency" || arg === "-c") {
305
- concurrency = parsePositiveInteger(
306
- requireArgValue(argv, index, arg),
307
- arg,
308
- );
289
+ concurrency = parsePositiveInteger(requireArgValue(argv, index, arg), arg);
309
290
  index += 1;
310
291
  continue;
311
292
  }
312
293
 
313
294
  if (arg.startsWith("--concurrency=")) {
314
- concurrency = parsePositiveInteger(
315
- arg.slice("--concurrency=".length),
316
- "--concurrency",
317
- );
295
+ concurrency = parsePositiveInteger(arg.slice("--concurrency=".length), "--concurrency");
318
296
  continue;
319
297
  }
320
298
 
@@ -354,11 +332,7 @@ function parseArgs(argv: string[]): CliArgs {
354
332
  };
355
333
  }
356
334
 
357
- function requireArgValue(
358
- argv: string[],
359
- index: number,
360
- option: string,
361
- ): string {
335
+ function requireArgValue(argv: string[], index: number, option: string): string {
362
336
  const value = argv[index + 1];
363
337
  if (!value) {
364
338
  throw new Error(`Missing value for ${option}`);
@@ -396,9 +370,7 @@ function resolveProviderEntry(providerDirectory: string): string {
396
370
  throw new Error(`Provider entry not found in ${providerDirectory}`);
397
371
  }
398
372
 
399
- async function loadProvider(
400
- providerEntry: string,
401
- ): Promise<ProviderDefinition> {
373
+ async function loadProvider(providerEntry: string): Promise<ProviderDefinition> {
402
374
  const mod = (await import(pathToFileURL(providerEntry).href)) as {
403
375
  default?: ProviderDefinition;
404
376
  };
@@ -469,9 +441,7 @@ function resolveInputTemplate(
469
441
  }
470
442
  }
471
443
 
472
- async function loadFixtureReplay(
473
- providerDirectory: string,
474
- ): Promise<FixtureReplay | null> {
444
+ async function loadFixtureReplay(providerDirectory: string): Promise<FixtureReplay | null> {
475
445
  const fixturePath = resolve(providerDirectory, "__fixtures__", "raw.json");
476
446
  if (!existsSync(fixturePath)) {
477
447
  return null;
@@ -521,15 +491,8 @@ async function runProfileSuite(options: {
521
491
  }
522
492
 
523
493
  const runs: RunResult[] = [];
524
- for (
525
- let index = 0;
526
- index < options.args.runs;
527
- index += options.args.concurrency
528
- ) {
529
- const batchSize = Math.min(
530
- options.args.concurrency,
531
- options.args.runs - index,
532
- );
494
+ for (let index = 0; index < options.args.runs; index += options.args.concurrency) {
495
+ const batchSize = Math.min(options.args.concurrency, options.args.runs - index);
533
496
  const batch = Array.from({ length: batchSize }, () =>
534
497
  profileRun({
535
498
  config: options.config,
@@ -552,8 +515,7 @@ async function runProfileSuite(options: {
552
515
  const breakdown = [...groupedSpans.entries()]
553
516
  .filter(([name]) => name !== rootSpanName)
554
517
  .map(([name, durations]) => {
555
- const avgMs =
556
- durations.reduce((sum, value) => sum + value, 0) / durations.length;
518
+ const avgMs = durations.reduce((sum, value) => sum + value, 0) / durations.length;
557
519
  const percent = stats.avg > 0 ? (avgMs / stats.avg) * 100 : 0;
558
520
  return { avgMs, name, percent };
559
521
  })
@@ -611,9 +573,7 @@ async function executeProfileRun(options: {
611
573
  proxyEnabled: boolean;
612
574
  }): Promise<RunResult> {
613
575
  const _operation = getOperation(options.provider, options.operationName);
614
- const traceContext = createTraceContext(
615
- resolveTraceContextOptions(options.config.trace),
616
- );
576
+ const traceContext = createTraceContext(resolveTraceContextOptions(options.config.trace));
617
577
  const baseContext = createBaseContext({
618
578
  config: options.config,
619
579
  provider: options.provider,
@@ -638,9 +598,7 @@ async function executeProfileRun(options: {
638
598
  normalizedInput,
639
599
  );
640
600
 
641
- return ctx.trace.span("transformResponse", async () =>
642
- options.outputSchema.parse(result),
643
- );
601
+ return ctx.trace.span("transformResponse", async () => options.outputSchema.parse(result));
644
602
  });
645
603
  const durationMs = performance.now() - startedAt;
646
604
  const spans = ctx.trace.getSpans();
@@ -790,36 +748,24 @@ function createBrowserStub(): BrowserClient {
790
748
  engine: "playwright-stealth",
791
749
  async close() {},
792
750
  async newPage() {
793
- throw new ProviderError(
794
- "Browser runtime is not supported by apifuse perf yet.",
795
- {
796
- code: "BROWSER_RUNTIME_UNSUPPORTED",
797
- },
798
- );
751
+ throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
752
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
753
+ });
799
754
  },
800
755
  async rawPage() {
801
- throw new ProviderError(
802
- "Browser runtime is not supported by apifuse perf yet.",
803
- {
804
- code: "BROWSER_RUNTIME_UNSUPPORTED",
805
- },
806
- );
756
+ throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
757
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
758
+ });
807
759
  },
808
760
  async withIsolatedContext() {
809
- throw new ProviderError(
810
- "Browser runtime is not supported by apifuse perf yet.",
811
- {
812
- code: "BROWSER_RUNTIME_UNSUPPORTED",
813
- },
814
- );
761
+ throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
762
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
763
+ });
815
764
  },
816
765
  async solveChallenge() {
817
- throw new ProviderError(
818
- "Browser runtime is not supported by apifuse perf yet.",
819
- {
820
- code: "BROWSER_RUNTIME_UNSUPPORTED",
821
- },
822
- );
766
+ throw new ProviderError("Browser runtime is not supported by apifuse perf yet.", {
767
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
768
+ });
823
769
  },
824
770
  };
825
771
  }
@@ -833,9 +779,7 @@ function buildInsights(
833
779
  const allSpans = runs.flatMap((run) => run.spans);
834
780
  const stealthSpans = allSpans.filter((span) => span.name === "stealth.fetch");
835
781
  const dnsSpans = allSpans.filter((span) => span.name === "dns");
836
- const transform = breakdown.find(
837
- (entry) => entry.name === "transformResponse",
838
- );
782
+ const transform = breakdown.find((entry) => entry.name === "transformResponse");
839
783
  const responseSizes = allSpans
840
784
  .map((span) => span.attributes.response_size)
841
785
  .filter((value): value is number => typeof value === "number");
@@ -844,9 +788,7 @@ function buildInsights(
844
788
  .filter((value): value is boolean => typeof value === "boolean");
845
789
 
846
790
  if (reuseFlags.length > 0) {
847
- const reusePercent = Math.round(
848
- (reuseFlags.filter(Boolean).length / reuseFlags.length) * 100,
849
- );
791
+ const reusePercent = Math.round((reuseFlags.filter(Boolean).length / reuseFlags.length) * 100);
850
792
  insights.push(
851
793
  reusePercent >= 80
852
794
  ? `✓ Stealth connection reuse: ${reusePercent}% (good)`
@@ -863,24 +805,16 @@ function buildInsights(
863
805
  }
864
806
 
865
807
  if (dnsSpans.length > 0) {
866
- const dnsAvg =
867
- dnsSpans.reduce((sum, span) => sum + span.duration_ms, 0) /
868
- dnsSpans.length;
808
+ const dnsAvg = dnsSpans.reduce((sum, span) => sum + span.duration_ms, 0) / dnsSpans.length;
869
809
  if (dnsAvg >= 5) {
870
- insights.push(
871
- `⚠ DNS resolution: ${formatDuration(dnsAvg)} avg — consider DNS caching`,
872
- );
810
+ insights.push(`⚠ DNS resolution: ${formatDuration(dnsAvg)} avg — consider DNS caching`);
873
811
  }
874
812
  }
875
813
 
876
814
  if (responseSizes.length > 0) {
877
- const avgSize =
878
- responseSizes.reduce((sum, value) => sum + value, 0) /
879
- responseSizes.length;
815
+ const avgSize = responseSizes.reduce((sum, value) => sum + value, 0) / responseSizes.length;
880
816
  if (avgSize >= 4096) {
881
- insights.push(
882
- `⚠ Response size: ${formatBytes(avgSize)} avg — check gzip (Accept-Encoding)`,
883
- );
817
+ insights.push(`⚠ Response size: ${formatBytes(avgSize)} avg — check gzip (Accept-Encoding)`);
884
818
  }
885
819
  }
886
820
 
@@ -925,13 +859,10 @@ function renderReport(options: {
925
859
 
926
860
  if (options.proxySuite) {
927
861
  const delta = options.proxySuite.stats.p50 - options.suite.stats.p50;
928
- const deltaPercent =
929
- options.suite.stats.p50 > 0 ? (delta / options.suite.stats.p50) * 100 : 0;
862
+ const deltaPercent = options.suite.stats.p50 > 0 ? (delta / options.suite.stats.p50) * 100 : 0;
930
863
  lines.push("│");
931
864
  lines.push("│ Proxy Comparison (--compare-proxy):");
932
- lines.push(
933
- `│ proxy: off → p50: ${formatDuration(options.suite.stats.p50)}`,
934
- );
865
+ lines.push(`│ proxy: off → p50: ${formatDuration(options.suite.stats.p50)}`);
935
866
  lines.push(
936
867
  `│ proxy: on → p50: ${formatDuration(options.proxySuite.stats.p50)} (${delta >= 0 ? "+" : ""}${Math.round(deltaPercent)}%)`,
937
868
  );
@@ -979,9 +910,7 @@ function formatBytes(value: number): string {
979
910
  }
980
911
 
981
912
  function selectRepresentativeRun(runs: RunResult[]): RunResult {
982
- const sorted = [...runs].sort(
983
- (left, right) => left.durationMs - right.durationMs,
984
- );
913
+ const sorted = [...runs].sort((left, right) => left.durationMs - right.durationMs);
985
914
  const middle = sorted[Math.floor(sorted.length / 2)];
986
915
  if (middle) {
987
916
  return middle;
@@ -1065,17 +994,10 @@ function buildFlamegraphSvg(
1065
994
 
1066
995
  const rects = flattened
1067
996
  .map((node) => {
1068
- const x =
1069
- 12 +
1070
- ((node.span.startedAt - root.startedAt) / totalDuration) * (width - 24);
1071
- const rectWidth = Math.max(
1072
- 1,
1073
- (node.span.duration_ms / totalDuration) * (width - 24),
1074
- );
997
+ const x = 12 + ((node.span.startedAt - root.startedAt) / totalDuration) * (width - 24);
998
+ const rectWidth = Math.max(1, (node.span.duration_ms / totalDuration) * (width - 24));
1075
999
  const y = chartTop + node.depth * rowHeight;
1076
- const label = escapeXml(
1077
- `${node.span.name} (${formatDuration(node.span.duration_ms)})`,
1078
- );
1000
+ const label = escapeXml(`${node.span.name} (${formatDuration(node.span.duration_ms)})`);
1079
1001
  const title = escapeXml(
1080
1002
  `${node.span.name}\n${formatDuration(node.span.duration_ms)}\nstatus: ${node.span.status}`,
1081
1003
  );
@@ -1094,9 +1016,7 @@ function buildFlamegraphSvg(
1094
1016
  }
1095
1017
 
1096
1018
  function buildFlameTree(spans: Span[]): FlameNode[] {
1097
- const sorted = [...spans].sort(
1098
- (left, right) => left.startedAt - right.startedAt,
1099
- );
1019
+ const sorted = [...spans].sort((left, right) => left.startedAt - right.startedAt);
1100
1020
  const nodeMap = new Map<string, FlameNode>();
1101
1021
 
1102
1022
  for (const span of sorted) {
@@ -1139,9 +1059,7 @@ function findRootSpan(spans: Span[]): Span {
1139
1059
  return root;
1140
1060
  }
1141
1061
 
1142
- const sorted = [...spans].sort(
1143
- (left, right) => left.startedAt - right.startedAt,
1144
- );
1062
+ const sorted = [...spans].sort((left, right) => left.startedAt - right.startedAt);
1145
1063
  const first = sorted[0];
1146
1064
  if (!first) {
1147
1065
  throw new Error("Expected at least one span");
@@ -20,8 +20,8 @@ import {
20
20
  type StealthClient,
21
21
  TransportError,
22
22
  ValidationError,
23
- } from "../src";
24
- import { createMemoryProviderRuntimeState } from "../src/runtime/state";
23
+ } from "../src/index.js";
24
+ import { createMemoryProviderRuntimeState } from "../src/runtime/state.js";
25
25
 
26
26
  type CliArgs = {
27
27
  append: boolean;
@@ -42,7 +42,7 @@ Calls a real upstream-backed operation through ctx.http or ctx.stealth and write
42
42
  Options:
43
43
  --operation, -o <name> operation to call
44
44
  --params, -p <json> JSON input passed to the operation (default: {})
45
- --append append to an existing array fixture
45
+ --append preserve the existing fixture and append this capture
46
46
  --sanitize redact common token/header fields (default)
47
47
  --no-sanitize write the captured upstream payload as-is
48
48
  --help, -h show this help
@@ -64,31 +64,18 @@ export async function main() {
64
64
  resolveOperationBaseUrl(provider, operationName),
65
65
  );
66
66
 
67
- console.log(
68
- `[apifuse record] Calling ${operationName} on ${provider.id}...`,
69
- );
67
+ console.log(`[apifuse record] Calling ${operationName} on ${provider.id}...`);
70
68
 
71
- const result = await executeOperation(
72
- provider,
73
- operationName,
74
- capture.ctx,
75
- parsedParams,
76
- );
69
+ const result = await executeOperation(provider, operationName, capture.ctx, parsedParams);
77
70
  const captured = capture.getCapturedRaw();
78
71
 
79
72
  if (captured === undefined) {
80
- throw new Error(
81
- `No upstream response was captured for ${provider.id}.${operationName}.`,
82
- );
73
+ throw new Error(`No upstream response was captured for ${provider.id}.${operationName}.`);
83
74
  }
84
75
 
85
76
  const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
86
77
  const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
87
- const nextPayload = await prepareFixturePayload(
88
- fixturePath,
89
- rawPayload,
90
- args.append,
91
- );
78
+ const nextPayload = await prepareFixturePayload(fixturePath, rawPayload, args.append);
92
79
 
93
80
  await mkdir(dirname(fixturePath), { recursive: true });
94
81
  await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
@@ -98,9 +85,7 @@ export async function main() {
98
85
  Buffer.byteLength(JSON.stringify(rawPayload)),
99
86
  )})`,
100
87
  );
101
- console.log(
102
- `[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`,
103
- );
88
+ console.log(`[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`);
104
89
 
105
90
  void result;
106
91
  } catch (error) {
@@ -210,11 +195,7 @@ function formatCliError(error: unknown): string {
210
195
  }
211
196
 
212
197
  if (error instanceof ProviderError || error instanceof ValidationError) {
213
- return [
214
- error.message,
215
- error.code ? `code=${error.code}` : undefined,
216
- error.fix,
217
- ]
198
+ return [error.message, error.code ? `code=${error.code}` : undefined, error.fix]
218
199
  .filter(Boolean)
219
200
  .join(" ");
220
201
  }
@@ -269,8 +250,7 @@ function findProviderRoot(startDirectory: string): string | undefined {
269
250
 
270
251
  function looksLikeProviderRoot(directory: string): boolean {
271
252
  return (
272
- existsSync(resolve(directory, "index.ts")) &&
273
- existsSync(resolve(directory, "package.json"))
253
+ existsSync(resolve(directory, "index.ts")) && existsSync(resolve(directory, "package.json"))
274
254
  );
275
255
  }
276
256
 
@@ -287,15 +267,10 @@ async function loadProvider(rootDir: string): Promise<ProviderRuntime> {
287
267
  return module.default;
288
268
  }
289
269
 
290
- function resolveOperationName(
291
- provider: ProviderRuntime,
292
- operationName?: string,
293
- ): string {
270
+ function resolveOperationName(provider: ProviderRuntime, operationName?: string): string {
294
271
  if (operationName) {
295
272
  if (!(operationName in provider.operations)) {
296
- throw new Error(
297
- `Unknown operation "${operationName}" for provider "${provider.id}".`,
298
- );
273
+ throw new Error(`Unknown operation "${operationName}" for provider "${provider.id}".`);
299
274
  }
300
275
 
301
276
  return operationName;
@@ -309,10 +284,7 @@ function resolveOperationName(
309
284
  return firstOperation;
310
285
  }
311
286
 
312
- function parseParams(
313
- operation: ProviderRuntime["operations"][string],
314
- value: string,
315
- ): unknown {
287
+ function parseParams(operation: ProviderRuntime["operations"][string], value: string): unknown {
316
288
  let parsed: unknown;
317
289
 
318
290
  try {
@@ -326,10 +298,7 @@ function parseParams(
326
298
  return operation.input ? operation.input.parse(parsed) : parsed;
327
299
  }
328
300
 
329
- function resolveOperationBaseUrl(
330
- provider: ProviderRuntime,
331
- operationName: string,
332
- ): string {
301
+ function resolveOperationBaseUrl(provider: ProviderRuntime, operationName: string): string {
333
302
  const baseUrl = provider.operations[operationName]?.upstream?.baseUrl;
334
303
  if (!baseUrl) {
335
304
  throw new Error(
@@ -346,12 +315,9 @@ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
346
315
  const http = proxyHttpClient(createHttpClient(baseUrl), (response) => {
347
316
  capturedRaw = response.data;
348
317
  });
349
- const stealth = proxyStealthClient(
350
- createStealthClient(baseUrl),
351
- (response) => {
352
- capturedRaw = normalizeCapturedStealthResponse(response);
353
- },
354
- );
318
+ const stealth = proxyStealthClient(createStealthClient(baseUrl), (response) => {
319
+ capturedRaw = normalizeCapturedStealthResponse(response);
320
+ });
355
321
 
356
322
  const env = {
357
323
  get: (key: string) => process.env[key],
@@ -464,9 +430,7 @@ function proxyStealthSession(
464
430
  };
465
431
  }
466
432
 
467
- function normalizeCapturedStealthResponse(
468
- response: Awaited<ReturnType<StealthClient["fetch"]>>,
469
- ) {
433
+ function normalizeCapturedStealthResponse(response: Awaited<ReturnType<StealthClient["fetch"]>>) {
470
434
  try {
471
435
  return JSON.parse(response.body);
472
436
  } catch {
@@ -483,15 +447,13 @@ function sanitizeFixture(value: unknown): unknown {
483
447
  return value;
484
448
  }
485
449
 
486
- const entries = Object.entries(value as MutableRecord).map(
487
- ([key, entryValue]) => {
488
- if (isSensitiveKey(key)) {
489
- return [key, "[REDACTED]"] as const;
490
- }
450
+ const entries = Object.entries(value as MutableRecord).map(([key, entryValue]) => {
451
+ if (isSensitiveKey(key)) {
452
+ return [key, "[REDACTED]"] as const;
453
+ }
491
454
 
492
- return [key, sanitizeFixture(entryValue)] as const;
493
- },
494
- );
455
+ return [key, sanitizeFixture(entryValue)] as const;
456
+ });
495
457
 
496
458
  return Object.fromEntries(entries);
497
459
  }
@@ -500,7 +462,7 @@ function isSensitiveKey(key: string): boolean {
500
462
  return /authorization|token|api[-_]?key/i.test(key);
501
463
  }
502
464
 
503
- async function prepareFixturePayload(
465
+ export async function prepareFixturePayload(
504
466
  fixturePath: string,
505
467
  payload: unknown,
506
468
  append: boolean,
@@ -509,15 +471,34 @@ async function prepareFixturePayload(
509
471
  return payload;
510
472
  }
511
473
 
474
+ let fixtureSource: string;
512
475
  try {
513
- const existing = JSON.parse(readFileSync(fixturePath, "utf8")) as unknown;
514
- if (Array.isArray(existing)) {
515
- return [...existing, payload];
516
- }
517
- } catch {
518
- // Fall through to overwrite with the new payload.
476
+ fixtureSource = readFileSync(fixturePath, "utf8");
477
+ } catch (error) {
478
+ throw new Error(
479
+ `Cannot append to existing fixture "${fixturePath}" because it could not be read: ${
480
+ error instanceof Error ? error.message : String(error)
481
+ }. Fix its permissions or delete it, then run apifuse record --append again.`,
482
+ );
483
+ }
484
+
485
+ let existing: unknown;
486
+ try {
487
+ existing = JSON.parse(fixtureSource) as unknown;
488
+ } catch (error) {
489
+ throw new Error(
490
+ `Cannot append to corrupt fixture "${fixturePath}" because it is not valid JSON: ${
491
+ error instanceof Error ? error.message : String(error)
492
+ }. Fix or delete the fixture, then run apifuse record --append again.`,
493
+ );
519
494
  }
520
495
 
496
+ if (Array.isArray(existing)) {
497
+ return [...existing, payload];
498
+ }
499
+ if (existing !== null) {
500
+ return [existing, payload];
501
+ }
521
502
  return payload;
522
503
  }
523
504
 
@@ -529,4 +510,6 @@ function formatBytes(bytes: number): string {
529
510
  return `${(bytes / 1024).toFixed(1)} KB`;
530
511
  }
531
512
 
532
- await main();
513
+ if (import.meta.main) {
514
+ await main();
515
+ }