@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.31
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 +493 -5
- package/CHANGELOG.md +131 -1
- package/README.md +52 -6
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +106 -62
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +63 -55
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +78 -82
- package/bin/apifuse-pack-types.ts +583 -0
- package/bin/apifuse-perf.ts +59 -140
- package/bin/apifuse-record.ts +698 -113
- package/bin/apifuse-submit-check.ts +517 -44
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml.ts +1 -1
- package/dist/auth-turn/index.d.ts +4 -4
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +16 -2
- package/dist/auth.js +76 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +117 -29
- 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/cli/templates/provider/README.md.tpl +4 -4
- package/dist/config/loader.d.ts +176 -17
- package/dist/config/loader.js +434 -161
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +7 -14
- package/dist/contract-types.d.ts +3 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +6 -6
- package/dist/declaration-validation.d.ts +23 -0
- package/dist/declaration-validation.js +159 -0
- package/dist/define.d.ts +13 -1
- package/dist/define.js +391 -122
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +123 -0
- package/dist/errors.d.ts +19 -1
- package/dist/errors.js +41 -3
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +50 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +6 -1
- package/dist/lint.js +370 -18
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +13 -11
- package/dist/provider.js +10 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +3 -1
- package/dist/runtime/auth-flow.js +8 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +138 -40
- package/dist/runtime/cache.d.ts +2 -1
- package/dist/runtime/cache.js +173 -23
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +14 -1
- package/dist/runtime/choice.js +566 -101
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +25 -3
- package/dist/runtime/http.d.ts +3 -2
- package/dist/runtime/http.js +517 -55
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +371 -23
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +58 -52
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +5 -5
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +3 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +20 -0
- package/dist/runtime/resolver-vendors/browser.js +282 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +368 -0
- package/dist/runtime/resolver-vendors/types.d.ts +83 -0
- package/dist/runtime/resolver-vendors/types.js +69 -0
- package/dist/runtime/resolver.d.ts +59 -0
- package/dist/runtime/resolver.js +705 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +5 -2
- package/dist/runtime/state.js +280 -74
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +30 -5
- package/dist/runtime/stealth.js +523 -259
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +12 -27
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +117 -3
- package/dist/server/self-test.js +787 -151
- package/dist/server/serve-implementation.d.ts +210 -0
- package/dist/server/serve-implementation.js +2078 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1143
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +21 -21
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +3 -2
- package/dist/testing/index.js +3 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +488 -28
- package/dist/types.d.ts +566 -19
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +42 -7
- package/src/auth-turn/index.ts +2 -2
- package/src/auth.ts +146 -86
- package/src/ceremonies/index.ts +167 -92
- 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/cli/templates/provider/README.md.tpl +4 -4
- package/src/config/loader.ts +667 -289
- package/src/contract-serialization.ts +10 -18
- package/src/contract-types.ts +3 -2
- package/src/contract.ts +14 -28
- package/src/declaration-validation.ts +202 -0
- package/src/define.ts +631 -495
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +128 -0
- package/src/errors.ts +56 -11
- package/src/fixture-sanitization.ts +247 -0
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +158 -44
- package/src/lint.ts +488 -154
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +66 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +13 -7
- package/src/runtime/browser.ts +252 -207
- package/src/runtime/cache.ts +209 -81
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +758 -197
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +37 -19
- package/src/runtime/http.ts +645 -65
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +530 -67
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +84 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +10 -13
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +18 -0
- package/src/runtime/resolver-shared.ts +17 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +408 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
- package/src/runtime/resolver-vendors/types.ts +173 -0
- package/src/runtime/resolver.ts +1060 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +399 -161
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +681 -295
- package/src/runtime/stt.ts +39 -113
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +6 -1
- package/src/server/index.ts +30 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +1030 -180
- package/src/server/serve-implementation.ts +3062 -0
- package/src/server/serve.ts +1 -1781
- package/src/server/types.ts +12 -13
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +27 -33
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +16 -20
- package/src/testing/index.ts +11 -2
- package/src/testing/run.ts +668 -74
- package/src/types.ts +665 -35
- package/src/user-input.ts +118 -0
- 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
|
@@ -11,16 +11,22 @@ import * as acorn from "acorn";
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
|
|
13
13
|
import packageJson from "../package.json";
|
|
14
|
-
import
|
|
14
|
+
import { formatPromptAssetIssues, verifyPromptAssets } from "../src/cli/prompt-assets.js";
|
|
15
15
|
import {
|
|
16
16
|
loadProviderLocaleCatalogs,
|
|
17
17
|
type ProviderLocale,
|
|
18
18
|
validateProviderLocaleCatalogs,
|
|
19
|
-
} from "../src/i18n";
|
|
20
|
-
import {
|
|
21
|
-
import { safeParseSchemaSync } from "../src/schema";
|
|
22
|
-
import {
|
|
23
|
-
|
|
19
|
+
} from "../src/i18n/index.js";
|
|
20
|
+
import type { ProviderDefinition } from "../src/index.js";
|
|
21
|
+
import { APIFUSE_DESCRIPTION_KEY_META_KEY, safeParseSchemaSync } from "../src/schema.js";
|
|
22
|
+
import {
|
|
23
|
+
findStreamCaptureGroup,
|
|
24
|
+
hasStreamEvidenceMarker,
|
|
25
|
+
parseStreamEvidenceRecord,
|
|
26
|
+
} from "../src/stream-evidence.js";
|
|
27
|
+
import { type CheckResult, PROMPT_ASSETS_CHECK_MESSAGE, runChecks } from "./apifuse-check.js";
|
|
28
|
+
import { hasSubstantiveDelimitedTextStructure } from "./submit-check-delimited-text.js";
|
|
29
|
+
import { hasSubstantiveXmlStructure } from "./submit-check-xml.js";
|
|
24
30
|
|
|
25
31
|
const TIERS = ["bronze", "silver", "gold", "diamond"] as const;
|
|
26
32
|
const TIER_VALUES: ReadonlySet<string> = new Set(TIERS);
|
|
@@ -267,7 +273,15 @@ export async function buildSubmitCheckReport(
|
|
|
267
273
|
const baseChecks = await safeRunChecks(providerRoot);
|
|
268
274
|
const provider = await safeLoadProvider(providerRoot);
|
|
269
275
|
|
|
270
|
-
|
|
276
|
+
// Prompt-asset freshness is reported by its own dedicated zero-point
|
|
277
|
+
// blocker below; filter the base-check duplicate so it is not double
|
|
278
|
+
// penalized under the definition category.
|
|
279
|
+
checks.push(
|
|
280
|
+
...scoreBaseChecks(
|
|
281
|
+
baseChecks.filter((result) => result.message !== PROMPT_ASSETS_CHECK_MESSAGE),
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
checks.push(scorePromptAssetFreshness(providerRoot));
|
|
271
285
|
checks.push(scoreProviderIdSlug(providerRoot, provider));
|
|
272
286
|
checks.push(scoreNoVendorShim(providerRoot));
|
|
273
287
|
checks.push(scoreNoVendorImport(providerRoot));
|
|
@@ -283,6 +297,7 @@ export async function buildSubmitCheckReport(
|
|
|
283
297
|
if (provider) {
|
|
284
298
|
const smokeResult = args.smoke ? await runSubmitCheckSmoke(providerRoot, provider) : undefined;
|
|
285
299
|
checks.push(scoreCredentialUsage(providerRoot, provider));
|
|
300
|
+
checks.push(scoreSdkOwnedSecretPresence(providerRoot, provider));
|
|
286
301
|
checks.push(scoreLocaleCatalog(providerRoot, provider));
|
|
287
302
|
checks.push(scoreOperationMetadata(provider));
|
|
288
303
|
checks.push(scoreFixtureCoverage(provider));
|
|
@@ -1424,6 +1439,159 @@ function scoreCredentialUsage(providerRoot: string, provider: ProviderDefinition
|
|
|
1424
1439
|
);
|
|
1425
1440
|
}
|
|
1426
1441
|
|
|
1442
|
+
// ---------------------------------------------------------------------------
|
|
1443
|
+
// sdk-owned-secret-presence (warn): provider-local double validation of
|
|
1444
|
+
// declared env secrets.
|
|
1445
|
+
//
|
|
1446
|
+
// The SDK runtime is the single source of truth for secret presence: declared
|
|
1447
|
+
// `required: true` secrets are validated before every handler/auth-flow
|
|
1448
|
+
// invocation and fail with the canonical structured MISSING_SECRET error
|
|
1449
|
+
// (HTTP 400, category credential_unavailable). Provider-local presence guards
|
|
1450
|
+
// (requireServiceKey/requireApiKey style) are dead weight that historically
|
|
1451
|
+
// diverged into inconsistent shapes (CONFIGURATION_ERROR vs MISSING_SECRET,
|
|
1452
|
+
// with/without category), which broke uniform incident attribution when nine
|
|
1453
|
+
// providers shipped with unprovisioned secrets (2026-07-22).
|
|
1454
|
+
//
|
|
1455
|
+
// Heuristic, warn-only: a line reading a declared `required: true` secret via
|
|
1456
|
+
// `.env.get(...)` (string literal or a const alias of a declared name)
|
|
1457
|
+
// followed within a small window by a falsy presence check plus a `throw`.
|
|
1458
|
+
// The rule flags duplication of the SDK gate ONLY: env names that are not
|
|
1459
|
+
// declared in defineProvider secrets[], and optional declarations
|
|
1460
|
+
// (`required: false`/omitted) that the runtime deliberately does not enforce,
|
|
1461
|
+
// are out of scope. Escape hatch:
|
|
1462
|
+
// `// @apifuse-allow sdk-owned-secret-presence: <reason>`.
|
|
1463
|
+
// ---------------------------------------------------------------------------
|
|
1464
|
+
|
|
1465
|
+
const SDK_OWNED_SECRET_PRESENCE_RULE_ID = "sdk-owned-secret-presence";
|
|
1466
|
+
const SECRET_PRESENCE_GUARD_LOOKAHEAD_LINES = 10;
|
|
1467
|
+
|
|
1468
|
+
const ENV_GET_CALL_PATTERN =
|
|
1469
|
+
/\.env\.get\(\s*(?:"([^"]+)"|'([^']+)'|`([^`$]+)`|([A-Za-z_$][\w$]*))\s*\)/;
|
|
1470
|
+
|
|
1471
|
+
const SECRET_ALIAS_CONST_PATTERN =
|
|
1472
|
+
/\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*(?:"([^"]+)"|'([^']+)'|`([^`$]+)`)/g;
|
|
1473
|
+
|
|
1474
|
+
const ENV_GET_ASSIGNMENT_PATTERN = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=[^;]*\.env\.get\(/;
|
|
1475
|
+
|
|
1476
|
+
// Const aliases of declared secret names (e.g. `const SERVICE_KEY_ENV =
|
|
1477
|
+
// "APIFUSE__PROVIDER__X__SERVICE_KEY"`) so aliased `.env.get(SERVICE_KEY_ENV)`
|
|
1478
|
+
// guards are detected, not just direct string literals.
|
|
1479
|
+
function buildDeclaredSecretAliasMap(
|
|
1480
|
+
providerRoot: string,
|
|
1481
|
+
declaredNames: ReadonlySet<string>,
|
|
1482
|
+
): Map<string, string> {
|
|
1483
|
+
const aliases = new Map<string, string>();
|
|
1484
|
+
for (const filePath of listNonTestTypeScriptFiles(providerRoot)) {
|
|
1485
|
+
const content = readFileSync(filePath, "utf8");
|
|
1486
|
+
for (const match of content.matchAll(SECRET_ALIAS_CONST_PATTERN)) {
|
|
1487
|
+
const alias = match[1];
|
|
1488
|
+
const name = match[2] ?? match[3] ?? match[4];
|
|
1489
|
+
if (alias && name && declaredNames.has(name)) {
|
|
1490
|
+
aliases.set(alias, name);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return aliases;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function hasLocalSecretPresenceGuard(
|
|
1498
|
+
line: string,
|
|
1499
|
+
remainingLines: readonly string[],
|
|
1500
|
+
declaredNames: ReadonlySet<string>,
|
|
1501
|
+
aliases: ReadonlyMap<string, string>,
|
|
1502
|
+
): boolean {
|
|
1503
|
+
const match = ENV_GET_CALL_PATTERN.exec(line);
|
|
1504
|
+
if (!match) {
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
const literal = match[1] ?? match[2] ?? match[3];
|
|
1508
|
+
const identifier = match[4];
|
|
1509
|
+
const readsDeclaredSecret =
|
|
1510
|
+
literal !== undefined
|
|
1511
|
+
? declaredNames.has(literal)
|
|
1512
|
+
: identifier !== undefined && aliases.has(identifier);
|
|
1513
|
+
if (!readsDeclaredSecret) {
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
const window = [line, ...remainingLines.slice(0, SECRET_PRESENCE_GUARD_LOOKAHEAD_LINES)];
|
|
1518
|
+
if (!window.some((candidate) => /\bthrow\b/.test(candidate))) {
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// Assigned read (`const key = ctx.env.get(...)`): only a falsy/undefined
|
|
1523
|
+
// check on THAT variable counts as a presence guard. Anchoring on the
|
|
1524
|
+
// assigned identifier avoids false positives from unrelated guards/throws
|
|
1525
|
+
// that merely sit near the env read (mirrors the aliased runtime-guard rule).
|
|
1526
|
+
const assigned = ENV_GET_ASSIGNMENT_PATTERN.exec(line)?.[1];
|
|
1527
|
+
if (assigned) {
|
|
1528
|
+
const escaped = assigned.replace(/\$/g, "\\$");
|
|
1529
|
+
const guardPattern = new RegExp(
|
|
1530
|
+
`(?:!\\s*${escaped}\\b|\\b${escaped}\\s*===?\\s*(?:undefined|null)\\b|\\b${escaped}\\s*==\\s*null\\b|\\b${escaped}(?:\\?\\.|\\.)length\\s*===?\\s*0\\b)`,
|
|
1531
|
+
);
|
|
1532
|
+
return window.some((candidate) => guardPattern.test(candidate));
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// Un-assigned read: only an inline presence check on the same line counts,
|
|
1536
|
+
// e.g. `if (!ctx.env.get(KEY)) throw ...`.
|
|
1537
|
+
return /(?:if\s*\(\s*!|===?\s*undefined\b|==\s*null\b)/.test(line);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function scoreSdkOwnedSecretPresence(
|
|
1541
|
+
providerRoot: string,
|
|
1542
|
+
provider: ProviderDefinition,
|
|
1543
|
+
): SubmitCheck {
|
|
1544
|
+
const passMessage = "Provider relies on SDK-owned secret presence validation.";
|
|
1545
|
+
// Only `required: true` declarations: those are exactly what the runtime
|
|
1546
|
+
// gate enforces. A presence guard over an optional secret is conditional
|
|
1547
|
+
// business logic the SDK will not replace, not double validation.
|
|
1548
|
+
const declaredNames: ReadonlySet<string> = new Set(
|
|
1549
|
+
(provider.secrets ?? [])
|
|
1550
|
+
.filter((secret) => secret.required === true)
|
|
1551
|
+
.map((secret) => secret.name),
|
|
1552
|
+
);
|
|
1553
|
+
if (declaredNames.size === 0) {
|
|
1554
|
+
return pass(SDK_OWNED_SECRET_PRESENCE_RULE_ID, SDK_NATIVE_CATEGORY, passMessage, 0);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const aliases = buildDeclaredSecretAliasMap(providerRoot, declaredNames);
|
|
1558
|
+
const findings = findSourceFindings(providerRoot, (line, remainingLines) =>
|
|
1559
|
+
hasLocalSecretPresenceGuard(line, remainingLines, declaredNames, aliases),
|
|
1560
|
+
);
|
|
1561
|
+
if (findings.length === 0) {
|
|
1562
|
+
return pass(SDK_OWNED_SECRET_PRESENCE_RULE_ID, SDK_NATIVE_CATEGORY, passMessage, 0);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
const { violations, overridden } = partitionAllowOverrides(
|
|
1566
|
+
providerRoot,
|
|
1567
|
+
findings,
|
|
1568
|
+
SDK_OWNED_SECRET_PRESENCE_RULE_ID,
|
|
1569
|
+
);
|
|
1570
|
+
if (violations.length === 0) {
|
|
1571
|
+
return pass(
|
|
1572
|
+
SDK_OWNED_SECRET_PRESENCE_RULE_ID,
|
|
1573
|
+
SDK_NATIVE_CATEGORY,
|
|
1574
|
+
`${passMessage} ${overridden.length} acknowledged @apifuse-allow override(s).`,
|
|
1575
|
+
0,
|
|
1576
|
+
formatSourceFindings(overridden),
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
return {
|
|
1581
|
+
id: SDK_OWNED_SECRET_PRESENCE_RULE_ID,
|
|
1582
|
+
category: SDK_NATIVE_CATEGORY,
|
|
1583
|
+
level: "warn",
|
|
1584
|
+
status: "warn",
|
|
1585
|
+
points: 0,
|
|
1586
|
+
maxPoints: 0,
|
|
1587
|
+
message:
|
|
1588
|
+
"Provider source re-validates declared env secret presence locally; the SDK owns this check.",
|
|
1589
|
+
remediation:
|
|
1590
|
+
"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>`.",
|
|
1591
|
+
evidence: formatSourceFindings(violations),
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1427
1595
|
function findSourceLineMatches(
|
|
1428
1596
|
providerRoot: string,
|
|
1429
1597
|
pattern: RegExp | ((line: string) => boolean),
|
|
@@ -1523,8 +1691,25 @@ function isScannableProviderSourceFile(relativePath: string): boolean {
|
|
|
1523
1691
|
);
|
|
1524
1692
|
}
|
|
1525
1693
|
|
|
1694
|
+
const EXCLUDED_SOURCE_DIRECTORY_SEGMENTS = new Set([
|
|
1695
|
+
".git",
|
|
1696
|
+
".worktree",
|
|
1697
|
+
"node_modules",
|
|
1698
|
+
"dist",
|
|
1699
|
+
"build",
|
|
1700
|
+
"coverage",
|
|
1701
|
+
]);
|
|
1702
|
+
|
|
1703
|
+
// `.agents`/`.apifuse` stay IN scope on purpose: managed content there is
|
|
1704
|
+
// markdown/JSON (never scannable), while a planted `.ts`/`.sh` under those
|
|
1705
|
+
// directories must not become a scan-exempt hiding place for secrets, raw
|
|
1706
|
+
// fetch, or vendor imports. Nested dependency/build output directories under
|
|
1707
|
+
// them use the same segment exclusions; `.worktree` is excluded because task
|
|
1708
|
+
// worktrees are duplicate repository checkouts, not provider source.
|
|
1526
1709
|
function shouldScanSourceDirectory(relativePath: string): boolean {
|
|
1527
|
-
return
|
|
1710
|
+
return relativePath
|
|
1711
|
+
.split("/")
|
|
1712
|
+
.every((segment) => !EXCLUDED_SOURCE_DIRECTORY_SEGMENTS.has(segment));
|
|
1528
1713
|
}
|
|
1529
1714
|
|
|
1530
1715
|
function isExcludedTestSource(relativePath: string): boolean {
|
|
@@ -1545,6 +1730,27 @@ function formatSourceFindings(findings: readonly SourceFinding[]): string[] {
|
|
|
1545
1730
|
return findings.map((finding) => `${finding.file}:${finding.line}`);
|
|
1546
1731
|
}
|
|
1547
1732
|
|
|
1733
|
+
function scorePromptAssetFreshness(providerRoot: string): SubmitCheck {
|
|
1734
|
+
const verification = verifyPromptAssets(providerRoot);
|
|
1735
|
+
if (verification.ok) {
|
|
1736
|
+
return pass(
|
|
1737
|
+
"prompt-assets-fresh",
|
|
1738
|
+
"docs",
|
|
1739
|
+
"SDK-managed agent prompt assets match the installed SDK version.",
|
|
1740
|
+
0,
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
return blocker(
|
|
1745
|
+
"prompt-assets-fresh",
|
|
1746
|
+
"docs",
|
|
1747
|
+
"SDK-managed agent prompt assets are missing, stale, or modified.",
|
|
1748
|
+
"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.",
|
|
1749
|
+
0,
|
|
1750
|
+
formatPromptAssetIssues(verification),
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1548
1754
|
function scoreRepositoryDx(providerRoot: string): SubmitCheck {
|
|
1549
1755
|
const missing: string[] = [];
|
|
1550
1756
|
if (!existsSync(resolve(providerRoot, ".gitignore"))) {
|
|
@@ -1980,17 +2186,19 @@ function scoreFixtureCoverage(provider: ProviderDefinition): SubmitCheck {
|
|
|
1980
2186
|
|
|
1981
2187
|
const GENERATED_LOCAL_ONLY_SCAFFOLD_REASON = /generated local-only scaffold/i;
|
|
1982
2188
|
|
|
1983
|
-
function scoreFixtureProvenance(
|
|
1984
|
-
providerRoot: string,
|
|
1985
|
-
provider: ProviderDefinition,
|
|
1986
|
-
): SubmitCheck {
|
|
2189
|
+
function scoreFixtureProvenance(providerRoot: string, provider: ProviderDefinition): SubmitCheck {
|
|
1987
2190
|
const rawPath = resolve(providerRoot, "__fixtures__", "raw.json");
|
|
1988
2191
|
let hasRecordedEvidence = false;
|
|
2192
|
+
let fixtureValidationError: string | undefined;
|
|
1989
2193
|
if (existsSync(rawPath)) {
|
|
1990
2194
|
try {
|
|
1991
|
-
hasRecordedEvidence =
|
|
1992
|
-
|
|
2195
|
+
hasRecordedEvidence = recordedFixtureStats(
|
|
2196
|
+
JSON.parse(readFileSync(rawPath, "utf8")),
|
|
2197
|
+
0,
|
|
2198
|
+
).hasNestedSubstance;
|
|
2199
|
+
} catch (error) {
|
|
1993
2200
|
hasRecordedEvidence = false;
|
|
2201
|
+
fixtureValidationError = error instanceof Error ? error.message : String(error);
|
|
1994
2202
|
}
|
|
1995
2203
|
}
|
|
1996
2204
|
|
|
@@ -2002,6 +2210,16 @@ function scoreFixtureProvenance(
|
|
|
2002
2210
|
0,
|
|
2003
2211
|
);
|
|
2004
2212
|
}
|
|
2213
|
+
if (fixtureValidationError) {
|
|
2214
|
+
return blocker(
|
|
2215
|
+
"fixture-provenance",
|
|
2216
|
+
"fixtures",
|
|
2217
|
+
`Malformed recorded fixture evidence in __fixtures__/raw.json: ${fixtureValidationError}`,
|
|
2218
|
+
"Re-run `bun run record` to replace the malformed stream evidence, or repair the named field using the stream evidence contract.",
|
|
2219
|
+
0,
|
|
2220
|
+
["__fixtures__/raw.json"],
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2005
2223
|
|
|
2006
2224
|
if (allOperationsAreGeneratedLocalScaffold(provider)) {
|
|
2007
2225
|
return {
|
|
@@ -2029,14 +2247,32 @@ function scoreFixtureProvenance(
|
|
|
2029
2247
|
);
|
|
2030
2248
|
}
|
|
2031
2249
|
|
|
2032
|
-
function hasNonEmptyRecordedFixture(value: unknown): boolean {
|
|
2033
|
-
|
|
2250
|
+
export function hasNonEmptyRecordedFixture(value: unknown): boolean {
|
|
2251
|
+
try {
|
|
2252
|
+
return recordedFixtureStats(value, 0).hasNestedSubstance;
|
|
2253
|
+
} catch {
|
|
2254
|
+
return false;
|
|
2255
|
+
}
|
|
2034
2256
|
}
|
|
2035
2257
|
|
|
2036
2258
|
function recordedFixtureStats(
|
|
2037
2259
|
value: unknown,
|
|
2038
2260
|
depth: number,
|
|
2039
2261
|
): { hasNestedSubstance: boolean; leafValues: number } {
|
|
2262
|
+
if (
|
|
2263
|
+
value !== null &&
|
|
2264
|
+
typeof value === "object" &&
|
|
2265
|
+
!Array.isArray(value) &&
|
|
2266
|
+
(value as Record<string, unknown>).__apifuse_capture__ === true
|
|
2267
|
+
) {
|
|
2268
|
+
const group = findStreamCaptureGroup(value);
|
|
2269
|
+
if (!group) throw new Error("Stream capture envelope is invalid.");
|
|
2270
|
+
return { hasNestedSubstance: true, leafValues: group.items.length };
|
|
2271
|
+
}
|
|
2272
|
+
if (hasStreamEvidenceMarker(value)) {
|
|
2273
|
+
parseStreamEvidenceRecord(value);
|
|
2274
|
+
return { hasNestedSubstance: true, leafValues: 1 };
|
|
2275
|
+
}
|
|
2040
2276
|
if (value === null || value === undefined) {
|
|
2041
2277
|
return { hasNestedSubstance: false, leafValues: 0 };
|
|
2042
2278
|
}
|
|
@@ -2071,10 +2307,14 @@ function recordedFixtureStats(
|
|
|
2071
2307
|
if (value.length === 0) {
|
|
2072
2308
|
return { hasNestedSubstance: false, leafValues: 0 };
|
|
2073
2309
|
}
|
|
2074
|
-
// A recorded operation value may be a raw XML
|
|
2075
|
-
// substantive
|
|
2076
|
-
// string as a leaf so existing JSON provenance heuristics are unchanged.
|
|
2077
|
-
return {
|
|
2310
|
+
// A recorded operation value may be a raw XML or delimited-text success
|
|
2311
|
+
// payload. Treat a substantive one as nested evidence while still counting
|
|
2312
|
+
// the string as a leaf so existing JSON provenance heuristics are unchanged.
|
|
2313
|
+
return {
|
|
2314
|
+
hasNestedSubstance:
|
|
2315
|
+
hasSubstantiveXmlStructure(value) || hasSubstantiveDelimitedTextStructure(value),
|
|
2316
|
+
leafValues: 1,
|
|
2317
|
+
};
|
|
2078
2318
|
}
|
|
2079
2319
|
return { hasNestedSubstance: false, leafValues: 1 };
|
|
2080
2320
|
}
|
|
@@ -2090,12 +2330,17 @@ function allOperationsAreGeneratedLocalScaffold(provider: ProviderDefinition): b
|
|
|
2090
2330
|
}
|
|
2091
2331
|
|
|
2092
2332
|
function scoreVendorKeyLeak(providerRoot: string): SubmitCheck {
|
|
2093
|
-
return escapeHatchResult(
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2333
|
+
return escapeHatchResult(
|
|
2334
|
+
providerRoot,
|
|
2335
|
+
"vendor-key-leak",
|
|
2336
|
+
findVendorKeyLeakFindings(providerRoot),
|
|
2337
|
+
{
|
|
2338
|
+
blockerMessage: "Public schema keys leak raw vendor field names.",
|
|
2339
|
+
remediation:
|
|
2340
|
+
"Normalize public request/response fields to APIFuse-standard lowerCamelCase names (e.g. isOpen24h, latitude); keep raw vendor keys only in upstream-parsing schemas (const upstream... = z.object(...)). Add `// @apifuse-allow vendor-key-leak` only with a comment explaining why the vendor name is genuinely canonical.",
|
|
2341
|
+
passMessage: "No vendor field-name leaks detected in public schemas.",
|
|
2342
|
+
},
|
|
2343
|
+
);
|
|
2099
2344
|
}
|
|
2100
2345
|
|
|
2101
2346
|
function scoreVendorTimestampLeak(providerRoot: string): SubmitCheck {
|
|
@@ -2282,9 +2527,7 @@ function collectTopLevelObjectKeys(
|
|
|
2282
2527
|
if (computedQuote === '"' || computedQuote === "'") {
|
|
2283
2528
|
const literalEnd = findStringEnd(source, literalStart);
|
|
2284
2529
|
const afterLiteral =
|
|
2285
|
-
literalEnd === -1
|
|
2286
|
-
? -1
|
|
2287
|
-
: skipWhitespaceAndComments(masked, literalEnd + 1, computedEnd);
|
|
2530
|
+
literalEnd === -1 ? -1 : skipWhitespaceAndComments(masked, literalEnd + 1, computedEnd);
|
|
2288
2531
|
if (literalEnd !== -1 && afterLiteral === computedEnd) {
|
|
2289
2532
|
key = source.slice(literalStart + 1, literalEnd);
|
|
2290
2533
|
}
|
|
@@ -2402,8 +2645,13 @@ function findNamedConstValueRanges(source: string): NamedObjectRange[] {
|
|
|
2402
2645
|
return ranges;
|
|
2403
2646
|
}
|
|
2404
2647
|
|
|
2405
|
-
function findConstValueRangeContaining(
|
|
2406
|
-
|
|
2648
|
+
function findConstValueRangeContaining(
|
|
2649
|
+
source: string,
|
|
2650
|
+
offset: number,
|
|
2651
|
+
): NamedObjectRange | undefined {
|
|
2652
|
+
return findNamedConstValueRanges(source).find(
|
|
2653
|
+
(range) => offset >= range.start && offset <= range.end,
|
|
2654
|
+
);
|
|
2407
2655
|
}
|
|
2408
2656
|
|
|
2409
2657
|
function findStringLiteralsInRange(
|
|
@@ -3541,9 +3789,47 @@ function scoreProviderDocs(providerRoot: string): SubmitCheck[] {
|
|
|
3541
3789
|
];
|
|
3542
3790
|
}
|
|
3543
3791
|
|
|
3792
|
+
// Splits secret findings into still-active findings and acknowledged
|
|
3793
|
+
// `// @apifuse-allow secret-scan` overrides, mirroring partitionAllowOverrides
|
|
3794
|
+
// (same pragma placement: the finding line or the line directly above it).
|
|
3795
|
+
// Every finding source carries a line number (entropy candidates and located
|
|
3796
|
+
// SECRET_PATTERNS matches); a finding that somehow lacks one stays active
|
|
3797
|
+
// defensively.
|
|
3798
|
+
function partitionSecretScanAllowOverrides(
|
|
3799
|
+
providerRoot: string,
|
|
3800
|
+
findings: readonly SecretFinding[],
|
|
3801
|
+
): { active: SecretFinding[]; overridden: SecretFinding[] } {
|
|
3802
|
+
const fileLineCache = new Map<string, string[]>();
|
|
3803
|
+
const active: SecretFinding[] = [];
|
|
3804
|
+
const overridden: SecretFinding[] = [];
|
|
3805
|
+
|
|
3806
|
+
for (const finding of findings) {
|
|
3807
|
+
if (finding.line === undefined) {
|
|
3808
|
+
active.push(finding);
|
|
3809
|
+
continue;
|
|
3810
|
+
}
|
|
3811
|
+
const absolute = resolve(providerRoot, finding.file);
|
|
3812
|
+
let lines = fileLineCache.get(absolute);
|
|
3813
|
+
if (lines === undefined) {
|
|
3814
|
+
lines = existsSync(absolute) ? readFileSync(absolute, "utf8").split(/\r?\n/) : [];
|
|
3815
|
+
fileLineCache.set(absolute, lines);
|
|
3816
|
+
}
|
|
3817
|
+
if (hasAllowOverride(lines, finding.line, "secret-scan")) {
|
|
3818
|
+
overridden.push(finding);
|
|
3819
|
+
} else {
|
|
3820
|
+
active.push(finding);
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
return { active, overridden };
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3544
3827
|
function scoreSecrets(providerRoot: string, provider?: ProviderDefinition): SubmitCheck {
|
|
3545
|
-
const
|
|
3546
|
-
|
|
3828
|
+
const { active, overridden } = partitionSecretScanAllowOverrides(
|
|
3829
|
+
providerRoot,
|
|
3830
|
+
findSecretFindings(providerRoot, provider?.id),
|
|
3831
|
+
);
|
|
3832
|
+
const blockerFindings = active.filter((finding) => finding.level !== "warn");
|
|
3547
3833
|
if (blockerFindings.length > 0) {
|
|
3548
3834
|
return {
|
|
3549
3835
|
id: "secret-scan",
|
|
@@ -3563,7 +3849,15 @@ function scoreSecrets(providerRoot: string, provider?: ProviderDefinition): Subm
|
|
|
3563
3849
|
),
|
|
3564
3850
|
};
|
|
3565
3851
|
}
|
|
3566
|
-
if (
|
|
3852
|
+
if (active.length > 0 || overridden.length > 0) {
|
|
3853
|
+
const messageBase =
|
|
3854
|
+
active.length > 0
|
|
3855
|
+
? "High-entropy source strings were found without secret-like identifier context; they may be false positives."
|
|
3856
|
+
: "Potential credential-like strings were found in shareable files.";
|
|
3857
|
+
const message =
|
|
3858
|
+
overridden.length > 0
|
|
3859
|
+
? `${messageBase} ${overridden.length} acknowledged @apifuse-allow override(s).`
|
|
3860
|
+
: messageBase;
|
|
3567
3861
|
return {
|
|
3568
3862
|
id: "secret-scan",
|
|
3569
3863
|
category: "security",
|
|
@@ -3571,11 +3865,10 @@ function scoreSecrets(providerRoot: string, provider?: ProviderDefinition): Subm
|
|
|
3571
3865
|
status: "warn",
|
|
3572
3866
|
points: 8,
|
|
3573
3867
|
maxPoints: CATEGORY_MAX_POINTS.security,
|
|
3574
|
-
message
|
|
3575
|
-
"High-entropy source strings were found without secret-like identifier context; they may be false positives.",
|
|
3868
|
+
message,
|
|
3576
3869
|
remediation:
|
|
3577
|
-
'Review the listed strings. If any are credentials, move them to env vars read via `ctx.env.get("APIFUSE__PROVIDER__<ID>__<NAME>")` and rotate the leaked credential; otherwise keep generated blobs in fixtures/tests or document why they are public
|
|
3578
|
-
evidence:
|
|
3870
|
+
'Review the listed strings. If any are credentials, move them to env vars read via `ctx.env.get("APIFUSE__PROVIDER__<ID>__<NAME>")` and rotate the leaked credential; otherwise keep generated blobs in fixtures/tests or document why they are public with `// @apifuse-allow secret-scan: <reason>`.',
|
|
3871
|
+
evidence: [...active, ...overridden].map(
|
|
3579
3872
|
(finding) =>
|
|
3580
3873
|
finding.evidence ??
|
|
3581
3874
|
`${finding.file}${finding.line ? `:${finding.line}` : ""}: ${finding.label}`,
|
|
@@ -3606,8 +3899,19 @@ function findSecretFindings(providerRoot: string, providerId = "<ID>"): SecretFi
|
|
|
3606
3899
|
if (!existsSync(filePath)) continue;
|
|
3607
3900
|
const content = readFileSync(filePath, "utf8");
|
|
3608
3901
|
for (const [label, pattern] of SECRET_PATTERNS) {
|
|
3609
|
-
|
|
3610
|
-
|
|
3902
|
+
// Locate every match to its line so pattern findings carry the line
|
|
3903
|
+
// information hasAllowOverride needs: `// @apifuse-allow secret-scan`
|
|
3904
|
+
// must behave uniformly across entropy findings and pattern findings.
|
|
3905
|
+
const globalPattern = new RegExp(
|
|
3906
|
+
pattern.source,
|
|
3907
|
+
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`,
|
|
3908
|
+
);
|
|
3909
|
+
const seenLines = new Set<number>();
|
|
3910
|
+
for (const match of content.matchAll(globalPattern)) {
|
|
3911
|
+
const line = offsetToLine(content, match.index);
|
|
3912
|
+
if (seenLines.has(line)) continue;
|
|
3913
|
+
seenLines.add(line);
|
|
3914
|
+
findings.push({ label, file: relativePath, line });
|
|
3611
3915
|
}
|
|
3612
3916
|
}
|
|
3613
3917
|
}
|
|
@@ -3665,7 +3969,7 @@ export function extractStringLiteralCandidates(line: string): string[] {
|
|
|
3665
3969
|
continue;
|
|
3666
3970
|
}
|
|
3667
3971
|
if (char === quote) {
|
|
3668
|
-
if (cursor - contentStart >=
|
|
3972
|
+
if (cursor - contentStart >= ENTROPY_CANDIDATE_MIN_LENGTH) {
|
|
3669
3973
|
candidates.push(line.slice(contentStart, cursor));
|
|
3670
3974
|
}
|
|
3671
3975
|
index = cursor;
|
|
@@ -3690,6 +3994,25 @@ function classifyEntropyCandidate(input: {
|
|
|
3690
3994
|
if (!charset) return undefined;
|
|
3691
3995
|
const entropy = shannonEntropy(value);
|
|
3692
3996
|
const secretishContext = SECRETISH_IDENTIFIER_PATTERN.test(input.line);
|
|
3997
|
+
// Word-like SCREAMING_SNAKE values (e.g. error-code constants such as
|
|
3998
|
+
// "AUTH_PASSWORD_LOGIN_CAPTCHA_REQUIRED") may contain secret-ish words
|
|
3999
|
+
// (AUTH/PASSWORD/...) in their own text and would otherwise be permanently
|
|
4000
|
+
// blocker-flagged. They are never skipped — entropy classification always
|
|
4001
|
+
// runs — but when the secret-ish context comes solely from identifier-
|
|
4002
|
+
// constant-shaped literal text (the line with those literals stripped
|
|
4003
|
+
// carries no secret-ish identifier), the finding is capped at a
|
|
4004
|
+
// non-blocking warning instead of a blocker. Stripping constant-shaped
|
|
4005
|
+
// siblings — not just the candidate — matters for lines holding several
|
|
4006
|
+
// constants (e.g. an ERROR_CODES array), while quoted property keys and
|
|
4007
|
+
// header names ("Authorization", "apiKey") stay visible as genuine
|
|
4008
|
+
// external context. Assignments to `apiKey`/`token`/`secret`-style names
|
|
4009
|
+
// still escalate to blockers via the identifier side, and
|
|
4010
|
+
// `// @apifuse-allow secret-scan` remains the reviewed way to silence the
|
|
4011
|
+
// warning.
|
|
4012
|
+
const selfContextOnlyConstant =
|
|
4013
|
+
secretishContext &&
|
|
4014
|
+
isScreamingSnakeConstantValue(value) &&
|
|
4015
|
+
!SECRETISH_IDENTIFIER_PATTERN.test(stripIdentifierConstantLiterals(input.line, value));
|
|
3693
4016
|
const threshold = charset === "hex" ? 3.0 : secretishContext ? 4.0 : 4.5;
|
|
3694
4017
|
if (entropy < threshold) return undefined;
|
|
3695
4018
|
|
|
@@ -3700,16 +4023,161 @@ function classifyEntropyCandidate(input: {
|
|
|
3700
4023
|
charset === "hex"
|
|
3701
4024
|
? `high-entropy hex string (${entropy.toFixed(2)} bits/char)`
|
|
3702
4025
|
: `high-entropy base64-like string (${entropy.toFixed(2)} bits/char)`;
|
|
4026
|
+
const contextNote = selfContextOnlyConstant
|
|
4027
|
+
? "; identifier-like constant (downgraded to warning)"
|
|
4028
|
+
: secretishContext
|
|
4029
|
+
? ""
|
|
4030
|
+
: "; may be a false positive";
|
|
3703
4031
|
return {
|
|
3704
4032
|
label,
|
|
3705
4033
|
file: input.file,
|
|
3706
4034
|
line: input.lineNumber,
|
|
3707
|
-
level: secretishContext ? "blocker" : "warn",
|
|
4035
|
+
level: secretishContext && !selfContextOnlyConstant ? "blocker" : "warn",
|
|
3708
4036
|
remediation: `Move ${location} to an env var read via \`ctx.env.get("${envName}")\` and rotate the leaked credential.`,
|
|
3709
|
-
evidence: `${location}: ${label}; preview ${preview}${
|
|
4037
|
+
evidence: `${location}: ${label}; preview ${preview}${contextNote}`,
|
|
3710
4038
|
};
|
|
3711
4039
|
}
|
|
3712
4040
|
|
|
4041
|
+
// Word-like SCREAMING_SNAKE identifier shape: at least two underscore-
|
|
4042
|
+
// separated segments, each essentially pure alphabetic — letters optionally
|
|
4043
|
+
// followed by a SHORT digit suffix (at most 2, e.g. version markers like
|
|
4044
|
+
// "V2") — and at most 15% digits across the whole value. Dictionary-style
|
|
4045
|
+
// constants like "AUTH_PASSWORD_LOGIN_CAPTCHA_REQUIRED" or
|
|
4046
|
+
// "PROVIDER_CONTRACT_V2_REQUIRED" match; digit-heavy segmented material
|
|
4047
|
+
// (e.g. license/credential shapes like "ABCD1234_EFGH5678_IJKL9012"),
|
|
4048
|
+
// uppercase blobs ("XK9J_Q2ZP_M7VN"), and underscore-free hex-like values
|
|
4049
|
+
// ("A1B2C3D4...") do not. This shape gate never skips entropy classification;
|
|
4050
|
+
// it only decides whether a finding whose secret-ish context comes solely
|
|
4051
|
+
// from the literal's own text is downgraded from blocker to warning, so it
|
|
4052
|
+
// deliberately stays strict: values that merely contain a secret-ish word but
|
|
4053
|
+
// are not word-like constants keep full blocker severity.
|
|
4054
|
+
function isScreamingSnakeConstantValue(value: string): boolean {
|
|
4055
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(value) || !value.includes("_")) return false;
|
|
4056
|
+
const segments = value.split("_");
|
|
4057
|
+
if (segments.length < 2) return false;
|
|
4058
|
+
if (!segments.every((segment) => /^[A-Z]+[0-9]{0,2}$/.test(segment))) return false;
|
|
4059
|
+
const digitCount = value.match(/[0-9]/g)?.length ?? 0;
|
|
4060
|
+
return digitCount / value.length <= 0.15;
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
type LineStringLiteral = {
|
|
4064
|
+
// Index of the opening quote.
|
|
4065
|
+
start: number;
|
|
4066
|
+
// Index just past the closing quote (line end when unterminated).
|
|
4067
|
+
end: number;
|
|
4068
|
+
content: string;
|
|
4069
|
+
closed: boolean;
|
|
4070
|
+
role: "key" | "value";
|
|
4071
|
+
// Nearest unclosed bracket enclosing the literal's start, if any.
|
|
4072
|
+
container?: { bracket: "[" | "(" | "{"; index: number };
|
|
4073
|
+
};
|
|
4074
|
+
|
|
4075
|
+
// Stable identity for the container a literal sits in ("top" when the
|
|
4076
|
+
// literal is not inside any bracket on the line).
|
|
4077
|
+
function literalContainerKey(literal: LineStringLiteral): string {
|
|
4078
|
+
return literal.container ? `${literal.container.bracket}${literal.container.index}` : "top";
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
// Single-pass line tokenizer: extracts every string literal with its span and
|
|
4082
|
+
// classifies its syntactic role once. A literal is a KEY when it is preceded
|
|
4083
|
+
// (ignoring whitespace) by "{", ",", "(", or the line start AND followed
|
|
4084
|
+
// (ignoring whitespace) by ":" — i.e. it names the value next to it. Every
|
|
4085
|
+
// other literal is a VALUE: ternary arms (preceded by "?" or ":"), array
|
|
4086
|
+
// elements, call arguments, and assignment right-hand sides, even when a
|
|
4087
|
+
// ternary's ":" happens to follow them. Uses the same quote/escape walking as
|
|
4088
|
+
// extractStringLiteralCandidates.
|
|
4089
|
+
function tokenizeLineStringLiterals(line: string): LineStringLiteral[] {
|
|
4090
|
+
const literals: LineStringLiteral[] = [];
|
|
4091
|
+
const bracketStack: Array<{ bracket: "[" | "(" | "{"; index: number }> = [];
|
|
4092
|
+
let index = 0;
|
|
4093
|
+
while (index < line.length) {
|
|
4094
|
+
const char = line[index];
|
|
4095
|
+
if (char !== '"' && char !== "'" && char !== "`") {
|
|
4096
|
+
if (char === "[" || char === "(" || char === "{") {
|
|
4097
|
+
bracketStack.push({ bracket: char, index });
|
|
4098
|
+
} else if (char === "]" || char === ")" || char === "}") {
|
|
4099
|
+
bracketStack.pop();
|
|
4100
|
+
}
|
|
4101
|
+
index += 1;
|
|
4102
|
+
continue;
|
|
4103
|
+
}
|
|
4104
|
+
const quote = char;
|
|
4105
|
+
const start = index;
|
|
4106
|
+
const contentStart = index + 1;
|
|
4107
|
+
let cursor = contentStart;
|
|
4108
|
+
let closed = false;
|
|
4109
|
+
while (cursor < line.length) {
|
|
4110
|
+
const inner = line[cursor];
|
|
4111
|
+
if (inner === "\\") {
|
|
4112
|
+
cursor += 2;
|
|
4113
|
+
continue;
|
|
4114
|
+
}
|
|
4115
|
+
if (inner === quote) {
|
|
4116
|
+
closed = true;
|
|
4117
|
+
break;
|
|
4118
|
+
}
|
|
4119
|
+
cursor += 1;
|
|
4120
|
+
}
|
|
4121
|
+
const contentEnd = Math.min(cursor, line.length);
|
|
4122
|
+
const end = closed ? cursor + 1 : line.length;
|
|
4123
|
+
const before = line.slice(0, start).trimEnd();
|
|
4124
|
+
const keyPreceded = before === "" || /[{,(]$/.test(before);
|
|
4125
|
+
const keyFollowed = closed && /^\s*:/.test(line.slice(end));
|
|
4126
|
+
literals.push({
|
|
4127
|
+
start,
|
|
4128
|
+
end,
|
|
4129
|
+
content: line.slice(contentStart, contentEnd),
|
|
4130
|
+
closed,
|
|
4131
|
+
role: keyPreceded && keyFollowed ? "key" : "value",
|
|
4132
|
+
container: bracketStack[bracketStack.length - 1],
|
|
4133
|
+
});
|
|
4134
|
+
index = end;
|
|
4135
|
+
}
|
|
4136
|
+
return literals;
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
// Builds the context text used to decide whether a candidate's secret-ish
|
|
4140
|
+
// context is genuine. The candidate's own literal is ALWAYS stripped
|
|
4141
|
+
// (self-context rule). SIBLING identifier-constant-shaped candidate literals
|
|
4142
|
+
// (VALUE role, SCREAMING_SNAKE shape, >= ENTROPY_CANDIDATE_MIN_LENGTH) are
|
|
4143
|
+
// stripped only when they share the candidate's non-call container — the same
|
|
4144
|
+
// `[...]` array, the same `{...}` object value list, or the bracket-free top
|
|
4145
|
+
// level (ternary arms) — so a value list of error codes cannot poison its own
|
|
4146
|
+
// members' context. Call-argument siblings (inside `(...)`) always keep their
|
|
4147
|
+
// context: in `headers.set("X_LONG_AUTH_TOKEN_NAME", "QWERTY_...")` the first
|
|
4148
|
+
// argument genuinely describes the second, so stripping it would erase real
|
|
4149
|
+
// auth/token context. KEY-role literals are never stripped.
|
|
4150
|
+
function stripIdentifierConstantLiterals(line: string, candidate: string): string {
|
|
4151
|
+
const literals = tokenizeLineStringLiterals(line);
|
|
4152
|
+
const candidateContainers = new Set<string>();
|
|
4153
|
+
for (const literal of literals) {
|
|
4154
|
+
if (literal.content === candidate) {
|
|
4155
|
+
candidateContainers.add(literalContainerKey(literal));
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
let result = "";
|
|
4159
|
+
let previousEnd = 0;
|
|
4160
|
+
for (const literal of literals) {
|
|
4161
|
+
result += line.slice(previousEnd, literal.start);
|
|
4162
|
+
const isSelf = literal.content === candidate;
|
|
4163
|
+
const isSameNonCallContainerSibling =
|
|
4164
|
+
literal.role === "value" &&
|
|
4165
|
+
literal.content.length >= ENTROPY_CANDIDATE_MIN_LENGTH &&
|
|
4166
|
+
isScreamingSnakeConstantValue(literal.content) &&
|
|
4167
|
+
literal.container?.bracket !== "(" &&
|
|
4168
|
+
candidateContainers.has(literalContainerKey(literal));
|
|
4169
|
+
if (isSelf || isSameNonCallContainerSibling) {
|
|
4170
|
+
const quote = line[literal.start] ?? "";
|
|
4171
|
+
result += quote + (literal.closed ? quote : "");
|
|
4172
|
+
} else {
|
|
4173
|
+
result += line.slice(literal.start, literal.end);
|
|
4174
|
+
}
|
|
4175
|
+
previousEnd = literal.end;
|
|
4176
|
+
}
|
|
4177
|
+
result += line.slice(previousEnd);
|
|
4178
|
+
return result;
|
|
4179
|
+
}
|
|
4180
|
+
|
|
3713
4181
|
function shouldConsiderEntropyValue(value: string): boolean {
|
|
3714
4182
|
const lower = value.toLowerCase();
|
|
3715
4183
|
if (/^(?:dev-only|local|example|sample|your-|replace|<)/i.test(value)) {
|
|
@@ -3725,7 +4193,7 @@ function shouldConsiderEntropyValue(value: string): boolean {
|
|
|
3725
4193
|
if (lower.includes("/") && /\.[a-z0-9]{1,8}(?:$|[/?#])/i.test(value)) {
|
|
3726
4194
|
return false;
|
|
3727
4195
|
}
|
|
3728
|
-
return value.length >=
|
|
4196
|
+
return value.length >= ENTROPY_CANDIDATE_MIN_LENGTH;
|
|
3729
4197
|
}
|
|
3730
4198
|
|
|
3731
4199
|
function classifyEntropyCharset(value: string): "base64" | "hex" | undefined {
|
|
@@ -3759,6 +4227,11 @@ function guessSecretName(line: string): string {
|
|
|
3759
4227
|
|
|
3760
4228
|
const SECRETISH_IDENTIFIER_PATTERN = /key|token|secret|password|credential|auth/i;
|
|
3761
4229
|
|
|
4230
|
+
// Minimum length for a string literal to be considered an entropy candidate.
|
|
4231
|
+
// Shared by candidate extraction, entropy screening, and the context strip so
|
|
4232
|
+
// the three stay coherent.
|
|
4233
|
+
const ENTROPY_CANDIDATE_MIN_LENGTH = 20;
|
|
4234
|
+
|
|
3762
4235
|
const SECRET_PATTERNS: Array<[string, RegExp]> = [
|
|
3763
4236
|
["JWT-like token", /eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}/],
|
|
3764
4237
|
["GitHub token", /gh[pousr]_[A-Za-z0-9_]{30,}/],
|