@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.2
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 +134 -0
- package/CHANGELOG.md +61 -0
- package/README.md +21 -9
- package/SUBMISSION.md +10 -11
- package/bin/apifuse-pack-check.ts +22 -0
- package/bin/apifuse-submit-check.ts +1747 -388
- package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
- package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/dist/auth-turn/fixtures/valid/abort.json +8 -0
- package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
- package/dist/auth-turn/fixtures/valid/complete.json +13 -0
- package/dist/auth-turn/fixtures/valid/form.json +14 -0
- package/dist/auth-turn/fixtures/valid/message.json +13 -0
- package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/dist/auth-turn/fixtures/valid/pending.json +5 -0
- package/dist/auth-turn/fixtures/valid/poll.json +9 -0
- package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
- package/dist/auth-turn/fixtures/valid/retry.json +8 -0
- package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/dist/auth-turn/index.d.ts +195 -0
- package/dist/auth-turn/index.js +133 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +436 -0
- package/dist/ceremonies/index.js +7 -31
- package/dist/cli/create.js +45 -30
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +163 -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/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/contract.js +1 -0
- package/dist/define.d.ts +6 -1
- package/dist/define.js +140 -70
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +27 -0
- package/dist/provider.d.ts +4 -2
- package/dist/provider.js +2 -1
- package/dist/runtime/auth-flow.js +2 -0
- package/dist/runtime/browser.js +203 -0
- package/dist/runtime/http.js +46 -336
- package/dist/runtime/proxy-retry-policy.d.ts +40 -0
- package/dist/runtime/proxy-retry-policy.js +326 -0
- package/dist/runtime/stealth.d.ts +5 -2
- package/dist/runtime/stealth.js +206 -206
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +4 -0
- package/dist/server/self-test-input-tokens.d.ts +1 -0
- package/dist/server/self-test-input-tokens.js +37 -0
- package/dist/server/self-test-redaction.d.ts +20 -0
- package/dist/server/self-test-redaction.js +70 -0
- package/dist/server/self-test-token.d.ts +30 -0
- package/dist/server/self-test-token.js +50 -0
- package/dist/server/self-test.d.ts +98 -0
- package/dist/server/self-test.js +555 -0
- package/dist/server/serve.d.ts +6 -0
- package/dist/server/serve.js +38 -10
- package/dist/server/types.d.ts +1 -0
- package/dist/server/types.js +1 -0
- package/dist/testing/run.js +5 -1
- package/dist/types.d.ts +150 -0
- package/package.json +9 -2
- package/src/auth-turn/auth-turn.v1.schema.json +89 -0
- package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/src/auth-turn/fixtures/valid/abort.json +8 -0
- package/src/auth-turn/fixtures/valid/challenge.json +17 -0
- package/src/auth-turn/fixtures/valid/complete.json +13 -0
- package/src/auth-turn/fixtures/valid/form.json +14 -0
- package/src/auth-turn/fixtures/valid/message.json +13 -0
- package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/src/auth-turn/fixtures/valid/pending.json +5 -0
- package/src/auth-turn/fixtures/valid/poll.json +9 -0
- package/src/auth-turn/fixtures/valid/redirect.json +16 -0
- package/src/auth-turn/fixtures/valid/retry.json +8 -0
- package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/src/auth-turn/index.ts +177 -0
- package/src/auth.ts +786 -0
- package/src/ceremonies/index.ts +9 -43
- package/src/cli/create.ts +60 -97
- package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/src/cli/templates/provider/README.md.tpl +7 -4
- package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/src/contract.ts +1 -0
- package/src/define.ts +198 -71
- package/src/index.ts +16 -0
- package/src/lint.ts +33 -0
- package/src/provider.ts +27 -0
- package/src/runtime/auth-flow.ts +2 -0
- package/src/runtime/browser.ts +293 -1
- package/src/runtime/http.ts +84 -530
- package/src/runtime/proxy-retry-policy.ts +469 -0
- package/src/runtime/stealth.ts +269 -353
- package/src/server/index.ts +36 -0
- package/src/server/self-test-input-tokens.ts +46 -0
- package/src/server/self-test-redaction.ts +97 -0
- package/src/server/self-test-token.ts +70 -0
- package/src/server/self-test.ts +725 -0
- package/src/server/serve.ts +75 -6
- package/src/server/types.ts +1 -0
- package/src/testing/run.ts +9 -1
- package/src/types.ts +186 -0
package/src/ceremonies/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
4
4
|
|
|
5
|
+
import { AUTH_TURN_SCHEMA, type KnownAuthTurnKind } from "../auth-turn";
|
|
5
6
|
import {
|
|
6
7
|
FlowExpiredError,
|
|
7
8
|
ProviderSecretError,
|
|
@@ -15,53 +16,18 @@ import type {
|
|
|
15
16
|
FlowContext,
|
|
16
17
|
} from "../types";
|
|
17
18
|
|
|
18
|
-
type TurnKind =
|
|
19
|
-
| "abort"
|
|
20
|
-
| "challenge"
|
|
21
|
-
| "complete"
|
|
22
|
-
| "form"
|
|
23
|
-
| "message"
|
|
24
|
-
| "multi_choice"
|
|
25
|
-
| "poll"
|
|
26
|
-
| "redirect"
|
|
27
|
-
| "retry";
|
|
19
|
+
type TurnKind = KnownAuthTurnKind;
|
|
28
20
|
|
|
29
21
|
type CeremonyHandler = AuthFlowInputHandler;
|
|
30
22
|
|
|
31
23
|
type JsonObject = Record<string, unknown>;
|
|
32
24
|
|
|
33
|
-
const ajv = new
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
properties: {
|
|
40
|
-
kind: { type: "string", minLength: 1 },
|
|
41
|
-
turnId: { type: "string", minLength: 1 },
|
|
42
|
-
expiresAt: { type: "string", minLength: 1 },
|
|
43
|
-
data: {
|
|
44
|
-
type: "object",
|
|
45
|
-
additionalProperties: true,
|
|
46
|
-
},
|
|
47
|
-
expectedInput: {
|
|
48
|
-
type: "object",
|
|
49
|
-
additionalProperties: true,
|
|
50
|
-
},
|
|
51
|
-
hint: { type: "string" },
|
|
52
|
-
hintKey: { type: "string" },
|
|
53
|
-
timing: {
|
|
54
|
-
type: "object",
|
|
55
|
-
additionalProperties: false,
|
|
56
|
-
properties: {
|
|
57
|
-
suggestedPollIntervalMs: { type: "number", minimum: 1 },
|
|
58
|
-
maxWaitMs: { type: "number", minimum: 1 },
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
} as const;
|
|
63
|
-
|
|
64
|
-
const validateAuthTurn = ajv.compile(authTurnSchema);
|
|
25
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, strictSchema: true });
|
|
26
|
+
|
|
27
|
+
// Runtime ceremony-output validation derives from the exported versioned
|
|
28
|
+
// contract: it compiles the exact AUTH_TURN_SCHEMA document shipped at
|
|
29
|
+
// dist/auth-turn/auth-turn.v1.schema.json, so the two cannot drift.
|
|
30
|
+
const validateAuthTurn = ajv.compile(AUTH_TURN_SCHEMA);
|
|
65
31
|
|
|
66
32
|
const OAUTH2_STATE_KEY = "__oauth2_state";
|
|
67
33
|
const OAUTH2_PKCE_VERIFIER_KEY = "__oauth2_pkce_verifier";
|
package/src/cli/create.ts
CHANGED
|
@@ -4,15 +4,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, relative, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
cancel,
|
|
9
|
-
intro,
|
|
10
|
-
isCancel,
|
|
11
|
-
note,
|
|
12
|
-
outro,
|
|
13
|
-
select,
|
|
14
|
-
text,
|
|
15
|
-
} from "@clack/prompts";
|
|
7
|
+
import { cancel, intro, isCancel, note, outro, select, text } from "@clack/prompts";
|
|
16
8
|
import { z } from "zod";
|
|
17
9
|
|
|
18
10
|
import packageJson from "../../package.json";
|
|
@@ -28,12 +20,7 @@ export const CATEGORY_OPTIONS = [
|
|
|
28
20
|
"communication",
|
|
29
21
|
"other",
|
|
30
22
|
] as const;
|
|
31
|
-
export const AUTH_MODE_OPTIONS = [
|
|
32
|
-
"none",
|
|
33
|
-
"platform-managed",
|
|
34
|
-
"credentials",
|
|
35
|
-
"oauth2",
|
|
36
|
-
] as const;
|
|
23
|
+
export const AUTH_MODE_OPTIONS = ["none", "platform-managed", "credentials", "oauth2"] as const;
|
|
37
24
|
export const RUNTIME_OPTIONS = ["standard", "browser"] as const;
|
|
38
25
|
export const PRESET_OPTIONS = ["standalone", "monorepo"] as const;
|
|
39
26
|
|
|
@@ -95,9 +82,7 @@ export type ProviderCreatePlan = {
|
|
|
95
82
|
workspaceRoot?: string;
|
|
96
83
|
};
|
|
97
84
|
|
|
98
|
-
const TEMPLATE_DIR = fileURLToPath(
|
|
99
|
-
new URL("./templates/provider/", import.meta.url),
|
|
100
|
-
);
|
|
85
|
+
const TEMPLATE_DIR = fileURLToPath(new URL("./templates/provider/", import.meta.url));
|
|
101
86
|
const HELP_TEXT = `Usage: apifuse create <provider-name> [options]
|
|
102
87
|
Examples:
|
|
103
88
|
apifuse create my-provider
|
|
@@ -126,9 +111,7 @@ export async function main() {
|
|
|
126
111
|
}
|
|
127
112
|
|
|
128
113
|
const parsed = parseArgs(normalizedArgs);
|
|
129
|
-
const config = parsed.configPath
|
|
130
|
-
? await loadConfig(parsed.configPath)
|
|
131
|
-
: undefined;
|
|
114
|
+
const config = parsed.configPath ? await loadConfig(parsed.configPath) : undefined;
|
|
132
115
|
const resolved = await resolveCreateOptions(parsed, config, process.cwd());
|
|
133
116
|
const plan = await buildProviderCreatePlan(resolved, process.cwd());
|
|
134
117
|
|
|
@@ -195,18 +178,10 @@ function parseArgs(argv: string[]): ParsedArgs {
|
|
|
195
178
|
parsed.displayName = ensureValue(flag, consumeValue());
|
|
196
179
|
break;
|
|
197
180
|
case "--category":
|
|
198
|
-
parsed.category = parseEnum(
|
|
199
|
-
"category",
|
|
200
|
-
consumeValue(),
|
|
201
|
-
CATEGORY_OPTIONS,
|
|
202
|
-
);
|
|
181
|
+
parsed.category = parseEnum("category", consumeValue(), CATEGORY_OPTIONS);
|
|
203
182
|
break;
|
|
204
183
|
case "--auth-mode":
|
|
205
|
-
parsed.authMode = parseEnum(
|
|
206
|
-
"auth mode",
|
|
207
|
-
consumeValue(),
|
|
208
|
-
AUTH_MODE_OPTIONS,
|
|
209
|
-
);
|
|
184
|
+
parsed.authMode = parseEnum("auth mode", consumeValue(), AUTH_MODE_OPTIONS);
|
|
210
185
|
break;
|
|
211
186
|
case "--runtime":
|
|
212
187
|
parsed.runtime = parseEnum("runtime", consumeValue(), RUNTIME_OPTIONS);
|
|
@@ -253,9 +228,7 @@ function parseEnum<T extends readonly string[]>(
|
|
|
253
228
|
const resolvedValue = ensureValue(`--${label}`, value);
|
|
254
229
|
const matchedValue = options.find((option) => option === resolvedValue);
|
|
255
230
|
if (!matchedValue) {
|
|
256
|
-
throw new Error(
|
|
257
|
-
`Invalid ${label}: ${resolvedValue}. Expected one of: ${options.join(", ")}`,
|
|
258
|
-
);
|
|
231
|
+
throw new Error(`Invalid ${label}: ${resolvedValue}. Expected one of: ${options.join(", ")}`);
|
|
259
232
|
}
|
|
260
233
|
return matchedValue;
|
|
261
234
|
}
|
|
@@ -282,9 +255,7 @@ async function resolveCreateOptions(
|
|
|
282
255
|
authMode: parsed.authMode ?? config?.authMode,
|
|
283
256
|
runtime: parsed.runtime ?? config?.runtime,
|
|
284
257
|
sdkSpecifier:
|
|
285
|
-
parsed.sdkSpecifier ??
|
|
286
|
-
config?.sdkSpecifier ??
|
|
287
|
-
process.env.APIFUSE__SDK__SPECIFIER,
|
|
258
|
+
parsed.sdkSpecifier ?? config?.sdkSpecifier ?? process.env.APIFUSE__SDK__SPECIFIER,
|
|
288
259
|
dryRun: parsed.dryRun,
|
|
289
260
|
json: parsed.json,
|
|
290
261
|
yes: parsed.yes,
|
|
@@ -298,9 +269,7 @@ async function resolveCreateOptions(
|
|
|
298
269
|
|
|
299
270
|
if (partial.yes) {
|
|
300
271
|
if (!partial.name) {
|
|
301
|
-
throw new Error(
|
|
302
|
-
"--yes requires a provider name (positional or via config).",
|
|
303
|
-
);
|
|
272
|
+
throw new Error("--yes requires a provider name (positional or via config).");
|
|
304
273
|
}
|
|
305
274
|
|
|
306
275
|
return {
|
|
@@ -410,9 +379,7 @@ export async function buildProviderCreatePlan(
|
|
|
410
379
|
cwd: string,
|
|
411
380
|
): Promise<ProviderCreatePlan> {
|
|
412
381
|
const resolvedWorkspaceRoot =
|
|
413
|
-
options.preset === "monorepo"
|
|
414
|
-
? findApifuseInternalWorkspaceRoot(cwd)
|
|
415
|
-
: undefined;
|
|
382
|
+
options.preset === "monorepo" ? findApifuseInternalWorkspaceRoot(cwd) : undefined;
|
|
416
383
|
if (options.preset === "monorepo" && !resolvedWorkspaceRoot) {
|
|
417
384
|
throw new Error(
|
|
418
385
|
"Monorepo preset is internal to the APIFuse repository. External bounty workspaces are one-provider repositories; use the standalone default create flow.",
|
|
@@ -424,9 +391,7 @@ export async function buildProviderCreatePlan(
|
|
|
424
391
|
if (options.outputDir) {
|
|
425
392
|
providerRoot = resolve(cwd, options.outputDir);
|
|
426
393
|
installCwd =
|
|
427
|
-
options.preset === "monorepo" && resolvedWorkspaceRoot
|
|
428
|
-
? resolvedWorkspaceRoot
|
|
429
|
-
: providerRoot;
|
|
394
|
+
options.preset === "monorepo" && resolvedWorkspaceRoot ? resolvedWorkspaceRoot : providerRoot;
|
|
430
395
|
} else if (options.preset === "monorepo" && resolvedWorkspaceRoot) {
|
|
431
396
|
providerRoot = resolve(resolvedWorkspaceRoot, "providers", options.name);
|
|
432
397
|
installCwd = resolvedWorkspaceRoot;
|
|
@@ -439,10 +404,7 @@ export async function buildProviderCreatePlan(
|
|
|
439
404
|
throw new Error(`Target directory already exists: ${providerRoot}`);
|
|
440
405
|
}
|
|
441
406
|
|
|
442
|
-
if (
|
|
443
|
-
options.sdkSpecifier?.startsWith("workspace:") &&
|
|
444
|
-
!resolvedWorkspaceRoot
|
|
445
|
-
) {
|
|
407
|
+
if (options.sdkSpecifier?.startsWith("workspace:") && !resolvedWorkspaceRoot) {
|
|
446
408
|
throw new Error(
|
|
447
409
|
"workspace:* is only valid inside the APIFuse monorepo because public Provider SDK scaffolds must install from npm or an explicit tarball/file specifier.",
|
|
448
410
|
);
|
|
@@ -576,6 +538,38 @@ export async function buildProviderCreatePlan(
|
|
|
576
538
|
PROVIDER_ID: options.name,
|
|
577
539
|
}),
|
|
578
540
|
},
|
|
541
|
+
{
|
|
542
|
+
path: resolve(providerRoot, "AGENTS.md"),
|
|
543
|
+
content: await renderTemplate("AGENTS.md.tpl", {}),
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
path: resolve(providerRoot, "CLAUDE.md"),
|
|
547
|
+
content: await renderTemplate("CLAUDE.md.tpl", {}),
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
path: resolve(providerRoot, "skills", "normalization-standards", "SKILL.md"),
|
|
551
|
+
content: await renderTemplate("skills/normalization-standards/SKILL.md.tpl", {}),
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
path: resolve(providerRoot, "skills", "upstream-contract-verification", "SKILL.md"),
|
|
555
|
+
content: await renderTemplate("skills/upstream-contract-verification/SKILL.md.tpl", {}),
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
path: resolve(providerRoot, "skills", "fixtures-and-recording", "SKILL.md"),
|
|
559
|
+
content: await renderTemplate("skills/fixtures-and-recording/SKILL.md.tpl", {}),
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
path: resolve(providerRoot, "skills", "pagination-and-counts", "SKILL.md"),
|
|
563
|
+
content: await renderTemplate("skills/pagination-and-counts/SKILL.md.tpl", {}),
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
path: resolve(providerRoot, "skills", "health-checks-and-fail-closed", "SKILL.md"),
|
|
567
|
+
content: await renderTemplate("skills/health-checks-and-fail-closed/SKILL.md.tpl", {}),
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
path: resolve(providerRoot, "skills", "upstream-notes", "README.md"),
|
|
571
|
+
content: await renderTemplate("skills/upstream-notes/README.md.tpl", {}),
|
|
572
|
+
},
|
|
579
573
|
];
|
|
580
574
|
|
|
581
575
|
return {
|
|
@@ -592,17 +586,14 @@ export async function buildProviderCreatePlan(
|
|
|
592
586
|
validationCommands: [
|
|
593
587
|
"bun run check",
|
|
594
588
|
"bun run type-check",
|
|
595
|
-
"bun run submit-check",
|
|
589
|
+
"bun run submit-check -- --smoke",
|
|
596
590
|
"bun run test",
|
|
597
591
|
],
|
|
598
592
|
workspaceRoot: resolvedWorkspaceRoot,
|
|
599
593
|
};
|
|
600
594
|
}
|
|
601
595
|
|
|
602
|
-
async function renderTemplate(
|
|
603
|
-
fileName: string,
|
|
604
|
-
values: Record<string, string>,
|
|
605
|
-
): Promise<string> {
|
|
596
|
+
async function renderTemplate(fileName: string, values: Record<string, string>): Promise<string> {
|
|
606
597
|
const templatePath = resolve(TEMPLATE_DIR, fileName);
|
|
607
598
|
const template = await readFile(templatePath, "utf8");
|
|
608
599
|
return template.replace(/\{\{([A-Z_]+)\}\}/g, (_match, key: string) => {
|
|
@@ -610,10 +601,7 @@ async function renderTemplate(
|
|
|
610
601
|
});
|
|
611
602
|
}
|
|
612
603
|
|
|
613
|
-
function renderPackageJson(input: {
|
|
614
|
-
packageName: string;
|
|
615
|
-
sdkSpecifier: string;
|
|
616
|
-
}): string {
|
|
604
|
+
function renderPackageJson(input: { packageName: string; sdkSpecifier: string }): string {
|
|
617
605
|
return `${JSON.stringify(
|
|
618
606
|
{
|
|
619
607
|
name: input.packageName,
|
|
@@ -625,11 +613,9 @@ function renderPackageJson(input: {
|
|
|
625
613
|
dev: "apifuse dev .",
|
|
626
614
|
check: "apifuse check . && bun run type-check",
|
|
627
615
|
"type-check": "tsc --noEmit",
|
|
628
|
-
"submit-check":
|
|
629
|
-
"apifuse submit-check . --markdown submission-report.md",
|
|
616
|
+
"submit-check": "apifuse submit-check . --markdown submission-report.md",
|
|
630
617
|
test: "apifuse test .",
|
|
631
618
|
record: "apifuse record .",
|
|
632
|
-
"perf:sample": "apifuse perf . --operation ping --runs 3",
|
|
633
619
|
start: "bun start.ts",
|
|
634
620
|
},
|
|
635
621
|
dependencies: {
|
|
@@ -666,10 +652,7 @@ function renderTsconfig(): string {
|
|
|
666
652
|
)}\n`;
|
|
667
653
|
}
|
|
668
654
|
|
|
669
|
-
function renderStarterLocaleCatalog(
|
|
670
|
-
displayName: string,
|
|
671
|
-
locale: "en" | "ko",
|
|
672
|
-
): string {
|
|
655
|
+
function renderStarterLocaleCatalog(displayName: string, locale: "en" | "ko"): string {
|
|
673
656
|
const catalog = {
|
|
674
657
|
meta: {
|
|
675
658
|
displayName,
|
|
@@ -734,8 +717,8 @@ function renderAuthBlock(authMode: CreateAuthMode): string {
|
|
|
734
717
|
return `{
|
|
735
718
|
mode: "credentials",
|
|
736
719
|
flow: {
|
|
737
|
-
start: async () => ({
|
|
738
|
-
kind: "
|
|
720
|
+
start: async (_ctx) => ({
|
|
721
|
+
kind: "form",
|
|
739
722
|
turnId: crypto.randomUUID(),
|
|
740
723
|
expectedInput: {
|
|
741
724
|
schema: {
|
|
@@ -778,7 +761,7 @@ function renderAuthBlock(authMode: CreateAuthMode): string {
|
|
|
778
761
|
return `{
|
|
779
762
|
mode: "oauth2",
|
|
780
763
|
flow: {
|
|
781
|
-
start: async () => ({
|
|
764
|
+
start: async (_ctx) => ({
|
|
782
765
|
kind: "redirect",
|
|
783
766
|
turnId: crypto.randomUUID(),
|
|
784
767
|
data: {
|
|
@@ -871,9 +854,7 @@ function isApifuseInternalWorkspaceRoot(workspaceRoot: string): boolean {
|
|
|
871
854
|
return false;
|
|
872
855
|
}
|
|
873
856
|
try {
|
|
874
|
-
const packageJson = JSON.parse(
|
|
875
|
-
readFileSync(providerSdkPackageJsonPath, "utf8"),
|
|
876
|
-
);
|
|
857
|
+
const packageJson = JSON.parse(readFileSync(providerSdkPackageJsonPath, "utf8"));
|
|
877
858
|
return (
|
|
878
859
|
typeof packageJson === "object" &&
|
|
879
860
|
packageJson !== null &&
|
|
@@ -892,27 +873,17 @@ async function writePlan(plan: ProviderCreatePlan): Promise<void> {
|
|
|
892
873
|
}
|
|
893
874
|
}
|
|
894
875
|
|
|
895
|
-
async function installDependencies(
|
|
896
|
-
plan: ProviderCreatePlan,
|
|
897
|
-
jsonMode: boolean,
|
|
898
|
-
): Promise<void> {
|
|
876
|
+
async function installDependencies(plan: ProviderCreatePlan, jsonMode: boolean): Promise<void> {
|
|
899
877
|
await runCommand(plan.installCommand, plan.installCwd, jsonMode);
|
|
900
878
|
}
|
|
901
879
|
|
|
902
|
-
async function runBaselineValidation(
|
|
903
|
-
plan: ProviderCreatePlan,
|
|
904
|
-
jsonMode: boolean,
|
|
905
|
-
): Promise<void> {
|
|
880
|
+
async function runBaselineValidation(plan: ProviderCreatePlan, jsonMode: boolean): Promise<void> {
|
|
906
881
|
for (const command of plan.validationCommands) {
|
|
907
882
|
await runCommand(command, plan.providerRoot, jsonMode);
|
|
908
883
|
}
|
|
909
884
|
}
|
|
910
885
|
|
|
911
|
-
async function runCommand(
|
|
912
|
-
command: string,
|
|
913
|
-
cwd: string,
|
|
914
|
-
jsonMode: boolean,
|
|
915
|
-
): Promise<void> {
|
|
886
|
+
async function runCommand(command: string, cwd: string, jsonMode: boolean): Promise<void> {
|
|
916
887
|
const [binary, ...args] = command.split(" ");
|
|
917
888
|
if (!binary) {
|
|
918
889
|
throw new Error(`Cannot run empty command in ${cwd}`);
|
|
@@ -944,9 +915,7 @@ async function runCommand(
|
|
|
944
915
|
rejectPromise(
|
|
945
916
|
new Error(
|
|
946
917
|
`Command failed (${command}) in ${cwd}${
|
|
947
|
-
stdout || stderr
|
|
948
|
-
? `\n${[stdout, stderr].filter(Boolean).join("\n")}`
|
|
949
|
-
: ""
|
|
918
|
+
stdout || stderr ? `\n${[stdout, stderr].filter(Boolean).join("\n")}` : ""
|
|
950
919
|
}`,
|
|
951
920
|
),
|
|
952
921
|
);
|
|
@@ -954,11 +923,7 @@ async function runCommand(
|
|
|
954
923
|
});
|
|
955
924
|
}
|
|
956
925
|
|
|
957
|
-
function printResult(
|
|
958
|
-
plan: ProviderCreatePlan,
|
|
959
|
-
jsonMode: boolean,
|
|
960
|
-
dryRun: boolean,
|
|
961
|
-
) {
|
|
926
|
+
function printResult(plan: ProviderCreatePlan, jsonMode: boolean, dryRun: boolean) {
|
|
962
927
|
const payload = {
|
|
963
928
|
success: true,
|
|
964
929
|
dryRun,
|
|
@@ -975,9 +940,7 @@ function printResult(
|
|
|
975
940
|
},
|
|
976
941
|
validationCommands: plan.validationCommands,
|
|
977
942
|
nextDevCommand: plan.nextDevCommand,
|
|
978
|
-
files: plan.files.map(
|
|
979
|
-
(file) => relative(plan.providerRoot, file.path) || file.path,
|
|
980
|
-
),
|
|
943
|
+
files: plan.files.map((file) => relative(plan.providerRoot, file.path) || file.path),
|
|
981
944
|
};
|
|
982
945
|
|
|
983
946
|
if (jsonMode) {
|
|
@@ -993,7 +956,7 @@ function printResult(
|
|
|
993
956
|
}
|
|
994
957
|
console.log(`Next local dev: ${plan.nextDevCommand}`);
|
|
995
958
|
console.log(
|
|
996
|
-
"Submission evidence: run `bun run submit-check
|
|
959
|
+
"Submission evidence: run `bun run submit-check -- --smoke` to archive measured `/health` and `POST /v1/{operation}` results.",
|
|
997
960
|
);
|
|
998
961
|
if (plan.files.some((file) => file.content.includes('runtime: "browser"'))) {
|
|
999
962
|
console.log(
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# APIFuse Provider Workspace — Agent Guide
|
|
2
|
+
|
|
3
|
+
You are building an APIFuse provider. APIFuse turns messy upstream APIs into
|
|
4
|
+
normalized, typed, evidence-backed public APIs. A provider that merely proxies
|
|
5
|
+
the upstream is a failed provider, even if every check passes.
|
|
6
|
+
|
|
7
|
+
This file is the core contract. Detailed procedures live in `skills/` — load
|
|
8
|
+
the matching skill BEFORE working on that area (index at the bottom).
|
|
9
|
+
|
|
10
|
+
## Non-negotiable principles
|
|
11
|
+
|
|
12
|
+
### 1. Normalize, don't proxy
|
|
13
|
+
Public output is an APIFuse contract, not the upstream's shape.
|
|
14
|
+
- Field names: `snake_case`, semantic, English. Never leak vendor keys
|
|
15
|
+
(`dutyTel1`, `hvec`, `MKioskTy7`) into public output.
|
|
16
|
+
- Timestamps: ISO 8601 in public output. Vendor formats (`20260707222855`)
|
|
17
|
+
are parsed inside mappers only. If a value cannot be parsed, omit/null it —
|
|
18
|
+
never pass the raw vendor string through.
|
|
19
|
+
- Enums: normalize vendor status text/codes (`Y` / `불가능` / `정보미제공`) into a
|
|
20
|
+
declared enum. Mixed raw-text passthrough is a contract failure.
|
|
21
|
+
- Units: every numeric field name states its unit (`distance_meters`), and the
|
|
22
|
+
mapper proves the conversion. Never relabel an upstream number without
|
|
23
|
+
verifying its unit against docs or live data.
|
|
24
|
+
|
|
25
|
+
### 2. Fail closed, never fabricate
|
|
26
|
+
- Never invent output values to satisfy a schema. Missing upstream data → null
|
|
27
|
+
field or structured error, never a plausible dummy.
|
|
28
|
+
- Parse failures are errors, not defaults. Returning `0`, `[]`, or `null`
|
|
29
|
+
when the upstream shape changed hides breakage from every downstream gate.
|
|
30
|
+
- If a non-empty upstream collection normalizes to zero rows, throw
|
|
31
|
+
`UPSTREAM_SCHEMA_ERROR` — silent empty success is the worst failure mode.
|
|
32
|
+
- Model the upstream's real value domain. Check live data before adding
|
|
33
|
+
constraints like `nonnegative()` — some upstreams legitimately return
|
|
34
|
+
negative counts (e.g. overcapacity) and a wrong constraint silently
|
|
35
|
+
nulls real data.
|
|
36
|
+
|
|
37
|
+
### 3. Preserve every input or fail loudly
|
|
38
|
+
- Every accepted input must be representable in the upstream request. If it
|
|
39
|
+
isn't, reject at the schema or throw — never silently drop it.
|
|
40
|
+
- Upstream parameter dependencies (param B ignored without param A) must be
|
|
41
|
+
enforced in YOUR schema. The upstream ignoring input silently is not an
|
|
42
|
+
excuse for your provider to do the same.
|
|
43
|
+
|
|
44
|
+
### 4. Evidence over assumption
|
|
45
|
+
- Upstream parameter names and response fields must be verified against the
|
|
46
|
+
official spec AND at least one live call. Do not guess casing or
|
|
47
|
+
underscores; do not copy from a sibling API without re-verifying.
|
|
48
|
+
- No speculative field probing (`row.distance ?? row.dist ?? row.Distance`).
|
|
49
|
+
Map exactly the fields you have evidence for. One verified name per field.
|
|
50
|
+
- Fixtures are recorded live evidence (`bun run record`), never hand-written.
|
|
51
|
+
Placeholder-looking values (`02-1234-5678`, "테헤란로 123") mean the fixture
|
|
52
|
+
is fabricated and the submission is not reviewable.
|
|
53
|
+
- An empty result set from a dense query (0 hospitals within 5km of a city
|
|
54
|
+
center) is a request bug, not a valid fixture. Investigate before recording.
|
|
55
|
+
|
|
56
|
+
### 5. Honest pagination and counts
|
|
57
|
+
- If you filter rows client-side, the upstream `totalCount` is no longer your
|
|
58
|
+
`total_count`. Either expose upstream semantics honestly (documented) or
|
|
59
|
+
don't expose a total at all. A count the caller cannot page against is a lie.
|
|
60
|
+
|
|
61
|
+
### 6. Health checks must detect real regressions
|
|
62
|
+
- `Array.isArray(data.items)` alone can never fail. Every list operation's
|
|
63
|
+
health check must also flag the zero-rows case for a query that is known to
|
|
64
|
+
return data (dense-area query), so a broken upstream contract degrades
|
|
65
|
+
visibly instead of passing forever.
|
|
66
|
+
|
|
67
|
+
## Verification loop (before every submit)
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bun run check # apifuse check + type-check
|
|
71
|
+
bun run test # your tests — cover mappers, error paths, edge rows
|
|
72
|
+
bun run submit-check # structural score; a high score does NOT prove quality
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`submit-check` is a structural gate. Every principle above can be violated
|
|
76
|
+
while scoring 95/100 — reviewers and CI audit for exactly these classes.
|
|
77
|
+
|
|
78
|
+
## Skill index — load before working on:
|
|
79
|
+
|
|
80
|
+
| Area | Load |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| Output schemas, mappers, field naming, timestamps, enums | `skills/normalization-standards/SKILL.md` |
|
|
83
|
+
| Upstream request params, new endpoint wiring, field mapping | `skills/upstream-contract-verification/SKILL.md` |
|
|
84
|
+
| Recording fixtures, writing tests against fixtures | `skills/fixtures-and-recording/SKILL.md` |
|
|
85
|
+
| List operations, paging, totals, client-side filtering | `skills/pagination-and-counts/SKILL.md` |
|
|
86
|
+
| healthCheck blocks, error classification, fail-closed guards | `skills/health-checks-and-fail-closed/SKILL.md` |
|
|
87
|
+
| Upstream-specific known pitfalls for THIS bounty | `skills/upstream-notes/` (read every file) |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -8,7 +8,8 @@ Generated with `apifuse create`.
|
|
|
8
8
|
bun run dev
|
|
9
9
|
bun run check
|
|
10
10
|
bun run test
|
|
11
|
-
bun run submit-check
|
|
11
|
+
bun run submit-check -- --smoke
|
|
12
|
+
bunx apifuse perf . --operation <operation-id> --runs 3
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
|
|
@@ -34,15 +35,17 @@ review when `index.ts` remains a short composition root.
|
|
|
34
35
|
Before posting bounty evidence, run:
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
|
-
bun run submit-check
|
|
38
|
+
bun run submit-check -- --smoke
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
This writes `submission-report.md` with a review-readiness score, blockers,
|
|
41
42
|
warnings, health coverage notes, fixture/schema evidence, and remediation. A
|
|
42
43
|
score is not a payout guarantee; blockers must be fixed before maintainer
|
|
43
44
|
review. The generated `ping` starter intentionally warns until you replace it
|
|
44
|
-
with real upstream-backed Operations.
|
|
45
|
-
|
|
45
|
+
with real upstream-backed Operations. `APIFUSE__PROVIDER__*` env vars enable
|
|
46
|
+
live upstream calls; without them, structured provider errors can still verify
|
|
47
|
+
runtime routing. The full public-only checklist is shipped in
|
|
48
|
+
`node_modules/@apifuse/provider-sdk/SUBMISSION.md`.
|
|
46
49
|
|
|
47
50
|
|
|
48
51
|
## Operation guide
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fixtures-and-recording
|
|
3
|
+
description: Recording live fixtures and writing trustworthy tests from them. Load before touching __fixtures__/ or writing operation tests.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fixtures and recording
|
|
7
|
+
|
|
8
|
+
Fixtures are evidence, not examples. Reviewers treat fixtures as proof your
|
|
9
|
+
provider ran against the real upstream.
|
|
10
|
+
|
|
11
|
+
## Recording
|
|
12
|
+
- Always record from the live upstream: `bun run record -- --operation <op>
|
|
13
|
+
--params '<json>'` with the real service key configured.
|
|
14
|
+
- Record queries that RETURN DATA. Choose dense/known-good inputs (major city
|
|
15
|
+
district, a real entity id from a prior list call).
|
|
16
|
+
- Re-record after any request-param or mapper change; stale fixtures make
|
|
17
|
+
every downstream test meaningless.
|
|
18
|
+
|
|
19
|
+
## Forbidden fixture states
|
|
20
|
+
- **Hand-written fixtures.** Values like `02-1234-5678`, "테헤란로 123",
|
|
21
|
+
round-number coordinates, or sequential ids are fabrication tells. If it
|
|
22
|
+
wasn't returned by the upstream, it cannot be in `__fixtures__/`.
|
|
23
|
+
- **Empty-result fixtures for dense queries.** `items: [], total_count: 0`
|
|
24
|
+
for "hospitals within 5km of Gangnam" is not a fixture — it is an
|
|
25
|
+
unfixed request bug (wrong param name/format). Investigate first.
|
|
26
|
+
- **Fixtures that contradict each other.** If one operation's fixture proves
|
|
27
|
+
`total_count: 541` while returning 1 filtered row, your count semantics are
|
|
28
|
+
broken (see pagination skill), not your fixture.
|
|
29
|
+
|
|
30
|
+
## Fixture shape
|
|
31
|
+
`apifuse record` (`bun run record`) writes the captured RAW UPSTREAM payload
|
|
32
|
+
to `__fixtures__/raw.json` (secrets sanitized). With `--append` it
|
|
33
|
+
accumulates an array of raw payloads. The recorder does NOT write your
|
|
34
|
+
normalized output — raw.json is upstream evidence only.
|
|
35
|
+
|
|
36
|
+
Derive normalized expectations in TESTS, not in the fixture file: load the
|
|
37
|
+
recorded raw payload, run your mapper over it, and assert the exact expected
|
|
38
|
+
normalized rows inline in the test. If you keep expected-output snapshots,
|
|
39
|
+
generate them from the mapper and review them row by row — never hand-author
|
|
40
|
+
values that the upstream did not return.
|
|
41
|
+
|
|
42
|
+
## Tests to derive from fixtures
|
|
43
|
+
- Mapper: `map(recordedUpstreamRow)` equals the expected normalized row
|
|
44
|
+
(toEqual, not toMatchObject, for full rows — partial matching hides
|
|
45
|
+
dropped fields).
|
|
46
|
+
- Edge rows: single-item object vs array (`items.item` unwrapping), missing
|
|
47
|
+
optional fields, unpadded/numeric time values, vendor error headers.
|
|
48
|
+
- Error paths: upstream error `resultCode`, HTTP failure, missing secret,
|
|
49
|
+
NO_DATA — each asserts the structured `ProviderError` code.
|
|
50
|
+
- Handler-level: run the operation handler against a mock ctx that returns
|
|
51
|
+
the fixture upstream body; assert the full normalized envelope.
|
|
52
|
+
|
|
53
|
+
## Checklist
|
|
54
|
+
- [ ] Every fixture recorded live; no placeholder-looking values
|
|
55
|
+
- [ ] No empty-result fixture for a query that must have data
|
|
56
|
+
- [ ] normalized expectations derived from mapper(recorded raw), not
|
|
57
|
+
hand-authored
|
|
58
|
+
- [ ] Error and edge-shape rows covered, not just the happy row
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: health-checks-and-fail-closed
|
|
3
|
+
description: Writing health checks that can actually fail, and fail-closed guards at envelope and row level. Load before writing healthCheck blocks or error handling.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Health checks and fail-closed guards
|
|
7
|
+
|
|
8
|
+
## Health checks that can actually fail
|
|
9
|
+
`Array.isArray(data.items)` alone can never fail. Every list operation's
|
|
10
|
+
health check must be able to detect the zero-rows regression.
|
|
11
|
+
|
|
12
|
+
Assertion contract (per SDK `HealthCheckCase`): THROW to fail the case
|
|
13
|
+
(recorded as `down`); return `{ status: "degraded", label }` to flag without
|
|
14
|
+
failing; return nothing for `ok`. There is no `"down"` return value.
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
assertions: ({ status, data }) => {
|
|
18
|
+
if (status !== 200) {
|
|
19
|
+
throw new Error(`<op> request failed with status ${status}`);
|
|
20
|
+
}
|
|
21
|
+
if (!Array.isArray(data.items)) {
|
|
22
|
+
throw new Error("<op> missing items array");
|
|
23
|
+
}
|
|
24
|
+
// Dense query MUST return rows; zero rows = upstream contract drift
|
|
25
|
+
if (data.items.length === 0) {
|
|
26
|
+
return { status: "degraded", label: "<op> dense query returned 0 rows" };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- Choose health-check inputs that are guaranteed-dense (major city district,
|
|
32
|
+
a stable well-known entity id). Verify the id still exists when picking it.
|
|
33
|
+
- Also assert one semantic field on the first row (e.g. `items[0].name` is a
|
|
34
|
+
non-empty string) so a mapper regression that empties fields degrades too.
|
|
35
|
+
|
|
36
|
+
## Fail-closed: envelope level
|
|
37
|
+
- Upstream error headers/codes → structured `ProviderError` with a stable
|
|
38
|
+
`code` (`UPSTREAM_AUTH_ERROR`, `UPSTREAM_ERROR`, `NO_DATA`, ...).
|
|
39
|
+
- Non-JSON body, unexpected content type → `UPSTREAM_SCHEMA_ERROR`.
|
|
40
|
+
- HTTP non-2xx → classified error; never a fake empty success envelope.
|
|
41
|
+
Fixture-based tests cannot catch swallowed errors — write an explicit test:
|
|
42
|
+
mock a non-ok response and assert the handler REJECTS.
|
|
43
|
+
|
|
44
|
+
## Fail-closed: row level
|
|
45
|
+
Envelope guards are not enough. The silent killer is: response is valid,
|
|
46
|
+
array is non-empty, but every row normalizes to nothing.
|
|
47
|
+
- If a non-empty upstream collection produces zero normalized rows, throw
|
|
48
|
+
`UPSTREAM_SCHEMA_ERROR` instead of returning `items: []`.
|
|
49
|
+
- Identity fields (id, name) missing on a row → throw, don't skip the row
|
|
50
|
+
silently.
|
|
51
|
+
- Regression-test both layers separately: a bad envelope AND a good envelope
|
|
52
|
+
with unmappable rows.
|
|
53
|
+
|
|
54
|
+
## Error message hygiene
|
|
55
|
+
`ProviderError.message` reaches the tenant verbatim. Never interpolate
|
|
56
|
+
upstream free text that may contain personal data (names, phone numbers,
|
|
57
|
+
addresses); allowlist known code tokens and keep raw bodies in server-side
|
|
58
|
+
details/logs only.
|
|
59
|
+
|
|
60
|
+
## Checklist
|
|
61
|
+
- [ ] Every list op health check flags 0 rows on a dense query
|
|
62
|
+
- [ ] One semantic field asserted on a real row
|
|
63
|
+
- [ ] Swallowed-error test exists (non-ok mock → handler rejects)
|
|
64
|
+
- [ ] Non-empty upstream → zero normalized rows throws
|
|
65
|
+
- [ ] No upstream free text in customer-facing error messages
|