@apifuse/provider-sdk 2.2.0-beta.5 → 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 (45) hide show
  1. package/AUTHORING.md +53 -0
  2. package/CHANGELOG.md +8 -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 +193 -2
  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/serve.d.ts +5 -0
  22. package/dist/server/serve.js +39 -0
  23. package/package.json +1 -1
  24. package/src/cli/commands.ts +10 -0
  25. package/src/cli/create.ts +42 -35
  26. package/src/cli/prompt-assets.ts +865 -0
  27. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  28. package/src/index.ts +5 -0
  29. package/src/runtime/executor.ts +8 -0
  30. package/src/runtime/secrets.ts +64 -0
  31. package/src/server/serve.ts +53 -0
  32. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  33. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  34. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  35. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  36. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  37. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  38. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  39. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  40. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  41. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  42. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  43. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  44. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  45. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -4,8 +4,8 @@ You are building an APIFuse provider. APIFuse turns messy upstream APIs into
4
4
  normalized, typed, evidence-backed public APIs. A provider that merely proxies
5
5
  the upstream is a failed provider, even if every check passes.
6
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).
7
+ This file is the core contract. Detailed procedures live in `.agents/skills/` —
8
+ load the matching skill BEFORE working on that area (index at the bottom).
9
9
 
10
10
  ## Non-negotiable principles
11
11
 
@@ -75,13 +75,22 @@ bun run submit-check # structural score; a high score does NOT prove quality
75
75
  `submit-check` is a structural gate. Every principle above can be violated
76
76
  while scoring 95/100 — reviewers and CI audit for exactly these classes.
77
77
 
78
+ ## Managed prompt assets
79
+
80
+ `AGENTS.md` and `.agents/skills/**` are generated by the SDK; `CLAUDE.md`,
81
+ `.claude`, and `.codex` are symlinks onto them so every agent CLI reads the
82
+ same contract. Do not hand-edit these files — regenerate them with
83
+ `bun run sync-assets` (or `bunx apifuse sync-assets .`). `apifuse check` and
84
+ `submit-check` enforce a freshness gate: stale or modified prompt assets
85
+ (tracked in `.apifuse/prompt-assets.json`) block submission until re-synced.
86
+
78
87
  ## Skill index — load before working on:
79
88
 
80
89
  | Area | Load |
81
90
  | --- | --- |
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) |
91
+ | Output schemas, mappers, field naming, timestamps, enums | `.agents/skills/normalization-standards/SKILL.md` |
92
+ | Upstream request params, new endpoint wiring, field mapping | `.agents/skills/upstream-contract-verification/SKILL.md` |
93
+ | Recording fixtures, writing tests against fixtures | `.agents/skills/fixtures-and-recording/SKILL.md` |
94
+ | List operations, paging, totals, client-side filtering | `.agents/skills/pagination-and-counts/SKILL.md` |
95
+ | healthCheck blocks, error classification, fail-closed guards | `.agents/skills/health-checks-and-fail-closed/SKILL.md` |
96
+ | Upstream-specific known pitfalls for THIS bounty | `.agents/skills/upstream-notes/` (read every file) |
package/src/index.ts CHANGED
@@ -74,6 +74,11 @@ export {
74
74
  } from "./runtime/instrumentation.js";
75
75
  export { type PrevalidateResult, prevalidate } from "./runtime/prevalidate.js";
76
76
  export { getProviderBaseUrl } from "./runtime/provider.js";
77
+ export {
78
+ assertRequiredSecretsPresent,
79
+ listMissingRequiredSecrets,
80
+ MISSING_SECRET_CODE,
81
+ } from "./runtime/secrets.js";
77
82
  export {
78
83
  createUnsupportedProviderRuntimeState,
79
84
  UnsupportedProviderStateError,
@@ -1,6 +1,7 @@
1
1
  import { isSessionExpiredError, ProviderError, SessionExpiredError } from "../errors.js";
2
2
  import { parseSchema } from "../schema.js";
3
3
  import type { ProviderContext, ProviderDefinition } from "../types.js";
4
+ import { assertRequiredSecretsPresent } from "./secrets.js";
4
5
 
5
6
  export function isStreamingOperation(provider: ProviderDefinition, operationId: string): boolean {
6
7
  const kind = provider.operations[operationId]?.transport?.kind ?? "json";
@@ -34,6 +35,13 @@ export async function executeOperation(
34
35
  });
35
36
  }
