@apifuse/provider-sdk 2.1.0-beta.3 → 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 +187 -8
- package/CHANGELOG.md +13 -1
- package/README.md +40 -18
- package/SUBMISSION.md +4 -4
- package/bin/apifuse-dev.ts +12 -5
- package/bin/apifuse-pack-check.ts +9 -2
- package/bin/apifuse-pack-smoke.ts +127 -6
- package/bin/apifuse-perf.ts +76 -31
- package/bin/apifuse-record.ts +148 -94
- package/bin/apifuse-submit-check.ts +243 -7
- package/bin/apifuse.ts +1 -1
- package/package.json +17 -8
- package/src/choice-token.ts +164 -0
- package/src/cli/commands.ts +4 -7
- package/src/cli/create.ts +180 -51
- 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 +42 -7
- 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 -47
- 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 +23 -0
- package/src/cli/templates/provider/schemas/ping.ts.tpl +16 -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 +1206 -9
- package/src/define.ts +1620 -106
- package/src/errors.ts +12 -0
- package/src/i18n/catalog.ts +121 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/keys.ts +64 -0
- package/src/index.ts +149 -8
- package/src/lint.ts +306 -51
- package/src/observability.ts +41 -0
- package/src/provider.ts +60 -3
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +7 -0
- package/src/runtime/browser.ts +77 -21
- package/src/runtime/cache.ts +582 -0
- package/src/runtime/executor.ts +13 -1
- 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/request-options.ts +66 -0
- package/src/runtime/state.ts +76 -0
- package/src/runtime/stealth.ts +1145 -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 +816 -58
- package/src/server/types.ts +35 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +17 -4
- package/src/types.ts +876 -53
- package/src/runtime/tls.ts +0 -434
- package/src/types/playwright-stealth.d.ts +0 -9
package/bin/apifuse-record.ts
CHANGED
|
@@ -7,13 +7,18 @@ 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
|
-
|
|
12
|
+
createStealthClient,
|
|
13
|
+
createSttClientFromEnv,
|
|
12
14
|
executeOperation,
|
|
13
15
|
type HttpClient,
|
|
14
16
|
type ProviderContext,
|
|
15
17
|
type ProviderDefinition,
|
|
16
|
-
|
|
18
|
+
ProviderError,
|
|
19
|
+
type StealthClient,
|
|
20
|
+
TransportError,
|
|
21
|
+
ValidationError,
|
|
17
22
|
} from "../src";
|
|
18
23
|
|
|
19
24
|
type CliArgs = {
|
|
@@ -28,55 +33,77 @@ type ProviderRuntime = ProviderDefinition;
|
|
|
28
33
|
|
|
29
34
|
type MutableRecord = Record<string, unknown>;
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
const args = parseArgs(normalizeArgs(process.argv.slice(2)));
|
|
33
|
-
const location = resolveProviderLocation(args.providerPath);
|
|
34
|
-
const provider = await loadProvider(location.rootDir);
|
|
35
|
-
const operationName = resolveOperationName(provider, args.operation);
|
|
36
|
-
const operation = provider.operations[operationName];
|
|
37
|
-
const parsedParams = parseParams(operation, args.params);
|
|
38
|
-
|
|
39
|
-
const capture = createCaptureContext(
|
|
40
|
-
resolveOperationBaseUrl(provider, operationName),
|
|
41
|
-
);
|
|
36
|
+
const HELP_TEXT = `Usage: apifuse record [path] --operation <operation> --params '<json>'
|
|
42
37
|
|
|
43
|
-
|
|
38
|
+
Calls a real upstream-backed operation through ctx.http or ctx.stealth and writes __fixtures__/raw.json.
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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),
|
|
56
63
|
);
|
|
57
|
-
}
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
fixturePath,
|
|
63
|
-
rawPayload,
|
|
64
|
-
args.append,
|
|
65
|
-
);
|
|
65
|
+
console.log(
|
|
66
|
+
`[apifuse record] Calling ${operationName} on ${provider.id}...`,
|
|
67
|
+
);
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
const result = await executeOperation(
|
|
70
|
+
provider,
|
|
71
|
+
operationName,
|
|
72
|
+
capture.ctx,
|
|
73
|
+
parsedParams,
|
|
74
|
+
);
|
|
75
|
+
const captured = capture.getCapturedRaw();
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
);
|
|
90
|
+
|
|
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
|
+
);
|
|
78
102
|
|
|
79
|
-
|
|
103
|
+
void result;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
handleCliError(error);
|
|
106
|
+
}
|
|
80
107
|
}
|
|
81
108
|
|
|
82
109
|
function normalizeArgs(argv: string[]): string[] {
|
|
@@ -104,6 +131,11 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
104
131
|
continue;
|
|
105
132
|
}
|
|
106
133
|
|
|
134
|
+
if (arg === "--help" || arg === "-h") {
|
|
135
|
+
console.log(HELP_TEXT);
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
107
139
|
if (arg.startsWith("--operation=")) {
|
|
108
140
|
operation = arg.slice("--operation=".length);
|
|
109
141
|
continue;
|
|
@@ -155,6 +187,43 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
155
187
|
return { append, providerPath, operation, params, sanitize };
|
|
156
188
|
}
|
|
157
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
|
+
|
|
158
227
|
function resolveProviderLocation(inputPath?: string) {
|
|
159
228
|
const originalInput = inputPath ?? process.cwd();
|
|
160
229
|
const resolvedInput = resolve(process.cwd(), originalInput);
|
|
@@ -269,15 +338,18 @@ function resolveOperationBaseUrl(
|
|
|
269
338
|
return baseUrl;
|
|
270
339
|
}
|
|
271
340
|
|
|
272
|
-
function createCaptureContext(baseUrl: string) {
|
|
341
|
+
function createCaptureContext(provider: ProviderRuntime, baseUrl: string) {
|
|
273
342
|
let capturedRaw: unknown;
|
|
274
343
|
|
|
275
344
|
const http = proxyHttpClient(createHttpClient(baseUrl), (response) => {
|
|
276
345
|
capturedRaw = response.data;
|
|
277
346
|
});
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
347
|
+
const stealth = proxyStealthClient(
|
|
348
|
+
createStealthClient(baseUrl),
|
|
349
|
+
(response) => {
|
|
350
|
+
capturedRaw = normalizeCapturedStealthResponse(response);
|
|
351
|
+
},
|
|
352
|
+
);
|
|
281
353
|
|
|
282
354
|
const ctx: ProviderContext = {
|
|
283
355
|
env: {
|
|
@@ -291,7 +363,8 @@ function createCaptureContext(baseUrl: string) {
|
|
|
291
363
|
getScopes: () => [],
|
|
292
364
|
},
|
|
293
365
|
http,
|
|
294
|
-
|
|
366
|
+
cache: createBypassProviderCache({ providerId: provider.id }),
|
|
367
|
+
stealth,
|
|
295
368
|
browser: {
|
|
296
369
|
engine: "playwright-stealth",
|
|
297
370
|
newPage: async () => {
|
|
@@ -306,6 +379,7 @@ function createCaptureContext(baseUrl: string) {
|
|
|
306
379
|
throw new Error("Auth prompts are not available in apifuse record.");
|
|
307
380
|
},
|
|
308
381
|
},
|
|
382
|
+
stt: createSttClientFromEnv(provider.stt),
|
|
309
383
|
};
|
|
310
384
|
|
|
311
385
|
return {
|
|
@@ -335,59 +409,39 @@ function proxyHttpClient(
|
|
|
335
409
|
}) as HttpClient;
|
|
336
410
|
}
|
|
337
411
|
|
|
338
|
-
|
|
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
|
-
}
|
|
412
|
+
type StealthSession = ReturnType<StealthClient["createSession"]>;
|
|
362
413
|
|
|
363
|
-
|
|
414
|
+
function proxyStealthClient(
|
|
415
|
+
client: StealthClient,
|
|
416
|
+
onResponse: (response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
|
|
417
|
+
): StealthClient {
|
|
418
|
+
return {
|
|
419
|
+
fetch: async (...args: Parameters<StealthClient["fetch"]>) => {
|
|
420
|
+
const response = await client.fetch(...args);
|
|
421
|
+
onResponse(response);
|
|
422
|
+
return response;
|
|
364
423
|
},
|
|
365
|
-
|
|
424
|
+
createSession: (...args: Parameters<StealthClient["createSession"]>) =>
|
|
425
|
+
proxyStealthSession(client.createSession(...args), onResponse),
|
|
426
|
+
};
|
|
366
427
|
}
|
|
367
428
|
|
|
368
|
-
function
|
|
369
|
-
session:
|
|
370
|
-
onResponse: (response: Awaited<ReturnType<
|
|
371
|
-
) {
|
|
372
|
-
return
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
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;
|
|
429
|
+
function proxyStealthSession(
|
|
430
|
+
session: StealthSession,
|
|
431
|
+
onResponse: (response: Awaited<ReturnType<StealthClient["fetch"]>>) => void,
|
|
432
|
+
): StealthSession {
|
|
433
|
+
return {
|
|
434
|
+
fetch: async (...args: Parameters<StealthSession["fetch"]>) => {
|
|
435
|
+
const response = await session.fetch(...args);
|
|
436
|
+
onResponse(response);
|
|
437
|
+
return response;
|
|
385
438
|
},
|
|
386
|
-
|
|
439
|
+
close: () => session.close(),
|
|
440
|
+
};
|
|
387
441
|
}
|
|
388
442
|
|
|
389
|
-
function
|
|
390
|
-
response: Awaited<ReturnType<
|
|
443
|
+
function normalizeCapturedStealthResponse(
|
|
444
|
+
response: Awaited<ReturnType<StealthClient["fetch"]>>,
|
|
391
445
|
) {
|
|
392
446
|
try {
|
|
393
447
|
return JSON.parse(response.body);
|
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import { writeFile } from "node:fs/promises";
|
|
5
|
-
import { basename, dirname, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
8
10
|
import packageJson from "../package.json";
|
|
9
11
|
import type { ProviderDefinition } from "../src";
|
|
12
|
+
import {
|
|
13
|
+
loadProviderLocaleCatalogs,
|
|
14
|
+
type ProviderLocale,
|
|
15
|
+
validateProviderLocaleCatalogs,
|
|
16
|
+
} from "../src/i18n";
|
|
17
|
+
import { APIFUSE_DESCRIPTION_KEY_META_KEY } from "../src/schema";
|
|
10
18
|
import { type CheckResult, runChecks } from "./apifuse-check";
|
|
11
19
|
|
|
12
20
|
const TIERS = ["bronze", "silver", "gold", "diamond"] as const;
|
|
@@ -77,6 +85,11 @@ const CATEGORY_MAX_POINTS = {
|
|
|
77
85
|
docs: 10,
|
|
78
86
|
} as const;
|
|
79
87
|
|
|
88
|
+
const REQUIRED_PUBLIC_PROVIDER_LOCALES = [
|
|
89
|
+
"en",
|
|
90
|
+
"ko",
|
|
91
|
+
] as const satisfies readonly ProviderLocale[];
|
|
92
|
+
|
|
80
93
|
const HELP_TEXT = `Usage: apifuse submit-check [path] [--tier bronze|silver|gold|diamond] [--json] [--markdown <path>] [--smoke-note <text>]
|
|
81
94
|
Alias: apifuse bounty-check [path]
|
|
82
95
|
Default: apifuse submit-check .`;
|
|
@@ -220,12 +233,14 @@ export async function buildSubmitCheckReport(
|
|
|
220
233
|
checks.push(...scoreBaseChecks(baseChecks));
|
|
221
234
|
|
|
222
235
|
if (provider) {
|
|
236
|
+
checks.push(scoreLocaleCatalog(providerRoot, provider));
|
|
223
237
|
checks.push(scoreOperationMetadata(provider));
|
|
224
238
|
checks.push(scoreFixtureCoverage(provider));
|
|
225
239
|
checks.push(scoreHealthCoverage(provider));
|
|
226
240
|
checks.push(scoreAuthSafety(provider));
|
|
227
241
|
checks.push(scoreSmokeEvidence(args.smokeNote));
|
|
228
242
|
checks.push(...scoreProviderDocs(providerRoot));
|
|
243
|
+
checks.push(scoreRepositoryDx(providerRoot));
|
|
229
244
|
checks.push(scoreSecrets(providerRoot));
|
|
230
245
|
} else {
|
|
231
246
|
checks.push(
|
|
@@ -273,6 +288,69 @@ export async function buildSubmitCheckReport(
|
|
|
273
288
|
};
|
|
274
289
|
}
|
|
275
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
|
+
|
|
276
354
|
async function safeRunChecks(providerRoot: string): Promise<CheckResult[]> {
|
|
277
355
|
try {
|
|
278
356
|
return await runChecks(providerRoot);
|
|
@@ -322,12 +400,170 @@ function scoreBaseChecks(results: CheckResult[]): SubmitCheck[] {
|
|
|
322
400
|
];
|
|
323
401
|
}
|
|
324
402
|
|
|
403
|
+
function scoreLocaleCatalog(
|
|
404
|
+
providerRoot: string,
|
|
405
|
+
provider: ProviderDefinition,
|
|
406
|
+
): SubmitCheck {
|
|
407
|
+
const requiredKeys = collectProviderRequiredLocaleKeys(provider);
|
|
408
|
+
if (requiredKeys.length === 0) {
|
|
409
|
+
return pass(
|
|
410
|
+
"locale-catalog",
|
|
411
|
+
"operations",
|
|
412
|
+
"No key-owned provider metadata or operation metadata requires locale catalog validation.",
|
|
413
|
+
0,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const availableLocales = REQUIRED_PUBLIC_PROVIDER_LOCALES.filter((locale) =>
|
|
419
|
+
existsSync(join(providerRoot, "locales", `${locale}.json`)),
|
|
420
|
+
);
|
|
421
|
+
const catalogs = loadProviderLocaleCatalogs({
|
|
422
|
+
providerDir: providerRoot,
|
|
423
|
+
locales: availableLocales,
|
|
424
|
+
});
|
|
425
|
+
const validation = validateProviderLocaleCatalogs({
|
|
426
|
+
catalogs,
|
|
427
|
+
requiredLocales: REQUIRED_PUBLIC_PROVIDER_LOCALES,
|
|
428
|
+
requiredKeys,
|
|
429
|
+
});
|
|
430
|
+
if (!validation.ok) {
|
|
431
|
+
return blocker(
|
|
432
|
+
"locale-catalog",
|
|
433
|
+
"operations",
|
|
434
|
+
"Provider locale catalog is missing required public-provider copy.",
|
|
435
|
+
"Add provider-local locales/en.json and locales/ko.json values for every provider metadata key, operation descriptionKey, and .describeKey() or describeKey() schema field.",
|
|
436
|
+
0,
|
|
437
|
+
validation.issues.map(
|
|
438
|
+
(issue) => `${issue.locale}:${issue.key}: ${issue.message}`,
|
|
439
|
+
),
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
} catch (error) {
|
|
443
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
444
|
+
return blocker(
|
|
445
|
+
"locale-catalog",
|
|
446
|
+
"operations",
|
|
447
|
+
"Provider locale catalog is missing required public-provider copy.",
|
|
448
|
+
"Add provider-local locales/en.json and locales/ko.json values for every provider metadata key, operation descriptionKey, and .describeKey() or describeKey() schema field.",
|
|
449
|
+
0,
|
|
450
|
+
[`*:*: ${message}`],
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return pass(
|
|
455
|
+
"locale-catalog",
|
|
456
|
+
"operations",
|
|
457
|
+
"Required provider and operation locale keys resolve in locales/en.json and locales/ko.json.",
|
|
458
|
+
0,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function collectProviderRequiredLocaleKeys(
|
|
463
|
+
provider: ProviderDefinition,
|
|
464
|
+
): string[] {
|
|
465
|
+
const keys = new Set<string>();
|
|
466
|
+
|
|
467
|
+
addLocaleKeys(keys, [
|
|
468
|
+
provider.meta.descriptionKey,
|
|
469
|
+
provider.meta.docTitleKey,
|
|
470
|
+
provider.meta.docDescriptionKey,
|
|
471
|
+
provider.meta.docSummaryKey,
|
|
472
|
+
provider.meta.docMarkdownKey,
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
const publicProfile = provider.meta.publicProfile;
|
|
476
|
+
if (publicProfile) {
|
|
477
|
+
addLocaleKeys(keys, [
|
|
478
|
+
publicProfile.displayNameKey,
|
|
479
|
+
publicProfile.shortDescriptionKey,
|
|
480
|
+
publicProfile.longDescriptionKey,
|
|
481
|
+
publicProfile.setupSummaryKey,
|
|
482
|
+
...(publicProfile.capabilityKeys ?? []),
|
|
483
|
+
...(publicProfile.examplePromptKeys ?? []),
|
|
484
|
+
...(publicProfile.requirementKeys ?? []),
|
|
485
|
+
...(publicProfile.limitationKeys ?? []),
|
|
486
|
+
]);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
for (const operation of Object.values(provider.operations)) {
|
|
490
|
+
addLocaleKeys(keys, [
|
|
491
|
+
operation.descriptionKey,
|
|
492
|
+
operation.docs?.titleKey,
|
|
493
|
+
operation.docs?.descriptionKey,
|
|
494
|
+
operation.docs?.summaryKey,
|
|
495
|
+
operation.docs?.markdownKey,
|
|
496
|
+
...(operation.whenToUseKeys ?? []),
|
|
497
|
+
...(operation.whenNotToUseKeys ?? []),
|
|
498
|
+
...collectSchemaDescriptionKeys(operation.input),
|
|
499
|
+
...collectSchemaDescriptionKeys(operation.output),
|
|
500
|
+
]);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return Array.from(keys);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function addLocaleKeys(keys: Set<string>, values: readonly unknown[]): void {
|
|
507
|
+
for (const key of values) {
|
|
508
|
+
if (typeof key === "string" && key.length > 0) {
|
|
509
|
+
keys.add(key);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function collectSchemaDescriptionKeys(schema: unknown): string[] {
|
|
515
|
+
if (!(schema instanceof z.ZodType)) {
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
const jsonSchema = z.toJSONSchema(schema);
|
|
519
|
+
if (!isRecord(jsonSchema)) {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
const keys: string[] = [];
|
|
523
|
+
collectJsonSchemaDescriptionKeys(jsonSchema, keys);
|
|
524
|
+
return keys;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function collectJsonSchemaDescriptionKeys(
|
|
528
|
+
schema: Record<string, unknown>,
|
|
529
|
+
keys: string[],
|
|
530
|
+
): void {
|
|
531
|
+
const descriptionKey = schema[APIFUSE_DESCRIPTION_KEY_META_KEY];
|
|
532
|
+
if (typeof descriptionKey === "string" && descriptionKey.length > 0) {
|
|
533
|
+
keys.push(descriptionKey);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
for (const value of Object.values(schema)) {
|
|
537
|
+
if (isRecord(value)) {
|
|
538
|
+
collectJsonSchemaDescriptionKeys(value, keys);
|
|
539
|
+
} else if (Array.isArray(value)) {
|
|
540
|
+
for (const item of value) {
|
|
541
|
+
if (isRecord(item)) {
|
|
542
|
+
collectJsonSchemaDescriptionKeys(item, keys);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
550
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
551
|
+
}
|
|
552
|
+
|
|
325
553
|
function scoreOperationMetadata(provider: ProviderDefinition): SubmitCheck {
|
|
326
554
|
const operations = Object.entries(provider.operations);
|
|
327
555
|
const weakDescriptions = operations
|
|
328
|
-
.filter(
|
|
329
|
-
|
|
330
|
-
|
|
556
|
+
.filter(([, operation]) => {
|
|
557
|
+
// Hard-cut providers move operation copy into locale catalogs via
|
|
558
|
+
// descriptionKey instead of raw inline prose; the resolved text length
|
|
559
|
+
// is enforced at registry catalog-build time, matching how lintOperation
|
|
560
|
+
// skips the raw-description min-length rule when a descriptionKey is set.
|
|
561
|
+
const hasDescriptionKey =
|
|
562
|
+
typeof operation.descriptionKey === "string" &&
|
|
563
|
+
operation.descriptionKey.length > 0;
|
|
564
|
+
if (hasDescriptionKey) return false;
|
|
565
|
+
return true;
|
|
566
|
+
})
|
|
331
567
|
.map(([operationId]) => operationId);
|
|
332
568
|
const missingAnnotations = operations
|
|
333
569
|
.filter(([, operation]) => !operation.annotations)
|
|
@@ -520,7 +756,7 @@ function scoreSmokeEvidence(smokeNote: string | undefined): SubmitCheck {
|
|
|
520
756
|
maxPoints: CATEGORY_MAX_POINTS.smoke,
|
|
521
757
|
message: "No local smoke evidence was provided.",
|
|
522
758
|
remediation:
|
|
523
|
-
"Start `bun run dev`, call `/health` and at least one `POST /v1/{operation}`, then rerun with `--smoke-note` or paste notes in the
|
|
759
|
+
"Start `bun run dev`, call `/health` and at least one `POST /v1/{operation}`, then rerun with `--smoke-note` or paste notes in the assigned workspace PR.",
|
|
524
760
|
};
|
|
525
761
|
}
|
|
526
762
|
|
|
@@ -784,7 +1020,7 @@ function blocker(
|
|
|
784
1020
|
|
|
785
1021
|
export function renderText(report: SubmitCheckReport): string {
|
|
786
1022
|
const lines = [
|
|
787
|
-
`
|
|
1023
|
+
`APIFuse Provider Submission Score: ${report.score.total} / ${report.score.max}`,
|
|
788
1024
|
`Verdict: ${report.score.verdict.toUpperCase()}`,
|
|
789
1025
|
`Provider: ${report.provider.id}@${report.provider.version} (${report.provider.runtime}, auth: ${report.provider.authMode})`,
|
|
790
1026
|
`Blockers: ${report.summary.blockers} Warnings: ${report.summary.warnings} Passed: ${report.summary.passed}`,
|
|
@@ -811,7 +1047,7 @@ export function renderText(report: SubmitCheckReport): string {
|
|
|
811
1047
|
|
|
812
1048
|
export function renderMarkdown(report: SubmitCheckReport): string {
|
|
813
1049
|
const lines = [
|
|
814
|
-
"#
|
|
1050
|
+
"# APIFuse Provider Submission Report",
|
|
815
1051
|
"",
|
|
816
1052
|
`- **Provider**: ${report.provider.id}@${report.provider.version}`,
|
|
817
1053
|
`- **SDK**: ${report.provider.sdkVersion}`,
|