@intentius/chant 0.46.0 → 0.50.0

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 (377) hide show
  1. package/dist/audit/catalog.d.ts +13 -3
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +30 -3
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +9 -2
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/fetch.d.ts.map +1 -1
  8. package/dist/audit/report-html.d.ts.map +1 -1
  9. package/dist/audit/report-model.d.ts +6 -0
  10. package/dist/audit/report-model.d.ts.map +1 -1
  11. package/dist/audit/report.d.ts.map +1 -1
  12. package/dist/audit/rules-doc.d.ts.map +1 -1
  13. package/dist/audit/secrets.d.ts +95 -0
  14. package/dist/audit/secrets.d.ts.map +1 -0
  15. package/dist/audit/wrangler.d.ts +33 -0
  16. package/dist/audit/wrangler.d.ts.map +1 -0
  17. package/dist/build.d.ts +3 -3
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/cli/commands/audit.d.ts +7 -0
  20. package/dist/cli/commands/audit.d.ts.map +1 -1
  21. package/dist/cli/commands/build.d.ts +23 -0
  22. package/dist/cli/commands/build.d.ts.map +1 -1
  23. package/dist/cli/commands/lint.d.ts.map +1 -1
  24. package/dist/cli/handlers/build.d.ts.map +1 -1
  25. package/dist/cli/handlers/components.d.ts +31 -0
  26. package/dist/cli/handlers/components.d.ts.map +1 -1
  27. package/dist/cli/handlers/lifecycle.d.ts +12 -1
  28. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  29. package/dist/cli/handlers/operator.d.ts +32 -0
  30. package/dist/cli/handlers/operator.d.ts.map +1 -0
  31. package/dist/cli/handlers/scenario.d.ts +39 -0
  32. package/dist/cli/handlers/scenario.d.ts.map +1 -0
  33. package/dist/cli/main.d.ts.map +1 -1
  34. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  35. package/dist/cli/mcp/server.d.ts +35 -2
  36. package/dist/cli/mcp/server.d.ts.map +1 -1
  37. package/dist/cli/mcp/tools/explain.d.ts +6 -0
  38. package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
  39. package/dist/cli/mcp/types.d.ts +29 -1
  40. package/dist/cli/mcp/types.d.ts.map +1 -1
  41. package/dist/cli/plugins.d.ts +1 -1
  42. package/dist/cli/plugins.d.ts.map +1 -1
  43. package/dist/cli/registry.d.ts +14 -2
  44. package/dist/cli/registry.d.ts.map +1 -1
  45. package/dist/cli/reporters/stylish.d.ts +15 -1
  46. package/dist/cli/reporters/stylish.d.ts.map +1 -1
  47. package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
  48. package/dist/components/auto-release.d.ts +4 -0
  49. package/dist/components/auto-release.d.ts.map +1 -1
  50. package/dist/components/capability.d.ts +17 -2
  51. package/dist/components/capability.d.ts.map +1 -1
  52. package/dist/components/cli-support.d.ts +7 -0
  53. package/dist/components/cli-support.d.ts.map +1 -1
  54. package/dist/components/component.d.ts +15 -0
  55. package/dist/components/component.d.ts.map +1 -1
  56. package/dist/components/driver.d.ts.map +1 -1
  57. package/dist/components/starter-plugin.d.ts +2 -0
  58. package/dist/components/starter-plugin.d.ts.map +1 -1
  59. package/dist/components/verbs/ensure-secret.d.ts +50 -0
  60. package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
  61. package/dist/components/verbs/index.d.ts +13 -0
  62. package/dist/components/verbs/index.d.ts.map +1 -1
  63. package/dist/components/verbs/r2-sync.d.ts +76 -0
  64. package/dist/components/verbs/r2-sync.d.ts.map +1 -0
  65. package/dist/components/verbs/run-agent.d.ts +499 -0
  66. package/dist/components/verbs/run-agent.d.ts.map +1 -0
  67. package/dist/components/verbs/sign.d.ts +30 -0
  68. package/dist/components/verbs/sign.d.ts.map +1 -1
  69. package/dist/components/verbs/wrangler.d.ts +108 -0
  70. package/dist/components/verbs/wrangler.d.ts.map +1 -0
  71. package/dist/composite.d.ts +6 -1
  72. package/dist/composite.d.ts.map +1 -1
  73. package/dist/config.d.ts +26 -0
  74. package/dist/config.d.ts.map +1 -1
  75. package/dist/deep-observation.d.ts +14 -0
  76. package/dist/deep-observation.d.ts.map +1 -1
  77. package/dist/discovery/collect.d.ts.map +1 -1
  78. package/dist/discovery/fold-import.d.ts +15 -1
  79. package/dist/discovery/fold-import.d.ts.map +1 -1
  80. package/dist/discovery/fold-rank.d.ts +66 -0
  81. package/dist/discovery/fold-rank.d.ts.map +1 -0
  82. package/dist/discovery/index.d.ts +15 -0
  83. package/dist/discovery/index.d.ts.map +1 -1
  84. package/dist/discovery/param-deps.d.ts +17 -0
  85. package/dist/discovery/param-deps.d.ts.map +1 -0
  86. package/dist/effect-receipt.d.ts +177 -0
  87. package/dist/effect-receipt.d.ts.map +1 -0
  88. package/dist/fold/fold.d.ts +55 -2
  89. package/dist/fold/fold.d.ts.map +1 -1
  90. package/dist/fold/subset.d.ts +20 -0
  91. package/dist/fold/subset.d.ts.map +1 -1
  92. package/dist/index.d.ts +4 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/lexicon-schema.d.ts +2 -0
  95. package/dist/lexicon-schema.d.ts.map +1 -1
  96. package/dist/lexicon.d.ts +137 -3
  97. package/dist/lexicon.d.ts.map +1 -1
  98. package/dist/lifecycle/change-set.d.ts +33 -5
  99. package/dist/lifecycle/change-set.d.ts.map +1 -1
  100. package/dist/lifecycle/converge-ledger.d.ts +90 -0
  101. package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
  102. package/dist/lifecycle/deep-diff.d.ts +18 -0
  103. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  104. package/dist/lifecycle/deep-observe.d.ts +9 -1
  105. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  106. package/dist/lifecycle/gate-ledger.d.ts +33 -0
  107. package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
  108. package/dist/lifecycle/git.d.ts +145 -21
  109. package/dist/lifecycle/git.d.ts.map +1 -1
  110. package/dist/lifecycle/index.d.ts +6 -0
  111. package/dist/lifecycle/index.d.ts.map +1 -1
  112. package/dist/lifecycle/lease.d.ts +113 -0
  113. package/dist/lifecycle/lease.d.ts.map +1 -0
  114. package/dist/lifecycle/observation-baseline.d.ts +21 -3
  115. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  116. package/dist/lifecycle/receipt-plan.d.ts +62 -0
  117. package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
  118. package/dist/lifecycle/release-ledger.d.ts +20 -0
  119. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  120. package/dist/lifecycle/scenario-eval.d.ts +42 -0
  121. package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
  122. package/dist/lifecycle/scenario.d.ts +163 -0
  123. package/dist/lifecycle/scenario.d.ts.map +1 -0
  124. package/dist/lifecycle/symptoms.d.ts +63 -0
  125. package/dist/lifecycle/symptoms.d.ts.map +1 -0
  126. package/dist/lifecycle/teardown.d.ts +6 -4
  127. package/dist/lifecycle/teardown.d.ts.map +1 -1
  128. package/dist/lifecycle/unobserved-gate.d.ts +67 -0
  129. package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
  130. package/dist/lint/knowledge-checks.d.ts +48 -0
  131. package/dist/lint/knowledge-checks.d.ts.map +1 -0
  132. package/dist/lint/output-checks.d.ts +5 -0
  133. package/dist/lint/output-checks.d.ts.map +1 -0
  134. package/dist/lint/output-docs.d.ts +94 -0
  135. package/dist/lint/output-docs.d.ts.map +1 -0
  136. package/dist/lint/pipeline-change-gate.d.ts +101 -0
  137. package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
  138. package/dist/lint/post-synth.d.ts +41 -0
  139. package/dist/lint/post-synth.d.ts.map +1 -1
  140. package/dist/lint/receipt-checks.d.ts +9 -0
  141. package/dist/lint/receipt-checks.d.ts.map +1 -0
  142. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
  143. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
  144. package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
  145. package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
  146. package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
  147. package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
  148. package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
  149. package/dist/lint/rules/index.d.ts +3 -1
  150. package/dist/lint/rules/index.d.ts.map +1 -1
  151. package/dist/lsp/lexicon-providers.d.ts +7 -0
  152. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  153. package/dist/okf-read.d.ts +78 -0
  154. package/dist/okf-read.d.ts.map +1 -0
  155. package/dist/op/activity-contract.d.ts +139 -0
  156. package/dist/op/activity-contract.d.ts.map +1 -0
  157. package/dist/op/builders.d.ts +140 -3
  158. package/dist/op/builders.d.ts.map +1 -1
  159. package/dist/op/converge-rule.d.ts +161 -0
  160. package/dist/op/converge-rule.d.ts.map +1 -0
  161. package/dist/op/generate-pipeline.d.ts +39 -0
  162. package/dist/op/generate-pipeline.d.ts.map +1 -0
  163. package/dist/op/index.d.ts +18 -2
  164. package/dist/op/index.d.ts.map +1 -1
  165. package/dist/op/local-executor.d.ts +2 -1
  166. package/dist/op/local-executor.d.ts.map +1 -1
  167. package/dist/op/op-verb-class.d.ts +42 -0
  168. package/dist/op/op-verb-class.d.ts.map +1 -0
  169. package/dist/op/operator.d.ts +128 -0
  170. package/dist/op/operator.d.ts.map +1 -0
  171. package/dist/op/receipt-store.d.ts +138 -0
  172. package/dist/op/receipt-store.d.ts.map +1 -0
  173. package/dist/op/step-output-ref.d.ts +187 -0
  174. package/dist/op/step-output-ref.d.ts.map +1 -0
  175. package/dist/op/types.d.ts +49 -2
  176. package/dist/op/types.d.ts.map +1 -1
  177. package/dist/provenance.d.ts +73 -3
  178. package/dist/provenance.d.ts.map +1 -1
  179. package/dist/runtime-adapter.d.ts +7 -1
  180. package/dist/runtime-adapter.d.ts.map +1 -1
  181. package/dist/secret-materialization.d.ts +138 -0
  182. package/dist/secret-materialization.d.ts.map +1 -0
  183. package/dist/secret-provenance.d.ts +218 -0
  184. package/dist/secret-provenance.d.ts.map +1 -0
  185. package/dist/serializer.d.ts +29 -0
  186. package/dist/serializer.d.ts.map +1 -1
  187. package/dist/toml.d.ts +40 -5
  188. package/dist/toml.d.ts.map +1 -1
  189. package/dist/yaml.d.ts.map +1 -1
  190. package/package.json +4 -1
  191. package/src/audit/catalog.test.ts +1 -1
  192. package/src/audit/catalog.ts +75 -3
  193. package/src/audit/core.test.ts +57 -0
  194. package/src/audit/core.ts +0 -0
  195. package/src/audit/detect-bundle.test.ts +1 -1
  196. package/src/audit/discover.test.ts +24 -0
  197. package/src/audit/discover.ts +40 -4
  198. package/src/audit/fetch.test.ts +216 -3
  199. package/src/audit/fetch.ts +270 -59
  200. package/src/audit/report-html.ts +5 -2
  201. package/src/audit/report-model.ts +9 -0
  202. package/src/audit/report.test.ts +22 -0
  203. package/src/audit/report.ts +3 -2
  204. package/src/audit/rules-doc.ts +13 -1
  205. package/src/audit/secrets.test.ts +303 -0
  206. package/src/audit/secrets.ts +406 -0
  207. package/src/audit/wrangler.test.ts +230 -0
  208. package/src/audit/wrangler.ts +290 -0
  209. package/src/build.test.ts +41 -0
  210. package/src/build.ts +39 -6
  211. package/src/cli/command-group.ts +1 -1
  212. package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
  213. package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
  214. package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
  215. package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
  216. package/src/cli/commands/audit.test.ts +268 -1
  217. package/src/cli/commands/audit.ts +87 -18
  218. package/src/cli/commands/build.test.ts +245 -0
  219. package/src/cli/commands/build.ts +210 -21
  220. package/src/cli/commands/lint.ts +15 -3
  221. package/src/cli/handlers/build.ts +2 -0
  222. package/src/cli/handlers/components.test.ts +199 -1
  223. package/src/cli/handlers/components.ts +160 -3
  224. package/src/cli/handlers/explain.test.ts +70 -1
  225. package/src/cli/handlers/graph.test.ts +20 -0
  226. package/src/cli/handlers/graph.ts +12 -3
  227. package/src/cli/handlers/lifecycle.test.ts +115 -1
  228. package/src/cli/handlers/lifecycle.ts +96 -15
  229. package/src/cli/handlers/operator.test.ts +255 -0
  230. package/src/cli/handlers/operator.ts +240 -0
  231. package/src/cli/handlers/scenario.test.ts +456 -0
  232. package/src/cli/handlers/scenario.ts +330 -0
  233. package/src/cli/main.test.ts +23 -0
  234. package/src/cli/main.ts +72 -1
  235. package/src/cli/mcp/resource-handlers.ts +38 -1
  236. package/src/cli/mcp/server.test.ts +323 -3
  237. package/src/cli/mcp/server.ts +84 -7
  238. package/src/cli/mcp/tools/explain.ts +51 -2
  239. package/src/cli/mcp/types.ts +27 -1
  240. package/src/cli/plugins.ts +4 -2
  241. package/src/cli/registry.ts +14 -2
  242. package/src/cli/reporters/stylish.test.ts +154 -0
  243. package/src/cli/reporters/stylish.ts +154 -33
  244. package/src/codegen/docs-rule-scanning.test.ts +42 -0
  245. package/src/codegen/docs-rule-scanning.ts +25 -2
  246. package/src/components/README.md +7 -0
  247. package/src/components/auto-release.ts +6 -0
  248. package/src/components/capability.ts +17 -2
  249. package/src/components/cli-support.test.ts +17 -0
  250. package/src/components/cli-support.ts +13 -1
  251. package/src/components/component-schema.test.ts +32 -0
  252. package/src/components/component.schema.json +6 -0
  253. package/src/components/component.test.ts +21 -0
  254. package/src/components/component.ts +15 -0
  255. package/src/components/driver.ts +12 -4
  256. package/src/components/registry.test.ts +7 -2
  257. package/src/components/starter-plugin.ts +17 -0
  258. package/src/components/verbs/ensure-secret.test.ts +130 -0
  259. package/src/components/verbs/ensure-secret.ts +79 -0
  260. package/src/components/verbs/index.ts +13 -0
  261. package/src/components/verbs/r2-sync.test.ts +107 -0
  262. package/src/components/verbs/r2-sync.ts +124 -0
  263. package/src/components/verbs/run-agent.test.ts +683 -0
  264. package/src/components/verbs/run-agent.ts +786 -0
  265. package/src/components/verbs/sign.test.ts +19 -0
  266. package/src/components/verbs/sign.ts +34 -2
  267. package/src/components/verbs/wrangler.test.ts +170 -0
  268. package/src/components/verbs/wrangler.ts +241 -0
  269. package/src/composite.ts +31 -2
  270. package/src/config.test.ts +15 -0
  271. package/src/config.ts +30 -0
  272. package/src/deep-observation.test.ts +19 -0
  273. package/src/deep-observation.ts +17 -0
  274. package/src/discovery/collect.ts +11 -2
  275. package/src/discovery/fold-import.test.ts +54 -0
  276. package/src/discovery/fold-import.ts +178 -38
  277. package/src/discovery/fold-rank.test.ts +197 -0
  278. package/src/discovery/fold-rank.ts +346 -0
  279. package/src/discovery/index.ts +16 -1
  280. package/src/discovery/param-deps.test.ts +118 -0
  281. package/src/discovery/param-deps.ts +170 -0
  282. package/src/effect-receipt-exclusion.test.ts +190 -0
  283. package/src/effect-receipt.test.ts +419 -0
  284. package/src/effect-receipt.ts +412 -0
  285. package/src/fold/fold.test.ts +6 -2
  286. package/src/fold/fold.ts +184 -3
  287. package/src/fold/subset.test.ts +95 -6
  288. package/src/fold/subset.ts +66 -2
  289. package/src/index.ts +4 -0
  290. package/src/lexicon-schema.ts +3 -0
  291. package/src/lexicon.ts +151 -5
  292. package/src/lifecycle/change-set.ts +46 -7
  293. package/src/lifecycle/converge-ledger.test.ts +199 -0
  294. package/src/lifecycle/converge-ledger.ts +179 -0
  295. package/src/lifecycle/deep-diff.test.ts +79 -1
  296. package/src/lifecycle/deep-diff.ts +23 -0
  297. package/src/lifecycle/deep-observe.ts +13 -2
  298. package/src/lifecycle/gate-ledger.test.ts +103 -0
  299. package/src/lifecycle/gate-ledger.ts +140 -0
  300. package/src/lifecycle/git.test.ts +430 -0
  301. package/src/lifecycle/git.ts +446 -84
  302. package/src/lifecycle/index.ts +6 -0
  303. package/src/lifecycle/lease.test.ts +343 -0
  304. package/src/lifecycle/lease.ts +270 -0
  305. package/src/lifecycle/observation-baseline.test.ts +46 -0
  306. package/src/lifecycle/observation-baseline.ts +33 -1
  307. package/src/lifecycle/receipt-plan.test.ts +250 -0
  308. package/src/lifecycle/receipt-plan.ts +249 -0
  309. package/src/lifecycle/release-ledger.ts +20 -0
  310. package/src/lifecycle/scenario-eval.test.ts +199 -0
  311. package/src/lifecycle/scenario-eval.ts +158 -0
  312. package/src/lifecycle/scenario.test.ts +195 -0
  313. package/src/lifecycle/scenario.ts +321 -0
  314. package/src/lifecycle/symptoms.test.ts +116 -0
  315. package/src/lifecycle/symptoms.ts +126 -0
  316. package/src/lifecycle/teardown.test.ts +31 -0
  317. package/src/lifecycle/teardown.ts +6 -4
  318. package/src/lifecycle/unobserved-gate.test.ts +109 -0
  319. package/src/lifecycle/unobserved-gate.ts +102 -0
  320. package/src/lint/knowledge-checks.test.ts +80 -0
  321. package/src/lint/knowledge-checks.ts +74 -0
  322. package/src/lint/output-checks.test.ts +85 -0
  323. package/src/lint/output-checks.ts +99 -0
  324. package/src/lint/output-docs.test.ts +220 -0
  325. package/src/lint/output-docs.ts +204 -0
  326. package/src/lint/pipeline-change-gate.test.ts +144 -0
  327. package/src/lint/pipeline-change-gate.ts +153 -0
  328. package/src/lint/post-synth.test.ts +97 -0
  329. package/src/lint/post-synth.ts +60 -0
  330. package/src/lint/receipt-checks.test.ts +101 -0
  331. package/src/lint/receipt-checks.ts +93 -0
  332. package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
  333. package/src/lint/rules/comp/comp.test.ts +49 -1
  334. package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
  335. package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
  336. package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
  337. package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
  338. package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
  339. package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
  340. package/src/lint/rules/index.ts +7 -1
  341. package/src/lsp/lexicon-providers.test.ts +44 -0
  342. package/src/lsp/lexicon-providers.ts +11 -1
  343. package/src/okf-read.test.ts +149 -0
  344. package/src/okf-read.ts +197 -0
  345. package/src/op/activity-contract.test.ts +180 -0
  346. package/src/op/activity-contract.ts +278 -0
  347. package/src/op/builders-exports.test.ts +17 -1
  348. package/src/op/builders.ts +198 -6
  349. package/src/op/converge-rule.test.ts +179 -0
  350. package/src/op/converge-rule.ts +311 -0
  351. package/src/op/effect-step.test.ts +311 -0
  352. package/src/op/generate-pipeline.test.ts +53 -0
  353. package/src/op/generate-pipeline.ts +99 -0
  354. package/src/op/index.ts +40 -3
  355. package/src/op/local-executor.test.ts +92 -0
  356. package/src/op/local-executor.ts +212 -29
  357. package/src/op/op-verb-class.test.ts +126 -0
  358. package/src/op/op-verb-class.ts +115 -0
  359. package/src/op/op.test.ts +25 -2
  360. package/src/op/operator.test.ts +346 -0
  361. package/src/op/operator.ts +213 -0
  362. package/src/op/receipt-store.ts +211 -0
  363. package/src/op/step-output-ref.test.ts +334 -0
  364. package/src/op/step-output-ref.ts +453 -0
  365. package/src/op/types.ts +51 -2
  366. package/src/provenance.test.ts +151 -4
  367. package/src/provenance.ts +118 -4
  368. package/src/runtime-adapter.ts +31 -10
  369. package/src/secret-materialization.test.ts +199 -0
  370. package/src/secret-materialization.ts +235 -0
  371. package/src/secret-provenance.test.ts +388 -0
  372. package/src/secret-provenance.ts +475 -0
  373. package/src/serializer.ts +30 -0
  374. package/src/toml.test.ts +157 -384
  375. package/src/toml.ts +371 -5
  376. package/src/yaml.test.ts +88 -0
  377. package/src/yaml.ts +76 -6
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Secrets & credential detection (#443) — a finding family that is
3
+ * independent of lexicon. Every other finding family in this directory reads
4
+ * a specific CI dialect's structure (a GitHub Actions `steps:` array, a
5
+ * GitLab `variables:` block); this one scans the raw text of any file the
6
+ * audit already reads for shapes that look like a live credential, no matter
7
+ * what kind of file it lives in.
8
+ *
9
+ * Three layers, cheapest/highest-precision first:
10
+ * 1. Well-known provider prefixes (AWS `AKIA…`, GitHub `ghp_…`, Slack
11
+ * `xox…`, Google `AIza…`, Stripe `sk_live_…`) — near-zero false-positive
12
+ * rate because the shape is a vendor-documented constant.
13
+ * 2. Structural shapes (a PEM private-key block, a `scheme://user:pass@host`
14
+ * connection string) — still high precision.
15
+ * 3. A generic high-entropy-string heuristic as the catch-all for anything
16
+ * that doesn't match a known shape. This is the noisy layer, so it only
17
+ * fires on tokens with real character-class diversity, skips pure-hex
18
+ * runs (git SHAs, checksums), and defaults to a conservative threshold
19
+ * that callers can tune.
20
+ *
21
+ * Pure: no fs, no network, no Node-only global. Safe to run in the Workers
22
+ * runtime the hosted audit service uses (epic #350's edge constraint) as well
23
+ * as the CLI.
24
+ *
25
+ * Redaction is the load-bearing guarantee here, not an afterthought: nothing
26
+ * this module returns — message, entity, or any other field on the
27
+ * `AuditFinding` — ever contains the matched secret text. Callers get the
28
+ * rule id, a human label for the kind of secret, the file/line location, and
29
+ * a non-reversible fingerprint of the value (for the allowlist below) —
30
+ * never the value itself. See `secrets.test.ts`'s redaction-guarantee test.
31
+ */
32
+
33
+ import type { AuditFinding } from "./core";
34
+ import type { Severity } from "../lint/rule";
35
+
36
+ /** The minimal file shape the scanner needs (matches `discover.ts`'s `RepoFile`). */
37
+ export interface ScannableFile {
38
+ path: string;
39
+ content: string;
40
+ }
41
+
42
+ /**
43
+ * One allowlist entry. At least one of `ruleId` / `fingerprint` must be set —
44
+ * `file` alone is not enough to allow everything in a file, so a stray file
45
+ * entry can't silently blanket-suppress a real secret added later.
46
+ * `fingerprint` is `fingerprintSecret(value)` of the *redacted* value, so an
47
+ * allowlist entry never has to contain the secret itself.
48
+ */
49
+ export interface SecretAllowRule {
50
+ /** Restrict this allow entry to one rule id (e.g. "SEC010"). */
51
+ ruleId?: string;
52
+ /** Restrict to one file — exact relative path, or a path-suffix match. */
53
+ file?: string;
54
+ /** `fingerprintSecret(value)` of the specific occurrence to allow. */
55
+ fingerprint?: string;
56
+ }
57
+
58
+ export interface SecretsScanOptions {
59
+ /** Shannon-entropy threshold (bits/char) above which a token is flagged. Default 4.0 — tunable (#443). */
60
+ entropyThreshold?: number;
61
+ /** Minimum candidate-token length the entropy heuristic considers. Default 24. */
62
+ entropyMinLength?: number;
63
+ /** Ignore/allow entries (#443) — suppress specific findings without an inline marker. */
64
+ allow?: SecretAllowRule[];
65
+ }
66
+
67
+ const DEFAULT_ENTROPY_THRESHOLD = 4.0;
68
+ const DEFAULT_ENTROPY_MIN_LENGTH = 24;
69
+
70
+ // ── Redaction-safe fingerprinting ───────────────────────────────────────
71
+
72
+ /**
73
+ * A short fingerprint of a secret value (two intertwined 32-bit hash
74
+ * accumulators, 16 hex chars). Lets an allowlist entry — or a report —
75
+ * reference "this exact occurrence" without ever storing or displaying the
76
+ * secret itself. Not a cryptographic digest (no `crypto` dependency, so this
77
+ * stays pure/edge-safe); it only needs to key an allowlist entry, not resist
78
+ * a determined attacker who already suspects the plaintext.
79
+ */
80
+ export function fingerprintSecret(value: string): string {
81
+ let h1 = 0x811c9dc5;
82
+ let h2 = 0x9e3779b9;
83
+ for (let i = 0; i < value.length; i++) {
84
+ const c = value.charCodeAt(i);
85
+ h1 ^= c;
86
+ h1 = Math.imul(h1, 0x01000193);
87
+ h2 ^= c;
88
+ h2 = Math.imul(h2, 0x85ebca6b);
89
+ h2 = (h2 << 13) | (h2 >>> 19);
90
+ }
91
+ return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
92
+ }
93
+
94
+ // ── Inline ignore marker ────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Inline suppression marker: a comment containing `chant-audit-ignore`,
98
+ * optionally scoped to one or more rule ids (`chant-audit-ignore: SEC001`,
99
+ * `chant-audit-ignore SEC001,SEC010`). Bare (no rule ids) suppresses every
100
+ * secrets finding on the line. Works in any comment syntax (`#`, `//`, etc.)
101
+ * since only the marker text is matched, not the comment delimiter.
102
+ *
103
+ * Checked against the finding's own line AND the line immediately above it —
104
+ * a PEM block's `-----BEGIN…-----` line can't carry a trailing comment
105
+ * without corrupting the key, so the marker goes on the line before it.
106
+ */
107
+ const IGNORE_MARKER_RE = /chant-audit-ignore(?:[:=]?\s*([A-Za-z0-9,\s-]+))?/i;
108
+
109
+ function parseIgnoreMarker(line: string): { all: boolean; ids: Set<string> } | undefined {
110
+ const m = line.match(IGNORE_MARKER_RE);
111
+ if (!m) return undefined;
112
+ const raw = m[1]?.trim();
113
+ if (!raw) return { all: true, ids: new Set() };
114
+ const ids = raw
115
+ .split(/[,\s]+/)
116
+ .map((s) => s.trim().toUpperCase())
117
+ .filter(Boolean);
118
+ return ids.length === 0 ? { all: true, ids: new Set() } : { all: false, ids: new Set(ids) };
119
+ }
120
+
121
+ function isIgnoredByMarker(lines: string[], lineIdx0: number, ruleId: string): boolean {
122
+ for (const idx of [lineIdx0, lineIdx0 - 1]) {
123
+ if (idx < 0 || idx >= lines.length) continue;
124
+ const marker = parseIgnoreMarker(lines[idx]);
125
+ if (marker && (marker.all || marker.ids.has(ruleId))) return true;
126
+ }
127
+ return false;
128
+ }
129
+
130
+ // ── Config allowlist ────────────────────────────────────────────────────
131
+
132
+ function isAllowed(allow: SecretAllowRule[], f: { ruleId: string; file: string; fingerprint: string }): boolean {
133
+ return allow.some((a) => {
134
+ if (!a.ruleId && !a.fingerprint) return false; // `file` alone never allows everything in a file
135
+ if (a.ruleId && a.ruleId !== f.ruleId) return false;
136
+ if (a.fingerprint && a.fingerprint !== f.fingerprint) return false;
137
+ if (a.file && a.file !== f.file && !f.file.endsWith(`/${a.file}`)) return false;
138
+ return true;
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Parse a `.chant-audit.json`-style config's `secrets` section (or a flat
144
+ * object with the same shape) into scan options. Pure — a caller reads the
145
+ * file (CLI: fs; hosted: an already-fetched file), this only parses the
146
+ * text. Tolerant: malformed/missing content yields no options rather than
147
+ * throwing.
148
+ */
149
+ export function parseSecretsConfig(content: string): SecretsScanOptions {
150
+ let json: unknown;
151
+ try {
152
+ json = JSON.parse(content);
153
+ } catch {
154
+ return {};
155
+ }
156
+ const root = json && typeof json === "object" ? (json as Record<string, unknown>) : {};
157
+ const sec = root.secrets && typeof root.secrets === "object" ? (root.secrets as Record<string, unknown>) : root;
158
+
159
+ const opts: SecretsScanOptions = {};
160
+ if (typeof sec.entropyThreshold === "number") opts.entropyThreshold = sec.entropyThreshold;
161
+ if (typeof sec.entropyMinLength === "number") opts.entropyMinLength = sec.entropyMinLength;
162
+ if (Array.isArray(sec.allow)) {
163
+ opts.allow = sec.allow
164
+ .filter((a): a is Record<string, unknown> => !!a && typeof a === "object")
165
+ .map((a) => ({
166
+ ruleId: typeof a.ruleId === "string" ? a.ruleId : undefined,
167
+ file: typeof a.file === "string" ? a.file : undefined,
168
+ fingerprint: typeof a.fingerprint === "string" ? a.fingerprint : undefined,
169
+ }));
170
+ }
171
+ return opts;
172
+ }
173
+
174
+ // ── Shannon entropy ─────────────────────────────────────────────────────
175
+
176
+ function shannonEntropy(s: string): number {
177
+ const freq = new Map<string, number>();
178
+ for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
179
+ let entropy = 0;
180
+ for (const count of freq.values()) {
181
+ const p = count / s.length;
182
+ entropy -= p * Math.log2(p);
183
+ }
184
+ return entropy;
185
+ }
186
+
187
+ // ── Placeholder / low-signal exclusions ─────────────────────────────────
188
+
189
+ const PLACEHOLDER_RE = /example|placeholder|dummy|changeme|change-me|redacted|sample|fixme|todo|xxxxxxxx|your[-_]?(api[-_]?)?(key|token|secret)|fake[-_]?(key|token|secret)|test[-_]?(key|token|secret)/i;
190
+
191
+ /** Cheap, format-agnostic "this doesn't look like a real secret" filter. */
192
+ function looksLikePlaceholder(value: string): boolean {
193
+ if (PLACEHOLDER_RE.test(value)) return true;
194
+ const uniq = new Set(value.toLowerCase()).size;
195
+ if (value.length > 8 && uniq <= 2) return true; // e.g. "0000000000000000"
196
+ return false;
197
+ }
198
+
199
+ function connStringHasPlaceholderCreds(match: string): boolean {
200
+ const m = match.match(/:\/\/([^:@/\s]+):([^@/\s]+)@/);
201
+ if (!m) return false;
202
+ const bland = /^(user(name)?|admin|root|pass(word)?|changeme|example|test|dummy|guest|secret)$/i;
203
+ return bland.test(m[1]) && bland.test(m[2]);
204
+ }
205
+
206
+ // ── Pattern detectors ────────────────────────────────────────────────────
207
+
208
+ interface Detector {
209
+ ruleId: string;
210
+ kind: string;
211
+ severity: Severity;
212
+ regex: RegExp;
213
+ /** The sensitive substring within the full match, for placeholder/fingerprint purposes. Defaults to the full match. */
214
+ extract?: (m: RegExpMatchArray) => string;
215
+ /** Extra placeholder/false-positive filter beyond the generic one. */
216
+ extraFilter?: (fullMatch: string) => boolean;
217
+ }
218
+
219
+ const DETECTORS: Detector[] = [
220
+ {
221
+ ruleId: "SEC001",
222
+ kind: "AWS access key ID",
223
+ severity: "error",
224
+ regex: /\b(?:AKIA|ABIA|ACCA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA)[A-Z0-9]{16}\b/g,
225
+ },
226
+ {
227
+ ruleId: "SEC002",
228
+ kind: "AWS secret access key",
229
+ severity: "error",
230
+ regex: /\baws_secret_access_key\s*[:=]\s*['"]?([A-Za-z0-9/+]{40})['"]?/gi,
231
+ extract: (m) => m[1],
232
+ },
233
+ {
234
+ ruleId: "SEC003",
235
+ kind: "GitHub token",
236
+ severity: "error",
237
+ regex: /\bgh[pousr]_[A-Za-z0-9]{36,255}\b/g,
238
+ },
239
+ {
240
+ ruleId: "SEC004",
241
+ kind: "Slack token",
242
+ severity: "error",
243
+ regex: /\bxox[baprs]-[0-9A-Za-z-]{10,72}\b/g,
244
+ },
245
+ {
246
+ ruleId: "SEC005",
247
+ kind: "Google API key",
248
+ severity: "error",
249
+ regex: /\bAIza[0-9A-Za-z\-_]{35}\b/g,
250
+ },
251
+ {
252
+ ruleId: "SEC006",
253
+ kind: "Stripe live secret key",
254
+ severity: "error",
255
+ regex: /\b(?:sk|rk)_live_[0-9A-Za-z]{24,}\b/g,
256
+ },
257
+ {
258
+ ruleId: "SEC007",
259
+ kind: "PEM private key block",
260
+ severity: "error",
261
+ regex: /-----BEGIN ((?:RSA|EC|DSA|OPENSSH|ENCRYPTED|PGP) PRIVATE KEY|PRIVATE KEY)-----[\s\S]*?-----END \1-----/g,
262
+ },
263
+ {
264
+ ruleId: "SEC008",
265
+ kind: "Bearer/authorization token",
266
+ severity: "warning",
267
+ regex: /\b[Bb]earer\s+([A-Za-z0-9\-_.~+/]{20,}={0,2})/g,
268
+ extract: (m) => m[1],
269
+ },
270
+ {
271
+ ruleId: "SEC009",
272
+ kind: "Credentials embedded in a connection string",
273
+ severity: "error",
274
+ regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|ftp|sftp|ldap|sqlserver|jdbc:[a-z]+):\/\/[^\s'"/:@]+:[^\s'"/@]+@[^\s'"/]+/gi,
275
+ extraFilter: (full) => !connStringHasPlaceholderCreds(full),
276
+ },
277
+ ];
278
+
279
+ /** The high-entropy catch-all's rule id (kept distinct so callers can special-case it). */
280
+ export const ENTROPY_RULE_ID = "SEC010";
281
+
282
+ const HEX_ONLY_RE = /^[0-9a-f]+$/i;
283
+
284
+ /**
285
+ * A `word-word-word`/`word/word` slug reads as a dashed/slashed identifier —
286
+ * a model id (`anthropic/claude-sonnet-4-6`), an npm package spec, a docker
287
+ * image ref — not a secret: a real random secret's `-`/`_`/`/` runs don't
288
+ * line up with dictionary-shaped alphabetic chunks. Requires at least two
289
+ * separators so a single hyphen (common inside genuine tokens) doesn't
290
+ * trigger it.
291
+ */
292
+ export function looksLikeIdentifierSlug(value: string): boolean {
293
+ const parts = value.split(/[-_/]/).filter(Boolean);
294
+ if (parts.length < 3) return false;
295
+ const wordy = parts.filter((p) => /^[a-zA-Z]+$/.test(p) && p.length >= 3).length;
296
+ return wordy / parts.length >= 0.5;
297
+ }
298
+
299
+ /** At least 3 of {lower, upper, digit, symbol} present — cuts plain-word / all-hex false positives. */
300
+ function hasClassDiversity(value: string): boolean {
301
+ let classes = 0;
302
+ if (/[a-z]/.test(value)) classes++;
303
+ if (/[A-Z]/.test(value)) classes++;
304
+ if (/[0-9]/.test(value)) classes++;
305
+ if (/[^A-Za-z0-9]/.test(value)) classes++;
306
+ return classes >= 3;
307
+ }
308
+
309
+ // ── Line-number lookup ───────────────────────────────────────────────────
310
+
311
+ function lineStarts(content: string): number[] {
312
+ const starts = [0];
313
+ for (let i = 0; i < content.length; i++) if (content[i] === "\n") starts.push(i + 1);
314
+ return starts;
315
+ }
316
+
317
+ function lineNumberFor(starts: number[], index: number): number {
318
+ let lo = 0;
319
+ let hi = starts.length - 1;
320
+ let ans = 0;
321
+ while (lo <= hi) {
322
+ const mid = (lo + hi) >> 1;
323
+ if (starts[mid] <= index) {
324
+ ans = mid;
325
+ lo = mid + 1;
326
+ } else {
327
+ hi = mid - 1;
328
+ }
329
+ }
330
+ return ans + 1; // 1-based
331
+ }
332
+
333
+ // ── Scan ──────────────────────────────────────────────────────────────────
334
+
335
+ /**
336
+ * Scan a set of files for likely secrets/credentials. Pure with respect to
337
+ * the filesystem and network. Findings never carry the matched value —
338
+ * only its rule id, kind, file/line, and a redaction-safe fingerprint.
339
+ */
340
+ export function scanForSecrets(files: ScannableFile[], opts: SecretsScanOptions = {}): AuditFinding[] {
341
+ const entropyThreshold = opts.entropyThreshold ?? DEFAULT_ENTROPY_THRESHOLD;
342
+ const entropyMinLength = opts.entropyMinLength ?? DEFAULT_ENTROPY_MIN_LENGTH;
343
+ const allow = opts.allow ?? [];
344
+ const entropyRe = new RegExp(`[A-Za-z0-9_\\-+/=]{${entropyMinLength},}`, "g");
345
+
346
+ const findings: AuditFinding[] = [];
347
+
348
+ for (const file of files) {
349
+ if (!file.content) continue;
350
+ const lines = file.content.split("\n");
351
+ const starts = lineStarts(file.content);
352
+ const claimed: Array<[number, number]> = [];
353
+
354
+ // `claimed` marks text a pattern detector already matched, so the entropy
355
+ // catch-all doesn't re-flag the same value under SEC010 — regardless of
356
+ // whether the pattern-detector's own finding gets suppressed below. An
357
+ // ignored/allowed PEM block, for instance, must not have its base64 body
358
+ // re-flagged as a fresh "high-entropy string" (that would defeat the
359
+ // suppression, not just duplicate it).
360
+ const emit = (ruleId: string, kind: string, severity: Severity, index: number, length: number, secretValue: string): void => {
361
+ const lineNo = lineNumberFor(starts, index);
362
+ if (isIgnoredByMarker(lines, lineNo - 1, ruleId)) return;
363
+ const fingerprint = fingerprintSecret(secretValue);
364
+ if (isAllowed(allow, { ruleId, file: file.path, fingerprint })) return;
365
+ findings.push({
366
+ checkId: ruleId,
367
+ severity,
368
+ message: `Likely secret detected: ${kind} at ${file.path}:${lineNo} (value redacted, ${secretValue.length} chars, fingerprint ${fingerprint}). Remove it from source and rotate the credential.`,
369
+ file: file.path,
370
+ lexicon: "secrets",
371
+ entity: kind,
372
+ line: lineNo,
373
+ fingerprint,
374
+ });
375
+ };
376
+
377
+ for (const d of DETECTORS) {
378
+ for (const m of file.content.matchAll(d.regex)) {
379
+ const full = m[0];
380
+ const secretValue = d.extract ? d.extract(m) : full;
381
+ if (!secretValue) continue;
382
+ if (looksLikePlaceholder(secretValue)) continue;
383
+ if (d.extraFilter && !d.extraFilter(full)) continue;
384
+ const index = m.index ?? 0;
385
+ claimed.push([index, index + full.length]);
386
+ emit(d.ruleId, d.kind, d.severity, index, full.length, secretValue);
387
+ }
388
+ }
389
+
390
+ for (const m of file.content.matchAll(entropyRe)) {
391
+ const value = m[0];
392
+ const start = m.index ?? 0;
393
+ const end = start + value.length;
394
+ if (claimed.some(([s, e]) => start < e && end > s)) continue; // already assessed by a pattern detector
395
+ if (HEX_ONLY_RE.test(value)) continue; // likely a git SHA / checksum, not a secret
396
+ if (!hasClassDiversity(value)) continue;
397
+ if (looksLikePlaceholder(value)) continue;
398
+ if (looksLikeIdentifierSlug(value)) continue; // a model id / package spec / image ref, not a secret
399
+ const entropy = shannonEntropy(value);
400
+ if (entropy < entropyThreshold) continue;
401
+ emit(ENTROPY_RULE_ID, `high-entropy string (~${entropy.toFixed(2)} bits/char)`, "warning", start, value.length, value);
402
+ }
403
+ }
404
+
405
+ return findings;
406
+ }
@@ -0,0 +1,230 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { auditWranglerConfigs, isWranglerConfigPath, type ScannableFile } from "./wrangler";
3
+
4
+ function ids(files: ScannableFile[]): string[] {
5
+ return auditWranglerConfigs(files).map((f) => f.checkId);
6
+ }
7
+
8
+ describe("isWranglerConfigPath (detector)", () => {
9
+ test("matches wrangler.toml at any depth", () => {
10
+ expect(isWranglerConfigPath("wrangler.toml")).toBe(true);
11
+ expect(isWranglerConfigPath("apps/api/wrangler.toml")).toBe(true);
12
+ });
13
+ test("does not match other files", () => {
14
+ expect(isWranglerConfigPath("wrangler.json")).toBe(false);
15
+ expect(isWranglerConfigPath("wrangler.jsonc")).toBe(false);
16
+ expect(isWranglerConfigPath("Wrangler.toml")).toBe(false);
17
+ expect(isWranglerConfigPath("not-wrangler.toml")).toBe(false);
18
+ });
19
+ });
20
+
21
+ describe("auditWranglerConfigs — parser tolerance", () => {
22
+ test("a file that fails to parse contributes no findings, never throws", () => {
23
+ expect(() => auditWranglerConfigs([{ path: "wrangler.toml", content: "not = valid = toml = [" }])).not.toThrow();
24
+ expect(ids([{ path: "wrangler.toml", content: "not = valid = toml = [" }])).toEqual([]);
25
+ });
26
+ test("non-wrangler files are ignored entirely", () => {
27
+ expect(ids([{ path: "other.toml", content: 'workers_dev = true\n[env.production]\nworkers_dev = true\n' }])).toEqual([]);
28
+ });
29
+ });
30
+
31
+ describe("WRG001: workers_dev = true in a production-named environment", () => {
32
+ test("positive: production env exposed on the public subdomain", () => {
33
+ const content = `
34
+ name = "my-worker"
35
+
36
+ [env.production]
37
+ workers_dev = true
38
+ `;
39
+ expect(ids([{ path: "wrangler.toml", content }])).toContain("WRG001");
40
+ });
41
+
42
+ test("negative: workers_dev true at top-level (dev default) does not fire", () => {
43
+ const content = `
44
+ name = "my-worker"
45
+ workers_dev = true
46
+ `;
47
+ expect(ids([{ path: "wrangler.toml", content }])).not.toContain("WRG001");
48
+ });
49
+
50
+ test("negative: production env with workers_dev = false", () => {
51
+ const content = `
52
+ [env.production]
53
+ workers_dev = false
54
+ `;
55
+ expect(ids([{ path: "wrangler.toml", content }])).not.toContain("WRG001");
56
+ });
57
+ });
58
+
59
+ describe("WRG002: secret-shaped key in [vars]", () => {
60
+ test("positive: API key stored as a plain var", () => {
61
+ const content = `
62
+ [vars]
63
+ STRIPE_API_KEY = "sk_live_not_a_real_key_but_shaped_like_one"
64
+ ENVIRONMENT = "production"
65
+ `;
66
+ const findings = auditWranglerConfigs([{ path: "wrangler.toml", content }]);
67
+ const wrg002 = findings.filter((f) => f.checkId === "WRG002");
68
+ expect(wrg002.length).toBe(1);
69
+ expect(wrg002[0].entity).toBe("STRIPE_API_KEY");
70
+ // The value itself must never appear in the message (redaction discipline).
71
+ expect(wrg002[0].message).not.toContain("sk_live_not_a_real_key_but_shaped_like_one");
72
+ });
73
+
74
+ test("positive: fires per-environment too", () => {
75
+ const content = `
76
+ [env.staging.vars]
77
+ DB_PASSWORD = "hunter2"
78
+ `;
79
+ expect(ids([{ path: "wrangler.toml", content }])).toContain("WRG002");
80
+ });
81
+
82
+ test("negative: an ordinary var name does not fire", () => {
83
+ const content = `
84
+ [vars]
85
+ ENVIRONMENT = "production"
86
+ LOG_LEVEL = "info"
87
+ `;
88
+ expect(ids([{ path: "wrangler.toml", content }])).not.toContain("WRG002");
89
+ });
90
+ });
91
+
92
+ describe("WRG003: observability explicitly disabled", () => {
93
+ test("positive: enabled = false", () => {
94
+ const content = `
95
+ [observability]
96
+ enabled = false
97
+ `;
98
+ expect(ids([{ path: "wrangler.toml", content }])).toContain("WRG003");
99
+ });
100
+
101
+ test("negative: enabled = true", () => {
102
+ const content = `
103
+ [observability]
104
+ enabled = true
105
+ `;
106
+ expect(ids([{ path: "wrangler.toml", content }])).not.toContain("WRG003");
107
+ });
108
+
109
+ test("negative: no [observability] table at all", () => {
110
+ const content = `name = "my-worker"`;
111
+ expect(ids([{ path: "wrangler.toml", content }])).not.toContain("WRG003");
112
+ });
113
+ });
114
+
115
+ describe("WRG004: account-wide wildcard route", () => {
116
+ test("positive: bare wildcard route", () => {
117
+ const content = `routes = ["*/*"]`;
118
+ expect(ids([{ path: "wrangler.toml", content }])).toContain("WRG004");
119
+ });
120
+
121
+ test("positive: object-form wildcard route pattern", () => {
122
+ const content = `
123
+ [[routes]]
124
+ pattern = "*"
125
+ zone_name = "example.com"
126
+ `;
127
+ expect(ids([{ path: "wrangler.toml", content }])).toContain("WRG004");
128
+ });
129
+
130
+ test("negative: a zone-scoped route is fine", () => {
131
+ const content = `
132
+ [[routes]]
133
+ pattern = "example.com/*"
134
+ zone_name = "example.com"
135
+ `;
136
+ expect(ids([{ path: "wrangler.toml", content }])).not.toContain("WRG004");
137
+ });
138
+ });
139
+
140
+ describe("WRG005: non-prod environment shares a data store with production", () => {
141
+ test("positive: staging KV namespace reuses production's id", () => {
142
+ const content = `
143
+ [[kv_namespaces]]
144
+ binding = "MY_KV"
145
+ id = "shared-id-123"
146
+
147
+ [env.staging]
148
+ kv_namespaces = [{ binding = "MY_KV", id = "shared-id-123" }]
149
+ `;
150
+ const findings = auditWranglerConfigs([{ path: "wrangler.toml", content }]);
151
+ const wrg005 = findings.filter((f) => f.checkId === "WRG005");
152
+ expect(wrg005.length).toBe(1);
153
+ expect(wrg005[0].entity).toBe("kv_namespaces:shared-id-123");
154
+ });
155
+
156
+ test("negative: staging has its own distinct id", () => {
157
+ const content = `
158
+ [[kv_namespaces]]
159
+ binding = "MY_KV"
160
+ id = "prod-id"
161
+
162
+ [env.staging]
163
+ kv_namespaces = [{ binding = "MY_KV", id = "staging-id" }]
164
+ `;
165
+ expect(ids([{ path: "wrangler.toml", content }])).not.toContain("WRG005");
166
+ });
167
+
168
+ test("negative: a single-environment file (no dev/staging env) never trips on its own top-level id", () => {
169
+ const content = `
170
+ [[kv_namespaces]]
171
+ binding = "MY_KV"
172
+ id = "only-one-env"
173
+ `;
174
+ expect(ids([{ path: "wrangler.toml", content }])).not.toContain("WRG005");
175
+ });
176
+ });
177
+
178
+ describe("WRG006: static assets/site served from the project root", () => {
179
+ test("positive: [site].bucket = \".\"", () => {
180
+ const content = `
181
+ [site]
182
+ bucket = "."
183
+ `;
184
+ expect(ids([{ path: "wrangler.toml", content }])).toContain("WRG006");
185
+ });
186
+
187
+ test("positive: [assets].directory = \"/\"", () => {
188
+ const content = `
189
+ [assets]
190
+ directory = "/"
191
+ `;
192
+ expect(ids([{ path: "wrangler.toml", content }])).toContain("WRG006");
193
+ });
194
+
195
+ test("negative: a scoped assets directory", () => {
196
+ const content = `
197
+ [assets]
198
+ directory = "./public"
199
+ `;
200
+ expect(ids([{ path: "wrangler.toml", content }])).not.toContain("WRG006");
201
+ });
202
+ });
203
+
204
+ describe("a representative multi-check wrangler.toml", () => {
205
+ test("fires the expected set and nothing else", () => {
206
+ const content = `
207
+ name = "my-worker"
208
+ main = "src/index.ts"
209
+ compatibility_date = "2024-01-01"
210
+
211
+ [observability]
212
+ enabled = false
213
+
214
+ [vars]
215
+ LOG_LEVEL = "info"
216
+
217
+ [[routes]]
218
+ pattern = "example.com/*"
219
+ zone_name = "example.com"
220
+
221
+ [env.production]
222
+ workers_dev = true
223
+
224
+ [env.production.vars]
225
+ API_TOKEN = "not-a-real-value"
226
+ `;
227
+ const found = new Set(ids([{ path: "wrangler.toml", content }]));
228
+ expect(found).toEqual(new Set(["WRG001", "WRG002", "WRG003"]));
229
+ });
230
+ });