@apifuse/provider-sdk 2.1.0-beta.4 → 2.1.0-beta.6

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 (42) hide show
  1. package/AUTHORING.md +24 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +23 -2
  4. package/SUBMISSION.md +2 -1
  5. package/bin/apifuse-check.ts +60 -6
  6. package/bin/apifuse-dev.ts +48 -5
  7. package/bin/apifuse-perf.ts +106 -26
  8. package/bin/apifuse-record.ts +142 -52
  9. package/bin/apifuse-submit-check.ts +1489 -3
  10. package/package.json +107 -92
  11. package/src/ceremonies/index.ts +8 -2
  12. package/src/choice-token.ts +1 -0
  13. package/src/cli/commands.ts +10 -8
  14. package/src/cli/create.ts +49 -1
  15. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  16. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  17. package/src/cli/templates/provider/README.md.tpl +18 -0
  18. package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
  19. package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
  20. package/src/config/loader.ts +19 -1
  21. package/src/contract-json.ts +75 -0
  22. package/src/contract-serialization.ts +89 -0
  23. package/src/contract-types.ts +52 -0
  24. package/src/contract.ts +215 -0
  25. package/src/define.ts +40 -5
  26. package/src/errors.ts +15 -0
  27. package/src/i18n/catalog.ts +156 -0
  28. package/src/index.ts +22 -1
  29. package/src/lint.ts +265 -46
  30. package/src/provider.ts +45 -2
  31. package/src/runtime/browser.ts +685 -30
  32. package/src/runtime/cache.ts +35 -89
  33. package/src/runtime/choice.ts +760 -0
  34. package/src/runtime/executor.ts +19 -2
  35. package/src/runtime/redis.ts +116 -0
  36. package/src/runtime/state.ts +487 -0
  37. package/src/runtime/stealth.ts +8 -1
  38. package/src/runtime/trace.ts +1 -1
  39. package/src/server/serve.ts +361 -46
  40. package/src/server/types.ts +2 -0
  41. package/src/testing/run.ts +16 -3
  42. package/src/types.ts +225 -18
package/AUTHORING.md CHANGED
@@ -53,6 +53,30 @@ description:
53
53
 
54
54
  Use `defineOperation()` when an operation is large enough to live beside helper functions or in a separate module. It preserves the same type inference as inline `defineProvider()` operations and can be placed directly in the provider `operations` map. `defineProvider()` accepts Zod and Standard Schema v1-compatible schemas. If config validation fails, the SDK names the field to fix, for example `runtime`, `auth.mode`, `operations.<id>.handler`, or `operations.<id>.fixtures.response`.
55
55
 
56
+ ### Health assertion context
57
+
58
+ `healthCheck.cases[].assertions` receives a `HealthCheckAssertionContext` with
59
+ `data`, `status`, `durationMs`, and optional `meta`. `data` is typed from the
60
+ operation output schema, so assertions should inspect normalized output instead
61
+ of reaching into transport internals.
62
+
63
+ <!-- @magic-start:sample -->
64
+ ```ts
65
+ healthCheck: {
66
+ interval: "5m",
67
+ cases: [{
68
+ name: "lookup baseline",
69
+ input: { q: "btc" },
70
+ assertions: ({ data, status, durationMs }) => {
71
+ if (status !== 200 || data.results.length === 0 || durationMs > 3000) {
72
+ return { status: "degraded", label: "lookup baseline changed" };
73
+ }
74
+ },
75
+ }],
76
+ }
77
+ ```
78
+ <!-- @magic-end:sample -->
79
+
56
80
  ### Strongly recommended (warn-level rules)
57
81
 
58
82
  - `description` includes "use" AND "when" phrasing
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.1.0-beta.6
4
+
5
+ - Public repository clean-import release for `APIFuseHQ/provider-sdk`.
6
+ - Preserves the monorepo SDK exports required by ApiFuse provider registry cutover, including `./contract` and provider i18n helpers.
7
+
8
+
9
+ ## 2.1.0-beta.5
10
+
11
+ - Republish the bounty workspace DX hardening that accepts generated readonly metadata and factored `defineOperation()` maps during standalone TypeScript checks.
12
+ - Ensure new bounty workspaces can install the public SDK version that matches their generated scaffold and pass `bun run check` immediately after bootstrap.
13
+
3
14
  ## 2.1.0-beta.4
