@apifuse/provider-sdk 2.2.0-beta.4 → 2.2.0-beta.7

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 (51) hide show
  1. package/AUTHORING.md +92 -0
  2. package/CHANGELOG.md +12 -0
  3. package/README.md +5 -1
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +26 -1
  6. package/bin/apifuse-pack-check.ts +14 -0
  7. package/bin/apifuse-submit-check.ts +433 -15
  8. package/bin/apifuse-sync-assets.ts +117 -0
  9. package/dist/cli/commands.d.ts +1 -1
  10. package/dist/cli/commands.js +8 -0
  11. package/dist/cli/create.d.ts +3 -0
  12. package/dist/cli/create.js +34 -35
  13. package/dist/cli/prompt-assets.d.ts +80 -0
  14. package/dist/cli/prompt-assets.js +743 -0
  15. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/runtime/executor.js +7 -0
  19. package/dist/runtime/secrets.d.ts +27 -0
  20. package/dist/runtime/secrets.js +51 -0
  21. package/dist/server/index.d.ts +1 -1
  22. package/dist/server/index.js +1 -1
  23. package/dist/server/self-test.d.ts +101 -0
  24. package/dist/server/self-test.js +670 -112
  25. package/dist/server/serve.d.ts +5 -0
  26. package/dist/server/serve.js +41 -1
  27. package/package.json +1 -1
  28. package/src/cli/commands.ts +10 -0
  29. package/src/cli/create.ts +42 -35
  30. package/src/cli/prompt-assets.ts +865 -0
  31. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  32. package/src/index.ts +5 -0
  33. package/src/runtime/executor.ts +8 -0
  34. package/src/runtime/secrets.ts +64 -0
  35. package/src/server/index.ts +5 -0
  36. package/src/server/self-test.ts +852 -127
  37. package/src/server/serve.ts +60 -1
  38. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  39. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  40. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  41. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  42. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  43. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  44. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  45. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  46. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  47. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  48. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  49. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  50. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  51. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -35,6 +35,11 @@ export type ProviderServerLogEvent = (ProviderServerLogEventBase & {
35
35
  message: string;
36
36
  }>;
37
37
  }) | {
38
+ level: "warn";
39
+ event: "provider_secrets_missing";
40
+ providerId: string;
41
+ missingSecrets: string[];
42
+ } | {
38
43
  level: "warn";
39
44
  event: "provider_cleanup_failed";
40
45
  providerId: string;
@@ -18,6 +18,7 @@ import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
18
18
  import { getProviderBaseUrl } from "../runtime/provider.js";
19
19
  import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
20
20
  import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
21
+ import { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "../runtime/secrets.js";
21
22
  import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state.js";
22
23
  import { createStealthClient } from "../runtime/stealth.js";
23
24
  import { createSttClientFromEnv } from "../runtime/stt.js";
@@ -25,7 +26,7 @@ import { createTraceContext } from "../runtime/trace.js";
25
26
  import { parseSchema } from "../schema.js";
26
27
  import { getStealthProfile } from "../stealth/profiles.js";
27
28
  import { APIFUSE_STREAM_DONE_EVENT, APIFUSE_STREAM_ERROR_EVENT, encodeSseEvent, error as streamError, } from "../stream.js";
28
- import { createSelfTestApp, createSelfTestInvoke, resolveSelfTestPort } from "./self-test.js";
29
+ import { createSelfTestApp, createSelfTestAuthFlowInvoke, createSelfTestInvoke, resolveSelfTestPort, } from "./self-test.js";
29
30
  import { resolveSelfTestMasterSecrets } from "./self-test-token.js";
30
31
  import { AuthFlowRequestSchema, OperationRequestSchema, } from "./types.js";
31
32
  const DEFAULT_HOST = "0.0.0.0";
@@ -357,6 +358,18 @@ function providerObservabilityDetails(error) {
357
358
  retryable: error.options?.retryable ?? false,
358
359
  };
359
360
  }
361
+ // Missing-secret errors carry the canonical credential_unavailable category
362
+ // so Gateway/observability can attribute the failure to provisioning, not
363
+ // the upstream. Matched by code (not constructor) so both the SDK-owned
364
+ // runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
365
+ // serialize identically, including across duplicate SDK module instances.
366
+ if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
367
+ return {
368
+ category: error.options?.category ?? "credential_unavailable",
369
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
370
+ retryable: error.options?.retryable ?? false,
371
+ };
372
+ }
360
373
  if (!isTransportError(error)) {
361
374
  return undefined;
362
375
  }
