@intentius/chant 0.124.30 → 0.124.32

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 (346) hide show
  1. package/bin/chant +7 -2
  2. package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
  3. package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
  4. package/dist/cli/commands/check-lexicon-plugin.js +10 -7
  5. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  6. package/dist/cli/commands/check-lexicon.js +18 -12
  7. package/dist/cli/commands/import.d.ts +10 -0
  8. package/dist/cli/commands/import.d.ts.map +1 -1
  9. package/dist/cli/commands/import.js +37 -0
  10. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  11. package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
  12. package/dist/cli/commands/onboard.d.ts.map +1 -1
  13. package/dist/cli/commands/onboard.js +8 -4
  14. package/dist/cli/commands/update.d.ts.map +1 -1
  15. package/dist/cli/commands/update.js +2 -1
  16. package/dist/cli/dependencies.d.ts +18 -0
  17. package/dist/cli/dependencies.d.ts.map +1 -0
  18. package/dist/cli/dependencies.js +120 -0
  19. package/dist/cli/handlers/build.d.ts.map +1 -1
  20. package/dist/cli/handlers/build.js +23 -4
  21. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  22. package/dist/cli/handlers/fan-out.js +12 -4
  23. package/dist/cli/handlers/graph.d.ts.map +1 -1
  24. package/dist/cli/handlers/graph.js +16 -6
  25. package/dist/cli/handlers/lint.d.ts.map +1 -1
  26. package/dist/cli/handlers/lint.js +6 -0
  27. package/dist/cli/handlers/operator.d.ts +2 -2
  28. package/dist/cli/handlers/operator.js +3 -3
  29. package/dist/cli/handlers/pr.d.ts +14 -0
  30. package/dist/cli/handlers/pr.d.ts.map +1 -1
  31. package/dist/cli/handlers/pr.js +488 -32
  32. package/dist/cli/handlers/promote.d.ts.map +1 -1
  33. package/dist/cli/handlers/promote.js +3 -1
  34. package/dist/cli/handlers/run.d.ts +13 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +38 -15
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +74 -12
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +65 -0
  41. package/dist/cli/registry.d.ts +29 -2
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/registry.js +6 -2
  44. package/dist/codegen/generate-post-synth-barrel.js +2 -2
  45. package/dist/codegen/lexicon-regen.d.ts +4 -0
  46. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  47. package/dist/codegen/lexicon-regen.js +1 -1
  48. package/dist/codegen/pinned-upgrade.js +2 -2
  49. package/dist/components/capability.d.ts +44 -3
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/capability.js +21 -0
  52. package/dist/components/changed-files.d.ts +7 -1
  53. package/dist/components/changed-files.d.ts.map +1 -1
  54. package/dist/components/changed-files.js +9 -1
  55. package/dist/components/driver.d.ts +38 -0
  56. package/dist/components/driver.d.ts.map +1 -1
  57. package/dist/components/driver.js +47 -6
  58. package/dist/components/fan-out-record.d.ts +6 -1
  59. package/dist/components/fan-out-record.d.ts.map +1 -1
  60. package/dist/components/fan-out-record.js +24 -0
  61. package/dist/components/fan-out-run.d.ts +20 -0
  62. package/dist/components/fan-out-run.d.ts.map +1 -1
  63. package/dist/components/fan-out-run.js +21 -1
  64. package/dist/components/pr-pipeline.d.ts +65 -2
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +107 -15
  67. package/dist/components/pr-run.d.ts +127 -29
  68. package/dist/components/pr-run.d.ts.map +1 -1
  69. package/dist/components/pr-run.js +290 -70
  70. package/dist/components/wave-plan.d.ts +5 -2
  71. package/dist/components/wave-plan.d.ts.map +1 -1
  72. package/dist/components/wave-plan.js +12 -1
  73. package/dist/discovery/fold-import.d.ts.map +1 -1
  74. package/dist/discovery/fold-import.js +16 -6
  75. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  76. package/dist/discovery/sandbox/bundle.js +6 -0
  77. package/dist/discovery/specifier-resolve.d.ts.map +1 -1
  78. package/dist/discovery/specifier-resolve.js +8 -2
  79. package/dist/fold/fold.d.ts.map +1 -1
  80. package/dist/fold/fold.js +68 -1
  81. package/dist/fold/subset.d.ts +37 -0
  82. package/dist/fold/subset.d.ts.map +1 -1
  83. package/dist/fold/subset.js +88 -0
  84. package/dist/lexicon-module.d.ts +10 -0
  85. package/dist/lexicon-module.d.ts.map +1 -1
  86. package/dist/lexicon-module.js +53 -8
  87. package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
  88. package/dist/lexicon-plugin-helpers.js +6 -4
  89. package/dist/lexicon.d.ts +2 -0
  90. package/dist/lexicon.d.ts.map +1 -1
  91. package/dist/lifecycle/affected.d.ts.map +1 -1
  92. package/dist/lifecycle/affected.js +7 -1
  93. package/dist/lifecycle/release-ledger.d.ts +21 -0
  94. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  95. package/dist/lifecycle/release-ledger.js +24 -0
  96. package/dist/lifecycle/work-dir.d.ts +45 -0
  97. package/dist/lifecycle/work-dir.d.ts.map +1 -0
  98. package/dist/lifecycle/work-dir.js +78 -0
  99. package/dist/lint/discover.d.ts +17 -9
  100. package/dist/lint/discover.d.ts.map +1 -1
  101. package/dist/lint/discover.js +62 -54
  102. package/dist/module-source.d.ts +57 -0
  103. package/dist/module-source.d.ts.map +1 -0
  104. package/dist/module-source.js +104 -0
  105. package/dist/op/activities/factory.js +1 -1
  106. package/dist/op/discover.d.ts.map +1 -1
  107. package/dist/op/discover.js +13 -3
  108. package/dist/op/gate.d.ts.map +1 -1
  109. package/dist/op/gate.js +3 -0
  110. package/dist/op/local-executor.d.ts +12 -0
  111. package/dist/op/local-executor.d.ts.map +1 -1
  112. package/dist/op/local-executor.js +22 -3
  113. package/dist/op/op-waves-run.d.ts.map +1 -1
  114. package/dist/op/op-waves-run.js +20 -5
  115. package/dist/op/operator.d.ts +2 -2
  116. package/dist/op/operator.js +2 -2
  117. package/dist/op/redact.d.ts +34 -0
  118. package/dist/op/redact.d.ts.map +1 -0
  119. package/dist/op/redact.js +94 -0
  120. package/dist/op/step-console.d.ts +2 -0
  121. package/dist/op/step-console.d.ts.map +1 -1
  122. package/dist/op/step-console.js +5 -2
  123. package/dist/op/steward.d.ts +6 -4
  124. package/dist/op/steward.d.ts.map +1 -1
  125. package/dist/op/steward.js +5 -3
  126. package/dist/op/work-lease-run.d.ts +20 -7
  127. package/dist/op/work-lease-run.d.ts.map +1 -1
  128. package/dist/op/work-lease-run.js +56 -12
  129. package/dist/pr-forge.d.ts +27 -0
  130. package/dist/pr-forge.d.ts.map +1 -1
  131. package/dist/pr-forge.js +51 -6
  132. package/dist/pr-loop.d.ts +141 -4
  133. package/dist/pr-loop.d.ts.map +1 -1
  134. package/dist/pr-loop.js +122 -6
  135. package/dist/pr-report-url.d.ts +10 -2
  136. package/dist/pr-report-url.d.ts.map +1 -1
  137. package/dist/pr-report-url.js +52 -6
  138. package/dist/ts-loader.d.ts +38 -4
  139. package/dist/ts-loader.d.ts.map +1 -1
  140. package/dist/ts-loader.js +190 -16
  141. package/dist/wave-runtime.mjs +208 -208
  142. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
  143. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
  144. package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
  145. package/dist/workspace/chant-migrations.d.ts.map +1 -1
  146. package/dist/workspace/chant-migrations.js +2 -0
  147. package/dist/workspace/checks/boxes.d.ts +6 -6
  148. package/dist/workspace/checks/boxes.js +9 -9
  149. package/dist/workspace/ci-green-forge.d.ts +6 -4
  150. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  151. package/dist/workspace/ci-green.d.ts +28 -12
  152. package/dist/workspace/ci-green.d.ts.map +1 -1
  153. package/dist/workspace/ci-green.js +49 -21
  154. package/dist/workspace/ci-list.schema.json +12 -6
  155. package/dist/workspace/ci-report-url.d.ts +18 -0
  156. package/dist/workspace/ci-report-url.d.ts.map +1 -0
  157. package/dist/workspace/ci-report-url.js +33 -0
  158. package/dist/workspace/ci-run-index.d.ts +108 -0
  159. package/dist/workspace/ci-run-index.d.ts.map +1 -0
  160. package/dist/workspace/ci-run-index.js +164 -0
  161. package/dist/workspace/ci-run-index.schema.json +52 -0
  162. package/dist/workspace/ci-run.d.ts +159 -9
  163. package/dist/workspace/ci-run.d.ts.map +1 -1
  164. package/dist/workspace/ci-run.js +419 -50
  165. package/dist/workspace/ci-runner-key.d.ts +133 -0
  166. package/dist/workspace/ci-runner-key.d.ts.map +1 -0
  167. package/dist/workspace/ci-runner-key.js +237 -0
  168. package/dist/workspace/ci-secrets.d.ts +65 -0
  169. package/dist/workspace/ci-secrets.d.ts.map +1 -0
  170. package/dist/workspace/ci-secrets.js +211 -0
  171. package/dist/workspace/ci-serve.d.ts +67 -4
  172. package/dist/workspace/ci-serve.d.ts.map +1 -1
  173. package/dist/workspace/ci-serve.js +240 -35
  174. package/dist/workspace/declaration.d.ts +8 -0
  175. package/dist/workspace/declaration.d.ts.map +1 -1
  176. package/dist/workspace/declaration.js +11 -2
  177. package/dist/workspace/declaration.schema.json +7 -2
  178. package/dist/workspace/export.d.ts +1 -1
  179. package/dist/workspace/export.d.ts.map +1 -1
  180. package/dist/workspace/export.js +7 -3
  181. package/dist/workspace/ls.d.ts +6 -1
  182. package/dist/workspace/ls.d.ts.map +1 -1
  183. package/dist/workspace/ls.js +1 -0
  184. package/dist/workspace/ls.schema.json +7 -1
  185. package/dist/workspace/pin-integrity.d.ts.map +1 -1
  186. package/dist/workspace/pin-integrity.js +3 -2
  187. package/dist/workspace/pr-report.schema.json +74 -7
  188. package/dist/workspace/reason-codes.d.ts +7 -3
  189. package/dist/workspace/reason-codes.d.ts.map +1 -1
  190. package/dist/workspace/reason-codes.js +9 -4
  191. package/dist/workspace/run-event.schema.json +3 -1
  192. package/dist/workspace/status.schema.json +3 -1
  193. package/dist/workspace/trust/trust-write.d.ts +109 -0
  194. package/dist/workspace/trust/trust-write.d.ts.map +1 -0
  195. package/dist/workspace/trust/trust-write.js +320 -0
  196. package/dist/workspace/trust-write.schema.json +265 -0
  197. package/dist/workspace/write-scope.d.ts +9 -0
  198. package/dist/workspace/write-scope.d.ts.map +1 -1
  199. package/dist/workspace/write-scope.js +11 -1
  200. package/package.json +1 -1
  201. package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
  202. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
  203. package/src/cli/commands/audit.test.ts +2 -2
  204. package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
  205. package/src/cli/commands/check-lexicon-plugin.ts +11 -8
  206. package/src/cli/commands/check-lexicon.test.ts +1 -1
  207. package/src/cli/commands/check-lexicon.ts +16 -11
  208. package/src/cli/commands/import.ts +40 -0
  209. package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
  210. package/src/cli/commands/init-lexicon.test.ts +8 -5
  211. package/src/cli/commands/onboard.test.ts +45 -0
  212. package/src/cli/commands/onboard.ts +8 -4
  213. package/src/cli/commands/update.test.ts +6 -0
  214. package/src/cli/commands/update.ts +4 -1
  215. package/src/cli/compiled-cli.e2e.test.ts +76 -11
  216. package/src/cli/dependencies.e2e.test.ts +101 -0
  217. package/src/cli/dependencies.test.ts +93 -0
  218. package/src/cli/dependencies.ts +122 -0
  219. package/src/cli/handlers/build.test.ts +69 -0
  220. package/src/cli/handlers/build.ts +20 -4
  221. package/src/cli/handlers/fan-out.test.ts +73 -0
  222. package/src/cli/handlers/fan-out.ts +11 -4
  223. package/src/cli/handlers/graph.test.ts +76 -0
  224. package/src/cli/handlers/graph.ts +19 -12
  225. package/src/cli/handlers/lint.test.ts +42 -0
  226. package/src/cli/handlers/lint.ts +6 -0
  227. package/src/cli/handlers/operator.ts +3 -3
  228. package/src/cli/handlers/pr-waves.test.ts +449 -0
  229. package/src/cli/handlers/pr.test.ts +134 -0
  230. package/src/cli/handlers/pr.ts +523 -31
  231. package/src/cli/handlers/promote.test.ts +26 -1
  232. package/src/cli/handlers/promote.ts +3 -2
  233. package/src/cli/handlers/run-components-exit.test.ts +99 -0
  234. package/src/cli/handlers/run-signer.test.ts +79 -0
  235. package/src/cli/handlers/run.ts +34 -16
  236. package/src/cli/main.test.ts +19 -0
  237. package/src/cli/main.ts +73 -12
  238. package/src/cli/parse-args.ts +48 -0
  239. package/src/cli/registry.ts +35 -4
  240. package/src/codegen/generate-post-synth-barrel.ts +2 -2
  241. package/src/codegen/lexicon-regen.ts +1 -1
  242. package/src/codegen/pinned-upgrade.ts +2 -2
  243. package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
  244. package/src/components/capability.ts +51 -3
  245. package/src/components/changed-files.test.ts +16 -1
  246. package/src/components/changed-files.ts +9 -1
  247. package/src/components/driver.test.ts +36 -1
  248. package/src/components/driver.ts +76 -4
  249. package/src/components/fan-out-record.ts +29 -1
  250. package/src/components/fan-out-run.test.ts +35 -0
  251. package/src/components/fan-out-run.ts +40 -1
  252. package/src/components/pr-pipeline.test.ts +100 -0
  253. package/src/components/pr-pipeline.ts +132 -17
  254. package/src/components/pr-run.test.ts +323 -18
  255. package/src/components/pr-run.ts +354 -75
  256. package/src/components/wave-plan.ts +16 -4
  257. package/src/discovery/fold-host-method.test.ts +87 -0
  258. package/src/discovery/fold-import.ts +15 -5
  259. package/src/discovery/fold-package-composite.test.ts +27 -0
  260. package/src/discovery/fold-specifiers.test.ts +8 -0
  261. package/src/discovery/sandbox/bundle.ts +6 -0
  262. package/src/discovery/specifier-resolve.ts +8 -2
  263. package/src/fold/fold.test.ts +72 -0
  264. package/src/fold/fold.ts +71 -0
  265. package/src/fold/subset.test.ts +31 -0
  266. package/src/fold/subset.ts +99 -0
  267. package/src/lexicon-dist.e2e.test.ts +254 -0
  268. package/src/lexicon-module.test.ts +47 -0
  269. package/src/lexicon-module.ts +49 -7
  270. package/src/lexicon-plugin-helpers.ts +6 -4
  271. package/src/lexicon.ts +2 -0
  272. package/src/lifecycle/affected.test.ts +7 -0
  273. package/src/lifecycle/affected.ts +6 -1
  274. package/src/lifecycle/release-ledger.ts +34 -0
  275. package/src/lifecycle/work-dir.test.ts +77 -0
  276. package/src/lifecycle/work-dir.ts +82 -0
  277. package/src/lint/discover.test.ts +46 -2
  278. package/src/lint/discover.ts +55 -49
  279. package/src/module-source.test.ts +85 -0
  280. package/src/module-source.ts +112 -0
  281. package/src/op/activities/factory.ts +1 -1
  282. package/src/op/discover-root.test.ts +31 -2
  283. package/src/op/discover.ts +14 -4
  284. package/src/op/gate.test.ts +30 -0
  285. package/src/op/gate.ts +3 -0
  286. package/src/op/local-executor.ts +31 -3
  287. package/src/op/op-waves-run.ts +20 -5
  288. package/src/op/op-waves.test.ts +49 -0
  289. package/src/op/operator.ts +2 -2
  290. package/src/op/redact.ts +93 -0
  291. package/src/op/step-console.ts +7 -2
  292. package/src/op/steward.ts +6 -4
  293. package/src/op/work-lease-run.test.ts +88 -2
  294. package/src/op/work-lease-run.ts +52 -12
  295. package/src/pr-forge-bundle.test.ts +2 -2
  296. package/src/pr-forge.test.ts +71 -0
  297. package/src/pr-forge.ts +70 -6
  298. package/src/pr-loop-bundle.test.ts +2 -1
  299. package/src/pr-loop.test.ts +117 -1
  300. package/src/pr-loop.ts +252 -10
  301. package/src/pr-report-url.ts +58 -6
  302. package/src/terraform/__fixtures__/advise.test.ts +21 -2
  303. package/src/ts-loader.test.ts +89 -1
  304. package/src/ts-loader.ts +180 -13
  305. package/src/workspace/changes.test.ts +1 -1
  306. package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
  307. package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
  308. package/src/workspace/chant-migrations.e2e.test.ts +5 -0
  309. package/src/workspace/chant-migrations.ts +2 -0
  310. package/src/workspace/checks/boxes.ts +9 -9
  311. package/src/workspace/ci-green-forge.ts +6 -4
  312. package/src/workspace/ci-green-runs.test.ts +53 -11
  313. package/src/workspace/ci-green.test.ts +1 -1
  314. package/src/workspace/ci-green.ts +50 -22
  315. package/src/workspace/ci-list.schema.json +12 -6
  316. package/src/workspace/ci-report-url.test.ts +220 -0
  317. package/src/workspace/ci-report-url.ts +33 -0
  318. package/src/workspace/ci-run-index.schema.json +52 -0
  319. package/src/workspace/ci-run-index.test.ts +392 -0
  320. package/src/workspace/ci-run-index.ts +206 -0
  321. package/src/workspace/ci-run.test.ts +520 -42
  322. package/src/workspace/ci-run.ts +483 -51
  323. package/src/workspace/ci-runner-key.test.ts +204 -0
  324. package/src/workspace/ci-runner-key.ts +275 -0
  325. package/src/workspace/ci-secrets.test.ts +106 -0
  326. package/src/workspace/ci-secrets.ts +213 -0
  327. package/src/workspace/ci-serve.test.ts +282 -39
  328. package/src/workspace/ci-serve.ts +231 -34
  329. package/src/workspace/declaration.schema.json +7 -2
  330. package/src/workspace/declaration.test.ts +23 -0
  331. package/src/workspace/declaration.ts +19 -3
  332. package/src/workspace/export.ts +6 -3
  333. package/src/workspace/ls.schema.json +7 -1
  334. package/src/workspace/ls.ts +7 -2
  335. package/src/workspace/pin-integrity.test.ts +5 -0
  336. package/src/workspace/pin-integrity.ts +3 -2
  337. package/src/workspace/pr-report.schema.json +74 -7
  338. package/src/workspace/reason-codes.test.ts +2 -0
  339. package/src/workspace/reason-codes.ts +9 -4
  340. package/src/workspace/run-event.schema.json +3 -1
  341. package/src/workspace/status.schema.json +3 -1
  342. package/src/workspace/trust/trust-write.test.ts +179 -0
  343. package/src/workspace/trust/trust-write.ts +377 -0
  344. package/src/workspace/trust-write.schema.json +265 -0
  345. package/src/workspace/write-scope.test.ts +29 -1
  346. package/src/workspace/write-scope.ts +12 -1
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The runner key on a `chant ci serve` or `chant ci run` host (#4099, epic
3
+ * #4101).
4
+ *
5
+ * The host keeps its runner key in the serve's state directory, by default
6
+ * `<git common dir>/chant/ci-serve/runner-key.pem`: an Ed25519 private key in
7
+ * PEM, mode 0600, made by `--setup-key` when there is none. Its public half
8
+ * is the `ssh-ed25519` line `.chant/trust.json` lists under `runners`, and
9
+ * `--setup-key --trust` proposes that entry through `chant workspace trust
10
+ * runner add` (#3510) on a branch of its own, pushed for review. It never
11
+ * commits to the target branch: changing the trust config is a protected
12
+ * write, and the change is reviewed and merged like any other.
13
+ *
14
+ * `ci serve` refuses to start when its key is not listed, with the runner
15
+ * role, in `.chant/trust.json` at the target branch's head, since every run
16
+ * it signed would then be ignored by `ci tick --source runs`. `--unsigned`
17
+ * starts it without a key.
18
+ */
19
+ /** The runner key's file in the state directory. */
20
+ export declare const RUNNER_KEY_FILE = "runner-key.pem";
21
+ /** The principal `--setup-key --trust` lists the key for when `--principal` names none. */
22
+ export declare function defaultRunnerPrincipal(): string;
23
+ export interface HostRunnerKey {
24
+ path: string;
25
+ pem: string;
26
+ /** `ssh-ed25519 <base64>`. */
27
+ publicKey: string;
28
+ /** Whether this call made it. */
29
+ created: boolean;
30
+ }
31
+ /** The host's runner key in `stateDir`, or undefined when there is none. */
32
+ export declare function hostRunnerKey(stateDir: string): HostRunnerKey | undefined;
33
+ /** The host's runner key in `stateDir`, made when there is none: Ed25519, PEM, mode 0600 in a directory of mode 0700. */
34
+ export declare function ensureHostRunnerKey(stateDir: string): HostRunnerKey;
35
+ /**
36
+ * The target branch's head, as the readers of runners take it: the remote's
37
+ * branch after a fetch (best effort), else the local branch. Undefined when
38
+ * neither exists.
39
+ */
40
+ export declare function targetHead(top: string, branch: string, remote?: string): {
41
+ commit: string;
42
+ ref: string;
43
+ } | undefined;
44
+ /** The exact command that lists `publicKey` for `principal`. */
45
+ export declare function trustCommand(principal: string, publicKey: string): string;
46
+ /** Whether `.chant/trust.json` at `commit` lists the key as a runner holding the runner role: its principal, or why not. */
47
+ export declare function runnerKeyListed(top: string, commit: string, pem: string): {
48
+ ok: true;
49
+ principal: string;
50
+ } | {
51
+ ok: false;
52
+ reason: string;
53
+ };
54
+ export interface RunnerTrustProposal {
55
+ /** The branch holding the commit, or null when the key was already listed at the target's head. */
56
+ branch: string | null;
57
+ commit: string | null;
58
+ /** Whether the branch was pushed to the remote. */
59
+ pushed: boolean;
60
+ /** The trust config's digest on the branch. */
61
+ digest: string | null;
62
+ }
63
+ /**
64
+ * Propose the runner entry for review (#4099): in a temporary worktree at
65
+ * the target's head, run `trust runner add` (#3510), commit the change on
66
+ * `chant/trust-runner/<principal>`, and push that branch to `remote` when
67
+ * the clone has it. The target branch is never written. The commit is made
68
+ * with the host's git identity and signing settings; when the policy at base
69
+ * has signers, the change needs a signature by a policy writer before it
70
+ * merges.
71
+ */
72
+ export declare function proposeRunnerTrust(req: {
73
+ top: string;
74
+ branch: string;
75
+ principal: string;
76
+ publicKey: string;
77
+ by?: string;
78
+ remote?: string;
79
+ }): RunnerTrustProposal;
80
+ export interface RunnerKeySetup {
81
+ key: HostRunnerKey;
82
+ /** The target's head the trust config was read at, or null when there is no such branch. */
83
+ head: string | null;
84
+ /** The principal the key is listed for at the head, or null. */
85
+ listedAs: string | null;
86
+ principal: string;
87
+ command: string;
88
+ proposal?: RunnerTrustProposal;
89
+ }
90
+ /** `--setup-key [--trust]`: make or read the key, see whether the target's head lists it, and with `trust` propose the entry. */
91
+ export declare function setupRunnerKey(req: {
92
+ top: string;
93
+ stateDir: string;
94
+ branch: string;
95
+ principal?: string;
96
+ trust?: boolean;
97
+ by?: string;
98
+ }): RunnerKeySetup;
99
+ /** What `--setup-key` prints on stderr; the public key alone goes to stdout. */
100
+ export declare function formatRunnerKeySetup(s: RunnerKeySetup, branch: string): string;
101
+ /**
102
+ * The key `ci serve` signs with, checked before its first poll (#4099): the
103
+ * `--key` file, else `CHANT_RUNNER_KEY`, else the host's key in the state
104
+ * directory. Signing is opt-in (#4156):
105
+ *
106
+ * - With no key, `ci serve` serves unsigned and says so only in `note`.
107
+ * - A key `.chant/trust.json` at the target's head does not list with the
108
+ * runner role, or a target branch it can't find to check against, leaves
109
+ * it serving unsigned, with one `warning`.
110
+ * - With `required` (the declaration's `ci.green.signedSince`) each of
111
+ * those refuses to start, as before #4156.
112
+ *
113
+ * A key that is not an Ed25519 private key in PEM, or a `--key` file that
114
+ * can't be read, refuses either way. `unsigned` (`--unsigned`) skips the
115
+ * key without `required` and is ignored with it.
116
+ */
117
+ export declare function ciServeRunnerKey(req: {
118
+ top: string;
119
+ stateDir: string;
120
+ branch: string;
121
+ keyFile?: string;
122
+ envKey?: string;
123
+ unsigned?: boolean;
124
+ /** The declaration's `ci.green.signedSince`, when it sets one: every run must then be signed. */
125
+ required?: string | null;
126
+ }): {
127
+ pem?: string;
128
+ fromFlag: boolean;
129
+ source: string;
130
+ warning?: string;
131
+ note?: string;
132
+ };
133
+ //# sourceMappingURL=ci-runner-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci-runner-key.d.ts","sourceRoot":"","sources":["../../src/workspace/ci-runner-key.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,237 @@
1
+ /**
2
+ * The runner key on a `chant ci serve` or `chant ci run` host (#4099, epic
3
+ * #4101).
4
+ *
5
+ * The host keeps its runner key in the serve's state directory, by default
6
+ * `<git common dir>/chant/ci-serve/runner-key.pem`: an Ed25519 private key in
7
+ * PEM, mode 0600, made by `--setup-key` when there is none. Its public half
8
+ * is the `ssh-ed25519` line `.chant/trust.json` lists under `runners`, and
9
+ * `--setup-key --trust` proposes that entry through `chant workspace trust
10
+ * runner add` (#3510) on a branch of its own, pushed for review. It never
11
+ * commits to the target branch: changing the trust config is a protected
12
+ * write, and the change is reviewed and merged like any other.
13
+ *
14
+ * `ci serve` refuses to start when its key is not listed, with the runner
15
+ * role, in `.chant/trust.json` at the target branch's head, since every run
16
+ * it signed would then be ignored by `ci tick --source runs`. `--unsigned`
17
+ * starts it without a key.
18
+ */
19
+ import { execFileSync } from "node:child_process";
20
+ import { generateKeyPairSync } from "node:crypto";
21
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
22
+ import { hostname, tmpdir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { ciTagSigner } from "./ci-green.js";
25
+ import { CiRunError } from "./ci-run.js";
26
+ import { loadRunnerKey } from "./trust/dsse.js";
27
+ import { trustWrite } from "./trust/trust-write.js";
28
+ /** The runner key's file in the state directory. */
29
+ export const RUNNER_KEY_FILE = "runner-key.pem";
30
+ /** The principal `--setup-key --trust` lists the key for when `--principal` names none. */
31
+ export function defaultRunnerPrincipal() {
32
+ return `chant-runner:${hostname().split(".")[0].toLowerCase().replace(/[^a-z0-9-]/g, "-") || "host"}`;
33
+ }
34
+ const message = (err) => (err instanceof Error ? err.message : String(err));
35
+ function git(cwd, args) {
36
+ return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
37
+ }
38
+ function tryGit(cwd, args) {
39
+ try {
40
+ return git(cwd, args);
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ /** Read the key at `path`, refusing one others can read or one that is not an Ed25519 private key in PEM. */
47
+ function readHostKey(path) {
48
+ const mode = statSync(path).mode & 0o777;
49
+ if (mode & 0o077)
50
+ throw new CiRunError(`${path} can be read by other users (mode ${mode.toString(8)}); chmod 600 it, since it signs for this runner`);
51
+ const pem = readFileSync(path, "utf-8");
52
+ try {
53
+ return { path, pem, publicKey: loadRunnerKey(pem).publicKey, created: false };
54
+ }
55
+ catch (err) {
56
+ throw new CiRunError(`${path} is not an Ed25519 private key in PEM: ${message(err)}`);
57
+ }
58
+ }
59
+ /** The host's runner key in `stateDir`, or undefined when there is none. */
60
+ export function hostRunnerKey(stateDir) {
61
+ const path = join(stateDir, RUNNER_KEY_FILE);
62
+ return existsSync(path) ? readHostKey(path) : undefined;
63
+ }
64
+ /** The host's runner key in `stateDir`, made when there is none: Ed25519, PEM, mode 0600 in a directory of mode 0700. */
65
+ export function ensureHostRunnerKey(stateDir) {
66
+ const found = hostRunnerKey(stateDir);
67
+ if (found)
68
+ return found;
69
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
70
+ const path = join(stateDir, RUNNER_KEY_FILE);
71
+ const { privateKey } = generateKeyPairSync("ed25519");
72
+ const pem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
73
+ // wx: never replace a key another process made in the meantime.
74
+ writeFileSync(path, pem, { mode: 0o600, flag: "wx" });
75
+ chmodSync(path, 0o600);
76
+ return { path, pem, publicKey: loadRunnerKey(pem).publicKey, created: true };
77
+ }
78
+ /**
79
+ * The target branch's head, as the readers of runners take it: the remote's
80
+ * branch after a fetch (best effort), else the local branch. Undefined when
81
+ * neither exists.
82
+ */
83
+ export function targetHead(top, branch, remote = "origin") {
84
+ if (tryGit(top, ["remote", "get-url", remote]) !== undefined)
85
+ tryGit(top, ["fetch", "-q", remote, `+refs/heads/${branch}:refs/remotes/${remote}/${branch}`]);
86
+ for (const ref of [`refs/remotes/${remote}/${branch}`, `refs/heads/${branch}`]) {
87
+ const commit = tryGit(top, ["rev-parse", "--verify", "-q", `${ref}^{commit}`]);
88
+ if (commit)
89
+ return { commit, ref };
90
+ }
91
+ return undefined;
92
+ }
93
+ /** The exact command that lists `publicKey` for `principal`. */
94
+ export function trustCommand(principal, publicKey) {
95
+ return `chant workspace trust runner add ${principal} --key "${publicKey}"`;
96
+ }
97
+ /** Whether `.chant/trust.json` at `commit` lists the key as a runner holding the runner role: its principal, or why not. */
98
+ export function runnerKeyListed(top, commit, pem) {
99
+ try {
100
+ return { ok: true, principal: ciTagSigner(top, commit, pem, "the runs it signs").principal };
101
+ }
102
+ catch (err) {
103
+ return { ok: false, reason: message(err) };
104
+ }
105
+ }
106
+ /**
107
+ * Propose the runner entry for review (#4099): in a temporary worktree at
108
+ * the target's head, run `trust runner add` (#3510), commit the change on
109
+ * `chant/trust-runner/<principal>`, and push that branch to `remote` when
110
+ * the clone has it. The target branch is never written. The commit is made
111
+ * with the host's git identity and signing settings; when the policy at base
112
+ * has signers, the change needs a signature by a policy writer before it
113
+ * merges.
114
+ */
115
+ export function proposeRunnerTrust(req) {
116
+ const remote = req.remote ?? "origin";
117
+ const head = targetHead(req.top, req.branch, remote);
118
+ if (!head)
119
+ throw new CiRunError(`there is no branch ${req.branch} here or on ${remote}, so there is no trust config to add the runner to`);
120
+ const branch = `chant/trust-runner/${req.principal.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "")}`;
121
+ if (tryGit(req.top, ["rev-parse", "--verify", "-q", `refs/heads/${branch}`])) {
122
+ throw new CiRunError(`the branch ${branch} exists already; review or delete it, then run --setup-key --trust again`);
123
+ }
124
+ const dir = mkdtempSync(join(tmpdir(), "chant-trust-runner-"));
125
+ try {
126
+ git(req.top, ["worktree", "add", "-q", "-b", branch, dir, head.commit]);
127
+ const doc = trustWrite({ cwd: dir, action: "runner add", name: req.principal, key: req.publicKey, ...(req.by ? { by: req.by } : {}) });
128
+ if ("error" in doc)
129
+ throw new CiRunError(`trust runner add refused (${doc.error.code}): ${doc.error.message}`);
130
+ if (!doc.changed) {
131
+ git(req.top, ["worktree", "remove", "--force", dir]);
132
+ git(req.top, ["branch", "-q", "-D", branch]);
133
+ return { branch: null, commit: null, pushed: false, digest: doc.digest };
134
+ }
135
+ git(dir, ["add", "--", ".chant/trust.json"]);
136
+ git(dir, ["commit", "-q", "-m", `trust the ${req.principal} runner key\n\nchant ci --setup-key --trust: list this host's runner key in .chant/trust.json (#4099).`]);
137
+ const commit = git(dir, ["rev-parse", "HEAD"]);
138
+ let pushed = false;
139
+ if (tryGit(req.top, ["remote", "get-url", remote]) !== undefined) {
140
+ try {
141
+ git(dir, ["push", "-q", remote, `refs/heads/${branch}:refs/heads/${branch}`]);
142
+ pushed = true;
143
+ }
144
+ catch (err) {
145
+ throw new CiRunError(`committed ${branch} here, and pushing it to ${remote} failed: ${message(err).split("\n")[0]}`);
146
+ }
147
+ }
148
+ return { branch, commit, pushed, digest: doc.digest };
149
+ }
150
+ finally {
151
+ tryGit(req.top, ["worktree", "remove", "--force", dir]);
152
+ rmSync(dir, { recursive: true, force: true });
153
+ }
154
+ }
155
+ /** `--setup-key [--trust]`: make or read the key, see whether the target's head lists it, and with `trust` propose the entry. */
156
+ export function setupRunnerKey(req) {
157
+ const key = ensureHostRunnerKey(req.stateDir);
158
+ const head = targetHead(req.top, req.branch);
159
+ const listed = head ? runnerKeyListed(req.top, head.commit, key.pem) : undefined;
160
+ const principal = req.principal ?? defaultRunnerPrincipal();
161
+ const out = { key, head: head?.commit ?? null, listedAs: listed?.ok ? listed.principal : null, principal, command: trustCommand(principal, key.publicKey) };
162
+ if (req.trust && !out.listedAs)
163
+ out.proposal = proposeRunnerTrust({ top: req.top, branch: req.branch, principal, publicKey: key.publicKey, ...(req.by ? { by: req.by } : {}) });
164
+ return out;
165
+ }
166
+ /** What `--setup-key` prints on stderr; the public key alone goes to stdout. */
167
+ export function formatRunnerKeySetup(s, branch) {
168
+ const lines = [`runner key ${s.key.path} (${s.key.created ? "made now, mode 0600" : "already here"})`];
169
+ if (s.listedAs) {
170
+ lines.push(`.chant/trust.json at ${branch} ${s.head.slice(0, 12)} lists it for ${s.listedAs}, with the runner role`);
171
+ }
172
+ else if (s.proposal?.branch) {
173
+ lines.push(`committed the runner entry for ${s.principal} on ${s.proposal.branch} (${s.proposal.commit.slice(0, 12)})${s.proposal.pushed ? " and pushed it" : ""}; open a pull request into ${branch}, and the key counts once it merges`);
174
+ lines.push("changing .chant/trust.json is a protected write: when the trust policy has signers, an admin signs the commit before it merges");
175
+ }
176
+ else if (s.proposal) {
177
+ lines.push(`the runner entry for ${s.principal} is already at ${branch}'s head`);
178
+ }
179
+ else {
180
+ lines.push(`.chant/trust.json at ${branch}${s.head ? ` ${s.head.slice(0, 12)}` : ""} does not list it yet. In a checkout of ${branch}, run`, ` ${s.command}`, "commit the change through review, or run this again with --trust to push it on a branch");
181
+ }
182
+ return lines.join("\n");
183
+ }
184
+ /**
185
+ * The key `ci serve` signs with, checked before its first poll (#4099): the
186
+ * `--key` file, else `CHANT_RUNNER_KEY`, else the host's key in the state
187
+ * directory. Signing is opt-in (#4156):
188
+ *
189
+ * - With no key, `ci serve` serves unsigned and says so only in `note`.
190
+ * - A key `.chant/trust.json` at the target's head does not list with the
191
+ * runner role, or a target branch it can't find to check against, leaves
192
+ * it serving unsigned, with one `warning`.
193
+ * - With `required` (the declaration's `ci.green.signedSince`) each of
194
+ * those refuses to start, as before #4156.
195
+ *
196
+ * A key that is not an Ed25519 private key in PEM, or a `--key` file that
197
+ * can't be read, refuses either way. `unsigned` (`--unsigned`) skips the
198
+ * key without `required` and is ignored with it.
199
+ */
200
+ export function ciServeRunnerKey(req) {
201
+ const required = req.required ?? null;
202
+ const optIn = `ci.green.signedSince is ${required}, so every run must be signed`;
203
+ if (req.unsigned && !required)
204
+ return { fromFlag: false, source: "none (--unsigned)", note: "runs are not signed (--unsigned)" };
205
+ const host = req.keyFile === undefined && !req.envKey ? hostRunnerKey(req.stateDir) : undefined;
206
+ const pem = req.keyFile !== undefined ? readFileSync(req.keyFile, "utf-8") : req.envKey || host?.pem;
207
+ const source = req.keyFile !== undefined ? req.keyFile : req.envKey ? "CHANT_RUNNER_KEY" : (host?.path ?? join(req.stateDir, RUNNER_KEY_FILE));
208
+ if (pem === undefined) {
209
+ if (!required)
210
+ return { fromFlag: false, source: "none", note: "runs are not signed: there is no runner key. To verify who ran a job, add one with chant ci serve --setup-key --trust" };
211
+ throw new CiRunError(`${optIn}, and there is no runner key: no --key, no CHANT_RUNNER_KEY and no ${source}. Make one with chant ci serve --setup-key and list it with --trust`);
212
+ }
213
+ let publicKey;
214
+ try {
215
+ publicKey = loadRunnerKey(pem).publicKey;
216
+ }
217
+ catch (err) {
218
+ throw new CiRunError(`the runner key from ${source} is not an Ed25519 private key in PEM: ${message(err)}`);
219
+ }
220
+ const head = targetHead(req.top, req.branch);
221
+ if (!head) {
222
+ const why = `there is no branch ${req.branch} here or on origin, so the runner key can't be checked against its .chant/trust.json`;
223
+ if (required)
224
+ throw new CiRunError(`${optIn}, and ${why}; fetch it`);
225
+ return { fromFlag: false, source, warning: `${why}, so runs are not signed; fetch it to sign them` };
226
+ }
227
+ const listed = runnerKeyListed(req.top, head.commit, pem);
228
+ if (!listed.ok) {
229
+ const why = `the runner key from ${source} is not trusted at ${branchAt(req.branch, head.commit)}: ${listed.reason}`;
230
+ const fix = `List it on a branch for review with chant ci serve --setup-key --trust, or run ${trustCommand(defaultRunnerPrincipal(), publicKey)} in a checkout of ${req.branch} and merge the change.`;
231
+ if (required)
232
+ throw new CiRunError(`${optIn}, and ${why}\n ${fix}`);
233
+ return { fromFlag: false, source, warning: `${why}. Runs are not signed until it is. ${fix}` };
234
+ }
235
+ return { pem, fromFlag: req.keyFile !== undefined, source };
236
+ }
237
+ const branchAt = (branch, commit) => `${branch} ${commit.slice(0, 12)}`;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The chant runner's secrets file (#4096, epic #4101): the values a job gets
3
+ * from the host, such as cloud credentials and the endpoint, which a forge
4
+ * job would get from the forge's secrets.
5
+ *
6
+ * `chant ci run` and `chant ci serve` read it from `--secrets <file>`, or
7
+ * the file `CHANT_CI_SECRETS` names. It is a dotenv file of `NAME=value`
8
+ * lines, or a JSON object:
9
+ *
10
+ * - a map of name to value, `{ "AWS_SECRET_ACCESS_KEY": "..." }`;
11
+ * - or `{ "secrets": { ... }, "jobs": { "plan": [...], "apply": [...] } }`,
12
+ * where `jobs` lists, for a job, the names it gets. A job `jobs` doesn't
13
+ * name gets every secret.
14
+ *
15
+ * The allowlist lives in this file on the host, not in the repository, so a
16
+ * pull request can't widen what its plan gets.
17
+ *
18
+ * The file must belong to the user reading it and give group and others no
19
+ * permission at all, as ssh asks of a private key (`chmod 600`). A file
20
+ * that does is refused before any job runs.
21
+ *
22
+ * Every value in the file, and every value of the job's environment whose
23
+ * name looks secret ({@link SECRET_NAME}), is redacted from what the run
24
+ * writes about itself (../op/redact.ts).
25
+ */
26
+ import { type Redact } from "../op/redact.js";
27
+ import type { CiJobName } from "./ci-run.js";
28
+ /** The variable that names the secrets file when `--secrets` is not given. */
29
+ export declare const CI_SECRETS_ENV = "CHANT_CI_SECRETS";
30
+ /** A variable whose name matches this is taken as secret, and its value redacted. */
31
+ export declare const SECRET_NAME: RegExp;
32
+ /** The secrets file, read. */
33
+ export interface CiSecrets {
34
+ /** Where it was read from. */
35
+ file: string;
36
+ /** Every secret, by name. */
37
+ values: Record<string, string>;
38
+ /** For a job the file names, the secrets it gets. A job missing here gets every one. */
39
+ jobs: Partial<Record<CiJobName, string[]>>;
40
+ }
41
+ export declare class CiSecretsError extends Error {
42
+ constructor(message: string);
43
+ }
44
+ /**
45
+ * Refuse a secrets file anyone but its owner can read or write, or that
46
+ * belongs to another user, as ssh refuses a loose key. Not checked on
47
+ * Windows, whose files carry no such mode.
48
+ */
49
+ export declare function checkCiSecretsMode(file: string): void;
50
+ /** A dotenv text's `NAME=value` pairs. Errors name the line, never a value. */
51
+ export declare function parseDotenv(text: string, file: string): Record<string, string>;
52
+ /** The secrets in a file's text: JSON when it starts with `{`, else dotenv. */
53
+ export declare function parseCiSecrets(text: string, file: string): Omit<CiSecrets, "file">;
54
+ /** Read the secrets file, refusing one others can read. */
55
+ export declare function readCiSecrets(file: string): CiSecrets;
56
+ /** The secrets `job` gets: those `jobs` lists for it, else every one. */
57
+ export declare function ciSecretsFor(secrets: CiSecrets | undefined, job: CiJobName): Record<string, string>;
58
+ /**
59
+ * The redactor for a job: every value in the secrets file, whether the job
60
+ * gets it or not, and the value of each variable in the job's environment
61
+ * whose name looks secret, such as `GITHUB_TOKEN` or a `--pass-env
62
+ * AWS_SECRET_ACCESS_KEY`. Undefined when there is nothing to redact.
63
+ */
64
+ export declare function ciRedactor(jobEnv: Record<string, string>, secrets?: CiSecrets): Redact | undefined;
65
+ //# sourceMappingURL=ci-secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci-secrets.d.ts","sourceRoot":"","sources":["../../src/workspace/ci-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,8EAA8E;AAC9E,eAAO,MAAM,cAAc,qBAAqB,CAAC;AAEjD,qFAAqF;AACrF,eAAO,MAAM,WAAW,QAA+B,CAAC;AAMxD,8BAA8B;AAC9B,MAAM,WAAW,SAAS;IACxB,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,wFAAwF;IACxF,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;CAC5C;AAED,qBAAa,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAoBrD;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA0D9E;AAOD,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAmClF;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAGrD;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,EAAE,GAAG,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAInG;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAIlG"}
@@ -0,0 +1,211 @@
1
+ /**
2
+ * The chant runner's secrets file (#4096, epic #4101): the values a job gets
3
+ * from the host, such as cloud credentials and the endpoint, which a forge
4
+ * job would get from the forge's secrets.
5
+ *
6
+ * `chant ci run` and `chant ci serve` read it from `--secrets <file>`, or
7
+ * the file `CHANT_CI_SECRETS` names. It is a dotenv file of `NAME=value`
8
+ * lines, or a JSON object:
9
+ *
10
+ * - a map of name to value, `{ "AWS_SECRET_ACCESS_KEY": "..." }`;
11
+ * - or `{ "secrets": { ... }, "jobs": { "plan": [...], "apply": [...] } }`,
12
+ * where `jobs` lists, for a job, the names it gets. A job `jobs` doesn't
13
+ * name gets every secret.
14
+ *
15
+ * The allowlist lives in this file on the host, not in the repository, so a
16
+ * pull request can't widen what its plan gets.
17
+ *
18
+ * The file must belong to the user reading it and give group and others no
19
+ * permission at all, as ssh asks of a private key (`chmod 600`). A file
20
+ * that does is refused before any job runs.
21
+ *
22
+ * Every value in the file, and every value of the job's environment whose
23
+ * name looks secret ({@link SECRET_NAME}), is redacted from what the run
24
+ * writes about itself (../op/redact.ts).
25
+ */
26
+ import { readFileSync, statSync } from "node:fs";
27
+ import { redactorOf } from "../op/redact.js";
28
+ import { RUNNER_KEY_ENV } from "./ci-green.js";
29
+ /** The variable that names the secrets file when `--secrets` is not given. */
30
+ export const CI_SECRETS_ENV = "CHANT_CI_SECRETS";
31
+ /** A variable whose name matches this is taken as secret, and its value redacted. */
32
+ export const SECRET_NAME = /TOKEN|SECRET|KEY|PASSWORD/i;
33
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
34
+ const JOBS = ["plan", "apply"];
35
+ export class CiSecretsError extends Error {
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = "CiSecretsError";
39
+ }
40
+ }
41
+ /**
42
+ * Refuse a secrets file anyone but its owner can read or write, or that
43
+ * belongs to another user, as ssh refuses a loose key. Not checked on
44
+ * Windows, whose files carry no such mode.
45
+ */
46
+ export function checkCiSecretsMode(file) {
47
+ let st;
48
+ try {
49
+ st = statSync(file);
50
+ }
51
+ catch (err) {
52
+ throw new CiSecretsError(`the secrets file ${file} can't be read: ${err.code ?? String(err)}`);
53
+ }
54
+ if (!st.isFile())
55
+ throw new CiSecretsError(`the secrets file ${file} is not a file`);
56
+ if (process.platform === "win32")
57
+ return;
58
+ const loose = Number(st.mode) & 0o077;
59
+ if (loose !== 0) {
60
+ const mode = (Number(st.mode) & 0o777).toString(8).padStart(4, "0");
61
+ throw new CiSecretsError(`the secrets file ${file} has mode ${mode}, so its group or other users can ${loose & 0o044 ? "read" : "change"} it; a secrets file must be readable only by its owner, as ssh asks of a key: chmod 600 ${file}`);
62
+ }
63
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
64
+ if (uid !== undefined && Number(st.uid) !== uid) {
65
+ throw new CiSecretsError(`the secrets file ${file} belongs to uid ${st.uid}, not to this user (uid ${uid}); a secrets file must be its reader's own`);
66
+ }
67
+ }
68
+ /** A dotenv text's `NAME=value` pairs. Errors name the line, never a value. */
69
+ export function parseDotenv(text, file) {
70
+ const out = {};
71
+ let i = 0;
72
+ let line = 1;
73
+ const fail = (why) => {
74
+ throw new CiSecretsError(`${file}:${line}: ${why}`);
75
+ };
76
+ while (i < text.length) {
77
+ // One line: blank, a comment, or NAME=value.
78
+ const eol = text.indexOf("\n", i);
79
+ const rest = text.slice(i, eol === -1 ? text.length : eol);
80
+ const trimmed = rest.trim();
81
+ if (trimmed === "" || trimmed.startsWith("#")) {
82
+ i = eol === -1 ? text.length : eol + 1;
83
+ line++;
84
+ continue;
85
+ }
86
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*/.exec(rest);
87
+ if (!m)
88
+ fail("expected NAME=value");
89
+ const name = m[1];
90
+ let j = i + m[0].length;
91
+ const startLine = line;
92
+ let value = "";
93
+ const q = text[j];
94
+ if (q === '"' || q === "'") {
95
+ j++;
96
+ for (;;) {
97
+ if (j >= text.length) {
98
+ line = startLine;
99
+ fail(`the value of ${name} has no closing ${q}`);
100
+ }
101
+ const c = text[j];
102
+ if (c === q)
103
+ break;
104
+ if (c === "\n")
105
+ line++;
106
+ if (q === '"' && c === "\\" && j + 1 < text.length) {
107
+ const n = text[j + 1];
108
+ value += n === "n" ? "\n" : n === "r" ? "\r" : n === "t" ? "\t" : n;
109
+ j += 2;
110
+ continue;
111
+ }
112
+ value += c;
113
+ j++;
114
+ }
115
+ j++;
116
+ const end = text.indexOf("\n", j);
117
+ const after = text.slice(j, end === -1 ? text.length : end).trim();
118
+ if (after !== "" && !after.startsWith("#"))
119
+ fail(`unexpected text after the quoted value of ${name}`);
120
+ i = end === -1 ? text.length : end + 1;
121
+ }
122
+ else {
123
+ const end = text.indexOf("\n", j);
124
+ value = text.slice(j, end === -1 ? text.length : end).replace(/\s+#.*$/, "").trim();
125
+ i = end === -1 ? text.length : end + 1;
126
+ }
127
+ if (Object.prototype.hasOwnProperty.call(out, name))
128
+ fail(`${name} is set twice`);
129
+ out[name] = value;
130
+ line++;
131
+ }
132
+ return out;
133
+ }
134
+ function checkName(name, file) {
135
+ if (!ENV_NAME.test(name))
136
+ throw new CiSecretsError(`${file} names a secret ${JSON.stringify(name)}, which is not a variable name`);
137
+ if (name === RUNNER_KEY_ENV)
138
+ throw new CiSecretsError(`${file} sets ${RUNNER_KEY_ENV}, and the runner key is never passed to a job; give it to ci run or ci serve with --key or ${RUNNER_KEY_ENV}`);
139
+ }
140
+ /** The secrets in a file's text: JSON when it starts with `{`, else dotenv. */
141
+ export function parseCiSecrets(text, file) {
142
+ if (!text.trimStart().startsWith("{")) {
143
+ const values = parseDotenv(text, file);
144
+ for (const name of Object.keys(values))
145
+ checkName(name, file);
146
+ return { values, jobs: {} };
147
+ }
148
+ let raw;
149
+ try {
150
+ raw = JSON.parse(text);
151
+ }
152
+ catch {
153
+ // The parser's message quotes the text near the error, which may be a secret.
154
+ throw new CiSecretsError(`${file} starts with {, and is not valid JSON`);
155
+ }
156
+ const doc = raw;
157
+ const structured = doc.secrets !== null && typeof doc.secrets === "object" && !Array.isArray(doc.secrets);
158
+ const map = (structured ? doc.secrets : doc);
159
+ const values = {};
160
+ for (const [name, value] of Object.entries(map)) {
161
+ checkName(name, file);
162
+ if (typeof value !== "string")
163
+ throw new CiSecretsError(`${file}: the secret ${name} must be a string`);
164
+ values[name] = value;
165
+ }
166
+ const jobs = {};
167
+ if (structured) {
168
+ for (const key of Object.keys(doc))
169
+ if (key !== "secrets" && key !== "jobs")
170
+ throw new CiSecretsError(`${file} has ${JSON.stringify(key)} beside secrets and jobs`);
171
+ const given = doc.jobs ?? {};
172
+ if (given === null || typeof given !== "object" || Array.isArray(given))
173
+ throw new CiSecretsError(`${file}: jobs must map plan and apply to lists of secret names`);
174
+ for (const [job, names] of Object.entries(given)) {
175
+ if (!JOBS.includes(job))
176
+ throw new CiSecretsError(`${file}: jobs names ${JSON.stringify(job)}, and the jobs are plan and apply`);
177
+ if (!Array.isArray(names) || names.some((n) => typeof n !== "string"))
178
+ throw new CiSecretsError(`${file}: jobs.${job} must be a list of secret names`);
179
+ for (const n of names)
180
+ if (!Object.prototype.hasOwnProperty.call(values, n))
181
+ throw new CiSecretsError(`${file}: jobs.${job} names ${n}, which the file does not set`);
182
+ jobs[job] = [...names];
183
+ }
184
+ }
185
+ return { values, jobs };
186
+ }
187
+ /** Read the secrets file, refusing one others can read. */
188
+ export function readCiSecrets(file) {
189
+ checkCiSecretsMode(file);
190
+ return { file, ...parseCiSecrets(readFileSync(file, "utf-8"), file) };
191
+ }
192
+ /** The secrets `job` gets: those `jobs` lists for it, else every one. */
193
+ export function ciSecretsFor(secrets, job) {
194
+ if (!secrets)
195
+ return {};
196
+ const names = secrets.jobs[job] ?? Object.keys(secrets.values);
197
+ return Object.fromEntries(names.map((n) => [n, secrets.values[n]]));
198
+ }
199
+ /**
200
+ * The redactor for a job: every value in the secrets file, whether the job
201
+ * gets it or not, and the value of each variable in the job's environment
202
+ * whose name looks secret, such as `GITHUB_TOKEN` or a `--pass-env
203
+ * AWS_SECRET_ACCESS_KEY`. Undefined when there is nothing to redact.
204
+ */
205
+ export function ciRedactor(jobEnv, secrets) {
206
+ const values = Object.values(secrets?.values ?? {});
207
+ for (const [name, value] of Object.entries(jobEnv))
208
+ if (SECRET_NAME.test(name))
209
+ values.push(value);
210
+ return redactorOf(values);
211
+ }