@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.
Files changed (138) hide show
  1. package/AUTHORING.md +134 -0
  2. package/CHANGELOG.md +61 -0
  3. package/README.md +21 -9
  4. package/SUBMISSION.md +10 -11
  5. package/bin/apifuse-pack-check.ts +22 -0
  6. package/bin/apifuse-submit-check.ts +1747 -388
  7. package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
  8. package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  9. package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  10. package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  11. package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  12. package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  13. package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  14. package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  15. package/dist/auth-turn/fixtures/valid/abort.json +8 -0
  16. package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
  17. package/dist/auth-turn/fixtures/valid/complete.json +13 -0
  18. package/dist/auth-turn/fixtures/valid/form.json +14 -0
  19. package/dist/auth-turn/fixtures/valid/message.json +13 -0
  20. package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
  21. package/dist/auth-turn/fixtures/valid/pending.json +5 -0
  22. package/dist/auth-turn/fixtures/valid/poll.json +9 -0
  23. package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
  24. package/dist/auth-turn/fixtures/valid/retry.json +8 -0
  25. package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  26. package/dist/auth-turn/index.d.ts +195 -0
  27. package/dist/auth-turn/index.js +133 -0
  28. package/dist/auth.d.ts +76 -0
  29. package/dist/auth.js +436 -0
  30. package/dist/ceremonies/index.js +7 -31
  31. package/dist/cli/create.js +45 -30
  32. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  33. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  34. package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
  35. package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
  36. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  37. package/dist/cli/templates/provider/README.md.tpl +163 -0
  38. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  39. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  40. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  41. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  42. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  43. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  44. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  45. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  46. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  47. package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  48. package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  49. package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  50. package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  51. package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  52. package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  53. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  54. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  55. package/dist/contract.js +1 -0
  56. package/dist/define.d.ts +6 -1
  57. package/dist/define.js +140 -70
  58. package/dist/index.d.ts +3 -2
  59. package/dist/index.js +2 -1
  60. package/dist/lint.d.ts +1 -0
  61. package/dist/lint.js +27 -0
  62. package/dist/provider.d.ts +4 -2
  63. package/dist/provider.js +2 -1
  64. package/dist/runtime/auth-flow.js +2 -0
  65. package/dist/runtime/browser.js +203 -0
  66. package/dist/runtime/http.js +46 -336
  67. package/dist/runtime/proxy-retry-policy.d.ts +40 -0
  68. package/dist/runtime/proxy-retry-policy.js +326 -0
  69. package/dist/runtime/stealth.d.ts +5 -2
  70. package/dist/runtime/stealth.js +206 -206
  71. package/dist/server/index.d.ts +4 -0
  72. package/dist/server/index.js +4 -0
  73. package/dist/server/self-test-input-tokens.d.ts +1 -0
  74. package/dist/server/self-test-input-tokens.js +37 -0
  75. package/dist/server/self-test-redaction.d.ts +20 -0
  76. package/dist/server/self-test-redaction.js +70 -0
  77. package/dist/server/self-test-token.d.ts +30 -0
  78. package/dist/server/self-test-token.js +50 -0
  79. package/dist/server/self-test.d.ts +98 -0
  80. package/dist/server/self-test.js +555 -0
  81. package/dist/server/serve.d.ts +6 -0
  82. package/dist/server/serve.js +38 -10
  83. package/dist/server/types.d.ts +1 -0
  84. package/dist/server/types.js +1 -0
  85. package/dist/testing/run.js +5 -1
  86. package/dist/types.d.ts +150 -0
  87. package/package.json +9 -2
  88. package/src/auth-turn/auth-turn.v1.schema.json +89 -0
  89. package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
  90. package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
  91. package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
  92. package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
  93. package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
  94. package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
  95. package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
  96. package/src/auth-turn/fixtures/valid/abort.json +8 -0
  97. package/src/auth-turn/fixtures/valid/challenge.json +17 -0
  98. package/src/auth-turn/fixtures/valid/complete.json +13 -0
  99. package/src/auth-turn/fixtures/valid/form.json +14 -0
  100. package/src/auth-turn/fixtures/valid/message.json +13 -0
  101. package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
  102. package/src/auth-turn/fixtures/valid/pending.json +5 -0
  103. package/src/auth-turn/fixtures/valid/poll.json +9 -0
  104. package/src/auth-turn/fixtures/valid/redirect.json +16 -0
  105. package/src/auth-turn/fixtures/valid/retry.json +8 -0
  106. package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
  107. package/src/auth-turn/index.ts +177 -0
  108. package/src/auth.ts +786 -0
  109. package/src/ceremonies/index.ts +9 -43
  110. package/src/cli/create.ts +60 -97
  111. package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
  112. package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
  113. package/src/cli/templates/provider/README.md.tpl +7 -4
  114. package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  115. package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  116. package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  117. package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  118. package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  119. package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  120. package/src/contract.ts +1 -0
  121. package/src/define.ts +198 -71
  122. package/src/index.ts +16 -0
  123. package/src/lint.ts +33 -0
  124. package/src/provider.ts +27 -0
  125. package/src/runtime/auth-flow.ts +2 -0
  126. package/src/runtime/browser.ts +293 -1
  127. package/src/runtime/http.ts +84 -530
  128. package/src/runtime/proxy-retry-policy.ts +469 -0
  129. package/src/runtime/stealth.ts +269 -353
  130. package/src/server/index.ts +36 -0
  131. package/src/server/self-test-input-tokens.ts +46 -0
  132. package/src/server/self-test-redaction.ts +97 -0
  133. package/src/server/self-test-token.ts +70 -0
  134. package/src/server/self-test.ts +725 -0
  135. package/src/server/serve.ts +75 -6
  136. package/src/server/types.ts +1 -0
  137. package/src/testing/run.ts +9 -1
  138. package/src/types.ts +186 -0
@@ -1,7 +1,8 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
 
3
- import Ajv from "ajv";
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 Ajv({ allErrors: true, strict: true, strictSchema: true });
34
-
35
- const authTurnSchema = {
36
- type: "object",
37
- additionalProperties: false,
38
- required: ["kind", "turnId"],
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: "input",
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`, save the generated report, and note `/health` plus `POST /v1/{operation}` smoke results.",
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. The full public-only checklist is shipped
45
- in `node_modules/@apifuse/provider-sdk/SUBMISSION.md`.
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