@apifuse/provider-sdk 2.1.0-beta.2 → 2.1.0-beta.21

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 (231) hide show
  1. package/AUTHORING.md +330 -8
  2. package/CHANGELOG.md +89 -1
  3. package/README.md +64 -17
  4. package/SUBMISSION.md +86 -0
  5. package/bin/apifuse-check.ts +60 -6
  6. package/bin/apifuse-dev.ts +58 -8
  7. package/bin/apifuse-pack-check.ts +32 -2
  8. package/bin/apifuse-pack-smoke.ts +133 -6
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +3243 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/auth.d.ts +76 -0
  14. package/dist/auth.js +436 -0
  15. package/dist/ceremonies/index.d.ts +41 -0
  16. package/dist/ceremonies/index.js +490 -0
  17. package/dist/choice-token.d.ts +24 -0
  18. package/dist/choice-token.js +74 -0
  19. package/dist/cli/commands.d.ts +10 -0
  20. package/dist/cli/commands.js +80 -0
  21. package/dist/cli/create.d.ts +47 -0
  22. package/dist/cli/create.js +777 -0
  23. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  24. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  25. package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
  26. package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
  27. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  28. package/dist/cli/templates/provider/README.md.tpl +163 -0
  29. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  30. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  31. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  32. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  33. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  34. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  35. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  36. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  37. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  38. package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  39. package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  40. package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  41. package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  42. package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  43. package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  44. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  45. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  46. package/dist/config/loader.d.ts +107 -0
  47. package/dist/config/loader.js +935 -0
  48. package/dist/contract-json.d.ts +9 -0
  49. package/dist/contract-json.js +51 -0
  50. package/dist/contract-serialization.d.ts +4 -0
  51. package/dist/contract-serialization.js +78 -0
  52. package/dist/contract-types.d.ts +49 -0
  53. package/dist/contract-types.js +1 -0
  54. package/dist/contract.d.ts +6 -0
  55. package/dist/contract.js +156 -0
  56. package/dist/define.d.ts +100 -0
  57. package/dist/define.js +1383 -0
  58. package/dist/dev.d.ts +9 -0
  59. package/dist/dev.js +15 -0
  60. package/dist/errors.d.ts +59 -0
  61. package/dist/errors.js +97 -0
  62. package/dist/i18n/catalog.d.ts +29 -0
  63. package/dist/i18n/catalog.js +159 -0
  64. package/dist/i18n/index.d.ts +2 -0
  65. package/dist/i18n/index.js +2 -0
  66. package/dist/i18n/keys.d.ts +10 -0
  67. package/dist/i18n/keys.js +34 -0
  68. package/dist/index.d.ts +42 -0
  69. package/dist/index.js +38 -0
  70. package/dist/lint.d.ts +74 -0
  71. package/dist/lint.js +729 -0
  72. package/dist/observability.d.ts +5 -0
  73. package/dist/observability.js +39 -0
  74. package/dist/provider.d.ts +11 -0
  75. package/dist/provider.js +9 -0
  76. package/dist/public-schema-field-lint.d.ts +2 -0
  77. package/dist/public-schema-field-lint.js +158 -0
  78. package/dist/recipes/gov-api.d.ts +19 -0
  79. package/dist/recipes/gov-api.js +72 -0
  80. package/dist/recipes/rest-api.d.ts +21 -0
  81. package/dist/recipes/rest-api.js +115 -0
  82. package/dist/runtime/auth-flow.d.ts +14 -0
  83. package/dist/runtime/auth-flow.js +46 -0
  84. package/dist/runtime/browser.d.ts +25 -0
  85. package/dist/runtime/browser.js +1237 -0
  86. package/dist/runtime/cache.d.ts +10 -0
  87. package/dist/runtime/cache.js +372 -0
  88. package/dist/runtime/choice.d.ts +15 -0
  89. package/dist/runtime/choice.js +435 -0
  90. package/dist/runtime/credential.d.ts +8 -0
  91. package/dist/runtime/credential.js +61 -0
  92. package/dist/runtime/env.d.ts +2 -0
  93. package/dist/runtime/env.js +10 -0
  94. package/dist/runtime/executor.d.ts +16 -0
  95. package/dist/runtime/executor.js +51 -0
  96. package/dist/runtime/http.d.ts +8 -0
  97. package/dist/runtime/http.js +726 -0
  98. package/dist/runtime/insights.d.ts +9 -0
  99. package/dist/runtime/insights.js +324 -0
  100. package/dist/runtime/instrumentation.d.ts +8 -0
  101. package/dist/runtime/instrumentation.js +269 -0
  102. package/dist/runtime/key-derivation.d.ts +24 -0
  103. package/dist/runtime/key-derivation.js +73 -0
  104. package/dist/runtime/keyring.d.ts +25 -0
  105. package/dist/runtime/keyring.js +93 -0
  106. package/dist/runtime/namespace.d.ts +9 -0
  107. package/dist/runtime/namespace.js +19 -0
  108. package/dist/runtime/otlp.d.ts +39 -0
  109. package/dist/runtime/otlp.js +103 -0
  110. package/dist/runtime/perf.d.ts +12 -0
  111. package/dist/runtime/perf.js +52 -0
  112. package/dist/runtime/prevalidate.d.ts +12 -0
  113. package/dist/runtime/prevalidate.js +173 -0
  114. package/dist/runtime/provider.d.ts +2 -0
  115. package/dist/runtime/provider.js +11 -0
  116. package/dist/runtime/proxy-errors.d.ts +21 -0
  117. package/dist/runtime/proxy-errors.js +83 -0
  118. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  119. package/dist/runtime/proxy-telemetry.js +174 -0
  120. package/dist/runtime/redis.d.ts +17 -0
  121. package/dist/runtime/redis.js +82 -0
  122. package/dist/runtime/request-options.d.ts +3 -0
  123. package/dist/runtime/request-options.js +42 -0
  124. package/dist/runtime/state.d.ts +17 -0
  125. package/dist/runtime/state.js +344 -0
  126. package/dist/runtime/stealth.d.ts +21 -0
  127. package/dist/runtime/stealth.js +980 -0
  128. package/dist/runtime/stt.d.ts +22 -0
  129. package/dist/runtime/stt.js +480 -0
  130. package/dist/runtime/trace.d.ts +26 -0
  131. package/dist/runtime/trace.js +142 -0
  132. package/dist/runtime/waterfall.d.ts +12 -0
  133. package/dist/runtime/waterfall.js +147 -0
  134. package/dist/schema.d.ts +74 -0
  135. package/dist/schema.js +243 -0
  136. package/dist/serve.d.ts +1 -0
  137. package/dist/serve.js +1 -0
  138. package/dist/server/index.d.ts +3 -0
  139. package/dist/server/index.js +2 -0
  140. package/dist/server/serve.d.ts +64 -0
  141. package/dist/server/serve.js +1118 -0
  142. package/dist/server/types.d.ts +136 -0
  143. package/dist/server/types.js +86 -0
  144. package/dist/stealth/profiles.d.ts +4 -0
  145. package/dist/stealth/profiles.js +259 -0
  146. package/dist/stream.d.ts +44 -0
  147. package/dist/stream.js +151 -0
  148. package/dist/testing/helpers.d.ts +23 -0
  149. package/dist/testing/helpers.js +95 -0
  150. package/dist/testing/index.d.ts +2 -0
  151. package/dist/testing/index.js +2 -0
  152. package/dist/testing/run.d.ts +34 -0
  153. package/dist/testing/run.js +307 -0
  154. package/dist/types.d.ts +1467 -0
  155. package/dist/types.js +61 -0
  156. package/dist/utils/date.d.ts +6 -0
  157. package/dist/utils/date.js +101 -0
  158. package/dist/utils/parse.d.ts +16 -0
  159. package/dist/utils/parse.js +51 -0
  160. package/dist/utils/text.d.ts +4 -0
  161. package/dist/utils/text.js +14 -0
  162. package/dist/utils/transform.d.ts +8 -0
  163. package/dist/utils/transform.js +48 -0
  164. package/package.json +57 -29
  165. package/src/auth.ts +786 -0
  166. package/src/ceremonies/index.ts +8 -2
  167. package/src/choice-token.ts +165 -0
  168. package/src/cli/commands.ts +34 -11
  169. package/src/cli/create.ts +254 -128
  170. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  171. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  172. package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
  173. package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
  174. package/src/cli/templates/provider/README.md.tpl +87 -7
  175. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  176. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  177. package/src/cli/templates/provider/index.ts.tpl +5 -47
  178. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  179. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  180. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  181. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  182. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  183. package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
  184. package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
  185. package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
  186. package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
  187. package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
  188. package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
  189. package/src/cli/templates/provider/start.ts.tpl +1 -1
  190. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  191. package/src/config/loader.ts +1224 -9
  192. package/src/contract-json.ts +75 -0
  193. package/src/contract-serialization.ts +89 -0
  194. package/src/contract-types.ts +52 -0
  195. package/src/contract.ts +216 -0
  196. package/src/define.ts +1820 -70
  197. package/src/errors.ts +27 -0
  198. package/src/i18n/catalog.ts +277 -0
  199. package/src/i18n/index.ts +2 -0
  200. package/src/i18n/keys.ts +64 -0
  201. package/src/index.ts +189 -9
  202. package/src/lint.ts +580 -73
  203. package/src/observability.ts +41 -0
  204. package/src/provider.ts +131 -4
  205. package/src/public-schema-field-lint.ts +237 -0
  206. package/src/runtime/auth-flow.ts +9 -0
  207. package/src/runtime/browser.ts +1054 -51
  208. package/src/runtime/cache.ts +528 -0
  209. package/src/runtime/choice.ts +760 -0
  210. package/src/runtime/executor.ts +32 -3
  211. package/src/runtime/http.ts +980 -195
  212. package/src/runtime/insights.ts +11 -11
  213. package/src/runtime/instrumentation.ts +12 -4
  214. package/src/runtime/key-derivation.ts +1 -1
  215. package/src/runtime/keyring.ts +4 -3
  216. package/src/runtime/proxy-errors.ts +132 -0
  217. package/src/runtime/proxy-telemetry.ts +253 -0
  218. package/src/runtime/redis.ts +116 -0
  219. package/src/runtime/request-options.ts +66 -0
  220. package/src/runtime/state.ts +563 -0
  221. package/src/runtime/stealth.ts +1336 -0
  222. package/src/runtime/stt.ts +629 -0
  223. package/src/runtime/trace.ts +1 -1
  224. package/src/schema.ts +363 -1
  225. package/src/server/serve.ts +1192 -75
  226. package/src/server/types.ts +37 -0
  227. package/src/stream.ts +210 -0
  228. package/src/testing/run.ts +40 -6
  229. package/src/types.ts +1283 -59
  230. package/src/runtime/tls.ts +0 -434
  231. package/src/types/playwright-stealth.d.ts +0 -9
