@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.
- package/AUTHORING.md +208 -2
- package/CHANGELOG.md +47 -0
- package/README.md +114 -10
- package/SUBMISSION.md +87 -0
- package/bin/apifuse-check.ts +86 -4
- package/bin/apifuse-dev.ts +87 -13
- package/bin/apifuse-pack-check.ts +80 -0
- package/bin/apifuse-pack-smoke.ts +303 -2
- package/bin/apifuse-perf.ts +142 -49
- package/bin/apifuse-record.ts +182 -104
- package/bin/apifuse-submit-check.ts +2538 -0
- package/bin/apifuse.ts +1 -1
- package/dist/ceremonies/index.d.ts +41 -0
- package/dist/ceremonies/index.js +490 -0
- package/dist/choice-token.d.ts +24 -0
- package/dist/choice-token.js +74 -0
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +80 -0
- package/dist/cli/create.d.ts +47 -0
- package/dist/cli/create.js +762 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +160 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/config/loader.d.ts +107 -0
- package/dist/config/loader.js +935 -0
- package/dist/contract-json.d.ts +9 -0
- package/dist/contract-json.js +51 -0
- package/dist/contract-serialization.d.ts +4 -0
- package/dist/contract-serialization.js +78 -0
- package/dist/contract-types.d.ts +49 -0
- package/dist/contract-types.js +1 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +155 -0
- package/dist/define.d.ts +97 -0
- package/dist/define.js +1320 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.js +15 -0
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +97 -0
- package/dist/i18n/catalog.d.ts +29 -0
- package/dist/i18n/catalog.js +159 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/keys.d.ts +10 -0
- package/dist/i18n/keys.js +34 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +37 -0
- package/dist/lint.d.ts +73 -0
- package/dist/lint.js +702 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +9 -0
- package/dist/provider.js +8 -0
- package/dist/public-schema-field-lint.d.ts +2 -0
- package/dist/public-schema-field-lint.js +158 -0
- package/dist/recipes/gov-api.d.ts +19 -0
- package/dist/recipes/gov-api.js +72 -0
- package/dist/recipes/rest-api.d.ts +21 -0
- package/dist/recipes/rest-api.js +115 -0
- package/dist/runtime/auth-flow.d.ts +14 -0
- package/dist/runtime/auth-flow.js +44 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1034 -0
- package/dist/runtime/cache.d.ts +10 -0
- package/dist/runtime/cache.js +372 -0
- package/dist/runtime/choice.d.ts +15 -0
- package/dist/runtime/choice.js +435 -0
- package/dist/runtime/credential.d.ts +8 -0
- package/dist/runtime/credential.js +61 -0
- package/dist/runtime/env.d.ts +2 -0
- package/dist/runtime/env.js +10 -0
- package/dist/runtime/executor.d.ts +16 -0
- package/dist/runtime/executor.js +51 -0
- package/dist/runtime/http.d.ts +8 -0
- package/dist/runtime/http.js +706 -0
- package/dist/runtime/insights.d.ts +9 -0
- package/dist/runtime/insights.js +324 -0
- package/dist/runtime/instrumentation.d.ts +8 -0
- package/dist/runtime/instrumentation.js +269 -0
- package/dist/runtime/key-derivation.d.ts +24 -0
- package/dist/runtime/key-derivation.js +73 -0
- package/dist/runtime/keyring.d.ts +25 -0
- package/dist/runtime/keyring.js +93 -0
- package/dist/runtime/namespace.d.ts +9 -0
- package/dist/runtime/namespace.js +19 -0
- package/dist/runtime/otlp.d.ts +39 -0
- package/dist/runtime/otlp.js +103 -0
- package/dist/runtime/perf.d.ts +12 -0
- package/dist/runtime/perf.js +52 -0
- package/dist/runtime/prevalidate.d.ts +12 -0
- package/dist/runtime/prevalidate.js +173 -0
- package/dist/runtime/provider.d.ts +2 -0
- package/dist/runtime/provider.js +11 -0
- package/dist/runtime/proxy-errors.d.ts +21 -0
- package/dist/runtime/proxy-errors.js +83 -0
- package/dist/runtime/proxy-telemetry.d.ts +8 -0
- package/dist/runtime/proxy-telemetry.js +174 -0
- package/dist/runtime/redis.d.ts +17 -0
- package/dist/runtime/redis.js +82 -0
- package/dist/runtime/request-options.d.ts +3 -0
- package/dist/runtime/request-options.js +42 -0
- package/dist/runtime/state.d.ts +17 -0
- package/dist/runtime/state.js +344 -0
- package/dist/runtime/stealth.d.ts +18 -0
- package/dist/runtime/stealth.js +834 -0
- package/dist/runtime/stt.d.ts +22 -0
- package/dist/runtime/stt.js +480 -0
- package/dist/runtime/trace.d.ts +26 -0
- package/dist/runtime/trace.js +142 -0
- package/dist/runtime/waterfall.d.ts +12 -0
- package/dist/runtime/waterfall.js +147 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.js +243 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/serve.d.ts +64 -0
- package/dist/server/serve.js +1110 -0
- package/dist/server/types.d.ts +136 -0
- package/dist/server/types.js +86 -0
- package/dist/stealth/profiles.d.ts +4 -0
- package/dist/stealth/profiles.js +259 -0
- package/dist/stream.d.ts +44 -0
- package/dist/stream.js +151 -0
- package/dist/testing/helpers.d.ts +23 -0
- package/dist/testing/helpers.js +95 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/run.d.ts +34 -0
- package/dist/testing/run.js +303 -0
- package/dist/types.d.ts +1326 -0
- package/dist/types.js +61 -0
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/date.js +101 -0
- package/dist/utils/parse.d.ts +16 -0
- package/dist/utils/parse.js +51 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +14 -0
- package/dist/utils/transform.d.ts +8 -0
- package/dist/utils/transform.js +48 -0
- package/package.json +57 -30
- package/src/ceremonies/index.ts +8 -2
- package/src/choice-token.ts +165 -0
- package/src/cli/commands.ts +34 -11
- package/src/cli/create.ts +214 -52
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- package/src/cli/templates/provider/README.md.tpl +120 -1
- package/src/cli/templates/provider/dev.ts.tpl +1 -1
- package/src/cli/templates/provider/domain/README.md.tpl +3 -0
- package/src/cli/templates/provider/index.ts.tpl +5 -48
- package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/src/cli/templates/provider/meta.ts.tpl +7 -0
- package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/src/cli/templates/provider/start.ts.tpl +1 -1
- package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/src/config/loader.ts +1224 -9
- package/src/contract-json.ts +75 -0
- package/src/contract-serialization.ts +89 -0
- package/src/contract-types.ts +52 -0
- package/src/contract.ts +215 -0
- package/src/define.ts +1688 -48
- package/src/errors.ts +27 -0
- package/src/i18n/catalog.ts +277 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/keys.ts +64 -0
- package/src/index.ts +174 -9
- package/src/lint.ts +547 -73
- package/src/observability.ts +41 -0
- package/src/provider.ts +104 -4
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +7 -0
- package/src/runtime/browser.ts +762 -51
- package/src/runtime/cache.ts +528 -0
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +32 -3
- package/src/runtime/http.ts +939 -195
- package/src/runtime/insights.ts +11 -11
- package/src/runtime/instrumentation.ts +12 -4
- package/src/runtime/key-derivation.ts +1 -1
- package/src/runtime/keyring.ts +4 -3
- package/src/runtime/proxy-errors.ts +132 -0
- package/src/runtime/proxy-telemetry.ts +253 -0
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/request-options.ts +66 -0
- package/src/runtime/state.ts +563 -0
- package/src/runtime/stealth.ts +1159 -0
- package/src/runtime/stt.ts +629 -0
- package/src/runtime/trace.ts +1 -1
- package/src/schema.ts +363 -1
- package/src/server/serve.ts +1157 -75
- package/src/server/types.ts +37 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +31 -5
- package/src/types.ts +1107 -59
- package/src/runtime/tls.ts +0 -434
- package/src/types/playwright-stealth.d.ts +0 -9
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
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.
|
|
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
|
+
}
|
package/bin/apifuse-perf.ts
CHANGED
|
@@ -8,8 +8,11 @@ import { pathToFileURL } from "node:url";
|
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
type ApiFuseConfig,
|
|
11
|
+
createBypassProviderCache,
|
|
11
12
|
createHttpClient,
|
|
12
|
-
|
|
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
|
|
22
|
-
type
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
685
|
+
const stealth =
|
|
641
686
|
options.forceFixtureReplay && options.fixtureReplay
|
|
642
|
-
?
|
|
643
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
699
|
-
const createResponse = async (): Promise<
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
|
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 =
|
|
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
|
-
? `✓
|
|
760
|
-
: `⚠
|
|
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
|
|