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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/AUTHORING.md +172 -8
  2. package/CHANGELOG.md +15 -1
  3. package/README.md +29 -15
  4. package/SUBMISSION.md +86 -0
  5. package/bin/apifuse-dev.ts +12 -5
  6. package/bin/apifuse-pack-check.ts +17 -2
  7. package/bin/apifuse-pack-smoke.ts +133 -6
  8. package/bin/apifuse-perf.ts +19 -15
  9. package/bin/apifuse-record.ts +41 -53
  10. package/bin/apifuse-submit-check.ts +1052 -0
  11. package/bin/apifuse.ts +1 -1
  12. package/package.json +19 -9
  13. package/src/choice-token.ts +164 -0
  14. package/src/cli/commands.ts +24 -3
  15. package/src/cli/create.ts +166 -51
  16. package/src/cli/templates/provider/README.md.tpl +66 -7
  17. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  18. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  19. package/src/cli/templates/provider/index.ts.tpl +5 -47
  20. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  21. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  22. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  23. package/src/cli/templates/provider/operations/ping.ts.tpl +23 -0
  24. package/src/cli/templates/provider/schemas/ping.ts.tpl +16 -0
  25. package/src/cli/templates/provider/start.ts.tpl +1 -1
  26. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  27. package/src/config/loader.ts +1206 -9
  28. package/src/define.ts +1648 -43
  29. package/src/errors.ts +12 -0
  30. package/src/i18n/catalog.ts +121 -0
  31. package/src/i18n/index.ts +2 -0
  32. package/src/i18n/keys.ts +64 -0
  33. package/src/index.ts +152 -8
  34. package/src/lint.ts +297 -42
  35. package/src/observability.ts +41 -0
  36. package/src/provider.ts +60 -3
  37. package/src/public-schema-field-lint.ts +237 -0
  38. package/src/runtime/auth-flow.ts +7 -0
  39. package/src/runtime/browser.ts +77 -21
  40. package/src/runtime/cache.ts +582 -0
  41. package/src/runtime/executor.ts +13 -1
  42. package/src/runtime/http.ts +939 -195
  43. package/src/runtime/insights.ts +11 -11
  44. package/src/runtime/instrumentation.ts +12 -4
  45. package/src/runtime/key-derivation.ts +1 -1
  46. package/src/runtime/keyring.ts +4 -3
  47. package/src/runtime/proxy-errors.ts +132 -0
  48. package/src/runtime/proxy-telemetry.ts +253 -0
  49. package/src/runtime/request-options.ts +66 -0
  50. package/src/runtime/state.ts +76 -0
  51. package/src/runtime/stealth.ts +1145 -0
  52. package/src/runtime/stt.ts +629 -0
  53. package/src/schema.ts +363 -1
  54. package/src/server/serve.ts +827 -60
  55. package/src/server/types.ts +35 -0
  56. package/src/stream.ts +210 -0
  57. package/src/testing/run.ts +17 -4
  58. package/src/types.ts +889 -50
  59. package/src/runtime/tls.ts +0 -434
  60. package/src/types/playwright-stealth.d.ts +0 -9