36
37
 
38
+ // SDK-owned secret presence gate (single source of truth): declared
39
+ // `required: true` secrets are validated here, before input parsing and the
40
+ // handler, so every invocation path (serve /v1, self-test probes, perf,
41
+ // record) fails with the same structured MISSING_SECRET error instead of a
42
+ // handler-specific crash. Providers must not re-check presence locally.
43
+ assertRequiredSecretsPresent(provider, ctx.env);
44
+
37
45
  const validatedInput = await parseSchema(
38
46
  operation.input,
39
47
  input,
@@ -0,0 +1,64 @@
1
+ import { ProviderSecretError } from "../errors.js";
2
+ import type { EnvContext, ProviderDefinition } from "../types.js";
3
+
4
+ /**
5
+ * Canonical error code for a declared-but-unprovisioned provider secret.
6
+ *
7
+ * The SDK is the single source of truth for env/secret presence validation:
8
+ * providers declare secrets in `defineProvider({ secrets: [...] })` and the
9
+ * runtime enforces presence before any handler or auth-flow code runs.
10
+ * Provider-local presence guards (requireServiceKey/requireApiKey style) are a
11
+ * deprecated antipattern — see the `sdk-owned-secret-presence` submit-check
12
+ * rule.
13
+ */
14
+ export const MISSING_SECRET_CODE = "MISSING_SECRET";
15
+
16
+ /**
17
+ * Names of declared `required: true` secrets whose env values are unset or
18
+ * whitespace-only. Whitespace-only values count as missing for parity with the
19
+ * `.trim()` guards well-built providers used before the SDK owned this check —
20
+ * a blank value provisioned by a broken secret pipeline must not pass the gate.
21
+ */
22
+ export function listMissingRequiredSecrets(
23
+ provider: ProviderDefinition,
24
+ env: EnvContext,
25
+ ): string[] {
26
+ const missing: string[] = [];
27
+ for (const secret of provider.secrets ?? []) {
28
+ if (secret.required !== true) {
29
+ continue;
30
+ }
31
+ const value = env.get(secret.name);
32
+ if (value === undefined || value.trim() === "") {
33
+ missing.push(secret.name);
34
+ }
35
+ }
36
+ return missing;
37
+ }
38
+
39
+ /**
40
+ * Throws the canonical structured missing-secret error when any declared
41
+ * `required: true` secret is absent. All missing names are reported in a
42
+ * single error so operators can provision the full set in one pass instead of
43
+ * discovering them one deploy at a time (the 2026-07-22 unprovisioned-secret
44
+ * incident failure mode).
45
+ */
46
+ export function assertRequiredSecretsPresent(
47
+ provider: ProviderDefinition,
48
+ env: EnvContext,
49
+ ): void {
50
+ const missing = listMissingRequiredSecrets(provider, env);
51
+ if (missing.length === 0) {
52
+ return;
53
+ }
54
+ const names = missing.join(", ");
55
+ throw new ProviderSecretError(
56
+ `Missing required provider secret${missing.length > 1 ? "s" : ""}: ${names}`,
57
+ {
58
+ code: MISSING_SECRET_CODE,
59
+ category: "credential_unavailable",
60
+ retryable: false,
61
+ fix: `Provision ${names} in the provider environment (e.g. Doppler). Declared in defineProvider({ secrets: [...] }).`,
62
+ },
63
+ );
64
+ }
@@ -42,6 +42,11 @@ import {
42
42
  PROXY_POOL_EXHAUSTED_CODE,
43
43
  } from "../runtime/proxy-errors.js";
44
44
  import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
45
+ import {
46
+ assertRequiredSecretsPresent,
47
+ listMissingRequiredSecrets,
48
+ MISSING_SECRET_CODE,
49
+ } from "../runtime/secrets.js";
45
50
  import {
46
51
  createProviderRuntimeStateFromEnv,
47
52
  createUnsupportedProviderRuntimeState,
@@ -408,6 +413,12 @@ export type ProviderServerLogEvent =
408
413
  retryable?: boolean;
409
414
  issues?: Array<{ path: string; code: string; message: string }>;
410
415
  })
