@apifuse/provider-sdk 2.1.0-beta.4 → 2.1.0-beta.5
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 +24 -0
- package/CHANGELOG.md +5 -0
- package/README.md +23 -2
- package/bin/apifuse-perf.ts +57 -16
- package/bin/apifuse-record.ts +108 -42
- package/bin/apifuse-submit-check.ts +64 -0
- package/package.json +1 -1
- package/src/cli/commands.ts +3 -4
- package/src/cli/create.ts +21 -1
- 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 +18 -0
- package/src/define.ts +3 -3
- package/src/lint.ts +17 -17
- package/src/runtime/trace.ts +1 -1
- package/src/types.ts +16 -12
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,10 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.0-beta.5
|
|
4
|
+
|
|
5
|
+
- Republish the bounty workspace DX hardening that accepts generated readonly metadata and factored `defineOperation()` maps during standalone TypeScript checks.
|
|
6
|
+
- Ensure new bounty workspaces can install the public SDK version that matches their generated scaffold and pass `bun run check` immediately after bootstrap.
|
|
7
|
+
|
|
3
8
|
## 2.1.0-beta.4
|
|
4
9
|
|
|
5
10
|
- 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
|
|
219
|
+
apifuse record providers/airkorea --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
|
|
223
|
+
apifuse perf providers/airkorea --operation realtime --params '{"stationName":"종로구"}'
|
|
203
224
|
```
|
|
204
225
|
|
|
205
226
|
`apifuse record` is for real upstream-backed operations that declare
|
package/bin/apifuse-perf.ts
CHANGED
|
@@ -43,6 +43,7 @@ type CliArgs = {
|
|
|
43
43
|
exportPath?: string;
|
|
44
44
|
flame: boolean;
|
|
45
45
|
operation: string;
|
|
46
|
+
params?: string;
|
|
46
47
|
runs: number;
|
|
47
48
|
warmup: number;
|
|
48
49
|
};
|
|
@@ -81,6 +82,21 @@ const DEFAULT_RUNS = 10;
|
|
|
81
82
|
const DEFAULT_WARMUP = 2;
|
|
82
83
|
const DEFAULT_CONCURRENCY = 1;
|
|
83
84
|
const BAR_WIDTH = 20;
|
|
85
|
+
const HELP_TEXT = `Usage: apifuse perf <provider-path> --operation <operation> [options]
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
--operation, -o <name> operation to profile (required)
|
|
89
|
+
--params, -p <json> JSON input template; falls back to fixtures.request or {}
|
|
90
|
+
--runs, -n <number> number of runs (default: 10)
|
|
91
|
+
--warmup <number> warmup runs (default: 2)
|
|
92
|
+
--concurrency, -c <n> concurrent requests (default: 1)
|
|
93
|
+
--compare-proxy run with proxy on/off and compare
|
|
94
|
+
--export <path> export results to JSON file
|
|
95
|
+
--flame generate flamegraph SVG
|
|
96
|
+
--help, -h show this help
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
apifuse perf providers/airkorea --operation realtime --params '{"stationName":"jongno"}' --runs 5`;
|
|
84
100
|
|
|
85
101
|
export async function main() {
|
|
86
102
|
try {
|
|
@@ -94,7 +110,11 @@ export async function main() {
|
|
|
94
110
|
const inputSchema = getOperationSchema(provider, operation, "input");
|
|
95
111
|
const outputSchema = getOperationSchema(provider, operation, "output");
|
|
96
112
|
const fixtureReplay = await loadFixtureReplay(providerDirectory);
|
|
97
|
-
const inputTemplate = resolveInputTemplate(
|
|
113
|
+
const inputTemplate = resolveInputTemplate(
|
|
114
|
+
provider,
|
|
115
|
+
inputSchema,
|
|
116
|
+
args.params,
|
|
117
|
+
);
|
|
98
118
|
|
|
99
119
|
const directSuite = await runProfileSuite({
|
|
100
120
|
args,
|
|
@@ -184,6 +204,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
184
204
|
let compareProxy = false;
|
|
185
205
|
let exportPath: string | undefined;
|
|
186
206
|
let flame = false;
|
|
207
|
+
let params: string | undefined;
|
|
187
208
|
|
|
188
209
|
for (let index = 0; index < argv.length; index += 1) {
|
|
189
210
|
const arg = argv[index];
|
|
@@ -201,6 +222,11 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
201
222
|
continue;
|
|
202
223
|
}
|
|
203
224
|
|
|
225
|
+
if (arg === "--help" || arg === "-h") {
|
|
226
|
+
console.log(HELP_TEXT);
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
|
|
204
230
|
if (arg === "--compare-proxy") {
|
|
205
231
|
compareProxy = true;
|
|
206
232
|
continue;
|
|
@@ -222,6 +248,22 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
222
248
|
continue;
|
|
223
249
|
}
|
|
224
250
|
|
|
251
|
+
if (arg === "--params" || arg === "-p") {
|
|
252
|
+
params = requireArgValue(argv, index, arg);
|
|
253
|
+
index += 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (arg.startsWith("--params=")) {
|
|
258
|
+
params = arg.slice("--params=".length);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (arg.startsWith("-p=")) {
|
|
263
|
+
params = arg.slice("-p=".length);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
225
267
|
if (arg.startsWith("-o=")) {
|
|
226
268
|
operation = arg.slice("-o=".length);
|
|
227
269
|
continue;
|
|
@@ -294,20 +336,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
294
336
|
}
|
|
295
337
|
|
|
296
338
|
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
|
-
);
|
|
339
|
+
throw new Error(HELP_TEXT);
|
|
311
340
|
}
|
|
312
341
|
|
|
313
342
|
return {
|
|
@@ -317,6 +346,7 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
317
346
|
exportPath,
|
|
318
347
|
flame,
|
|
319
348
|
operation,
|
|
349
|
+
params,
|
|
320
350
|
runs,
|
|
321
351
|
warmup,
|
|
322
352
|
};
|
|
@@ -411,7 +441,18 @@ function isSchema(value: unknown): value is { parse(input: unknown): unknown } {
|
|
|
411
441
|
function resolveInputTemplate(
|
|
412
442
|
provider: ProviderDefinition,
|
|
413
443
|
inputSchema: { parse(input: unknown): unknown },
|
|
444
|
+
params: string | undefined,
|
|
414
445
|
): unknown {
|
|
446
|
+
if (params !== undefined) {
|
|
447
|
+
try {
|
|
448
|
+
return inputSchema.parse(JSON.parse(params));
|
|
449
|
+
} catch (error) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`Failed to parse --params JSON or validate input: ${error instanceof Error ? error.message : String(error)}`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
415
456
|
const firstOp = Object.values(provider.operations)[0];
|
|
416
457
|
if (firstOp?.fixtures?.request !== undefined) {
|
|
417
458
|
return firstOp.fixtures.request;
|
|
@@ -1095,7 +1136,7 @@ function cloneValue<T>(value: T): T {
|
|
|
1095
1136
|
|
|
1096
1137
|
function handleCliError(error: unknown): never {
|
|
1097
1138
|
const message = error instanceof Error ? error.message : String(error);
|
|
1098
|
-
console.error(message);
|
|
1139
|
+
console.error(`[apifuse perf] ${message}`);
|
|
1099
1140
|
process.exit(1);
|
|
1100
1141
|
}
|
|
1101
1142
|
|
package/bin/apifuse-record.ts
CHANGED
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
type HttpClient,
|
|
16
16
|
type ProviderContext,
|
|
17
17
|
type ProviderDefinition,
|
|
18
|
+
ProviderError,
|
|
18
19
|
type StealthClient,
|
|
20
|
+
TransportError,
|
|
21
|
+
ValidationError,
|
|
19
22
|
} from "../src";
|
|
20
23
|
|
|
21
24
|
type CliArgs = {
|
|
@@ -30,56 +33,77 @@ type ProviderRuntime = ProviderDefinition;
|
|
|
30
33
|
|
|
31
34
|
type MutableRecord = Record<string, unknown>;
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
const args = parseArgs(normalizeArgs(process.argv.slice(2)));
|
|
35
|
-
const location = resolveProviderLocation(args.providerPath);
|
|
36
|
-
const provider = await loadProvider(location.rootDir);
|
|
37
|
-
const operationName = resolveOperationName(provider, args.operation);
|
|
38
|
-
const operation = provider.operations[operationName];
|
|
39
|
-
const parsedParams = parseParams(operation, args.params);
|
|
40
|
-
|
|
41
|
-
const capture = createCaptureContext(
|
|
42
|
-
provider,
|
|
43
|
-
resolveOperationBaseUrl(provider, operationName),
|
|
44
|
-
);
|
|
36
|
+
const HELP_TEXT = `Usage: apifuse record [path] --operation <operation> --params '<json>'
|
|
45
37
|
|
|
46
|
-
|
|
38
|
+
Calls a real upstream-backed operation through ctx.http or ctx.stealth and writes __fixtures__/raw.json.
|
|
47
39
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
Options:
|
|
41
|
+
--operation, -o <name> operation to call
|
|
42
|
+
--params, -p <json> JSON input passed to the operation (default: {})
|
|
43
|
+
--append append to an existing array fixture
|
|
44
|
+
--sanitize redact common token/header fields (default)
|
|
45
|
+
--no-sanitize write the captured upstream payload as-is
|
|
46
|
+
--help, -h show this help
|
|
55
47
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
Example:
|
|
49
|
+
apifuse record providers/airkorea --operation realtime --params '{"stationName":"jongno"}'`;
|
|
50
|
+
|
|
51
|
+
export async function main() {
|
|
52
|
+
try {
|
|
53
|
+
const args = parseArgs(normalizeArgs(process.argv.slice(2)));
|
|
54
|
+
const location = resolveProviderLocation(args.providerPath);
|
|
55
|
+
const provider = await loadProvider(location.rootDir);
|
|
56
|
+
const operationName = resolveOperationName(provider, args.operation);
|
|
57
|
+
const operation = provider.operations[operationName];
|
|
58
|
+
const parsedParams = parseParams(operation, args.params);
|
|
59
|
+
|
|
60
|
+
const capture = createCaptureContext(
|
|
61
|
+
provider,
|
|
62
|
+
resolveOperationBaseUrl(provider, operationName),
|
|
59
63
|
);
|
|
60
|
-
}
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
fixturePath,
|
|
66
|
-
rawPayload,
|
|
67
|
-
args.append,
|
|
68
|
-
);
|
|
65
|
+
console.log(
|
|
66
|
+
`[apifuse record] Calling ${operationName} on ${provider.id}...`,
|
|
67
|
+
);
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const result = await executeOperation(
|
|
70
|
+
provider,
|
|
71
|
+
operationName,
|
|
72
|
+
capture.ctx,
|
|
73
|
+
parsedParams,
|
|
74
|
+
);
|
|
75
|
+
const captured = capture.getCapturedRaw();
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
if (captured === undefined) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`No upstream response was captured for ${provider.id}.${operationName}.`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const rawPayload = args.sanitize ? sanitizeFixture(captured) : captured;
|
|
84
|
+
const fixturePath = resolve(location.rootDir, "__fixtures__", "raw.json");
|
|
85
|
+
const nextPayload = await prepareFixturePayload(
|
|
86
|
+
fixturePath,
|
|
87
|
+
rawPayload,
|
|
88
|
+
args.append,
|
|
89
|
+
);
|
|
81
90
|
|
|
82
|
-
|
|
91
|
+
await mkdir(dirname(fixturePath), { recursive: true });
|
|
92
|
+
await writeFile(fixturePath, `${JSON.stringify(nextPayload, null, 2)}\n`);
|
|
93
|
+
|
|
94
|
+
console.log(
|
|
95
|
+
`[apifuse record] Captured response (${formatBytes(
|
|
96
|
+
Buffer.byteLength(JSON.stringify(rawPayload)),
|
|
97
|
+
)})`,
|
|
98
|
+
);
|
|
99
|
+
console.log(
|
|
100
|
+
`[apifuse record] Saved to ${relative(process.cwd(), fixturePath)}`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
void result;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
handleCliError(error);
|
|
106
|
+
}
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
function normalizeArgs(argv: string[]): string[] {
|
|
@@ -107,6 +131,11 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
107
131
|
continue;
|
|
108
132
|
}
|
|
109
133
|
|
|
134
|
+
if (arg === "--help" || arg === "-h") {
|
|
135
|
+
console.log(HELP_TEXT);
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
110
139
|
if (arg.startsWith("--operation=")) {
|
|
111
140
|
operation = arg.slice("--operation=".length);
|
|
112
141
|
continue;
|
|
@@ -158,6 +187,43 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
158
187
|
return { append, providerPath, operation, params, sanitize };
|
|
159
188
|
}
|
|
160
189
|
|
|
190
|
+
function handleCliError(error: unknown): never {
|
|
191
|
+
const message = formatCliError(error);
|
|
192
|
+
console.error(`[apifuse record] ${message}`);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function formatCliError(error: unknown): string {
|
|
197
|
+
if (error instanceof TransportError) {
|
|
198
|
+
return [
|
|
199
|
+
error.message,
|
|
200
|
+
error.upstreamStatus ? `status=${error.upstreamStatus}` : undefined,
|
|
201
|
+
error.options?.retryable !== undefined
|
|
202
|
+
? `retryable=${String(error.options.retryable)}`
|
|
203
|
+
: undefined,
|
|
204
|
+
error.fix ? `fix=${error.fix}` : undefined,
|
|
205
|
+
]
|
|
206
|
+
.filter(Boolean)
|
|
207
|
+
.join(" ");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (error instanceof ProviderError || error instanceof ValidationError) {
|
|
211
|
+
return [
|
|
212
|
+
error.message,
|
|
213
|
+
error.code ? `code=${error.code}` : undefined,
|
|
214
|
+
error.fix,
|
|
215
|
+
]
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
.join(" ");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (error instanceof Error) {
|
|
221
|
+
return error.message;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return String(error);
|
|
225
|
+
}
|
|
226
|
+
|
|
161
227
|
function resolveProviderLocation(inputPath?: string) {
|
|
162
228
|
const originalInput = inputPath ?? process.cwd();
|
|
163
229
|
const resolvedInput = resolve(process.cwd(), originalInput);
|
|
@@ -240,6 +240,7 @@ export async function buildSubmitCheckReport(
|
|
|
240
240
|
checks.push(scoreAuthSafety(provider));
|
|
241
241
|
checks.push(scoreSmokeEvidence(args.smokeNote));
|
|
242
242
|
checks.push(...scoreProviderDocs(providerRoot));
|
|
243
|
+
checks.push(scoreRepositoryDx(providerRoot));
|
|
243
244
|
checks.push(scoreSecrets(providerRoot));
|
|
244
245
|
} else {
|
|
245
246
|
checks.push(
|
|
@@ -287,6 +288,69 @@ export async function buildSubmitCheckReport(
|
|
|
287
288
|
};
|
|
288
289
|
}
|
|
289
290
|
|
|
291
|
+
function scoreRepositoryDx(providerRoot: string): SubmitCheck {
|
|
292
|
+
const missing: string[] = [];
|
|
293
|
+
if (!existsSync(resolve(providerRoot, ".gitignore"))) {
|
|
294
|
+
missing.push(".gitignore");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const packageJsonPath = resolve(providerRoot, "package.json");
|
|
298
|
+
const packageScripts = readPackageScripts(packageJsonPath);
|
|
299
|
+
if (typeof packageScripts?.["type-check"] !== "string") {
|
|
300
|
+
missing.push("package.json scripts.type-check");
|
|
301
|
+
}
|
|
302
|
+
if (!checkScriptRunsTypeCheck(packageScripts?.check)) {
|
|
303
|
+
missing.push("package.json scripts.check includes type-check");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (missing.length === 0) {
|
|
307
|
+
return pass(
|
|
308
|
+
"repository-dx",
|
|
309
|
+
"docs",
|
|
310
|
+
"Repository includes generated-provider DX guardrails.",
|
|
311
|
+
0,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
id: "repository-dx",
|
|
317
|
+
category: "docs",
|
|
318
|
+
level: "warn",
|
|
319
|
+
status: "warn",
|
|
320
|
+
points: 0,
|
|
321
|
+
maxPoints: 0,
|
|
322
|
+
message: `Generated repository DX guardrails are missing: ${missing.join(", ")}.`,
|
|
323
|
+
remediation:
|
|
324
|
+
"Regenerate with the current `apifuse create` template or add .gitignore plus `type-check: tsc --noEmit` and include it from `check`.",
|
|
325
|
+
evidence: missing,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function readPackageScripts(
|
|
330
|
+
packageJsonPath: string,
|
|
331
|
+
): Record<string, unknown> | undefined {
|
|
332
|
+
if (!existsSync(packageJsonPath)) {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
338
|
+
if (!isRecord(packageJson) || !isRecord(packageJson.scripts)) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
return packageJson.scripts;
|
|
342
|
+
} catch {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function checkScriptRunsTypeCheck(checkScript: unknown): boolean {
|
|
348
|
+
return (
|
|
349
|
+
typeof checkScript === "string" &&
|
|
350
|
+
/(?:^|&&|;)\s*bun\s+run\s+type-check(?:\s|$)/.test(checkScript)
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
290
354
|
async function safeRunChecks(providerRoot: string): Promise<CheckResult[]> {
|
|
291
355
|
try {
|
|
292
356
|
return await runChecks(providerRoot);
|
package/package.json
CHANGED
package/src/cli/commands.ts
CHANGED
|
@@ -72,7 +72,6 @@ export const COMMAND_MANIFEST: Record<
|
|
|
72
72
|
usage:
|
|
73
73
|
'apifuse record [path] --operation <operation> --params \'{"value":"hello"}\'',
|
|
74
74
|
examples: [
|
|
75
|
-
'apifuse record . --operation ping --params \'{"value":"hello"}\'',
|
|
76
75
|
'apifuse record providers/airkorea --operation realtime --params \'{"stationName":"종로구"}\'',
|
|
77
76
|
],
|
|
78
77
|
modulePath: "./apifuse-record",
|
|
@@ -88,10 +87,10 @@ export const COMMAND_MANIFEST: Record<
|
|
|
88
87
|
name: "perf",
|
|
89
88
|
summary:
|
|
90
89
|
"Profile a provider operation and export latency/trace diagnostics.",
|
|
91
|
-
usage:
|
|
90
|
+
usage:
|
|
91
|
+
"apifuse perf <path> --operation <operation> [--params '<json>'] [options]",
|
|
92
92
|
examples: [
|
|
93
|
-
|
|
94
|
-
"apifuse perf providers/airkorea --operation realtime --runs 5",
|
|
93
|
+
'apifuse perf providers/airkorea --operation realtime --params \'{"stationName":"종로구"}\' --runs 5',
|
|
95
94
|
],
|
|
96
95
|
modulePath: "./apifuse-perf",
|
|
97
96
|
},
|
package/src/cli/create.ts
CHANGED
|
@@ -474,6 +474,14 @@ export async function buildProviderCreatePlan(
|
|
|
474
474
|
};
|
|
475
475
|
|
|
476
476
|
const files: ProviderPlanFile[] = [
|
|
477
|
+
{
|
|
478
|
+
path: resolve(providerRoot, ".dockerignore"),
|
|
479
|
+
content: await renderTemplate(".dockerignore.tpl", {}),
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
path: resolve(providerRoot, ".gitignore"),
|
|
483
|
+
content: await renderTemplate(".gitignore.tpl", {}),
|
|
484
|
+
},
|
|
477
485
|
{
|
|
478
486
|
path: resolve(providerRoot, "index.ts"),
|
|
479
487
|
content: await renderTemplate("index.ts.tpl", templateValues),
|
|
@@ -574,6 +582,7 @@ export async function buildProviderCreatePlan(
|
|
|
574
582
|
providerRoot,
|
|
575
583
|
validationCommands: [
|
|
576
584
|
"bun run check",
|
|
585
|
+
"bun run type-check",
|
|
577
586
|
"bun run submit-check",
|
|
578
587
|
"bun run test",
|
|
579
588
|
],
|
|
@@ -605,7 +614,8 @@ function renderPackageJson(input: {
|
|
|
605
614
|
main: "./index.ts",
|
|
606
615
|
scripts: {
|
|
607
616
|
dev: "apifuse dev .",
|
|
608
|
-
check: "apifuse check .",
|
|
617
|
+
check: "apifuse check . && bun run type-check",
|
|
618
|
+
"type-check": "tsc --noEmit",
|
|
609
619
|
"submit-check":
|
|
610
620
|
"apifuse submit-check . --markdown submission-report.md",
|
|
611
621
|
test: "apifuse test .",
|
|
@@ -618,6 +628,7 @@ function renderPackageJson(input: {
|
|
|
618
628
|
},
|
|
619
629
|
devDependencies: {
|
|
620
630
|
"@types/bun": "latest",
|
|
631
|
+
typescript: "^6.0.3",
|
|
621
632
|
},
|
|
622
633
|
},
|
|
623
634
|
null,
|
|
@@ -636,6 +647,7 @@ function renderTsconfig(): string {
|
|
|
636
647
|
noEmit: true,
|
|
637
648
|
skipLibCheck: true,
|
|
638
649
|
resolveJsonModule: true,
|
|
650
|
+
types: ["bun"],
|
|
639
651
|
},
|
|
640
652
|
include: ["**/*.ts"],
|
|
641
653
|
exclude: ["node_modules"],
|
|
@@ -952,6 +964,14 @@ function printResult(
|
|
|
952
964
|
console.log(`Validation: (cd ${plan.providerRoot} && ${command})`);
|
|
953
965
|
}
|
|
954
966
|
console.log(`Next local dev: ${plan.nextDevCommand}`);
|
|
967
|
+
console.log(
|
|
968
|
+
"Submission evidence: run `bun run submit-check`, save the generated report, and note `/health` plus `POST /v1/{operation}` smoke results.",
|
|
969
|
+
);
|
|
970
|
+
if (plan.files.some((file) => file.content.includes('runtime: "browser"'))) {
|
|
971
|
+
console.log(
|
|
972
|
+
"Browser runtime: run `bunx playwright install chromium` locally or set `APIFUSE__CDP_POOL__URL` before browser-backed smoke tests.",
|
|
973
|
+
);
|
|
974
|
+
}
|
|
955
975
|
}
|
|
956
976
|
|
|
957
977
|
function escapeTemplate(value: string): string {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
.git/
|
|
3
|
+
.github/
|
|
4
|
+
|
|
5
|
+
# Environment and local secrets
|
|
6
|
+
.env
|
|
7
|
+
.env.*
|
|
8
|
+
!.env.example
|
|
9
|
+
|
|
10
|
+
# Local reports and generated artifacts
|
|
11
|
+
submission-report.md
|
|
12
|
+
coverage/
|
|
13
|
+
dist/
|
|
14
|
+
.cache/
|
|
15
|
+
.turbo/
|
|
16
|
+
.bun/
|
|
17
|
+
*.tsbuildinfo
|
|
18
|
+
|
|
19
|
+
# OS/editor junk
|
|
20
|
+
.DS_Store
|
|
21
|
+
Thumbs.db
|
|
22
|
+
*.swp
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
|
|
3
|
+
# Environment and local secrets
|
|
4
|
+
.env
|
|
5
|
+
.env.*
|
|
6
|
+
!.env.example
|
|
7
|
+
|
|
8
|
+
# Build, coverage, cache, and local runtime artifacts
|
|
9
|
+
coverage/
|
|
10
|
+
dist/
|
|
11
|
+
.cache/
|
|
12
|
+
.turbo/
|
|
13
|
+
.bun/
|
|
14
|
+
*.tsbuildinfo
|
|
15
|
+
|
|
16
|
+
# Bounty submission output
|
|
17
|
+
submission-report.md
|
|
18
|
+
|
|
19
|
+
# OS/editor junk
|
|
20
|
+
.DS_Store
|
|
21
|
+
Thumbs.db
|
|
22
|
+
*.swp
|
|
@@ -140,3 +140,21 @@ Every operation must declare exactly one of:
|
|
|
140
140
|
|
|
141
141
|
The generated `ping` operation uses `healthCheckUnsupported` only because it is
|
|
142
142
|
a local scaffold check, not a real upstream API probe.
|
|
143
|
+
|
|
144
|
+
`healthCheck.cases[].assertions` receives `{ data, status, durationMs, meta }`.
|
|
145
|
+
`data` is the parsed operation output. Use this shape in real operations:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
healthCheck: {
|
|
149
|
+
interval: "5m",
|
|
150
|
+
cases: [{
|
|
151
|
+
name: "lookup baseline",
|
|
152
|
+
input: { q: "btc" },
|
|
153
|
+
assertions: ({ data, status, durationMs }) => {
|
|
154
|
+
if (status !== 200 || data.results.length === 0 || durationMs > 3000) {
|
|
155
|
+
return { status: "degraded", label: "lookup baseline changed" };
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
}],
|
|
159
|
+
}
|
|
160
|
+
```
|
package/src/define.ts
CHANGED
|
@@ -181,7 +181,7 @@ type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> =
|
|
|
181
181
|
infer TInput,
|
|
182
182
|
infer TOutput
|
|
183
183
|
>
|
|
184
|
-
? OperationConfig<TInput, TOutput>
|
|
184
|
+
? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
|
|
185
185
|
: never;
|
|
186
186
|
};
|
|
187
187
|
type StreamOperationConfig<
|
|
@@ -255,13 +255,13 @@ export interface ProviderConfig<
|
|
|
255
255
|
displayNameKey?: string;
|
|
256
256
|
descriptionKey: string;
|
|
257
257
|
category: string;
|
|
258
|
-
tags?: string[];
|
|
258
|
+
tags?: readonly string[];
|
|
259
259
|
icon?: string;
|
|
260
260
|
docTitleKey?: string;
|
|
261
261
|
docDescriptionKey?: string;
|
|
262
262
|
docSummaryKey?: string;
|
|
263
263
|
docMarkdownKey?: string;
|
|
264
|
-
normalizationNotesKeys?: string[];
|
|
264
|
+
normalizationNotesKeys?: readonly string[];
|
|
265
265
|
environment?: "staging";
|
|
266
266
|
purpose?: string;
|
|
267
267
|
purposeKey?: string;
|
package/src/lint.ts
CHANGED
|
@@ -54,7 +54,7 @@ export interface LintDiagnostic {
|
|
|
54
54
|
|
|
55
55
|
function lintAllowedHosts(
|
|
56
56
|
providerId: string | undefined,
|
|
57
|
-
allowedHosts: string[] | undefined,
|
|
57
|
+
allowedHosts: readonly string[] | undefined,
|
|
58
58
|
): LintDiagnostic[] {
|
|
59
59
|
const prefix = providerId ? `Provider "${providerId}"` : "Provider";
|
|
60
60
|
|
|
@@ -114,7 +114,7 @@ function lintReviewed(
|
|
|
114
114
|
];
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
function hasReusableSecretKeys(keys: string[] | undefined): boolean {
|
|
117
|
+
function hasReusableSecretKeys(keys: readonly string[] | undefined): boolean {
|
|
118
118
|
if (!keys) {
|
|
119
119
|
return false;
|
|
120
120
|
}
|
|
@@ -154,12 +154,12 @@ function lintAuthModel(provider: {
|
|
|
154
154
|
id?: string;
|
|
155
155
|
auth?: ProviderAuthLike;
|
|
156
156
|
credential?: {
|
|
157
|
-
keys?: string[];
|
|
157
|
+
keys?: readonly string[];
|
|
158
158
|
storesReusableSecret?: boolean;
|
|
159
159
|
justification?: string;
|
|
160
160
|
};
|
|
161
161
|
context?: {
|
|
162
|
-
keys?: string[];
|
|
162
|
+
keys?: readonly string[];
|
|
163
163
|
};
|
|
164
164
|
authFlowSource?: string;
|
|
165
165
|
}): LintDiagnostic[] {
|
|
@@ -624,14 +624,14 @@ function lintStealthTransportUsage(provider: {
|
|
|
624
624
|
export function lintOperation(op: {
|
|
625
625
|
description?: string;
|
|
626
626
|
descriptionKey?: string;
|
|
627
|
-
whenToUse?: string[];
|
|
628
|
-
whenToUseKeys?: string[];
|
|
629
|
-
whenNotToUse?: string[];
|
|
630
|
-
whenNotToUseKeys?: string[];
|
|
627
|
+
whenToUse?: readonly string[];
|
|
628
|
+
whenToUseKeys?: readonly string[];
|
|
629
|
+
whenNotToUse?: readonly string[];
|
|
630
|
+
whenNotToUseKeys?: readonly string[];
|
|
631
631
|
input: unknown;
|
|
632
632
|
output: unknown;
|
|
633
633
|
fixtures?: unknown;
|
|
634
|
-
inputExamples?: unknown[];
|
|
634
|
+
inputExamples?: readonly unknown[];
|
|
635
635
|
derivations?: Record<string, string>;
|
|
636
636
|
}): LintDiagnostic[] {
|
|
637
637
|
const diagnostics: LintDiagnostic[] = [];
|
|
@@ -745,16 +745,16 @@ export function lintOperation(op: {
|
|
|
745
745
|
|
|
746
746
|
export function lintProvider(provider: {
|
|
747
747
|
id?: string;
|
|
748
|
-
allowedHosts?: string[];
|
|
748
|
+
allowedHosts?: readonly string[];
|
|
749
749
|
stealth?: unknown;
|
|
750
750
|
auth?: ProviderAuthLike;
|
|
751
751
|
credential?: {
|
|
752
|
-
keys?: string[];
|
|
752
|
+
keys?: readonly string[];
|
|
753
753
|
storesReusableSecret?: boolean;
|
|
754
754
|
justification?: string;
|
|
755
755
|
};
|
|
756
756
|
context?: {
|
|
757
|
-
keys?: string[];
|
|
757
|
+
keys?: readonly string[];
|
|
758
758
|
};
|
|
759
759
|
authFlowSource?: string;
|
|
760
760
|
operations?: Record<
|
|
@@ -762,14 +762,14 @@ export function lintProvider(provider: {
|
|
|
762
762
|
{
|
|
763
763
|
description?: string;
|
|
764
764
|
descriptionKey?: string;
|
|
765
|
-
whenToUse?: string[];
|
|
766
|
-
whenToUseKeys?: string[];
|
|
767
|
-
whenNotToUse?: string[];
|
|
768
|
-
whenNotToUseKeys?: string[];
|
|
765
|
+
whenToUse?: readonly string[];
|
|
766
|
+
whenToUseKeys?: readonly string[];
|
|
767
|
+
whenNotToUse?: readonly string[];
|
|
768
|
+
whenNotToUseKeys?: readonly string[];
|
|
769
769
|
input: unknown;
|
|
770
770
|
output: unknown;
|
|
771
771
|
fixtures?: unknown;
|
|
772
|
-
inputExamples?: unknown[];
|
|
772
|
+
inputExamples?: readonly unknown[];
|
|
773
773
|
derivations?: Record<string, string>;
|
|
774
774
|
handler?: unknown;
|
|
775
775
|
source?: string;
|
package/src/runtime/trace.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -798,15 +798,15 @@ export interface ProviderPublicProfile {
|
|
|
798
798
|
longDescriptionKey?: ProviderLocaleKeyInput;
|
|
799
799
|
logo?: ProviderLogoProfile;
|
|
800
800
|
category?: string;
|
|
801
|
-
tags?: string[];
|
|
802
|
-
capabilityKeys?: ProviderLocaleKeyInput[];
|
|
803
|
-
examplePromptKeys?: ProviderLocaleKeyInput[];
|
|
801
|
+
tags?: readonly string[];
|
|
802
|
+
capabilityKeys?: readonly ProviderLocaleKeyInput[];
|
|
803
|
+
examplePromptKeys?: readonly ProviderLocaleKeyInput[];
|
|
804
804
|
setupSummaryKey?: ProviderLocaleKeyInput;
|
|
805
805
|
connectionMode?: ProviderPublicConnectionMode;
|
|
806
|
-
requirementKeys?: ProviderLocaleKeyInput[];
|
|
807
|
-
limitationKeys?: ProviderLocaleKeyInput[];
|
|
806
|
+
requirementKeys?: readonly ProviderLocaleKeyInput[];
|
|
807
|
+
limitationKeys?: readonly ProviderLocaleKeyInput[];
|
|
808
808
|
availability?: {
|
|
809
|
-
regions?: string[];
|
|
809
|
+
regions?: readonly string[];
|
|
810
810
|
supportLevel?: ProviderSupportLevel;
|
|
811
811
|
};
|
|
812
812
|
/**
|
|
@@ -823,13 +823,13 @@ export interface ProviderMeta {
|
|
|
823
823
|
displayNameKey?: ProviderLocaleKeyInput;
|
|
824
824
|
descriptionKey: ProviderLocaleKeyInput;
|
|
825
825
|
category: string;
|
|
826
|
-
tags?: string[];
|
|
826
|
+
tags?: readonly string[];
|
|
827
827
|
icon?: string;
|
|
828
828
|
docTitleKey?: ProviderLocaleKeyInput;
|
|
829
829
|
docDescriptionKey?: ProviderLocaleKeyInput;
|
|
830
830
|
docSummaryKey?: ProviderLocaleKeyInput;
|
|
831
831
|
docMarkdownKey?: ProviderLocaleKeyInput;
|
|
832
|
-
normalizationNotesKeys?: ProviderLocaleKeyInput[];
|
|
832
|
+
normalizationNotesKeys?: readonly ProviderLocaleKeyInput[];
|
|
833
833
|
environment?: "staging";
|
|
834
834
|
purpose?: string;
|
|
835
835
|
purposeKey?: ProviderLocaleKeyInput;
|
|
@@ -1373,13 +1373,13 @@ export interface OperationDefinition<
|
|
|
1373
1373
|
> {
|
|
1374
1374
|
descriptionKey?: ProviderLocaleKeyInput;
|
|
1375
1375
|
docs?: OperationDocMeta;
|
|
1376
|
-
whenToUseKeys?: ProviderLocaleKeyInput[];
|
|
1377
|
-
whenNotToUseKeys?: ProviderLocaleKeyInput[];
|
|
1376
|
+
whenToUseKeys?: readonly ProviderLocaleKeyInput[];
|
|
1377
|
+
whenNotToUseKeys?: readonly ProviderLocaleKeyInput[];
|
|
1378
1378
|
derivations?: Record<string, string>;
|
|
1379
|
-
inputExamples?: OperationInputExample[];
|
|
1379
|
+
inputExamples?: readonly OperationInputExample[];
|
|
1380
1380
|
annotations?: OperationAnnotations;
|
|
1381
1381
|
contract?: OperationContractMetadata;
|
|
1382
|
-
tags?: string[];
|
|
1382
|
+
tags?: readonly string[];
|
|
1383
1383
|
relatedOperations?: OperationRelationships;
|
|
1384
1384
|
toolRouter?: OperationToolRouterMetadata;
|
|
1385
1385
|
observability?: OperationObservabilityConfig;
|
|
@@ -1396,6 +1396,10 @@ export interface OperationDefinition<
|
|
|
1396
1396
|
request: InferSchemaOutput<TInput>;
|
|
1397
1397
|
response: InferSchemaOutput<TOutput>;
|
|
1398
1398
|
};
|
|
1399
|
+
upstream?: {
|
|
1400
|
+
baseUrl?: string;
|
|
1401
|
+
proxy?: boolean | ProviderProxyPolicy;
|
|
1402
|
+
};
|
|
1399
1403
|
hints?: Record<string, string>;
|
|
1400
1404
|
healthCheck?: HealthCheckSuite<
|
|
1401
1405
|
InferSchemaOutput<TInput>,
|