4
15
 
5
16
  - Align `apifuse create` with the bounty program topology: external contributors use the standalone one-provider-repository scaffold even when their assigned repo contains workspace-like files.
package/README.md CHANGED
@@ -149,6 +149,9 @@ const search = defineOperation({
149
149
  async handler(ctx, input) {
150
150
  return { count: input.q.length }
151
151
  },
152
+ healthCheckUnsupported: {
153
+ reason: "Example operation only; replace with a real upstream probe.",
154
+ },
152
155
  })
153
156
 
154
157
  export default defineProvider({
@@ -174,6 +177,24 @@ Every operation must declare exactly one of:
174
177
  The generated `ping` operation uses `healthCheckUnsupported` only because it is
175
178
  a local scaffold check, not a real upstream API probe.
176
179
 
180
+ `healthCheck.cases[].assertions` receives `{ data, status, durationMs, meta }`.
181
+ `data` is the operation output parsed by the declared output schema:
182
+
183
+ ```ts
184
+ healthCheck: {
185
+ interval: "5m",
186
+ cases: [{
187
+ name: "search responds",
188
+ input: { q: "weather" },
189
+ assertions: ({ data, status, durationMs }) => {
190
+ if (status !== 200 || data.count < 1 || durationMs > 3000) {
191
+ return { status: "degraded", label: "unexpected search baseline" }
192
+ }
193
+ },
194
+ }],
195
+ }
196
+ ```
197
+
177
198
  ### Operation annotations
178
199
 
179
200
  Operations declare non-functional metadata via `annotations`:
@@ -195,11 +216,11 @@ Operations declare non-functional metadata via `annotations`:
195
216
  apifuse create <name>
196
217
  apifuse dev [path]
197
218
  apifuse check [path]
198
- apifuse record [path] --operation <operation> --params '{"value":"hello"}'
219
+ apifuse record providers/korea-air-quality --operation realtime --params '{"stationName":"종로구"}'
199
220
  apifuse test [path]
200
221
  apifuse submit-check [path] --tier bronze --markdown submission-report.md
201
222
  apifuse bounty-check [path]
202
- apifuse perf <path> --operation <operation>
223
+ apifuse perf providers/korea-air-quality --operation realtime --params '{"stationName":"종로구"}'
203
224
  ```
204
225
 
205
226
  `apifuse record` is for real upstream-backed operations that declare
package/SUBMISSION.md CHANGED
@@ -48,8 +48,9 @@ Fix all blockers before submitting:
48
48
  - Missing `healthCheck` or `healthCheckUnsupported` on any Operation.
49
49
  - Credential-backed auth mode without declared credential keys.
50
50
  - High-confidence secret or token material in source, README, package metadata, or fixtures.
51
+ - SDK-native source blockers: prefixed Provider ids, `vendor/` SDK shims or imports, raw `.describe()` prose instead of `describeKey`, raw global `fetch()` calls, and excessive `as Type` assertions.
51
52
 
52
- Warnings do not fail the command, but they should be addressed when practical. For example, the generated starter `ping` operation warns because it is not a real upstream-backed bounty Operation.
53
+ Warnings do not fail the command, but they should be addressed when practical. For example, the generated starter `ping` operation warns because it is not a real upstream-backed bounty Operation. SDK-native warnings also flag moderate `as Type` assertion counts and credentialed Providers that never reference `ctx.credential`.
53
54
 
54
55
  ## Safe local smoke evidence
55
56
 
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
 
7
7
  import { z } from "zod";
8
8
 
9
9
  import type { ProviderDefinition } from "../src";
10
- import { lintProvider } from "../src/lint";
10
+ import { lintProvider, type ProviderLintMode } from "../src/lint";
11
11
  import { safeParseSchemaSync } from "../src/schema";
12
12
 
13
13
  const HELP_TEXT = `Usage: apifuse check [path]
14
- Example: apifuse check providers/airkorea
14
+ Example: apifuse check providers/korea-air-quality
15
15
  Default: apifuse check .`;
16
16
 
17
17
  export type CheckResult = {
@@ -20,6 +20,10 @@ export type CheckResult = {
20
20
  details?: string[];
21
21
  };
22
22
 
23
+ export type RunChecksOptions = {
24
+ lintMode?: ProviderLintMode;
25
+ };
26
+
23
27
  type SafeParseResult =
24
28
  | { success: true; data: unknown }
25
29
  | { success: false; error: unknown };
@@ -104,7 +108,10 @@ function resolveFromParents(inputPath: string): string {
104
108
  }
105
109
  }
106
110
 
107
- export async function runChecks(providerRoot: string): Promise<CheckResult[]> {
111
+ export async function runChecks(
112
+ providerRoot: string,
113
+ options: RunChecksOptions = {},
114
+ ): Promise<CheckResult[]> {
108
115
  const indexPath = resolve(providerRoot, "index.ts");
109
116
  const dockerfilePath = resolve(providerRoot, "Dockerfile");
110
117
  const packageJsonPath = resolve(providerRoot, "package.json");
@@ -113,19 +120,61 @@ export async function runChecks(providerRoot: string): Promise<CheckResult[]> {
113
120
  ? await import(pathToFileURL(indexPath).href)
114
121
  : undefined;
115
122
  const provider = assertProviderDefinition(providerModule?.default);
123
+ const providerSourceFiles = collectProviderSourceFiles(providerRoot);
116
124
 
117
125
  return [
118
126
  checkIndex(indexPath, provider),
119
127
  checkOperations(provider),
120
128
  checkFixtures(provider),
121
129
  checkSchemas(provider),
122
- checkAuthoringLint(provider),
130
+ checkAuthoringLint(provider, providerSourceFiles, options.lintMode),
123
131
  checkProviderMetadata(provider),
124
132
  checkDockerfile(dockerfilePath),
125
133
  checkPackageJson(packageJsonPath),
126
134
  ];
127
135
  }
128
136
 
137
+ function isScannableProviderSourceFile(relativePath: string): boolean {
138
+ return (
139
+ /\.(?:ts|tsx|js|jsx|mjs|cjs|sh|bash)$/.test(relativePath) ||
140
+ /(?:^|\/)Dockerfile(?:\.|$)/.test(relativePath) ||
141
+ /(?:^|\/)entrypoint(?:\.|$)/.test(relativePath)
142
+ );
143
+ }
144
+
145
+ function collectProviderSourceFiles(
146
+ providerRoot: string,
147
+ ): Record<string, string> {
148
+ const sources: Record<string, string> = {};
149
+ const skipDirectories = new Set([
150
+ ".git",
151
+ "node_modules",
152
+ "dist",
153
+ "build",
154
+ ".next",
155
+ ]);
156
+ const visit = (directory: string) => {
157
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
158
+ const path = resolve(directory, entry.name);
159
+ if (entry.isDirectory()) {
160
+ if (!skipDirectories.has(entry.name)) {
161
+ visit(path);
162
+ }
163
+ continue;
164
+ }
165
+ if (
166
+ !entry.isFile() ||
167
+ !isScannableProviderSourceFile(path.slice(providerRoot.length + 1))
168
+ ) {
169
+ continue;
170
+ }
171
+ sources[path.slice(providerRoot.length + 1)] = readFileSync(path, "utf8");
172
+ }
173
+ };
174
+ visit(providerRoot);
175
+ return sources;
176
+ }
177
+
129
178
  function checkIndex(
130
179
  indexPath: string,
131
180
  provider: ProviderDefinition | undefined,
@@ -258,6 +307,8 @@ function checkSchemas(provider: ProviderDefinition | undefined): CheckResult {
258
307
 
259
308
  function checkAuthoringLint(
260
309
  provider: ProviderDefinition | undefined,
310
+ providerSourceFiles: Record<string, string>,
311
+ lintMode: ProviderLintMode = "official",
261
312
  ): CheckResult {
262
313
  if (!provider) {
263
314
  return {
@@ -266,7 +317,10 @@ function checkAuthoringLint(
266
317
  };
267
318
  }
268
319
 
269
- const diagnostics = lintProvider(provider);
320
+ const diagnostics = lintProvider(
321
+ { ...provider, providerSourceFiles },
322
+ { mode: lintMode },
323
+ );
270
324
  const errors = diagnostics.filter(
271
325
  (diagnostic) => diagnostic.level === "error",
272
326
  );
@@ -9,16 +9,18 @@ import {
9
9
  createEnvContext,
10
10
  createHttpClient,
11
11
  createProviderCache,
12
+ createProviderChoiceContext,
12
13
  createStealthClient,
13
14
  createSttClientFromEnv,
15
+ PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
14
16
  ProviderError,
15
17
  } from "../src";
16
- import { createUnsupportedProviderRuntimeState } from "../src/runtime/state";
18
+ import { createMemoryProviderRuntimeState } from "../src/runtime/state";
17
19
  import { createTraceContext } from "../src/runtime/trace";
18
20
  import type { BrowserClient, ProviderContext } from "../src/types";
19
21
 
20
22
  const HELP_TEXT = `Usage: apifuse dev [path]
21
- Example: apifuse dev providers/airkorea
23
+ Example: apifuse dev providers/korea-air-quality
22
24
  Default: apifuse dev .`;
23
25
 
24
26
  export async function main() {
@@ -76,22 +78,35 @@ export async function main() {
76
78
  export function createProviderContext(provider: ProviderDefinition): {
77
79
  ctx: ProviderContext;
78
80
  } {
81
+ const env = createEnvContext([
82
+ ...(provider.secrets?.map((secret) => secret.name) ?? []),
83
+ PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
84
+ ]);
85
+ const credential = createCredentialContext();
86
+ const state = createMemoryProviderRuntimeState();
79
87
  const ctx: ProviderContext = {
80
- env: createEnvContext(provider.secrets?.map((secret) => secret.name)),
81
- credential: createCredentialContext(),
88
+ env,
89
+ credential,
82
90
  auth: createUnsupportedAuthStub(),
83
91
  browser:
84
92
  provider.runtime === "browser"
85
93
  ? createBrowserClient({
94
+ allowedHosts: provider.allowedHosts,
86
95
  engine: provider.browser?.engine ?? "playwright-stealth",
87
96
  })
88
97
  : createUnsupportedBrowserStub(),
89
98
  http: createHttpClient(),
90
99
  cache: createProviderCache({ providerId: provider.id }),
91
- state: createUnsupportedProviderRuntimeState(),
100
+ state,
92
101
  trace: createTraceContext(),
93
102
  stealth: createStealthClient("http://localhost"),
94
103
  stt: createSttClientFromEnv(provider.stt),
104
+ choice: createProviderChoiceContext({
105
+ providerId: provider.id,
106
+ env,
107
+ credential,
108
+ state,
109
+ }),
95
110
  };
96
111
 
97
112
  return { ctx };
@@ -149,6 +164,7 @@ function shellSingleQuote(value: string): string {
149
164
  function createUnsupportedBrowserStub(): BrowserClient {
150
165
  return {
151
166
  engine: "playwright-stealth",
167
+ async close() {},
152
168
  async newPage() {
153
169
  throw new ProviderError(
154
170
  "Browser runtime is not enabled for this provider",
@@ -158,6 +174,33 @@ function createUnsupportedBrowserStub(): BrowserClient {
158
174
  },
159
175
  );
160
176
  },
177
+ async rawPage() {
178
+ throw new ProviderError(
179
+ "Browser runtime is not enabled for this provider",
180
+ {
181
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
182
+ fix: 'Set provider runtime to "browser" and APIFUSE__CDP_POOL__URL to use ctx.browser.rawPage',
183
+ },
184
+ );
185
+ },
186
+ async withIsolatedContext() {
187
+ throw new ProviderError(
188
+ "Browser runtime is not enabled for this provider",
189
+ {
190
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
191
+ fix: 'Set provider runtime to "browser" to use ctx.browser.withIsolatedContext',
192
+ },
193
+ );
194
+ },
195
+ async solveChallenge() {
196
+ throw new ProviderError(
197
+ "Browser runtime is not enabled for this provider",
198
+ {
199
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
200
+ fix: 'Set provider runtime to "browser" to use ctx.browser.solveChallenge',
201
+ },
202
+ );
203
+ },
161
204
  };
162
205
  }
163
206
 
@@ -10,6 +10,7 @@ import {
10
10
  type ApiFuseConfig,
11
11
  createBypassProviderCache,
12
12
  createHttpClient,
13
+ createProviderChoiceContext,
13
14
  createStealthClient,
14
15
  createSttClientFromEnv,
15
16
  executeOperation,
@@ -29,6 +30,7 @@ import {
29
30
  groupSpansByName,
30
31
  type PerfStats,
31
32
  } from "../src/runtime/perf";
33
+ import { createMemoryProviderRuntimeState } from "../src/runtime/state";
32
34
  import {
33
35
  createTraceContext,
34
36
  resolveTraceContextOptions,
@@ -43,6 +45,7 @@ type CliArgs = {
43
45
  exportPath?: string;
44
46
  flame: boolean;
45
47
  operation: string;
48
+ params?: string;
46
49
  runs: number;
47
50
  warmup: number;
48
51
  };
@@ -81,6 +84,21 @@ const DEFAULT_RUNS = 10;
81
84
  const DEFAULT_WARMUP = 2;
82
85
  const DEFAULT_CONCURRENCY = 1;
83
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`;
84
102
 
85
103
  export async function main() {
86
104
  try {
@@ -94,7 +112,11 @@ export async function main() {
94
112
  const inputSchema = getOperationSchema(provider, operation, "input");
95
113
  const outputSchema = getOperationSchema(provider, operation, "output");
96
114
  const fixtureReplay = await loadFixtureReplay(providerDirectory);
97
- const inputTemplate = resolveInputTemplate(provider, inputSchema);
115
+ const inputTemplate = resolveInputTemplate(
116
+ provider,
117
+ inputSchema,
118
+ args.params,
119
+ );
98
120
 
99
121
  const directSuite = await runProfileSuite({
100
122
  args,
@@ -184,6 +206,7 @@ function parseArgs(argv: string[]): CliArgs {
184
206
  let compareProxy = false;
185
207
  let exportPath: string | undefined;
186
208
  let flame = false;
209
+ let params: string | undefined;
187
210
 
188
211
  for (let index = 0; index < argv.length; index += 1) {
189
212
  const arg = argv[index];
@@ -201,6 +224,11 @@ function parseArgs(argv: string[]): CliArgs {
201
224
  continue;
202
225
  }
203
226
 
227
+ if (arg === "--help" || arg === "-h") {
228
+ console.log(HELP_TEXT);
229
+ process.exit(0);
230
+ }
231
+
204
232
  if (arg === "--compare-proxy") {
205
233
  compareProxy = true;
206
234
  continue;
@@ -222,6 +250,22 @@ function parseArgs(argv: string[]): CliArgs {
222
250
  continue;
223
251
  }
224
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
+
225
269
  if (arg.startsWith("-o=")) {
226
270
  operation = arg.slice("-o=".length);
227
271
  continue;
@@ -294,20 +338,7 @@ function parseArgs(argv: string[]): CliArgs {
294
338
  }
295
339
 
296
340
  if (!providerPath || !operation) {
297
- throw new Error(
298
- [
299
- "Usage: apifuse perf <provider-path> [options]",
300
- "",
301
- "Options:",
302
- " --operation, -o <name> operation to profile (required)",
303
- " --runs, -n <number> number of runs (default: 10)",
304
- " --warmup <number> warmup runs (default: 2)",
305
- " --concurrency, -c <n> concurrent requests (default: 1)",
306
- " --compare-proxy run with proxy on/off and compare",
307
- " --export <path> export results to JSON file",
308
- " --flame generate flamegraph SVG",
309
- ].join("\n"),
310
- );
341
+ throw new Error(HELP_TEXT);
311
342
  }
312
343
 
313
344
  return {
@@ -317,6 +348,7 @@ function parseArgs(argv: string[]): CliArgs {
317
348
  exportPath,
318
349
  flame,
319
350
  operation,
351
+ params,
320
352
  runs,
321
353
  warmup,
322
354
  };
@@ -411,7 +443,18 @@ function isSchema(value: unknown): value is { parse(input: unknown): unknown } {
411
443
  function resolveInputTemplate(
412
444
  provider: ProviderDefinition,
413
445
  inputSchema: { parse(input: unknown): unknown },
446
+ params: string | undefined,
414
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
+
415
458
  const firstOp = Object.values(provider.operations)[0];
416
459
  if (firstOp?.fixtures?.request !== undefined) {
417
460
  return firstOp.fixtures.request;
@@ -647,24 +690,36 @@ function createBaseContext(options: {
647
690
  upstream,
648
691
  });
649
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();
650
704
  return {
651
- env: {
652
- get: (key: string) => process.env[key],
653
- },
654
- credential: {
655
- mode: "none",
656
- get: () => undefined,
657
- getAll: () => ({}),
658
- getAccessToken: () => undefined,
659
- getScopes: () => [],
660
- },
705
+ env,
706
+ credential,
707
+ request: { headers: {} },
661
708
  http,
662
709
  cache: createBypassProviderCache({ providerId: options.provider.id }),
710
+ state,
663
711
  stealth,
664
712
  browser: createBrowserStub(),
665
713
  trace: options.traceContext,
666
714
  auth: createAuthStub(),
667
715
  stt: createSttClientFromEnv(options.provider.stt),
716
+ choice: createProviderChoiceContext({
717
+ providerId: options.provider.id,
718
+ env,
719
+ request: { headers: {} },
720
+ credential,
721
+ state,
722
+ }),
668
723
  };
669
724
  }
670
725
 
@@ -724,6 +779,7 @@ function createFixtureStealthClient(rawText: string): StealthClient {
724
779
  function createBrowserStub(): BrowserClient {
725
780
  return {
726
781
  engine: "playwright-stealth",
782
+ async close() {},
727
783
  async newPage() {
728
784
  throw new ProviderError(
729
785
  "Browser runtime is not supported by apifuse perf yet.",
@@ -732,6 +788,30 @@ function createBrowserStub(): BrowserClient {
732
788
  },
733
789
  );
734
790
  },
791
+ async rawPage() {
792
+ throw new ProviderError(
793
+ "Browser runtime is not supported by apifuse perf yet.",
794
+ {
795
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
796
+ },
797
+ );
798
+ },
799
+ async withIsolatedContext() {
800
+ throw new ProviderError(
801
+ "Browser runtime is not supported by apifuse perf yet.",
802
+ {
803
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
804
+ },
805
+ );
806
+ },
807
+ async solveChallenge() {
808
+ throw new ProviderError(
809
+ "Browser runtime is not supported by apifuse perf yet.",
810
+ {
811
+ code: "BROWSER_RUNTIME_UNSUPPORTED",
812
+ },
813
+ );
814
+ },
735
815
  };
736
816
  }
737
817
 
@@ -1095,7 +1175,7 @@ function cloneValue<T>(value: T): T {
1095
1175
 
1096
1176
  function handleCliError(error: unknown): never {
1097
1177
  const message = error instanceof Error ? error.message : String(error);
1098
- console.error(message);
1178
+ console.error(`[apifuse perf] ${message}`);
1099
1179
  process.exit(1);
1100
1180
  }
1101
1181