@@ -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);
@@ -93,9 +95,15 @@ try {
93
95
 
94
96
  const generatedProviderDir = join(consumerDir, "dx-smoke");
95
97
  run("bun", ["run", "check"], generatedProviderDir);
98
+ run("bun", ["run", "submit-check"], generatedProviderDir);
96
99
  run("bun", ["run", "test"], generatedProviderDir);
97
100
  assertGeneratedReadme(generatedProviderDir);
98
101
  await smokeGeneratedDevServer(generatedProviderDir);
102
+ assertExternalWorkspaceTopology(
103
+ cliBin,
104
+ externalWorkspaceDir,
105
+ tarballSpecifier,
106
+ );
99
107
 
100
108
  console.log(
101
109
  `Provider SDK packed-artifact smoke passed: ${tarballPath} -> ${generatedProviderDir}`,
@@ -108,6 +116,103 @@ try {
108
116
  }
109
117
  }
110
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
+
111
216
  function packSdk(destination: string): { filename: string } {
112
217
  const raw = execFileSync(
113
218
  "npm",
@@ -161,9 +266,14 @@ function assertGeneratedReadme(providerDir: string): void {
161
266
  "Generated README is missing browser runtime troubleshooting guidance.",
162
267
  );
163
268
  }
164
- if (!readme.includes("bun pm untrusted")) {
269
+ if (!readme.includes("impit")) {
165
270
  throw new Error(
166
- "Generated README is missing Bun trusted-dependency troubleshooting guidance.",
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.",
167
277
  );
168
278
  }
169
279
  if (!readme.includes("bun run record -- --operation <operation>")) {
@@ -177,7 +287,8 @@ async function smokeGeneratedDevServer(providerDir: string): Promise<void> {
177
287
  const port = await getAvailablePort();
178
288
  const server = spawn("bun", ["run", "dev"], {
179
289
  cwd: providerDir,
180
- env: { ...process.env, PORT: String(port) },
290
+ env: { ...process.env, APIFUSE__RUNTIME__PORT: String(port) },
291
+ detached: process.platform !== "win32",
181
292
  stdio: ["ignore", "pipe", "pipe"],
182
293
  });
183
294
  let output = "";
@@ -280,11 +391,11 @@ async function stopServer(server: ChildProcess): Promise<void> {
280
391
  if (server.exitCode !== null) {
281
392
  return;
282
393
  }
283
- server.kill("SIGTERM");
394
+ killProcessTree(server, "SIGTERM");
284
395
  await new Promise<void>((resolvePromise) => {
285
396
  const timeout = setTimeout(() => {
286
397
  if (server.exitCode === null) {
287
- server.kill("SIGKILL");
398
+ killProcessTree(server, "SIGKILL");
288
399
  }
289
400
  resolvePromise();
290
401
  }, 2_000);
@@ -294,3 +405,19 @@ async function stopServer(server: ChildProcess): Promise<void> {
294
405
  });
295
406
  });
296
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 {
@@ -440,12 +442,12 @@ async function loadFixtureReplay(
440
442
  }
441
443
 
442
444
  function assertProxyConfigured(config: ApiFuseConfig): void {
443
- if (config.proxy?.url || process.env.APIFUSE_PROXY_URL) {
445
+ if (config.proxy?.url || process.env.APIFUSE__PROXY__URL) {
444
446
  return;
445
447
  }
446
448
 
447
449
  throw new Error(
448
- "--compare-proxy requires a proxy URL in apifuse.config.ts or APIFUSE_PROXY_URL.",
450
+ "--compare-proxy requires a proxy URL in apifuse.config.ts or APIFUSE__PROXY__URL.",
449
451
  );
450
452
  }
451
453
 
@@ -637,10 +639,10 @@ function createBaseContext(options: {
637
639
  apifuseConfig,
638
640
  upstream,
639
641
  });
640
- const tls =
642
+ const stealth =
641
643
  options.forceFixtureReplay && options.fixtureReplay
642
- ? createFixtureTlsClient(options.fixtureReplay.rawText)
643
- : createTlsClient(getProviderBaseUrl(options.provider), {
644
+ ? createFixtureStealthClient(options.fixtureReplay.rawText)
645
+ : createStealthClient(getProviderBaseUrl(options.provider), {
644
646
  apifuseConfig,
645
647
  upstream,
646
648
  });
@@ -657,10 +659,12 @@ function createBaseContext(options: {
657
659
  getScopes: () => [],
658
660
  },
659
661
  http,
660
- tls,
662
+ cache: createBypassProviderCache({ providerId: options.provider.id }),
663
+ stealth,
661
664
  browser: createBrowserStub(),
662
665
  trace: options.traceContext,
663
666
  auth: createAuthStub(),
667
+ stt: createSttClientFromEnv(options.provider.stt),
664
668
  };
665
669
  }
666
670
 
@@ -695,8 +699,8 @@ function createFixtureResponse(raw: unknown) {
695
699
  };
696
700
  }
697
701
 
698
- function createFixtureTlsClient(rawText: string): TlsClient {
699
- const createResponse = async (): Promise<TlsResponse> => ({
702
+ function createFixtureStealthClient(rawText: string): StealthClient {
703
+ const createResponse = async (): Promise<StealthResponse> => ({
700
704
  status: 200,
701
705
  ok: true,
702
706
  headers: { "content-type": "application/json" },
@@ -738,7 +742,7 @@ function buildInsights(
738
742
  ): string[] {
739
743
  const insights: string[] = [];
740
744
  const allSpans = runs.flatMap((run) => run.spans);
741
- const tlsSpans = allSpans.filter((span) => span.name === "tls.fetch");
745
+ const stealthSpans = allSpans.filter((span) => span.name === "stealth.fetch");
742
746
  const dnsSpans = allSpans.filter((span) => span.name === "dns");
743
747
  const transform = breakdown.find(
744
748
  (entry) => entry.name === "transformResponse",
@@ -746,7 +750,7 @@ function buildInsights(
746
750
  const responseSizes = allSpans
747
751
  .map((span) => span.attributes.response_size)
748
752
  .filter((value): value is number => typeof value === "number");
749
- const reuseFlags = tlsSpans
753
+ const reuseFlags = stealthSpans
750
754
  .map((span) => span.attributes.connection_reused)
751
755
  .filter((value): value is boolean => typeof value === "boolean");
752
756
 
@@ -756,8 +760,8 @@ function buildInsights(
756
760
  );
757
761
  insights.push(
758
762
  reusePercent >= 80
759
- ? `✓ TLS connection reuse: ${reusePercent}% (good)`
760
- : `⚠ TLS connection reuse: ${reusePercent}% — consider session reuse`,
763
+ ? `✓ Stealth connection reuse: ${reusePercent}% (good)`
764
+ : `⚠ Stealth connection reuse: ${reusePercent}% — consider session reuse`,
761
765
  );
762
766
  }
763
767
 
@@ -7,13 +7,15 @@ import { basename, dirname, relative, resolve } from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
 
9
9
  import {
10
+ createBypassProviderCache,
10
11
  createHttpClient,
11
- createTlsClient,
12
+ createStealthClient,
13
+ createSttClientFromEnv,
12
14
  executeOperation,
13
15
  type HttpClient,
14
16
  type ProviderContext,
15
17
  type ProviderDefinition,
16
- type TlsClient,
18
+ type StealthClient,
17
19
  } from "../src";
18
20
 
19
21
  type CliArgs = {
@@ -37,6 +39,7 @@ export async function main() {
37
39
  const parsedParams = parseParams(operation, args.params);
38
40
 
39
41
  const capture = createCaptureContext(
42
+ provider,
40
43
  resolveOperationBaseUrl(provider, operationName),
41
44
  );
42
45
 
@@ -269,15 +272,18 @@ function resolveOperationBaseUrl(
269
272
  return baseUrl;
270
273
  }
271
274
 
272
- function createCaptureContext(baseUrl: string) {
275
+ function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
273
276
  let capturedRaw: unknown;
274
277
 
275
278
  const http = proxyHttpClient(createHttpClient(baseUrl), (response) => {
276
279
  capturedRaw = response.data;
277
280
  });
278
- const tls = proxyTlsClient(createTlsClient(baseUrl), (response) => {
279
- capturedRaw = normalizeCapturedTlsResponse(response);
280
- });
281
+ const stealth = proxyStealthClient(
282
+ createStealthClient(baseUrl),
283
+ (response) => {
284
+ capturedRaw = normalizeCapturedStealthResponse(response);
285
+ },
286
+ );
281
287
 
282
288
  const ctx: ProviderContext = {
283
289
  env: {
@@ -291,7 +297,8 @@ function createCaptureContext(baseUrl: string) {
291
297
  getScopes: () => [],
292
298
  },
293
299
  http,
294
- tls,
300
+ cache: createBypassProviderCache({ providerId: provider.id }),
301
+ stealth,
295
302
  browser: {
296
303
  engine: "playwright-stealth",
297
304
  newPage: async () => {
@@ -306,6 +313,7 @@ function createCaptureContext(baseUrl: string) {
306
313
  throw new Error("Auth prompts are not available in apifuse record.");
307
314
  },
308
315
  },
316
+ stt: createSttClientFromEnv(provider.stt),
309
317
  };
310
318
 
311
319
  return {
@@ -335,59 +343,39 @@ function proxyHttpClient(
335
343
  }) as HttpClient;
336
344
  }
337
345
 
338
- function proxyTlsClient(
339
- client: TlsClient,
340
- onResponse: (response: Awaited<ReturnType<TlsClient["fetch"]>>) => void,
341
- ): TlsClient {
342
- return new Proxy(client, {
343
- get(target, prop, receiver) {
344
- const value = Reflect.get(target, prop, receiver);
345
-
346
- if (prop === "fetch" && typeof value === "function") {
347
- return async (...args: unknown[]) => {
348
- const response = await value.apply(target, args);
349
- onResponse(response);
350
- return response;
351
- };
352
- }
353
-
354
- if (prop === "createSession" && typeof value === "function") {
355
- return (...args: unknown[]) => {
356
- const session = value.apply(target, args) as ReturnType<
357
- TlsClient["createSession"]
358
- >;
359
- return proxyTlsSession(session, onResponse);
360
- };
361
- }
346
+ type StealthSession = ReturnType<StealthClient["createSession"]>;
362
347
 
363
- return value;
348
+ function proxyStealthClient(
349
+ client: StealthClient,
350
+ onResponse: (response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
351
+ ): StealthClient {
352
+ return {
353
+ fetch: async (...args: Parameters<StealthClient["fetch"]>) => {
354
+ const response = await client.fetch(...args);
355
+ onResponse(response);
356
+ return response;
364
357
  },
365
- }) as TlsClient;
358
+ createSession: (...args: Parameters<StealthClient["createSession"]>) =>
359
+ proxyStealthSession(client.createSession(...args), onResponse),
360
+ };
366
361
  }
367
362
 
368
- function proxyTlsSession(
369
- session: ReturnType<TlsClient["createSession"]>,
370
- onResponse: (response: Awaited<ReturnType<TlsClient["fetch"]>>) => void,
371
- ) {
372
- return new Proxy(session, {
373
- get(target, prop, receiver) {
374
- const value = Reflect.get(target, prop, receiver);
375
-
376
- if (prop === "fetch" && typeof value === "function") {
377
- return async (...args: unknown[]) => {
378
- const response = await value.apply(target, args);
379
- onResponse(response);
380
- return response;
381
- };
382
- }
383
-
384
- return value;
363
+ function proxyStealthSession(
364
+ session: StealthSession,
365
+ onResponse: (response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
366
+ ): StealthSession {
367
+ return {
368
+ fetch: async (...args: Parameters<StealthSession["fetch"]>) => {
369
+ const response = await session.fetch(...args);
370
+ onResponse(response);
371
+ return response;
385
372
  },
386
- }) as ReturnType<TlsClient["createSession"]>;
373
+ close: () => session.close(),
374
+ };
387
375
  }
388
376
 
389
- function normalizeCapturedTlsResponse(
390
- response: Awaited<ReturnType<TlsClient["fetch"]>>,
377
+ function normalizeCapturedStealthResponse(
378
+ response: Awaited<ReturnType<StealthClient["fetch"]>>,
391
379
  ) {
392
380
  try {
393
381
  return JSON.parse(response.body);