@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +53 -0
- package/CHANGELOG.md +12 -0
- package/README.md +5 -1
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +26 -1
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-submit-check.ts +193 -2
- package/bin/apifuse-sync-assets.ts +117 -0
- package/dist/auth-turn/index.d.ts +2 -2
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/config/loader.d.ts +79 -6
- package/dist/config/loader.js +272 -48
- package/dist/define.js +27 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runtime/executor.js +7 -0
- package/dist/runtime/http.js +3 -0
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +34 -0
- package/dist/runtime/proxy-nodemaven.js +128 -0
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +39 -4
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/stealth.js +20 -9
- package/dist/server/serve.d.ts +5 -0
- package/dist/server/serve.js +39 -0
- package/dist/server/types.d.ts +9 -9
- package/dist/types.d.ts +30 -1
- package/package.json +4 -3
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +405 -61
- package/src/define.ts +35 -3
- package/src/index.ts +5 -0
- package/src/runtime/executor.ts +8 -0
- package/src/runtime/http.ts +3 -0
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +178 -0
- package/src/runtime/proxy-telemetry.ts +56 -5
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/stealth.ts +26 -10
- package/src/server/serve.ts +53 -0
- package/src/types.ts +30 -1
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/AUTHORING.md
CHANGED
|
@@ -309,6 +309,59 @@ External contributors are expected to submit standalone Provider source plus:
|
|
|
309
309
|
Maintainers own monorepo import under `providers/<id>/`, registry generation,
|
|
310
310
|
deployment projection checks, and release workflows.
|
|
311
311
|
|
|
312
|
+
### Declared secrets are SDK-enforced
|
|
313
|
+
|
|
314
|
+
Environment/secret presence validation is single-sourced in the SDK. Declare
|
|
315
|
+
every env secret the provider needs in `defineProvider`:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
secrets: [
|
|
319
|
+
{
|
|
320
|
+
name: "APIFUSE__PROVIDER__MY_PROVIDER__API_KEY",
|
|
321
|
+
required: true,
|
|
322
|
+
description: "Upstream API key from the vendor portal",
|
|
323
|
+
},
|
|
324
|
+
],
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The runtime validates every `required: true` declaration before any operation
|
|
328
|
+
handler or auth-flow handler (except `abort`) runs. When a required secret is
|
|
329
|
+
unset or whitespace-only, the invocation fails with the canonical structured
|
|
330
|
+
error — code `MISSING_SECRET`, HTTP 400, `details.category:
|
|
331
|
+
"credential_unavailable"`, `retryable: false`, and a `fix` naming every missing
|
|
332
|
+
secret — across `/v1/{operation}`, self-test probes, `apifuse perf`, and
|
|
333
|
+
`apifuse record`. The server also emits a `provider_secrets_missing` warn log
|
|
334
|
+
at boot so unprovisioned deployments are visible immediately without crashing
|
|
335
|
+
the pod.
|
|
336
|
+
|
|
337
|
+
Provider-local presence re-validation is **deprecated**: do not write
|
|
338
|
+
`requireServiceKey`/`requireApiKey`-style guards that re-check `ctx.env.get()`
|
|
339
|
+
and throw a hand-rolled `CONFIGURATION_ERROR`/`MISSING_SECRET`. Those guards
|
|
340
|
+
are dead weight (the SDK gate runs first) and historically diverged into
|
|
341
|
+
inconsistent error shapes. The `sdk-owned-secret-presence` submit-check rule
|
|
342
|
+
flags them at warn level; acknowledge a deliberate exception with
|
|
343
|
+
`// @apifuse-allow sdk-owned-secret-presence: <reason>`.
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
// Before (deprecated): provider-local double validation
|
|
347
|
+
function requireServiceKey(ctx: ProviderContext): string {
|
|
348
|
+
const value = ctx.env.get(SERVICE_KEY_ENV);
|
|
349
|
+
if (!value?.trim()) {
|
|
350
|
+
throw new ProviderError(`Missing required provider secret: ${SERVICE_KEY_ENV}`, {
|
|
351
|
+
code: "CONFIGURATION_ERROR",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return value;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// After: declare { name: SERVICE_KEY_ENV, required: true } and read directly.
|
|
358
|
+
const serviceKey = ctx.env.get(SERVICE_KEY_ENV);
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Note the asymmetry: the gate treats whitespace-only values as missing, but
|
|
362
|
+
`ctx.env.get()` still returns the raw value to handlers — trim at the point of
|
|
363
|
+
use if the upstream is whitespace-sensitive.
|
|
364
|
+
|
|
312
365
|
### Public local debugging checklist
|
|
313
366
|
|
|
314
367
|
- Operation smoke requests use the provider server envelope:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.8
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 9e8a3f028ee78b9cab29d4aa3f5494ac9cffa65f.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.7
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit 2ce4ea4bd36ce333eba8b3b474bf6e82b5e9216c.
|
|
10
|
+
|
|
11
|
+
## 2.2.0-beta.6
|
|
12
|
+
|
|
13
|
+
- Release candidate for main commit 17f4e41d44efe7c148ef875b950be4f2c7df1294.
|
|
14
|
+
|
|
3
15
|
## 2.2.0-beta.5
|
|
4
16
|
|
|
5
17
|
- Release candidate for main commit 82fa14e99a9af7edd44e3196aa3f4e87b4699edf.
|
package/README.md
CHANGED
|
@@ -119,7 +119,11 @@ the bad request path; provider/runtime failures include `code`, `message`, and
|
|
|
119
119
|
local-only values through `connection.secrets`. Read them in handlers with
|
|
120
120
|
`ctx.credential.get("key")` or `ctx.credential.getAccessToken()`.
|
|
121
121
|
- **Provider env secrets**: declare `secrets[]`, set values in your shell or
|
|
122
|
-
`.env`, and read only those names through `ctx.env.get("NAME")`.
|
|
122
|
+
`.env`, and read only those names through `ctx.env.get("NAME")`. The SDK
|
|
123
|
+
enforces presence of `required: true` declarations before handlers and auth
|
|
124
|
+
flows run, failing the invocation with a structured `MISSING_SECRET` error
|
|
125
|
+
(HTTP 400, category `credential_unavailable`) — do not re-check presence in
|
|
126
|
+
handlers.
|
|
123
127
|
- **Credentials auth flows**: prefer `defineCredentialsAuth()` over hand-written
|
|
124
128
|
`auth.flow`. Declare the form fields and credential keys once, then put the
|
|
125
129
|
upstream login/session creation in `login(ctx, input)`. Return
|
package/SUBMISSION.md
CHANGED
|
@@ -50,7 +50,7 @@ Fix all blockers before submitting:
|
|
|
50
50
|
- High-confidence secret or token material in source, README, package metadata, or fixtures.
|
|
51
51
|
- SDK-native source blockers: prefixed Provider ids, `vendor/` SDK shims or imports, raw `.describe()` prose instead of `describeKey`, raw global `fetch()` calls, and excessive `as Type` assertions.
|
|
52
52
|
|
|
53
|
-
Warnings do not fail the command, but they should be addressed when practical. For example, the generated starter `ping` operation warns because it is not a real upstream-backed bounty Operation. SDK-native warnings also flag moderate `as Type` assertion counts
|
|
53
|
+
Warnings do not fail the command, but they should be addressed when practical. For example, the generated starter `ping` operation warns because it is not a real upstream-backed bounty Operation. SDK-native warnings also flag moderate `as Type` assertion counts, credentialed Providers that never reference `ctx.credential`, and provider-local re-validation of declared env secrets (`sdk-owned-secret-presence`, 0 points): the SDK already rejects invocations with a structured `MISSING_SECRET` error when a declared `required: true` secret is unset, so `requireServiceKey`-style presence guards are deprecated dead weight — delete the guard and read the value directly with `ctx.env.get()`, or acknowledge a deliberate exception with `// @apifuse-allow sdk-owned-secret-presence: <reason>`.
|
|
54
54
|
|
|
55
55
|
## Measured local smoke
|
|
56
56
|
|
package/bin/apifuse-check.ts
CHANGED
|
@@ -6,6 +6,11 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
|
|
9
|
+
import {
|
|
10
|
+
formatPromptAssetIssues,
|
|
11
|
+
PROMPT_ASSET_SYNC_REMEDIATION,
|
|
12
|
+
verifyPromptAssets,
|
|
13
|
+
} from "../src/cli/prompt-assets.js";
|
|
9
14
|
import type { ProviderDefinition } from "../src/index.js";
|
|
10
15
|
import { lintProvider, type ProviderLintMode } from "../src/lint.js";
|
|
11
16
|
import { safeParseSchemaSync } from "../src/schema.js";
|
|
@@ -61,7 +66,7 @@ function normalizeArgs(argv: string[]): string[] {
|
|
|
61
66
|
return argv[0] === "check" ? argv.slice(1) : argv;
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
function resolveProviderRoot(inputPath: string): string {
|
|
69
|
+
export function resolveProviderRoot(inputPath: string): string {
|
|
65
70
|
const resolvedInput = resolveFromParents(inputPath);
|
|
66
71
|
|
|
67
72
|
if (!existsSync(resolvedInput)) {
|
|
@@ -129,9 +134,26 @@ export async function runChecks(
|
|
|
129
134
|
checkProviderMetadata(provider),
|
|
130
135
|
checkDockerfile(dockerfilePath),
|
|
131
136
|
checkPackageJson(packageJsonPath),
|
|
137
|
+
checkPromptAssets(providerRoot),
|
|
132
138
|
];
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
export const PROMPT_ASSETS_CHECK_MESSAGE =
|
|
142
|
+
"Agent prompt assets match the installed SDK version";
|
|
143
|
+
|
|
144
|
+
function checkPromptAssets(providerRoot: string): CheckResult {
|
|
145
|
+
const verification = verifyPromptAssets(providerRoot);
|
|
146
|
+
if (verification.ok) {
|
|
147
|
+
return { message: PROMPT_ASSETS_CHECK_MESSAGE, passed: true };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
message: PROMPT_ASSETS_CHECK_MESSAGE,
|
|
152
|
+
passed: false,
|
|
153
|
+
details: [...formatPromptAssetIssues(verification), PROMPT_ASSET_SYNC_REMEDIATION],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
135
157
|
function isScannableProviderSourceFile(relativePath: string): boolean {
|
|
136
158
|
return (
|
|
137
159
|
/\.(?:ts|tsx|js|jsx|mjs|cjs|sh|bash)$/.test(relativePath) ||
|
|
@@ -142,6 +164,9 @@ function isScannableProviderSourceFile(relativePath: string): boolean {
|
|
|
142
164
|
|
|
143
165
|
function collectProviderSourceFiles(providerRoot: string): Record<string, string> {
|
|
144
166
|
const sources: Record<string, string> = {};
|
|
167
|
+
// `.agents`/`.apifuse` are deliberately not skipped: managed content there
|
|
168
|
+
// is markdown/JSON (never matched by isScannableProviderSourceFile), and a
|
|
169
|
+
// planted `.ts`/`.sh` under those directories must stay in scanner scope.
|
|
145
170
|
const skipDirectories = new Set([".git", "node_modules", "dist", "build", ".next"]);
|
|
146
171
|
const visit = (directory: string) => {
|
|
147
172
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
@@ -48,6 +48,13 @@ const requiredPaths = [
|
|
|
48
48
|
"src/cli/templates/provider/operations/ping.ts.tpl",
|
|
49
49
|
"src/cli/templates/provider/schemas/ping.ts.tpl",
|
|
50
50
|
"src/cli/templates/provider/upstream/README.md.tpl",
|
|
51
|
+
"src/cli/templates/provider/AGENTS.md.tpl",
|
|
52
|
+
"src/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl",
|
|
53
|
+
"src/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl",
|
|
54
|
+
"src/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl",
|
|
55
|
+
"src/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl",
|
|
56
|
+
"src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl",
|
|
57
|
+
"src/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl",
|
|
51
58
|
"dist/cli/templates/provider/.dockerignore.tpl",
|
|
52
59
|
"dist/cli/templates/provider/.gitignore.tpl",
|
|
53
60
|
"dist/cli/templates/provider/Dockerfile.tpl",
|
|
@@ -60,6 +67,13 @@ const requiredPaths = [
|
|
|
60
67
|
"dist/cli/templates/provider/operations/ping.ts.tpl",
|
|
61
68
|
"dist/cli/templates/provider/schemas/ping.ts.tpl",
|
|
62
69
|
"dist/cli/templates/provider/upstream/README.md.tpl",
|
|
70
|
+
"dist/cli/templates/provider/AGENTS.md.tpl",
|
|
71
|
+
"dist/cli/templates/provider/.agents/skills/normalization-standards/SKILL.md.tpl",
|
|
72
|
+
"dist/cli/templates/provider/.agents/skills/upstream-contract-verification/SKILL.md.tpl",
|
|
73
|
+
"dist/cli/templates/provider/.agents/skills/fixtures-and-recording/SKILL.md.tpl",
|
|
74
|
+
"dist/cli/templates/provider/.agents/skills/pagination-and-counts/SKILL.md.tpl",
|
|
75
|
+
"dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl",
|
|
76
|
+
"dist/cli/templates/provider/.agents/skills/upstream-notes/README.md.tpl",
|
|
63
77
|
"dist/auth-turn/index.js",
|
|
64
78
|
"dist/auth-turn/index.d.ts",
|
|
65
79
|
"dist/auth-turn/auth-turn.v1.schema.json",
|
|
@@ -11,6 +11,10 @@ import * as acorn from "acorn";
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
|
|
13
13
|
import packageJson from "../package.json";
|
|
14
|
+
import {
|
|
15
|
+
formatPromptAssetIssues,
|
|
16
|
+
verifyPromptAssets,
|
|
17
|
+
} from "../src/cli/prompt-assets.js";
|
|
14
18
|
import type { ProviderDefinition } from "../src/index.js";
|
|
15
19
|
import {
|
|
16
20
|
loadProviderLocaleCatalogs,
|
|
@@ -19,7 +23,7 @@ import {
|
|
|
19
23
|
} from "../src/i18n/index.js";
|
|
20
24
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY } from "../src/schema.js";
|
|
21
25
|
import { safeParseSchemaSync } from "../src/schema.js";
|
|
22
|
-
import { type CheckResult, runChecks } from "./apifuse-check.js";
|
|
26
|
+
import { type CheckResult, PROMPT_ASSETS_CHECK_MESSAGE, runChecks } from "./apifuse-check.js";
|
|
23
27
|
import { hasSubstantiveXmlStructure } from "./submit-check-xml.js";
|
|
24
28
|
|
|
25
29
|
const TIERS = ["bronze", "silver", "gold", "diamond"] as const;
|
|
@@ -267,7 +271,15 @@ export async function buildSubmitCheckReport(
|
|
|
267
271
|
const baseChecks = await safeRunChecks(providerRoot);
|
|
268
272
|
const provider = await safeLoadProvider(providerRoot);
|
|
269
273
|
|
|
270
|
-
|
|
274
|
+
// Prompt-asset freshness is reported by its own dedicated zero-point
|
|
275
|
+
// blocker below; filter the base-check duplicate so it is not double
|
|
276
|
+
// penalized under the definition category.
|
|
277
|
+
checks.push(
|
|
278
|
+
...scoreBaseChecks(
|
|
279
|
+
baseChecks.filter((result) => result.message !== PROMPT_ASSETS_CHECK_MESSAGE),
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
checks.push(scorePromptAssetFreshness(providerRoot));
|
|
271
283
|
checks.push(scoreProviderIdSlug(providerRoot, provider));
|
|
272
284
|
checks.push(scoreNoVendorShim(providerRoot));
|
|
273
285
|
checks.push(scoreNoVendorImport(providerRoot));
|
|
@@ -283,6 +295,7 @@ export async function buildSubmitCheckReport(
|
|
|
283
295
|
if (provider) {
|
|
284
296
|
const smokeResult = args.smoke ? await runSubmitCheckSmoke(providerRoot, provider) : undefined;
|
|
285
297
|
checks.push(scoreCredentialUsage(providerRoot, provider));
|
|
298
|
+
checks.push(scoreSdkOwnedSecretPresence(providerRoot, provider));
|
|
286
299
|
checks.push(scoreLocaleCatalog(providerRoot, provider));
|
|
287
300
|
checks.push(scoreOperationMetadata(provider));
|
|
288
301
|
checks.push(scoreFixtureCoverage(provider));
|
|
@@ -1424,6 +1437,159 @@ function scoreCredentialUsage(providerRoot: string, provider: ProviderDefinition
|
|
|
1424
1437
|
);
|
|
1425
1438
|
}
|
|
1426
1439
|
|
|
1440
|
+
// ---------------------------------------------------------------------------
|
|
1441
|
+
// sdk-owned-secret-presence (warn): provider-local double validation of
|
|
1442
|
+
// declared env secrets.
|
|
1443
|
+
//
|
|
1444
|
+
// The SDK runtime is the single source of truth for secret presence: declared
|
|
1445
|
+
// `required: true` secrets are validated before every handler/auth-flow
|
|
1446
|
+
// invocation and fail with the canonical structured MISSING_SECRET error
|
|
1447
|
+
// (HTTP 400, category credential_unavailable). Provider-local presence guards
|
|
1448
|
+
// (requireServiceKey/requireApiKey style) are dead weight that historically
|
|
1449
|
+
// diverged into inconsistent shapes (CONFIGURATION_ERROR vs MISSING_SECRET,
|
|
1450
|
+
// with/without category), which broke uniform incident attribution when nine
|
|
1451
|
+
// providers shipped with unprovisioned secrets (2026-07-22).
|
|
1452
|
+
//
|
|
1453
|
+
// Heuristic, warn-only: a line reading a declared `required: true` secret via
|
|
1454
|
+
// `.env.get(...)` (string literal or a const alias of a declared name)
|
|
1455
|
+
// followed within a small window by a falsy presence check plus a `throw`.
|
|
1456
|
+
// The rule flags duplication of the SDK gate ONLY: env names that are not
|
|
1457
|
+
// declared in defineProvider secrets[], and optional declarations
|
|
1458
|
+
// (`required: false`/omitted) that the runtime deliberately does not enforce,
|
|
1459
|
+
// are out of scope. Escape hatch:
|
|
1460
|
+
// `// @apifuse-allow sdk-owned-secret-presence: <reason>`.
|
|
1461
|
+
// ---------------------------------------------------------------------------
|
|
1462
|
+
|
|
1463
|
+
const SDK_OWNED_SECRET_PRESENCE_RULE_ID = "sdk-owned-secret-presence";
|
|
1464
|
+
const SECRET_PRESENCE_GUARD_LOOKAHEAD_LINES = 10;
|
|
1465
|
+
|
|
1466
|
+
const ENV_GET_CALL_PATTERN =
|
|
1467
|
+
/\.env\.get\(\s*(?:"([^"]+)"|'([^']+)'|`([^`$]+)`|([A-Za-z_$][\w$]*))\s*\)/;
|
|
1468
|
+
|
|
1469
|
+
const SECRET_ALIAS_CONST_PATTERN =
|
|
1470
|
+
/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*(?:"([^"]+)"|'([^']+)'|`([^`$]+)`)/g;
|
|
1471
|
+
|
|
1472
|
+
const ENV_GET_ASSIGNMENT_PATTERN = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=[^;]*\.env\.get\(/;
|
|
1473
|
+
|
|
1474
|
+
// Const aliases of declared secret names (e.g. `const SERVICE_KEY_ENV =
|
|
1475
|
+
// "APIFUSE__PROVIDER__X__SERVICE_KEY"`) so aliased `.env.get(SERVICE_KEY_ENV)`
|
|
1476
|
+
// guards are detected, not just direct string literals.
|
|
1477
|
+
function buildDeclaredSecretAliasMap(
|
|
1478
|
+
providerRoot: string,
|
|
1479
|
+
declaredNames: ReadonlySet<string>,
|
|
1480
|
+
): Map<string, string> {
|
|
1481
|
+
const aliases = new Map<string, string>();
|
|
1482
|
+
for (const filePath of listNonTestTypeScriptFiles(providerRoot)) {
|
|
1483
|
+
const content = readFileSync(filePath, "utf8");
|
|
1484
|
+
for (const match of content.matchAll(SECRET_ALIAS_CONST_PATTERN)) {
|
|
1485
|
+
const alias = match[1];
|
|
1486
|
+
const name = match[2] ?? match[3] ?? match[4];
|
|
1487
|
+
if (alias && name && declaredNames.has(name)) {
|
|
1488
|
+
aliases.set(alias, name);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return aliases;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
function hasLocalSecretPresenceGuard(
|
|
1496
|
+
line: string,
|
|
1497
|
+
remainingLines: readonly string[],
|
|
1498
|
+
declaredNames: ReadonlySet<string>,
|
|
1499
|
+
aliases: ReadonlyMap<string, string>,
|
|
1500
|
+
): boolean {
|
|
1501
|
+
const match = ENV_GET_CALL_PATTERN.exec(line);
|
|
1502
|
+
if (!match) {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
const literal = match[1] ?? match[2] ?? match[3];
|
|
1506
|
+
const identifier = match[4];
|
|
1507
|
+
const readsDeclaredSecret =
|
|
1508
|
+
literal !== undefined
|
|
1509
|
+
? declaredNames.has(literal)
|
|
1510
|
+
: identifier !== undefined && aliases.has(identifier);
|
|
1511
|
+
if (!readsDeclaredSecret) {
|
|
1512
|
+
return false;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const window = [line, ...remainingLines.slice(0, SECRET_PRESENCE_GUARD_LOOKAHEAD_LINES)];
|
|
1516
|
+
if (!window.some((candidate) => /\bthrow\b/.test(candidate))) {
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Assigned read (`const key = ctx.env.get(...)`): only a falsy/undefined
|
|
1521
|
+
// check on THAT variable counts as a presence guard. Anchoring on the
|
|
1522
|
+
// assigned identifier avoids false positives from unrelated guards/throws
|
|
1523
|
+
// that merely sit near the env read (mirrors the aliased runtime-guard rule).
|
|
1524
|
+
const assigned = ENV_GET_ASSIGNMENT_PATTERN.exec(line)?.[1];
|
|
1525
|
+
if (assigned) {
|
|
1526
|
+
const escaped = assigned.replace(/\$/g, "\\$");
|
|
1527
|
+
const guardPattern = new RegExp(
|
|
1528
|
+
`(?:!\\s*${escaped}\\b|\\b${escaped}\\s*===?\\s*(?:undefined|null)\\b|\\b${escaped}\\s*==\\s*null\\b|\\b${escaped}(?:\\?\\.|\\.)length\\s*===?\\s*0\\b)`,
|
|
1529
|
+
);
|
|
1530
|
+
return window.some((candidate) => guardPattern.test(candidate));
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// Un-assigned read: only an inline presence check on the same line counts,
|
|
1534
|
+
// e.g. `if (!ctx.env.get(KEY)) throw ...`.
|
|
1535
|
+
return /(?:if\s*\(\s*!|===?\s*undefined\b|==\s*null\b)/.test(line);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function scoreSdkOwnedSecretPresence(
|
|
1539
|
+
providerRoot: string,
|
|
1540
|
+
provider: ProviderDefinition,
|
|
1541
|
+
): SubmitCheck {
|
|
1542
|
+
const passMessage = "Provider relies on SDK-owned secret presence validation.";
|
|
1543
|
+
// Only `required: true` declarations: those are exactly what the runtime
|
|
1544
|
+
// gate enforces. A presence guard over an optional secret is conditional
|
|
1545
|
+
// business logic the SDK will not replace, not double validation.
|
|
1546
|
+
const declaredNames: ReadonlySet<string> = new Set(
|
|
1547
|
+
(provider.secrets ?? [])
|
|
1548
|
+
.filter((secret) => secret.required === true)
|
|
1549
|
+
.map((secret) => secret.name),
|
|
1550
|
+
);
|
|
1551
|
+
if (declaredNames.size === 0) {
|
|
1552
|
+
return pass(SDK_OWNED_SECRET_PRESENCE_RULE_ID, SDK_NATIVE_CATEGORY, passMessage, 0);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const aliases = buildDeclaredSecretAliasMap(providerRoot, declaredNames);
|
|
1556
|
+
const findings = findSourceFindings(providerRoot, (line, remainingLines) =>
|
|
1557
|
+
hasLocalSecretPresenceGuard(line, remainingLines, declaredNames, aliases),
|
|
1558
|
+
);
|
|
1559
|
+
if (findings.length === 0) {
|
|
1560
|
+
return pass(SDK_OWNED_SECRET_PRESENCE_RULE_ID, SDK_NATIVE_CATEGORY, passMessage, 0);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
const { violations, overridden } = partitionAllowOverrides(
|
|
1564
|
+
providerRoot,
|
|
1565
|
+
findings,
|
|
1566
|
+
SDK_OWNED_SECRET_PRESENCE_RULE_ID,
|
|
1567
|
+
);
|
|
1568
|
+
if (violations.length === 0) {
|
|
1569
|
+
return pass(
|
|
1570
|
+
SDK_OWNED_SECRET_PRESENCE_RULE_ID,
|
|
1571
|
+
SDK_NATIVE_CATEGORY,
|
|
1572
|
+
`${passMessage} ${overridden.length} acknowledged @apifuse-allow override(s).`,
|
|
1573
|
+
0,
|
|
1574
|
+
formatSourceFindings(overridden),
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
return {
|
|
1579
|
+
id: SDK_OWNED_SECRET_PRESENCE_RULE_ID,
|
|
1580
|
+
category: SDK_NATIVE_CATEGORY,
|
|
1581
|
+
level: "warn",
|
|
1582
|
+
status: "warn",
|
|
1583
|
+
points: 0,
|
|
1584
|
+
maxPoints: 0,
|
|
1585
|
+
message:
|
|
1586
|
+
"Provider source re-validates declared env secret presence locally; the SDK owns this check.",
|
|
1587
|
+
remediation:
|
|
1588
|
+
"The provider SDK validates declared required secrets before handlers and auth flows run and returns the canonical MISSING_SECRET error (HTTP 400, category credential_unavailable). Declare the secret with required: true in defineProvider({ secrets: [...] }), delete the provider-local presence guard (requireServiceKey/requireApiKey style), and read the value directly with ctx.env.get(); the guard is dead weight and its divergent CONFIGURATION_ERROR-style shape is deprecated. Acknowledge intentional exceptions with `// @apifuse-allow sdk-owned-secret-presence: <reason>`.",
|
|
1589
|
+
evidence: formatSourceFindings(violations),
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1427
1593
|
function findSourceLineMatches(
|
|
1428
1594
|
providerRoot: string,
|
|
1429
1595
|
pattern: RegExp | ((line: string) => boolean),
|
|
@@ -1523,6 +1689,10 @@ function isScannableProviderSourceFile(relativePath: string): boolean {
|
|
|
1523
1689
|
);
|
|
1524
1690
|
}
|
|
1525
1691
|
|
|
1692
|
+
// `.agents`/`.apifuse` stay IN scope on purpose: managed content there is
|
|
1693
|
+
// markdown/JSON (never scannable), while a planted `.ts`/`.sh` under those
|
|
1694
|
+
// directories must not become a scan-exempt hiding place for secrets, raw
|
|
1695
|
+
// fetch, or vendor imports.
|
|
1526
1696
|
function shouldScanSourceDirectory(relativePath: string): boolean {
|
|
1527
1697
|
return ![".git", "node_modules", "dist", "build", "coverage"].includes(relativePath);
|
|
1528
1698
|
}
|
|
@@ -1545,6 +1715,27 @@ function formatSourceFindings(findings: readonly SourceFinding[]): string[] {
|
|
|
1545
1715
|
return findings.map((finding) => `${finding.file}:${finding.line}`);
|
|
1546
1716
|
}
|
|
1547
1717
|
|
|
1718
|
+
function scorePromptAssetFreshness(providerRoot: string): SubmitCheck {
|
|
1719
|
+
const verification = verifyPromptAssets(providerRoot);
|
|
1720
|
+
if (verification.ok) {
|
|
1721
|
+
return pass(
|
|
1722
|
+
"prompt-assets-fresh",
|
|
1723
|
+
"docs",
|
|
1724
|
+
"SDK-managed agent prompt assets match the installed SDK version.",
|
|
1725
|
+
0,
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
return blocker(
|
|
1730
|
+
"prompt-assets-fresh",
|
|
1731
|
+
"docs",
|
|
1732
|
+
"SDK-managed agent prompt assets are missing, stale, or modified.",
|
|
1733
|
+
"Run `bun run sync-assets` (or `bunx apifuse sync-assets .`) to regenerate AGENTS.md, .agents/skills/**, the CLAUDE.md/.claude/.codex symlinks, and .apifuse/prompt-assets.json for the installed SDK version.",
|
|
1734
|
+
0,
|
|
1735
|
+
formatPromptAssetIssues(verification),
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1548
1739
|
function scoreRepositoryDx(providerRoot: string): SubmitCheck {
|
|
1549
1740
|
const missing: string[] = [];
|
|
1550
1741
|
if (!existsSync(resolve(providerRoot, ".gitignore"))) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
formatPromptAssetIssues,
|
|
5
|
+
installedSdkVersion,
|
|
6
|
+
syncPromptAssets,
|
|
7
|
+
verifyPromptAssets,
|
|
8
|
+
} from "../src/cli/prompt-assets.js";
|
|
9
|
+
import { resolveProviderRoot } from "./apifuse-check.js";
|
|
10
|
+
|
|
11
|
+
const HELP_TEXT = `Usage: apifuse sync-assets [path] [--check]
|
|
12
|
+
Example: apifuse sync-assets .
|
|
13
|
+
Default: apifuse sync-assets .
|
|
14
|
+
|
|
15
|
+
Regenerates the SDK-managed agent prompt assets for the installed SDK version:
|
|
16
|
+
AGENTS.md, .agents/skills/**, the CLAUDE.md/.claude/.codex symlinks, and the
|
|
17
|
+
.apifuse/prompt-assets.json manifest. Legacy top-level skills/ layouts are
|
|
18
|
+
migrated. Idempotent.
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--check Verify only; exit 1 with a diff list when assets are stale/missing/modified
|
|
22
|
+
--help, -h Show this help`;
|
|
23
|
+
|
|
24
|
+
export async function main() {
|
|
25
|
+
const args = normalizeArgs(process.argv.slice(2));
|
|
26
|
+
|
|
27
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
28
|
+
console.log(HELP_TEXT);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let checkOnly = false;
|
|
33
|
+
let inputPath: string | undefined;
|
|
34
|
+
for (const arg of args) {
|
|
35
|
+
if (arg === "--check") {
|
|
36
|
+
checkOnly = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (arg.startsWith("-")) {
|
|
40
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
41
|
+
}
|
|
42
|
+
if (inputPath !== undefined) {
|
|
43
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
44
|
+
}
|
|
45
|
+
inputPath = arg;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const providerRoot = resolveProviderRoot(inputPath ?? ".");
|
|
49
|
+
|
|
50
|
+
if (checkOnly) {
|
|
51
|
+
const verification = verifyPromptAssets(providerRoot);
|
|
52
|
+
if (verification.ok) {
|
|
53
|
+
console.log(
|
|
54
|
+
`Prompt assets are in sync with the installed SDK (${installedSdkVersion()}): ${providerRoot}`,
|
|
55
|
+
);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
console.error(`Prompt assets are out of sync in ${providerRoot}:`);
|
|
59
|
+
for (const issue of formatPromptAssetIssues(verification)) {
|
|
60
|
+
console.error(` - ${issue}`);
|
|
61
|
+
}
|
|
62
|
+
console.error("\nRun `bun run sync-assets` (or `bunx apifuse sync-assets .`) to regenerate.");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result = syncPromptAssets(providerRoot);
|
|
67
|
+
|
|
68
|
+
// Honesty gate: writes alone never imply success. sync-assets intentionally
|
|
69
|
+
// PRESERVES (does not delete) unauthorized skills and symlinks under
|
|
70
|
+
// .agents/skills, so it can return changed:false while verify still fails.
|
|
71
|
+
// Re-verify AFTER writing and let the true post-sync state drive the exit
|
|
72
|
+
// code — never claim success while the freshness gate would reject the tree.
|
|
73
|
+
const verification = verifyPromptAssets(providerRoot);
|
|
74
|
+
|
|
75
|
+
if (result.changed) {
|
|
76
|
+
for (const removed of result.removed) {
|
|
77
|
+
console.log(`removed ${removed}`);
|
|
78
|
+
}
|
|
79
|
+
for (const wrote of result.wroteFiles) {
|
|
80
|
+
console.log(`wrote ${wrote}`);
|
|
81
|
+
}
|
|
82
|
+
for (const link of result.createdSymlinks) {
|
|
83
|
+
console.log(`symlink ${link}`);
|
|
84
|
+
}
|
|
85
|
+
console.log(`manifest ${result.manifestPath} (sdkVersion ${installedSdkVersion()})`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!verification.ok) {
|
|
89
|
+
console.error(`\nPrompt assets are still out of sync in ${providerRoot}:`);
|
|
90
|
+
for (const issue of formatPromptAssetIssues(verification)) {
|
|
91
|
+
console.error(` - ${issue}`);
|
|
92
|
+
}
|
|
93
|
+
console.error(
|
|
94
|
+
"\nsync-assets never deletes unrecognized content: resolve these by hand — remove any unauthorized skill directory or symlink under .agents/skills/, migrate a real .claude/.codex directory into .agents/ — then re-run `apifuse sync-assets .`.",
|
|
95
|
+
);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!result.changed) {
|
|
100
|
+
console.log(
|
|
101
|
+
`Prompt assets already in sync with the installed SDK (${installedSdkVersion()}): ${providerRoot}`,
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log(`\nPrompt assets synced: ${providerRoot}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeArgs(argv: string[]): string[] {
|
|
109
|
+
return argv[0] === "sync-assets" ? argv.slice(1) : argv;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (import.meta.main) {
|
|
113
|
+
await main().catch((error: unknown) => {
|
|
114
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -87,7 +87,7 @@ export declare const AUTH_TURN_SCHEMA: {
|
|
|
87
87
|
};
|
|
88
88
|
readonly $defs: {
|
|
89
89
|
readonly completeTurnData: {
|
|
90
|
-
readonly title:
|
|
90
|
+
readonly title: "Terminal payload for kind \"complete\"";
|
|
91
91
|
readonly description: "data payload of a complete turn. The gateway extracts data.credential for persistence; complete turns are never echoed to browsers.";
|
|
92
92
|
readonly type: "object";
|
|
93
93
|
readonly additionalProperties: true;
|
|
@@ -104,7 +104,7 @@ export declare const AUTH_TURN_SCHEMA: {
|
|
|
104
104
|
};
|
|
105
105
|
};
|
|
106
106
|
readonly abortTurnData: {
|
|
107
|
-
readonly title:
|
|
107
|
+
readonly title: "Terminal payload for kind \"abort\"";
|
|
108
108
|
readonly description: "data payload of an abort turn. code, when present, is the machine-readable abort reason.";
|
|
109
109
|
readonly type: "object";
|
|
110
110
|
readonly additionalProperties: true;
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ApifuseCommandName = "create" | "dev" | "check" | "submit-check" | "bounty-check" | "record" | "test" | "perf";
|
|
1
|
+
export type ApifuseCommandName = "create" | "dev" | "check" | "sync-assets" | "submit-check" | "bounty-check" | "record" | "test" | "perf";
|
|
2
2
|
export type ApifuseCommandManifest = {
|
|
3
3
|
name: ApifuseCommandName;
|
|
4
4
|
summary: string;
|
package/dist/cli/commands.js
CHANGED
|
@@ -23,6 +23,13 @@ export const COMMAND_MANIFEST = {
|
|
|
23
23
|
examples: ["apifuse check .", "apifuse check providers/korea-air-quality"],
|
|
24
24
|
modulePath: "./apifuse-check",
|
|
25
25
|
},
|
|
26
|
+
"sync-assets": {
|
|
27
|
+
name: "sync-assets",
|
|
28
|
+
summary: "Regenerate SDK-managed agent prompt assets (AGENTS.md, .agents/skills, symlinks, manifest) for the installed SDK version.",
|
|
29
|
+
usage: "apifuse sync-assets [path] [--check]",
|
|
30
|
+
examples: ["apifuse sync-assets .", "apifuse sync-assets . --check"],
|
|
31
|
+
modulePath: "./apifuse-sync-assets",
|
|
32
|
+
},
|
|
26
33
|
"submit-check": {
|
|
27
34
|
name: "submit-check",
|
|
28
35
|
summary: "Score provider bounty submission readiness and emit checklist evidence.",
|
|
@@ -73,6 +80,7 @@ export const COMMAND_ORDER = [
|
|
|
73
80
|
"create",
|
|
74
81
|
"dev",
|
|
75
82
|
"check",
|
|
83
|
+
"sync-assets",
|
|
76
84
|
"submit-check",
|
|
77
85
|
"record",
|
|
78
86
|
"test",
|
package/dist/cli/create.d.ts
CHANGED
|
@@ -26,7 +26,10 @@ export type CreateResolvedOptions = {
|
|
|
26
26
|
};
|
|
27
27
|
export type ProviderPlanFile = {
|
|
28
28
|
path: string;
|
|
29
|
+
/** File content, or the symlink target (relative path) for kind "symlink". */
|
|
29
30
|
content: string;
|
|
31
|
+
/** Absent kind means "file" (backward compatible with older consumers). */
|
|
32
|
+
kind?: "file" | "symlink";
|
|
30
33
|
};
|
|
31
34
|
export type ProviderCreatePlan = {
|
|
32
35
|
displayName: string;
|