@@ -422,6 +435,10 @@ function toStatusCode(error) {
422
435
  case "AUTH_REQUIRED":
423
436
  case "reauth_required":
424
437
  return 401;
438
+ // Unprovisioned declared secret: a deployment/config defect, never an
439
+ // upstream failure — explicit 400 (was only reached via fallthrough).
440
+ case MISSING_SECRET_CODE:
441
+ return 400;
425
442
  case "NOT_FOUND":
426
443
  case "not_found":
427
444
  case "NO_DATA":
@@ -885,8 +902,16 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
885
902
  code: "AUTH_FLOW_NOT_CONFIGURED",
886
903
  });
887
904
  }
905
+ // Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
906
+ // depend on declared secrets (client ids/secrets), so fail structured before
907
+ // any flow code runs instead of at whatever point the ceremony first reads
908
+ // the env. `abort` stays exempt: a user must always be able to cancel a
909
+ // stranded flow even when provisioning is broken.
888
910
  const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
889
911
  try {
912
+ if (route !== "abort") {
913
+ assertRequiredSecretsPresent(provider, context.env);
914
+ }
890
915
  const result = route === "start"
891
916
  ? await flow.start(context)
892
917
  : route === "continue"
@@ -933,6 +958,20 @@ export function createServerApp(provider, options = {}) {
933
958
  providerId: provider.id,
934
959
  allowMemoryFallback: options.allowMemoryStateFallback === true,
935
960
  });