416
+ | {
417
+ level: "warn";
418
+ event: "provider_secrets_missing";
419
+ providerId: string;
420
+ missingSecrets: string[];
421
+ }
411
422
  | {
412
423
  level: "warn";
413
424
  event: "provider_cleanup_failed";
@@ -558,6 +569,18 @@ function providerObservabilityDetails(error: unknown):
558
569
  retryable: error.options?.retryable ?? false,
559
570
  };
560
571
  }
572
+ // Missing-secret errors carry the canonical credential_unavailable category
573
+ // so Gateway/observability can attribute the failure to provisioning, not
574
+ // the upstream. Matched by code (not constructor) so both the SDK-owned
575
+ // runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
576
+ // serialize identically, including across duplicate SDK module instances.
577
+ if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
578
+ return {
579
+ category: error.options?.category ?? "credential_unavailable",
580
+ taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
581
+ retryable: error.options?.retryable ?? false,
582
+ };
583
+ }
561
584
  if (!isTransportError(error)) {
562
585
  return undefined;
563
586
  }
@@ -628,6 +651,10 @@ function toStatusCode(error: unknown): 400 | 401 | 404 | 429 | 500 | 502 | 503 |
628
651
  case "AUTH_REQUIRED":
629
652
  case "reauth_required":
630
653
  return 401;
654
+ // Unprovisioned declared secret: a deployment/config defect, never an
655
+ // upstream failure — explicit 400 (was only reached via fallthrough).
656
+ case MISSING_SECRET_CODE:
657
+ return 400;
631
658
  case "NOT_FOUND":
632
659
  case "not_found":
633
660
  case "NO_DATA":
@@ -1224,8 +1251,16 @@ async function handleAuthFlow(
1224
1251
  });
1225
1252
  }
1226
1253
 
1254
+ // Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
1255
+ // depend on declared secrets (client ids/secrets), so fail structured before
1256
+ // any flow code runs instead of at whatever point the ceremony first reads
1257
+ // the env. `abort` stays exempt: a user must always be able to cancel a
1258
+ // stranded flow even when provisioning is broken.
1227
1259
  const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
1228
1260
  try {
1261
+ if (route !== "abort") {
1262
+ assertRequiredSecretsPresent(provider, context.env);
1263
+ }
1229
1264
  const result =
1230
1265
  route === "start"
1231
1266
  ? await flow.start(context)
@@ -1280,6 +1315,24 @@ export function createServerApp(
1280
1315
  allowMemoryFallback: options.allowMemoryStateFallback === true,
1281
1316
  });
1282
1317
 
1318
+ // Boot-time visibility for unprovisioned declared secrets: emit a structured
1319
+ // warn so deploy tooling/alerting sees the gap the moment the pod boots,
1320
+ // instead of discovering it request-by-request. Deliberately log-only — a
1321
+ // boot crash would trade a structured MISSING_SECRET signal for
1322
+ // CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
1323
+ const missingSecretsAtBoot = listMissingRequiredSecrets(
1324
+ provider,
1325
+ createEnvContext(provider.secrets?.map((secret) => secret.name)),
1326
+ );
1327
+ if (missingSecretsAtBoot.length > 0) {
1328
+ logger({
1329
+ level: "warn",
1330
+ event: "provider_secrets_missing",
1331
+ providerId: provider.id,
1332
+ missingSecrets: missingSecretsAtBoot,
1333
+ });
1334
+ }
1335
+
1283
1336
  app.notFound((c) =>
1284
1337
  c.json(
1285
1338
  {
@@ -1 +0,0 @@
1
- @AGENTS.md
@@ -1 +0,0 @@
1
- @AGENTS.md