package/src/cli/create.ts CHANGED
@@ -1,18 +1,10 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  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,17 +82,13 @@ 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
104
- apifuse create my-provider --preset monorepo
105
89
  apifuse create --config ./apifuse.create.json --json
106
90
 
107
91
  Options:
108
- --preset <standalone|monorepo>
109
92
  --config <path>
110
93
  --output-dir <path>
111
94
  --display-name <name>
@@ -115,7 +98,7 @@ Options:
115
98
  --yes
116
99
  --dry-run
117
100
  --json
118
- --sdk-specifier <specifier> # internal/testing override for standalone dependency resolution
101
+ --sdk-specifier <specifier> # internal/testing override for dependency resolution
119
102
  --help, -h`;
120
103
 
121
104
  export async function main() {
@@ -128,9 +111,7 @@ export async function main() {
128
111
  }
129
112
 
130
113
  const parsed = parseArgs(normalizedArgs);
131
- const config = parsed.configPath
132
- ? await loadConfig(parsed.configPath)
133
- : undefined;
114
+ const config = parsed.configPath ? await loadConfig(parsed.configPath) : undefined;
134
115
  const resolved = await resolveCreateOptions(parsed, config, process.cwd());
135
116
  const plan = await buildProviderCreatePlan(resolved, process.cwd());
136
117
 
@@ -197,18 +178,10 @@ function parseArgs(argv: string[]): ParsedArgs {
197
178
  parsed.displayName = ensureValue(flag, consumeValue());
198
179
  break;
199
180
  case "--category":
200
- parsed.category = parseEnum(
201
- "category",
202
- consumeValue(),
203
- CATEGORY_OPTIONS,
204
- );
181
+ parsed.category = parseEnum("category", consumeValue(), CATEGORY_OPTIONS);
205
182
  break;
206
183
  case "--auth-mode":
207
- parsed.authMode = parseEnum(
208
- "auth mode",
209
- consumeValue(),
210
- AUTH_MODE_OPTIONS,
211
- );
184
+ parsed.authMode = parseEnum("auth mode", consumeValue(), AUTH_MODE_OPTIONS);
212
185
  break;
213
186
  case "--runtime":
214
187
  parsed.runtime = parseEnum("runtime", consumeValue(), RUNTIME_OPTIONS);
@@ -255,9 +228,7 @@ function parseEnum<T extends readonly string[]>(
255
228
  const resolvedValue = ensureValue(`--${label}`, value);
256
229
  const matchedValue = options.find((option) => option === resolvedValue);
257
230
  if (!matchedValue) {
258
- throw new Error(
259
- `Invalid ${label}: ${resolvedValue}. Expected one of: ${options.join(", ")}`,
260
- );
231
+ throw new Error(`Invalid ${label}: ${resolvedValue}. Expected one of: ${options.join(", ")}`);
261
232
  }
262
233
  return matchedValue;
263
234
  }
@@ -273,39 +244,32 @@ async function resolveCreateOptions(
273
244
  config: CreateConfigFile | undefined,
274
245
  cwd: string,
275
246
  ): Promise<CreateResolvedOptions> {
276
- const detectedWorkspaceRoot = findWorkspaceRoot(cwd);
277
- const detectedPreset: CreatePreset = detectedWorkspaceRoot
278
- ? "monorepo"
279
- : "standalone";
247
+ const internalWorkspaceRoot = findApifuseInternalWorkspaceRoot(cwd);
280
248
 
281
249
  const partial: Partial<CreateResolvedOptions> = {
282
250
  name: parsed.name ?? config?.name,
283
- preset: parsed.preset ?? config?.preset ?? detectedPreset,
251
+ preset: parsed.preset ?? config?.preset ?? "standalone",
284
252
  outputDir: parsed.outputDir ?? config?.outputDir,
285
253
  displayName: parsed.displayName ?? config?.displayName,
286
254
  category: parsed.category ?? config?.category,
287
255
  authMode: parsed.authMode ?? config?.authMode,
288
256
  runtime: parsed.runtime ?? config?.runtime,
289
257
  sdkSpecifier:
290
- parsed.sdkSpecifier ??
291
- config?.sdkSpecifier ??
292
- process.env.APIFUSE_SDK_SPECIFIER,
258
+ parsed.sdkSpecifier ?? config?.sdkSpecifier ?? process.env.APIFUSE__SDK__SPECIFIER,
293
259
  dryRun: parsed.dryRun,
294
260
  json: parsed.json,
295
261
  yes: parsed.yes,
296
262
  };
297
263
 
298
- if (partial.preset === "monorepo" && !detectedWorkspaceRoot) {
264
+ if (partial.preset === "monorepo" && !internalWorkspaceRoot) {
299
265
  throw new Error(
300
- "Monorepo preset requires a workspace root with a providers/ directory.",
266
+ "Monorepo preset is internal to the APIFuse repository. External bounty workspaces are one-provider repositories; use the standalone default create flow.",
301
267
  );
302
268
  }
303
269
 
304
270
  if (partial.yes) {
305
271
  if (!partial.name) {
306
- throw new Error(
307
- "--yes requires a provider name (positional or via config).",
308
- );
272
+ throw new Error("--yes requires a provider name (positional or via config).");
309
273
  }
310
274
 
311
275
  return {
@@ -314,7 +278,7 @@ async function resolveCreateOptions(
314
278
  category: partial.category ?? "other",
315
279
  authMode: partial.authMode ?? "none",
316
280
  runtime: partial.runtime ?? "standard",
317
- preset: partial.preset ?? detectedPreset,
281
+ preset: partial.preset ?? "standalone",
318
282
  outputDir: partial.outputDir,
319
283
  dryRun: partial.dryRun ?? false,
320
284
  json: partial.json ?? false,
@@ -324,10 +288,10 @@ async function resolveCreateOptions(
324
288
  }
325
289
 
326
290
  if (!partial.json) {
327
- intro("Create a new ApiFuse provider");
291
+ intro("Create a new APIFuse provider");
328
292
  note(
329
- `Preset precedence: explicit flags > config file > workspace detection > standalone default\nDetected workspace preset: ${detectedPreset}`,
330
- "Preset resolution",
293
+ "External bounty workspaces are one-provider repositories. The public create flow defaults to standalone.",
294
+ "Provider workspace",
331
295
  );
332
296
  }
333
297
 
@@ -392,22 +356,7 @@ async function resolveCreateOptions(
392
356
  initialValue: "standard",
393
357
  }),
394
358
  )),
395
- preset:
396
- partial.preset ??
397
- (await promptValue(
398
- select({
399
- message: "Generation preset",
400
- options: PRESET_OPTIONS.map((value) => ({
401
- label: value,
402
- value,
403
- hint:
404
- value === "standalone"
405
- ? "Create a clean-room npm-ready provider package."
406
- : "Create the provider inside the current ApiFuse monorepo.",
407
- })),
408
- initialValue: detectedPreset,
409
- }),
410
- )),
359
+ preset: partial.preset ?? "standalone",
411
360
  outputDir: partial.outputDir,
412
361
  dryRun: partial.dryRun ?? false,
413
362
  json: partial.json ?? false,
@@ -429,24 +378,20 @@ export async function buildProviderCreatePlan(
429
378
  options: CreateResolvedOptions,
430
379
  cwd: string,
431
380
  ): Promise<ProviderCreatePlan> {
432
- const workspaceRoot =
433
- options.preset === "monorepo" ? findWorkspaceRoot(cwd) : undefined;
434
- if (options.preset === "monorepo" && !workspaceRoot) {
381
+ const resolvedWorkspaceRoot =
382
+ options.preset === "monorepo" ? findApifuseInternalWorkspaceRoot(cwd) : undefined;
383
+ if (options.preset === "monorepo" && !resolvedWorkspaceRoot) {
435
384
  throw new Error(
436
- "Monorepo preset requires a workspace root with a providers/ directory.",
385
+ "Monorepo preset is internal to the APIFuse repository. External bounty workspaces are one-provider repositories; use the standalone default create flow.",
437
386
  );
438
387
  }
439
- const resolvedWorkspaceRoot =
440
- options.preset === "monorepo" ? workspaceRoot : undefined;
441
388
  let providerRoot: string;
442
389
  let installCwd: string;
443
390
 
444
391
  if (options.outputDir) {
445
392
  providerRoot = resolve(cwd, options.outputDir);
446
393
  installCwd =
447
- options.preset === "monorepo" && resolvedWorkspaceRoot
448
- ? resolvedWorkspaceRoot
449
- : providerRoot;
394
+ options.preset === "monorepo" && resolvedWorkspaceRoot ? resolvedWorkspaceRoot : providerRoot;
450
395
  } else if (options.preset === "monorepo" && resolvedWorkspaceRoot) {
451
396
  providerRoot = resolve(resolvedWorkspaceRoot, "providers", options.name);
452
397
  installCwd = resolvedWorkspaceRoot;
@@ -459,33 +404,101 @@ export async function buildProviderCreatePlan(
459
404
  throw new Error(`Target directory already exists: ${providerRoot}`);
460
405
  }
461
406
 
407
+ if (options.sdkSpecifier?.startsWith("workspace:") && !resolvedWorkspaceRoot) {
408
+ throw new Error(
409
+ "workspace:* is only valid inside the APIFuse monorepo because public Provider SDK scaffolds must install from npm or an explicit tarball/file specifier.",
410
+ );
411
+ }
412
+
462
413
  const sdkSpecifier =
463
414
  options.sdkSpecifier ??
464
- (options.preset === "monorepo" ? "workspace:*" : `^${packageJson.version}`);
415
+ (options.preset === "monorepo" && resolvedWorkspaceRoot
416
+ ? "workspace:*"
417
+ : `^${packageJson.version}`);
465
418
  const relativeProviderRoot = relative(cwd, providerRoot) || options.name;
466
419
  const nextDevCommand = `cd ${relativeProviderRoot} && bun run dev`;
467
420
  const packageName =
468
421
  options.preset === "monorepo"
469
422
  ? `@apifuse/provider-${options.name}`
470
423
  : `apifuse-provider-${options.name}`;
424
+ const templateValues = {
425
+ PROVIDER_ID: options.name,
426
+ DISPLAY_NAME: escapeTemplate(options.displayName),
427
+ CATEGORY: options.category,
428
+ RUNTIME: options.runtime,
429
+ BROWSER_BLOCK:
430
+ options.runtime === "browser"
431
+ ? ',\n browser: {\n engine: "playwright-stealth",\n }'
432
+ : "",
433
+ SECRETS_BLOCK: renderSecretsBlock(options.authMode),
434
+ CREDENTIAL_BLOCK: renderCredentialBlock(options.authMode),
435
+ AUTH_BLOCK: renderAuthBlock(options.authMode),
436
+ };
471
437
 
472
438
  const files: ProviderPlanFile[] = [
439
+ {
440
+ path: resolve(providerRoot, ".dockerignore"),
441
+ content: await renderTemplate(".dockerignore.tpl", {}),
442
+ },
443
+ {
444
+ path: resolve(providerRoot, ".gitignore"),
445
+ content: await renderTemplate(".gitignore.tpl", {}),
446
+ },
473
447
  {
474
448
  path: resolve(providerRoot, "index.ts"),
475
- content: await renderTemplate("index.ts.tpl", {
449
+ content: await renderTemplate("index.ts.tpl", templateValues),
450
+ },
451
+ {
452
+ path: resolve(providerRoot, "meta.ts"),
453
+ content: await renderTemplate("meta.ts.tpl", {
476
454
  PROVIDER_ID: options.name,
477
455
  DISPLAY_NAME: escapeTemplate(options.displayName),
478
456
  CATEGORY: options.category,
479
- RUNTIME: options.runtime,
480
- BROWSER_BLOCK:
457
+ }),
458
+ },
459
+ {
460
+ path: resolve(providerRoot, "operations", "index.ts"),
461
+ content: await renderTemplate("operations/index.ts.tpl", {}),
462
+ },
463
+ {
464
+ path: resolve(providerRoot, "operations", "ping.ts"),
465
+ content: await renderTemplate("operations/ping.ts.tpl", {
466
+ DISPLAY_NAME: escapeTemplate(options.displayName),
467
+ HANDLER_CTX: options.runtime === "browser" ? "ctx" : "_ctx",
468
+ BROWSER_HANDLER_BLOCK:
481
469
  options.runtime === "browser"
482
- ? ',\n browser: {\n engine: "playwright-stealth",\n }'
470
+ ? '\n const page = await ctx.browser.newPage();\n await page.goto("https://example.com");\n const title = await page.title();\n const frames = await page.frames();\n await page.close();\n'
471
+ : "",
472
+ BROWSER_RESPONSE_FIELDS:
473
+ options.runtime === "browser"
474
+ ? ",\n pageTitle: title,\n frameCount: frames.length"
483
475
  : "",
484
- SECRETS_BLOCK: renderSecretsBlock(options.authMode),
485
- CREDENTIAL_BLOCK: renderCredentialBlock(options.authMode),
486
- AUTH_BLOCK: renderAuthBlock(options.authMode),
487
476
  }),
488
477
  },
478
+ {
479
+ path: resolve(providerRoot, "schemas", "ping.ts"),
480
+ content: await renderTemplate("schemas/ping.ts.tpl", {}),
481
+ },
482
+ {
483
+ path: resolve(providerRoot, "upstream", "README.md"),
484
+ content: await renderTemplate("upstream/README.md.tpl", {}),
485
+ },
486
+ {
487
+ path: resolve(providerRoot, "mappers", "README.md"),
488
+ content: await renderTemplate("mappers/README.md.tpl", {}),
489
+ },
490
+ {
491
+ path: resolve(providerRoot, "domain", "README.md"),
492
+ content: await renderTemplate("domain/README.md.tpl", {}),
493
+ },
494
+ {
495
+ path: resolve(providerRoot, "locales", "en.json"),
496
+ content: renderStarterLocaleCatalog(options.displayName, "en"),
497
+ },
498
+ {
499
+ path: resolve(providerRoot, "locales", "ko.json"),
500
+ content: renderStarterLocaleCatalog(options.displayName, "ko"),
501
+ },
489
502
  {
490
503
  path: resolve(providerRoot, "package.json"),
491
504
  content: renderPackageJson({
@@ -525,6 +538,38 @@ export async function buildProviderCreatePlan(
525
538
  PROVIDER_ID: options.name,
526
539
  }),
527
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
+ },
528
573
  ];
529
574
 
530
575
  return {
@@ -538,15 +583,17 @@ export async function buildProviderCreatePlan(
538
583
  packageName,
539
584
  preset: options.preset,
540
585
  providerRoot,
541
- validationCommands: ["bun run check", "bun run test"],
586
+ validationCommands: [
587
+ "bun run check",
588
+ "bun run type-check",
589
+ "bun run submit-check -- --smoke",
590
+ "bun run test",
591
+ ],
542
592
  workspaceRoot: resolvedWorkspaceRoot,
543
593
  };
544
594
  }
545
595
 
546
- async function renderTemplate(
547
- fileName: string,
548
- values: Record<string, string>,
549
- ): Promise<string> {
596
+ async function renderTemplate(fileName: string, values: Record<string, string>): Promise<string> {
550
597
  const templatePath = resolve(TEMPLATE_DIR, fileName);
551
598
  const template = await readFile(templatePath, "utf8");
552
599
  return template.replace(/\{\{([A-Z_]+)\}\}/g, (_match, key: string) => {
@@ -554,10 +601,7 @@ async function renderTemplate(
554
601
  });
555
602
  }
556
603
 
557
- function renderPackageJson(input: {
558
- packageName: string;
559
- sdkSpecifier: string;
560
- }): string {
604
+ function renderPackageJson(input: { packageName: string; sdkSpecifier: string }): string {
561
605
  return `${JSON.stringify(
562
606
  {
563
607
  name: input.packageName,
@@ -567,10 +611,11 @@ function renderPackageJson(input: {
567
611
  main: "./index.ts",
568
612
  scripts: {
569
613
  dev: "apifuse dev .",
570
- check: "apifuse check .",
614
+ check: "apifuse check . && bun run type-check",
615
+ "type-check": "tsc --noEmit",
616
+ "submit-check": "apifuse submit-check . --markdown submission-report.md",
571
617
  test: "apifuse test .",
572
618
  record: "apifuse record .",
573
- "perf:sample": "apifuse perf . --operation ping --runs 3",
574
619
  start: "bun start.ts",
575
620
  },
576
621
  dependencies: {
@@ -578,6 +623,7 @@ function renderPackageJson(input: {
578
623
  },
579
624
  devDependencies: {
580
625
  "@types/bun": "latest",
626
+ typescript: "^6.0.3",
581
627
  },
582
628
  },
583
629
  null,
@@ -596,6 +642,7 @@ function renderTsconfig(): string {
596
642
  noEmit: true,
597
643
  skipLibCheck: true,
598
644
  resolveJsonModule: true,
645
+ types: ["bun"],
599
646
  },
600
647
  include: ["**/*.ts"],
601
648
  exclude: ["node_modules"],
@@ -605,6 +652,61 @@ function renderTsconfig(): string {
605
652
  )}\n`;
606
653
  }
607
654
 
655
+ function renderStarterLocaleCatalog(displayName: string, locale: "en" | "ko"): string {
656
+ const catalog = {
657
+ meta: {
658
+ displayName,
659
+ description:
660
+ locale === "ko"
661
+ ? `${displayName} APIFuse 커뮤니티 기여용 provider starter입니다.`
662
+ : `${displayName} provider starter for APIFuse community contributions.`,
663
+ },
664
+ operations: {
665
+ ping: {
666
+ description:
667
+ locale === "ko"
668
+ ? "생성된 provider wiring이 APIFuse runtime contract를 통해 작은 샘플 payload를 정상적으로 round-trip하는지 확인합니다. 로컬 개발, baseline check, 첫 bounty scaffold 검증에 사용합니다. production data retrieval이나 upstream-specific workflow에는 사용하지 마세요. 이 starter operation은 생성된 프로젝트가 compile, serve, input/output round-trip을 수행하는지 증명하기 위한 용도입니다."
669
+ : "Confirms the generated provider wiring is operational by echoing a small sample payload through the APIFuse runtime contract. Use when validating local development, baseline checks, or first-pass bounty scaffolds. Do NOT use for production data retrieval or upstream-specific workflows because this starter operation exists only to prove the generated project compiles, serves, and round-trips input/output correctly.",
670
+ },
671
+ },
672
+ schemaDescriptions: {
673
+ input: {
674
+ root:
675
+ locale === "ko"
676
+ ? "생성된 ping operation의 입력 payload"
677
+ : "Input payload for the generated ping operation.",
678
+ value:
679
+ locale === "ko"
680
+ ? "생성된 provider scaffold wiring 검증에 사용하는 샘플 입력값"
681
+ : "Sample input value used to verify the generated provider scaffold is wired correctly.",
682
+ },
683
+ output: {
684
+ root:
685
+ locale === "ko"
686
+ ? "생성된 ping operation이 반환하는 출력 payload"
687
+ : "Output payload returned by the generated ping operation.",
688
+ ok:
689
+ locale === "ko"
690
+ ? "생성된 provider가 샘플 요청을 성공적으로 처리했는지 여부"
691
+ : "Whether the generated provider handled the sample request successfully.",
692
+ message:
693
+ locale === "ko"
694
+ ? "생성된 provider가 샘플 payload를 round-trip했음을 보여주는 사람이 읽을 수 있는 확인 메시지"
695
+ : "Human-readable confirmation that the generated provider round-tripped the sample payload.",
696
+ pageTitle:
697
+ locale === "ko"
698
+ ? "browser 런타임 provider일 때 로드된 페이지의 제목 (해당되지 않으면 생략)"
699
+ : "Title of the loaded page when the provider uses the browser runtime; omitted otherwise.",
700
+ frameCount:
701
+ locale === "ko"
702
+ ? "browser 런타임 provider일 때 로드된 페이지의 frame 개수 (해당되지 않으면 생략)"
703
+ : "Number of frames in the loaded page when the provider uses the browser runtime; omitted otherwise.",
704
+ },
705
+ },
706
+ };
707
+ return `${JSON.stringify(catalog, null, 2)}\n`;
708
+ }
709
+
608
710
  function renderAuthBlock(authMode: CreateAuthMode): string {
609
711
  switch (authMode) {
610
712
  case "none":
@@ -615,8 +717,8 @@ function renderAuthBlock(authMode: CreateAuthMode): string {
615
717
  return `{
616
718
  mode: "credentials",
617
719
  flow: {
618
- start: async () => ({
619
- kind: "input",
720
+ start: async (_ctx) => ({
721
+ kind: "form",
620
722
  turnId: crypto.randomUUID(),
621
723
  expectedInput: {
622
724
  schema: {
@@ -642,13 +744,24 @@ function renderAuthBlock(authMode: CreateAuthMode): string {
642
744
  },
643
745
  hint: "Generated placeholder credential flow completed. Replace this with real auth logic.",
644
746
  }),
747
+ refresh: async () => ({
748
+ kind: "complete",
749
+ turnId: crypto.randomUUID(),
750
+ data: {
751
+ credential: {
752
+ username: "replace-with-refreshed-username",
753
+ password: "replace-with-refreshed-password",
754
+ },
755
+ },
756
+ hint: "Return refreshed credential data here, or throw AuthError with code AUTH_REQUIRED when silent refresh is not possible.",
757
+ }),
645
758
  },
646
759
  }`;
647
760
  case "oauth2":
648
761
  return `{
649
762
  mode: "oauth2",
650
763
  flow: {
651
- start: async () => ({
764
+ start: async (_ctx) => ({
652
765
  kind: "redirect",
653
766
  turnId: crypto.randomUUID(),
654
767
  data: {
@@ -713,11 +826,11 @@ export function toDisplayName(name: string): string {
713
826
  .join(" ");
714
827
  }
715
828
 
716
- function findWorkspaceRoot(cwd: string): string | undefined {
829
+ function findApifuseInternalWorkspaceRoot(cwd: string): string | undefined {
717
830
  let currentDirectory = cwd;
718
831
 
719
832
  while (true) {
720
- if (existsSync(resolve(currentDirectory, "providers"))) {
833
+ if (isApifuseInternalWorkspaceRoot(currentDirectory)) {
721
834
  return currentDirectory;
722
835
  }
723
836
 
@@ -730,6 +843,29 @@ function findWorkspaceRoot(cwd: string): string | undefined {
730
843
  }
731
844
  }
732
845
 
846
+ function isApifuseInternalWorkspaceRoot(workspaceRoot: string): boolean {
847
+ const providerSdkPackageJsonPath = resolve(
848
+ workspaceRoot,
849
+ "packages",
850
+ "provider-sdk",
851
+ "package.json",
852
+ );
853
+ if (!existsSync(providerSdkPackageJsonPath)) {
854
+ return false;
855
+ }
856
+ try {
857
+ const packageJson = JSON.parse(readFileSync(providerSdkPackageJsonPath, "utf8"));
858
+ return (
859
+ typeof packageJson === "object" &&
860
+ packageJson !== null &&
861
+ "name" in packageJson &&
862
+ packageJson.name === "@apifuse/provider-sdk"
863
+ );
864
+ } catch {
865
+ return false;
866
+ }
867
+ }
868
+
733
869
  async function writePlan(plan: ProviderCreatePlan): Promise<void> {
734
870
  for (const file of plan.files) {
735
871
  await mkdir(dirname(file.path), { recursive: true });
@@ -737,27 +873,17 @@ async function writePlan(plan: ProviderCreatePlan): Promise<void> {
737
873
  }
738
874
  }
739
875
 
740
- async function installDependencies(
741
- plan: ProviderCreatePlan,
742
- jsonMode: boolean,
743
- ): Promise<void> {
876
+ async function installDependencies(plan: ProviderCreatePlan, jsonMode: boolean): Promise<void> {
744
877
  await runCommand(plan.installCommand, plan.installCwd, jsonMode);
745
878
  }
746
879
 
747
- async function runBaselineValidation(
748
- plan: ProviderCreatePlan,
749
- jsonMode: boolean,
750
- ): Promise<void> {
880
+ async function runBaselineValidation(plan: ProviderCreatePlan, jsonMode: boolean): Promise<void> {
751
881
  for (const command of plan.validationCommands) {
752
882
  await runCommand(command, plan.providerRoot, jsonMode);
753
883
  }
754
884
  }
755
885
 
756
- async function runCommand(
757
- command: string,
758
- cwd: string,
759
- jsonMode: boolean,
760
- ): Promise<void> {
886
+ async function runCommand(command: string, cwd: string, jsonMode: boolean): Promise<void> {
761
887
  const [binary, ...args] = command.split(" ");
762
888
  if (!binary) {
763
889
  throw new Error(`Cannot run empty command in ${cwd}`);
@@ -789,9 +915,7 @@ async function runCommand(
789
915
  rejectPromise(
790
916
  new Error(
791
917
  `Command failed (${command}) in ${cwd}${
792
- stdout || stderr
793
- ? `\n${[stdout, stderr].filter(Boolean).join("\n")}`
794
- : ""
918
+ stdout || stderr ? `\n${[stdout, stderr].filter(Boolean).join("\n")}` : ""
795
919
  }`,
796
920
  ),
797
921
  );
@@ -799,11 +923,7 @@ async function runCommand(
799
923
  });
800
924
  }
801
925
 
802
- function printResult(
803
- plan: ProviderCreatePlan,
804
- jsonMode: boolean,
805
- dryRun: boolean,
806
- ) {
926
+ function printResult(plan: ProviderCreatePlan, jsonMode: boolean, dryRun: boolean) {
807
927
  const payload = {
808
928
  success: true,
809
929
  dryRun,
@@ -820,9 +940,7 @@ function printResult(
820
940
  },
821
941
  validationCommands: plan.validationCommands,
822
942
  nextDevCommand: plan.nextDevCommand,
823
- files: plan.files.map(
824
- (file) => relative(plan.providerRoot, file.path) || file.path,
825
- ),
943
+ files: plan.files.map((file) => relative(plan.providerRoot, file.path) || file.path),
826
944
  };
827
945
 
828
946
  if (jsonMode) {
@@ -837,6 +955,14 @@ function printResult(
837
955
  console.log(`Validation: (cd ${plan.providerRoot} && ${command})`);
838
956
  }
839
957
  console.log(`Next local dev: ${plan.nextDevCommand}`);
958
+ console.log(
959
+ "Submission evidence: run `bun run submit-check -- --smoke` to archive measured `/health` and `POST /v1/{operation}` results.",
960
+ );
961
+ if (plan.files.some((file) => file.content.includes('runtime: "browser"'))) {
962
+ console.log(
963
+ "Browser runtime: run `bunx playwright install chromium` locally or set `APIFUSE__CDP_POOL__URL` before browser-backed smoke tests.",
964
+ );
965
+ }
840
966
  }
841
967
 
842
968
  function escapeTemplate(value: string): string {
@@ -0,0 +1,22 @@
1
+ node_modules/
2
+ .git/
3
+ .github/
4
+
5
+ # Environment and local secrets
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Local reports and generated artifacts
11
+ submission-report.md
12
+ coverage/
13
+ dist/
14
+ .cache/
15
+ .turbo/
16
+ .bun/
17
+ *.tsbuildinfo
18
+
19
+ # OS/editor junk
20
+ .DS_Store
21
+ Thumbs.db
22
+ *.swp