961
+ // Boot-time visibility for unprovisioned declared secrets: emit a structured
962
+ // warn so deploy tooling/alerting sees the gap the moment the pod boots,
963
+ // instead of discovering it request-by-request. Deliberately log-only — a
964
+ // boot crash would trade a structured MISSING_SECRET signal for
965
+ // CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
966
+ const missingSecretsAtBoot = listMissingRequiredSecrets(provider, createEnvContext(provider.secrets?.map((secret) => secret.name)));
967
+ if (missingSecretsAtBoot.length > 0) {
968
+ logger({
969
+ level: "warn",
970
+ event: "provider_secrets_missing",
971
+ providerId: provider.id,
972
+ missingSecrets: missingSecretsAtBoot,
973
+ });
974
+ }
936
975
  app.notFound((c) => c.json({
937
976
  error: {
938
977
  code: "not_found",
@@ -1119,6 +1158,7 @@ export async function serve(provider, options = {}) {
1119
1158
  const selfTestApp = createSelfTestApp(provider, {
1120
1159
  secrets: selfTestSecrets,
1121
1160
  invoke: createSelfTestInvoke(app),
1161
+ authFlow: createSelfTestAuthFlowInvoke(app),
1122
1162
  });
1123
1163
  bunRuntime.serve({
1124
1164
  port: options.selfTestPort ?? resolveSelfTestPort(),
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.4",
2
+ "version": "2.2.0-beta.7",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -2,6 +2,7 @@ export type ApifuseCommandName =
2
2
  | "create"
3
3
  | "dev"
4
4
  | "check"
5
+ | "sync-assets"
5
6
  | "submit-check"
6
7
  | "bounty-check"
7
8
  | "record"
@@ -46,6 +47,14 @@ export const COMMAND_MANIFEST: Record<
46
47
  examples: ["apifuse check .", "apifuse check providers/korea-air-quality"],
47
48
  modulePath: "./apifuse-check",
48
49
  },
50
+ "sync-assets": {
51
+ name: "sync-assets",
52
+ summary:
53
+ "Regenerate SDK-managed agent prompt assets (AGENTS.md, .agents/skills, symlinks, manifest) for the installed SDK version.",
54
+ usage: "apifuse sync-assets [path] [--check]",
55
+ examples: ["apifuse sync-assets .", "apifuse sync-assets . --check"],
56
+ modulePath: "./apifuse-sync-assets",
57
+ },
49
58
  "submit-check": {
50
59
  name: "submit-check",
51
60
  summary:
@@ -103,6 +112,7 @@ export const COMMAND_ORDER: ApifuseCommandName[] = [
103
112
  "create",
104
113
  "dev",
105
114
  "check",
115
+ "sync-assets",
106
116
  "submit-check",
107
117
  "record",
108
118
  "test",
package/src/cli/create.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
4
4
  import { dirname, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
@@ -8,6 +8,11 @@ import { cancel, intro, isCancel, note, outro, select, text } from "@clack/promp
8
8
  import { z } from "zod";
9
9
 
10
10
  import packageJson from "../../package.json";
11
+ import {
12
+ buildPromptAssetManifest,
13
+ buildPromptAssetPlanEntries,
14
+ PROMPT_ASSET_MANIFEST_PATH,
15
+ } from "./prompt-assets.js";
11
16
 
12
17
  export const PROVIDER_NAME_REGEX = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
13
18
  export const CATEGORY_OPTIONS = [
@@ -64,7 +69,10 @@ export type CreateResolvedOptions = {
64
69
 
65
70
  export type ProviderPlanFile = {
66
71
  path: string;
72
+ /** File content, or the symlink target (relative path) for kind "symlink". */
67
73
  content: string;
74
+ /** Absent kind means "file" (backward compatible with older consumers). */
75
+ kind?: "file" | "symlink";
68
76
  };
69
77
 
70
78
  export type ProviderCreatePlan = {
@@ -410,11 +418,16 @@ export async function buildProviderCreatePlan(
410
418
  );
411
419
  }
412
420
 
421
+ // Exact pin, not a caret range: the prompt-asset freshness gate requires
422
+ // manifest.sdkVersion === installed SDK version, and the manifest records
423
+ // this CLI's own version. A caret range would let `bun install` resolve a
424
+ // newer SDK than the create binary, making a fresh scaffold fail its own
425
+ // gate. `sync-assets` after an explicit SDK upgrade re-records the version.
413
426
  const sdkSpecifier =
414
427
  options.sdkSpecifier ??
415
428
  (options.preset === "monorepo" && resolvedWorkspaceRoot
416
429
  ? "workspace:*"
417
- : `^${packageJson.version}`);
430
+ : packageJson.version);
418
431
  const relativeProviderRoot = relative(cwd, providerRoot) || options.name;
419
432
  const nextDevCommand = `cd ${relativeProviderRoot} && bun run dev`;
420
433
  const packageName =
@@ -538,40 +551,23 @@ export async function buildProviderCreatePlan(
538
551
  PROVIDER_ID: options.name,
539
552
  }),
540
553
  },
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
- },
573
554
  ];
574
555
 
556
+ const promptAssetEntries = await buildPromptAssetPlanEntries(renderTemplate);
557
+ for (const entry of promptAssetEntries) {
558
+ files.push({
559
+ path: resolve(providerRoot, entry.path),
560
+ content: entry.content,
561
+ ...(entry.kind === "symlink" ? { kind: "symlink" as const } : {}),
562
+ });
563
+ }
564
+ // Manifest last: writePlan writes in order, so a crash mid-scaffold never
565
+ // leaves a manifest that claims assets which were not written.
566
+ files.push({
567
+ path: resolve(providerRoot, PROMPT_ASSET_MANIFEST_PATH),
568
+ content: buildPromptAssetManifest(promptAssetEntries, packageJson.version),
569
+ });
570
+
575
571
  return {
576
572
  displayName: options.displayName,
577
573
  files,
@@ -614,6 +610,7 @@ function renderPackageJson(input: { packageName: string; sdkSpecifier: string })
614
610
  check: "apifuse check . && bun run type-check",
615
611
  "type-check": "tsc --noEmit",
616
612
  "submit-check": "apifuse submit-check . --markdown submission-report.md",
613
+ "sync-assets": "apifuse sync-assets .",
617
614
  test: "apifuse test .",
618
615
  record: "apifuse record .",
619
616
  start: "bun start.ts",
@@ -869,6 +866,13 @@ function isApifuseInternalWorkspaceRoot(workspaceRoot: string): boolean {
869
866
  async function writePlan(plan: ProviderCreatePlan): Promise<void> {
870
867
  for (const file of plan.files) {
871
868
  await mkdir(dirname(file.path), { recursive: true });
869
+ if (file.kind === "symlink") {
870
+ // rm operates on the link itself (lstat semantics) and tolerates
871
+ // directories, so whatever occupies the path is replaced by the link.
872
+ await rm(file.path, { recursive: true, force: true });
873
+ await symlink(file.content, file.path);
874
+ continue;
875
+ }
872
876
  await writeFile(file.path, file.content);
873
877
  }
874
878
  }
@@ -940,7 +944,10 @@ function printResult(plan: ProviderCreatePlan, jsonMode: boolean, dryRun: boolea
940
944
  },
941
945
  validationCommands: plan.validationCommands,
942
946
  nextDevCommand: plan.nextDevCommand,
943
- files: plan.files.map((file) => relative(plan.providerRoot, file.path) || file.path),
947
+ files: plan.files.map((file) => {
948
+ const relativePath = relative(plan.providerRoot, file.path) || file.path;
949
+ return file.kind === "symlink" ? `${relativePath} -> ${file.content} (symlink)` : relativePath;
950
+ }),
944
951
  };
945
952
 
946
953
  if (jsonMode) {