@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.
Files changed (342) hide show
  1. package/AUTHORING.md +493 -5
  2. package/CHANGELOG.md +131 -1
  3. package/README.md +52 -6
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +106 -62
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +63 -55
  8. package/bin/apifuse-pack-check.ts +22 -2
  9. package/bin/apifuse-pack-smoke.ts +78 -82
  10. package/bin/apifuse-pack-types.ts +583 -0
  11. package/bin/apifuse-perf.ts +59 -140
  12. package/bin/apifuse-record.ts +698 -113
  13. package/bin/apifuse-submit-check.ts +517 -44
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml.ts +1 -1
  18. package/dist/auth-turn/index.d.ts +4 -4
  19. package/dist/auth-turn/index.js +1 -1
  20. package/dist/auth.d.ts +16 -2
  21. package/dist/auth.js +76 -18
  22. package/dist/ceremonies/index.d.ts +9 -1
  23. package/dist/ceremonies/index.js +117 -29
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/cli/templates/provider/README.md.tpl +4 -4
  32. package/dist/config/loader.d.ts +176 -17
  33. package/dist/config/loader.js +434 -161
  34. package/dist/contract-serialization.d.ts +2 -2
  35. package/dist/contract-serialization.js +7 -14
  36. package/dist/contract-types.d.ts +3 -2
  37. package/dist/contract.d.ts +3 -3
  38. package/dist/contract.js +6 -6
  39. package/dist/declaration-validation.d.ts +23 -0
  40. package/dist/declaration-validation.js +159 -0
  41. package/dist/define.d.ts +13 -1
  42. package/dist/define.js +391 -122
  43. package/dist/dev.d.ts +1 -1
  44. package/dist/dev.js +1 -1
  45. package/dist/error-resolution.d.ts +4 -0
  46. package/dist/error-resolution.js +123 -0
  47. package/dist/errors.d.ts +19 -1
  48. package/dist/errors.js +41 -3
  49. package/dist/fixture-sanitization.d.ts +26 -0
  50. package/dist/fixture-sanitization.js +216 -0
  51. package/dist/i18n/catalog.d.ts +2 -2
  52. package/dist/i18n/catalog.js +4 -10
  53. package/dist/i18n/index.d.ts +2 -2
  54. package/dist/i18n/index.js +2 -2
  55. package/dist/i18n/keys.d.ts +2 -2
  56. package/dist/index.d.ts +50 -42
  57. package/dist/index.js +41 -37
  58. package/dist/lint.d.ts +6 -1
  59. package/dist/lint.js +370 -18
  60. package/dist/native-address.d.ts +43 -0
  61. package/dist/native-address.js +281 -0
  62. package/dist/native-egress-policy.d.ts +31 -0
  63. package/dist/native-egress-policy.js +288 -0
  64. package/dist/observability.d.ts +5 -2
  65. package/dist/observability.js +48 -1
  66. package/dist/provider.d.ts +13 -11
  67. package/dist/provider.js +10 -9
  68. package/dist/public-schema-field-lint.d.ts +1 -1
  69. package/dist/recipes/gov-api.js +1 -1
  70. package/dist/runtime/auth-flow.d.ts +3 -1
  71. package/dist/runtime/auth-flow.js +8 -3
  72. package/dist/runtime/browser.d.ts +1 -1
  73. package/dist/runtime/browser.js +138 -40
  74. package/dist/runtime/cache.d.ts +2 -1
  75. package/dist/runtime/cache.js +173 -23
  76. package/dist/runtime/choice-wordlist.d.ts +9 -0
  77. package/dist/runtime/choice-wordlist.js +138 -0
  78. package/dist/runtime/choice.d.ts +14 -1
  79. package/dist/runtime/choice.js +566 -101
  80. package/dist/runtime/credential.d.ts +1 -1
  81. package/dist/runtime/credential.js +1 -1
  82. package/dist/runtime/env.d.ts +1 -1
  83. package/dist/runtime/executor.d.ts +1 -1
  84. package/dist/runtime/executor.js +25 -3
  85. package/dist/runtime/http.d.ts +3 -2
  86. package/dist/runtime/http.js +517 -55
  87. package/dist/runtime/insights.d.ts +1 -1
  88. package/dist/runtime/insights.js +6 -13
  89. package/dist/runtime/instrumentation.d.ts +2 -2
  90. package/dist/runtime/instrumentation.js +371 -23
  91. package/dist/runtime/keyring.js +1 -1
  92. package/dist/runtime/namespace.js +1 -1
  93. package/dist/runtime/native-network-errors.d.ts +33 -0
  94. package/dist/runtime/native-network-errors.js +69 -0
  95. package/dist/runtime/native-network.d.ts +96 -0
  96. package/dist/runtime/native-network.js +1232 -0
  97. package/dist/runtime/ocr.d.ts +29 -0
  98. package/dist/runtime/ocr.js +440 -0
  99. package/dist/runtime/otlp.d.ts +1 -1
  100. package/dist/runtime/perf.d.ts +1 -1
  101. package/dist/runtime/provider.d.ts +1 -1
  102. package/dist/runtime/provider.js +1 -2
  103. package/dist/runtime/proxy-errors.d.ts +1 -1
  104. package/dist/runtime/proxy-errors.js +9 -7
  105. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  106. package/dist/runtime/proxy-nodemaven.js +146 -0
  107. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  108. package/dist/runtime/proxy-retry-policy.js +2 -2
  109. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  110. package/dist/runtime/proxy-telemetry.js +58 -52
  111. package/dist/runtime/redirects.d.ts +29 -0
  112. package/dist/runtime/redirects.js +36 -0
  113. package/dist/runtime/redis.d.ts +1 -1
  114. package/dist/runtime/redis.js +5 -5
  115. package/dist/runtime/request-options.d.ts +68 -1
  116. package/dist/runtime/request-options.js +548 -0
  117. package/dist/runtime/resolver-config.d.ts +6 -0
  118. package/dist/runtime/resolver-config.js +6 -0
  119. package/dist/runtime/resolver-public.d.ts +1 -0
  120. package/dist/runtime/resolver-public.js +1 -0
  121. package/dist/runtime/resolver-shared.d.ts +3 -0
  122. package/dist/runtime/resolver-shared.js +12 -0
  123. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  124. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  125. package/dist/runtime/resolver-vendors/browser.d.ts +20 -0
  126. package/dist/runtime/resolver-vendors/browser.js +282 -0
  127. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  128. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  129. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  130. package/dist/runtime/resolver-vendors/twocaptcha.js +368 -0
  131. package/dist/runtime/resolver-vendors/types.d.ts +83 -0
  132. package/dist/runtime/resolver-vendors/types.js +69 -0
  133. package/dist/runtime/resolver.d.ts +59 -0
  134. package/dist/runtime/resolver.js +705 -0
  135. package/dist/runtime/secrets.d.ts +27 -0
  136. package/dist/runtime/secrets.js +51 -0
  137. package/dist/runtime/state.d.ts +5 -2
  138. package/dist/runtime/state.js +280 -74
  139. package/dist/runtime/stealth-cookies.d.ts +20 -0
  140. package/dist/runtime/stealth-cookies.js +111 -0
  141. package/dist/runtime/stealth.d.ts +30 -5
  142. package/dist/runtime/stealth.js +523 -259
  143. package/dist/runtime/stt.d.ts +1 -1
  144. package/dist/runtime/stt.js +12 -27
  145. package/dist/runtime/timeout.d.ts +5 -0
  146. package/dist/runtime/timeout.js +12 -0
  147. package/dist/runtime/trace.d.ts +2 -2
  148. package/dist/runtime/trace.js +2 -4
  149. package/dist/runtime/waterfall.d.ts +1 -1
  150. package/dist/schema.d.ts +1 -1
  151. package/dist/schema.js +7 -15
  152. package/dist/serve.d.ts +1 -1
  153. package/dist/serve.js +1 -1
  154. package/dist/server/index.d.ts +7 -7
  155. package/dist/server/index.js +6 -6
  156. package/dist/server/self-test-input-tokens.d.ts +2 -1
  157. package/dist/server/self-test-input-tokens.js +18 -14
  158. package/dist/server/self-test-redaction.d.ts +1 -1
  159. package/dist/server/self-test-redaction.js +1 -1
  160. package/dist/server/self-test.d.ts +117 -3
  161. package/dist/server/self-test.js +787 -151
  162. package/dist/server/serve-implementation.d.ts +210 -0
  163. package/dist/server/serve-implementation.js +2078 -0
  164. package/dist/server/serve.d.ts +1 -70
  165. package/dist/server/serve.js +1 -1143
  166. package/dist/server/types.d.ts +34 -9
  167. package/dist/server/types.js +8 -1
  168. package/dist/stateful/errors.d.ts +19 -0
  169. package/dist/stateful/errors.js +24 -0
  170. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  171. package/dist/stateful/http-provider-event-emitter.js +237 -0
  172. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  173. package/dist/stateful/http-session-owner-registry.js +210 -0
  174. package/dist/stateful/index.d.ts +18 -0
  175. package/dist/stateful/index.js +18 -0
  176. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  177. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  178. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  179. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  180. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  181. package/dist/stateful/provider-event-pipeline.js +1 -0
  182. package/dist/stateful/provider-events.d.ts +101 -0
  183. package/dist/stateful/provider-events.js +289 -0
  184. package/dist/stateful/session-key.d.ts +15 -0
  185. package/dist/stateful/session-key.js +86 -0
  186. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  187. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  188. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  189. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  190. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  191. package/dist/stateful/stateful-provider-adapter.js +287 -0
  192. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  193. package/dist/stateful/stateful-provider-observability.js +161 -0
  194. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  195. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  196. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  197. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  198. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  199. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  200. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  201. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  202. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  203. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  204. package/dist/stateful-signing.d.ts +18 -0
  205. package/dist/stateful-signing.js +27 -0
  206. package/dist/stealth/profiles.d.ts +1 -1
  207. package/dist/stealth/profiles.js +21 -21
  208. package/dist/stream-evidence.d.ts +74 -0
  209. package/dist/stream-evidence.js +785 -0
  210. package/dist/stream.d.ts +1 -1
  211. package/dist/stream.js +7 -1
  212. package/dist/testing/index.d.ts +3 -2
  213. package/dist/testing/index.js +3 -2
  214. package/dist/testing/run.d.ts +32 -2
  215. package/dist/testing/run.js +488 -28
  216. package/dist/types.d.ts +566 -19
  217. package/dist/types.js +1 -0
  218. package/dist/user-input.d.ts +30 -0
  219. package/dist/user-input.js +66 -0
  220. package/package.json +42 -7
  221. package/src/auth-turn/index.ts +2 -2
  222. package/src/auth.ts +146 -86
  223. package/src/ceremonies/index.ts +167 -92
  224. package/src/cli/commands.ts +10 -0
  225. package/src/cli/create.ts +42 -35
  226. package/src/cli/prompt-assets.ts +865 -0
  227. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  228. package/src/cli/templates/provider/README.md.tpl +4 -4
  229. package/src/config/loader.ts +667 -289
  230. package/src/contract-serialization.ts +10 -18
  231. package/src/contract-types.ts +3 -2
  232. package/src/contract.ts +14 -28
  233. package/src/declaration-validation.ts +202 -0
  234. package/src/define.ts +631 -495
  235. package/src/dev.ts +4 -9
  236. package/src/error-resolution.ts +128 -0
  237. package/src/errors.ts +56 -11
  238. package/src/fixture-sanitization.ts +247 -0
  239. package/src/i18n/catalog.ts +10 -32
  240. package/src/i18n/index.ts +2 -2
  241. package/src/i18n/keys.ts +5 -11
  242. package/src/index.ts +158 -44
  243. package/src/lint.ts +488 -154
  244. package/src/native-address.ts +340 -0
  245. package/src/native-egress-policy.ts +358 -0
  246. package/src/observability.ts +51 -1
  247. package/src/provider.ts +66 -11
  248. package/src/public-schema-field-lint.ts +7 -33
  249. package/src/recipes/gov-api.ts +2 -5
  250. package/src/runtime/auth-flow.ts +13 -7
  251. package/src/runtime/browser.ts +252 -207
  252. package/src/runtime/cache.ts +209 -81
  253. package/src/runtime/choice-wordlist.ts +145 -0
  254. package/src/runtime/choice.ts +758 -197
  255. package/src/runtime/credential.ts +2 -2
  256. package/src/runtime/env.ts +1 -1
  257. package/src/runtime/executor.ts +37 -19
  258. package/src/runtime/http.ts +645 -65
  259. package/src/runtime/insights.ts +15 -53
  260. package/src/runtime/instrumentation.ts +530 -67
  261. package/src/runtime/keyring.ts +7 -19
  262. package/src/runtime/namespace.ts +2 -7
  263. package/src/runtime/native-network-errors.ts +99 -0
  264. package/src/runtime/native-network.ts +1605 -0
  265. package/src/runtime/ocr.ts +523 -0
  266. package/src/runtime/otlp.ts +12 -23
  267. package/src/runtime/perf.ts +1 -1
  268. package/src/runtime/provider.ts +4 -9
  269. package/src/runtime/proxy-errors.ts +29 -42
  270. package/src/runtime/proxy-nodemaven.ts +221 -0
  271. package/src/runtime/proxy-retry-policy.ts +3 -3
  272. package/src/runtime/proxy-telemetry.ts +84 -77
  273. package/src/runtime/redirects.ts +66 -0
  274. package/src/runtime/redis.ts +10 -13
  275. package/src/runtime/request-options.ts +679 -9
  276. package/src/runtime/resolver-config.ts +6 -0
  277. package/src/runtime/resolver-public.ts +18 -0
  278. package/src/runtime/resolver-shared.ts +17 -0
  279. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  280. package/src/runtime/resolver-vendors/browser.ts +408 -0
  281. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  282. package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
  283. package/src/runtime/resolver-vendors/types.ts +173 -0
  284. package/src/runtime/resolver.ts +1060 -0
  285. package/src/runtime/secrets.ts +64 -0
  286. package/src/runtime/state.ts +399 -161
  287. package/src/runtime/stealth-cookies.ts +132 -0
  288. package/src/runtime/stealth.ts +681 -295
  289. package/src/runtime/stt.ts +39 -113
  290. package/src/runtime/timeout.ts +18 -0
  291. package/src/runtime/trace.ts +14 -44
  292. package/src/runtime/waterfall.ts +5 -18
  293. package/src/schema.ts +23 -84
  294. package/src/serve.ts +6 -1
  295. package/src/server/index.ts +30 -7
  296. package/src/server/self-test-input-tokens.ts +29 -14
  297. package/src/server/self-test-redaction.ts +2 -2
  298. package/src/server/self-test.ts +1030 -180
  299. package/src/server/serve-implementation.ts +3062 -0
  300. package/src/server/serve.ts +1 -1781
  301. package/src/server/types.ts +12 -13
  302. package/src/stateful/README.md +146 -0
  303. package/src/stateful/errors.ts +35 -0
  304. package/src/stateful/http-provider-event-emitter.ts +314 -0
  305. package/src/stateful/http-session-owner-registry.ts +306 -0
  306. package/src/stateful/index.ts +18 -0
  307. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  308. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  309. package/src/stateful/provider-event-pipeline.ts +61 -0
  310. package/src/stateful/provider-events.ts +462 -0
  311. package/src/stateful/session-key.ts +111 -0
  312. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  313. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  314. package/src/stateful/stateful-provider-adapter.ts +562 -0
  315. package/src/stateful/stateful-provider-observability.ts +261 -0
  316. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  317. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  318. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  319. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  320. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  321. package/src/stateful-signing.ts +46 -0
  322. package/src/stealth/profiles.ts +27 -33
  323. package/src/stream-evidence.ts +988 -0
  324. package/src/stream.ts +16 -20
  325. package/src/testing/index.ts +11 -2
  326. package/src/testing/run.ts +668 -74
  327. package/src/types.ts +665 -35
  328. package/src/user-input.ts +118 -0
  329. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  330. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  331. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  332. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  333. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  334. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  335. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  336. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  337. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  338. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  339. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  340. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  341. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  342. /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 type { ProviderDefinition } from "../src";
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 { APIFUSE_DESCRIPTION_KEY_META_KEY } from "../src/schema";
21
- import { safeParseSchemaSync } from "../src/schema";
22
- import { type CheckResult, runChecks } from "./apifuse-check";
23
- import { hasSubstantiveXmlStructure } from "./submit-check-xml";
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
- checks.push(...scoreBaseChecks(baseChecks));
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 ![".git", "node_modules", "dist", "build", "coverage"].includes(relativePath);
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 = hasNonEmptyRecordedFixture(JSON.parse(readFileSync(rawPath, "utf8")));
1992
- } catch {
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
- return recordedFixtureStats(value, 0).hasNestedSubstance;
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 success payload; treat a
2075
- // substantive, well-formed one as nested evidence while still counting the
2076
- // string as a leaf so existing JSON provenance heuristics are unchanged.
2077
- return { hasNestedSubstance: hasSubstantiveXmlStructure(value), leafValues: 1 };
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(providerRoot, "vendor-key-leak", findVendorKeyLeakFindings(providerRoot), {
2094
- blockerMessage: "Public schema keys leak raw vendor field names.",
2095
- remediation:
2096
- "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.",
2097
- passMessage: "No vendor field-name leaks detected in public schemas.",
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(source: string, offset: number): NamedObjectRange | undefined {
2406
- return findNamedConstValueRanges(source).find((range) => offset >= range.start && offset <= range.end);
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 findings = findSecretFindings(providerRoot, provider?.id);
3546
- const blockerFindings = findings.filter((finding) => finding.level !== "warn");
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 (findings.length > 0) {
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: findings.map(
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
- if (pattern.test(content)) {
3610
- findings.push({ label, file: relativePath });
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 >= 20) {
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}${secretishContext ? "" : "; may be a false positive"}`,
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 >= 20;
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,}/],