@caelo-cms/provisioning 0.10.46 → 0.10.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/dist/adc-impersonation.d.ts.map +1 -1
  2. package/dist/adc-impersonation.js +23 -1
  3. package/dist/adc-impersonation.js.map +1 -1
  4. package/dist/agent/apply.d.ts +45 -0
  5. package/dist/agent/apply.d.ts.map +1 -0
  6. package/dist/agent/apply.js +83 -0
  7. package/dist/agent/apply.js.map +1 -0
  8. package/dist/agent/cli-provenance.d.ts +42 -0
  9. package/dist/agent/cli-provenance.d.ts.map +1 -0
  10. package/dist/agent/cli-provenance.js +138 -0
  11. package/dist/agent/cli-provenance.js.map +1 -0
  12. package/dist/agent/command.d.ts +79 -0
  13. package/dist/agent/command.d.ts.map +1 -0
  14. package/dist/agent/command.js +454 -0
  15. package/dist/agent/command.js.map +1 -0
  16. package/dist/agent/config.d.ts +30 -0
  17. package/dist/agent/config.d.ts.map +1 -0
  18. package/dist/agent/config.js +60 -0
  19. package/dist/agent/config.js.map +1 -0
  20. package/dist/agent/lock.d.ts +13 -0
  21. package/dist/agent/lock.d.ts.map +1 -0
  22. package/dist/agent/lock.js +100 -0
  23. package/dist/agent/lock.js.map +1 -0
  24. package/dist/agent/queue.d.ts +35 -0
  25. package/dist/agent/queue.d.ts.map +1 -0
  26. package/dist/agent/queue.js +50 -0
  27. package/dist/agent/queue.js.map +1 -0
  28. package/dist/agent/run.d.ts +97 -0
  29. package/dist/agent/run.d.ts.map +1 -0
  30. package/dist/agent/run.js +198 -0
  31. package/dist/agent/run.js.map +1 -0
  32. package/dist/agent/systemd.d.ts +55 -0
  33. package/dist/agent/systemd.d.ts.map +1 -0
  34. package/dist/agent/systemd.js +171 -0
  35. package/dist/agent/systemd.js.map +1 -0
  36. package/dist/agent/units.d.ts +44 -0
  37. package/dist/agent/units.d.ts.map +1 -0
  38. package/dist/agent/units.js +127 -0
  39. package/dist/agent/units.js.map +1 -0
  40. package/dist/agent-db.js +18362 -0
  41. package/dist/cli.js +122 -16
  42. package/dist/cli.js.map +1 -1
  43. package/dist/cloudsql-iam-user.d.ts +39 -0
  44. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  45. package/dist/cloudsql-iam-user.js +119 -0
  46. package/dist/cloudsql-iam-user.js.map +1 -0
  47. package/dist/compose.d.ts +16 -0
  48. package/dist/compose.d.ts.map +1 -1
  49. package/dist/compose.js +26 -1
  50. package/dist/compose.js.map +1 -1
  51. package/dist/dns/index.d.ts +2 -0
  52. package/dist/dns/index.d.ts.map +1 -1
  53. package/dist/dns/index.js +3 -2
  54. package/dist/dns/index.js.map +1 -1
  55. package/dist/dns/manual.d.ts +5 -0
  56. package/dist/dns/manual.d.ts.map +1 -1
  57. package/dist/dns/manual.js +28 -12
  58. package/dist/dns/manual.js.map +1 -1
  59. package/dist/gcloud.d.ts +2 -0
  60. package/dist/gcloud.d.ts.map +1 -1
  61. package/dist/gcloud.js +1 -1
  62. package/dist/gcloud.js.map +1 -1
  63. package/dist/gcp-names.d.ts +15 -0
  64. package/dist/gcp-names.d.ts.map +1 -1
  65. package/dist/gcp-names.js +23 -0
  66. package/dist/gcp-names.js.map +1 -1
  67. package/dist/index.d.ts +6 -0
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +4 -0
  70. package/dist/index.js.map +1 -1
  71. package/dist/infra-contract.d.ts +1 -1
  72. package/dist/infra-contract.js +7 -1
  73. package/dist/infra-contract.js.map +1 -1
  74. package/dist/install-job-runner.d.ts +13 -0
  75. package/dist/install-job-runner.d.ts.map +1 -0
  76. package/dist/install-job-runner.js +155 -0
  77. package/dist/install-job-runner.js.map +1 -0
  78. package/dist/install-job.d.ts +210 -0
  79. package/dist/install-job.d.ts.map +1 -0
  80. package/dist/install-job.js +449 -0
  81. package/dist/install-job.js.map +1 -0
  82. package/dist/install-state.d.ts +19 -1
  83. package/dist/install-state.d.ts.map +1 -1
  84. package/dist/install-state.js +31 -3
  85. package/dist/install-state.js.map +1 -1
  86. package/dist/lifecycle.d.ts.map +1 -1
  87. package/dist/lifecycle.js +24 -5
  88. package/dist/lifecycle.js.map +1 -1
  89. package/dist/migration-runner.d.ts +5 -0
  90. package/dist/migration-runner.d.ts.map +1 -1
  91. package/dist/migration-runner.js +1 -1
  92. package/dist/migration-runner.js.map +1 -1
  93. package/dist/operator-access.d.ts +14 -8
  94. package/dist/operator-access.d.ts.map +1 -1
  95. package/dist/operator-access.js +16 -102
  96. package/dist/operator-access.js.map +1 -1
  97. package/dist/run-mode.d.ts +27 -0
  98. package/dist/run-mode.d.ts.map +1 -0
  99. package/dist/run-mode.js +22 -0
  100. package/dist/run-mode.js.map +1 -0
  101. package/dist/runtime-secrets.d.ts +19 -0
  102. package/dist/runtime-secrets.d.ts.map +1 -1
  103. package/dist/runtime-secrets.js +44 -0
  104. package/dist/runtime-secrets.js.map +1 -1
  105. package/dist/self-hosted-roles.d.ts +22 -1
  106. package/dist/self-hosted-roles.d.ts.map +1 -1
  107. package/dist/self-hosted-roles.js +81 -12
  108. package/dist/self-hosted-roles.js.map +1 -1
  109. package/dist/self-hosted-upgrade.d.ts +17 -0
  110. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  111. package/dist/self-hosted-upgrade.js +16 -1
  112. package/dist/self-hosted-upgrade.js.map +1 -1
  113. package/dist/stack-contract.d.ts +6 -0
  114. package/dist/stack-contract.d.ts.map +1 -1
  115. package/dist/stack-contract.js +10 -1
  116. package/dist/stack-contract.js.map +1 -1
  117. package/dist/state-attach.d.ts +23 -0
  118. package/dist/state-attach.d.ts.map +1 -0
  119. package/dist/state-attach.js +112 -0
  120. package/dist/state-attach.js.map +1 -0
  121. package/dist/state-backend.d.ts +11 -0
  122. package/dist/state-backend.d.ts.map +1 -1
  123. package/dist/state-backend.js +20 -0
  124. package/dist/state-backend.js.map +1 -1
  125. package/dist/state-command.d.ts +20 -0
  126. package/dist/state-command.d.ts.map +1 -0
  127. package/dist/state-command.js +237 -0
  128. package/dist/state-command.js.map +1 -0
  129. package/dist/state-migrate.d.ts +121 -0
  130. package/dist/state-migrate.d.ts.map +1 -0
  131. package/dist/state-migrate.js +290 -0
  132. package/dist/state-migrate.js.map +1 -0
  133. package/dist/state-pulumi.d.ts +17 -0
  134. package/dist/state-pulumi.d.ts.map +1 -0
  135. package/dist/state-pulumi.js +218 -0
  136. package/dist/state-pulumi.js.map +1 -0
  137. package/dist/state-store.d.ts +27 -0
  138. package/dist/state-store.d.ts.map +1 -1
  139. package/dist/state-store.js +65 -0
  140. package/dist/state-store.js.map +1 -1
  141. package/dist/upgrade/converge.d.ts +6 -0
  142. package/dist/upgrade/converge.d.ts.map +1 -1
  143. package/dist/upgrade/converge.js +59 -2
  144. package/dist/upgrade/converge.js.map +1 -1
  145. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  146. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  147. package/dist/upgrade/gcp-lookup.js +25 -0
  148. package/dist/upgrade/gcp-lookup.js.map +1 -1
  149. package/dist/upgrade/release-images.d.ts +12 -0
  150. package/dist/upgrade/release-images.d.ts.map +1 -1
  151. package/dist/upgrade/release-images.js +31 -0
  152. package/dist/upgrade/release-images.js.map +1 -1
  153. package/dist/upgrade/upgrade.d.ts +5 -0
  154. package/dist/upgrade/upgrade.d.ts.map +1 -1
  155. package/dist/upgrade/upgrade.js +28 -1
  156. package/dist/upgrade/upgrade.js.map +1 -1
  157. package/dist/upgrader-assess.d.ts +82 -0
  158. package/dist/upgrader-assess.d.ts.map +1 -0
  159. package/dist/upgrader-assess.js +116 -0
  160. package/dist/upgrader-assess.js.map +1 -0
  161. package/dist/upgrader-gcp.d.ts +17 -0
  162. package/dist/upgrader-gcp.d.ts.map +1 -0
  163. package/dist/upgrader-gcp.js +87 -0
  164. package/dist/upgrader-gcp.js.map +1 -0
  165. package/dist/upgrader-job.d.ts +131 -0
  166. package/dist/upgrader-job.d.ts.map +1 -0
  167. package/dist/upgrader-job.js +259 -0
  168. package/dist/upgrader-job.js.map +1 -0
  169. package/dist/upgrader-reporter.d.ts +35 -0
  170. package/dist/upgrader-reporter.d.ts.map +1 -0
  171. package/dist/upgrader-reporter.js +79 -0
  172. package/dist/upgrader-reporter.js.map +1 -0
  173. package/dist/upgrader-setup.d.ts +59 -0
  174. package/dist/upgrader-setup.d.ts.map +1 -0
  175. package/dist/upgrader-setup.js +308 -0
  176. package/dist/upgrader-setup.js.map +1 -0
  177. package/dist/wizard.d.ts +6 -0
  178. package/dist/wizard.d.ts.map +1 -1
  179. package/dist/wizard.js +32 -4
  180. package/dist/wizard.js.map +1 -1
  181. package/dist/wizards/gcp-firebase.d.ts +3 -0
  182. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  183. package/dist/wizards/gcp-firebase.js +1 -0
  184. package/dist/wizards/gcp-firebase.js.map +1 -1
  185. package/dist/wizards/gcp-install-job.d.ts +74 -0
  186. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  187. package/dist/wizards/gcp-install-job.js +398 -0
  188. package/dist/wizards/gcp-install-job.js.map +1 -0
  189. package/dist/wizards/gcp-prompts.d.ts +3 -0
  190. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  191. package/dist/wizards/gcp-prompts.js +25 -0
  192. package/dist/wizards/gcp-prompts.js.map +1 -0
  193. package/dist/wizards/gcp-pulumi.d.ts +9 -2
  194. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  195. package/dist/wizards/gcp-pulumi.js +8 -6
  196. package/dist/wizards/gcp-pulumi.js.map +1 -1
  197. package/dist/wizards/gcp.d.ts +40 -0
  198. package/dist/wizards/gcp.d.ts.map +1 -1
  199. package/dist/wizards/gcp.js +291 -83
  200. package/dist/wizards/gcp.js.map +1 -1
  201. package/package.json +6 -4
  202. package/src/adapter.ts +97 -0
  203. package/src/adc-impersonation.ts +176 -0
  204. package/src/admin-domain.test.ts +254 -0
  205. package/src/admin-domain.ts +245 -0
  206. package/src/admin-memory.test.ts +151 -0
  207. package/src/agent/apply.test.ts +185 -0
  208. package/src/agent/apply.ts +124 -0
  209. package/src/agent/cli-provenance.test.ts +159 -0
  210. package/src/agent/cli-provenance.ts +191 -0
  211. package/src/agent/command.test.ts +77 -0
  212. package/src/agent/command.ts +564 -0
  213. package/src/agent/config.ts +86 -0
  214. package/src/agent/lock-config.test.ts +97 -0
  215. package/src/agent/lock.ts +110 -0
  216. package/src/agent/queue.test.ts +10 -0
  217. package/src/agent/queue.ts +91 -0
  218. package/src/agent/run.test.ts +382 -0
  219. package/src/agent/run.ts +295 -0
  220. package/src/agent/systemd.ts +209 -0
  221. package/src/agent/units.test.ts +223 -0
  222. package/src/agent/units.ts +163 -0
  223. package/src/azure-front-door.test.ts +119 -0
  224. package/src/azure-front-door.ts +56 -0
  225. package/src/bootstrap-token.ts +20 -0
  226. package/src/caddy.ts +112 -0
  227. package/src/cdn-copy.ts +91 -0
  228. package/src/cert-wait.test.ts +194 -0
  229. package/src/cert-wait.ts +163 -0
  230. package/src/checkpoint-secrets.test.ts +380 -0
  231. package/src/checkpoint-secrets.ts +283 -0
  232. package/src/cli-routing.test.ts +45 -0
  233. package/src/cli-routing.ts +43 -0
  234. package/src/cli.ts +1491 -0
  235. package/src/cloudsql-iam-user.ts +147 -0
  236. package/src/compose.ts +334 -0
  237. package/src/default-firewall.test.ts +411 -0
  238. package/src/default-firewall.ts +342 -0
  239. package/src/deployed-release.test.ts +231 -0
  240. package/src/deployed-release.ts +220 -0
  241. package/src/dns/cloudflare.ts +182 -0
  242. package/src/dns/groups.test.ts +76 -0
  243. package/src/dns/groups.ts +86 -0
  244. package/src/dns/index.ts +56 -0
  245. package/src/dns/manual.ts +130 -0
  246. package/src/dns/types.ts +27 -0
  247. package/src/gateway-credentials.test.ts +180 -0
  248. package/src/gateway-credentials.ts +144 -0
  249. package/src/gcloud-components.test.ts +76 -0
  250. package/src/gcloud-components.ts +85 -0
  251. package/src/gcloud-retry.ts +41 -0
  252. package/src/gcloud-runner.test.ts +61 -0
  253. package/src/gcloud.test.ts +99 -0
  254. package/src/gcloud.ts +322 -0
  255. package/src/gcp-names.ts +135 -0
  256. package/src/index.test.ts +332 -0
  257. package/src/index.ts +114 -0
  258. package/src/infra-contract.ts +32 -0
  259. package/src/install-job-runner.ts +166 -0
  260. package/src/install-job.test.ts +566 -0
  261. package/src/install-job.ts +542 -0
  262. package/src/install-state.test.ts +239 -0
  263. package/src/install-state.ts +583 -0
  264. package/src/internal-token.ts +35 -0
  265. package/src/lifecycle.ts +900 -0
  266. package/src/mcp-iap-stacks.test.ts +58 -0
  267. package/src/mcp-iap.test.ts +149 -0
  268. package/src/mcp-iap.ts +131 -0
  269. package/src/media-volume.test.ts +296 -0
  270. package/src/migration-runner.ts +365 -0
  271. package/src/operator-access-stacks.test.ts +55 -0
  272. package/src/operator-access.test.ts +332 -0
  273. package/src/operator-access.ts +554 -0
  274. package/src/ops-backend.test.ts +320 -0
  275. package/src/ops-backend.ts +414 -0
  276. package/src/provisioner-identity.test.ts +482 -0
  277. package/src/provisioner-identity.ts +407 -0
  278. package/src/redirects-emit.ts +175 -0
  279. package/src/region-data.ts +428 -0
  280. package/src/regions.test.ts +277 -0
  281. package/src/regions.ts +370 -0
  282. package/src/release-contract.ts +57 -0
  283. package/src/release-manifest.test.ts +293 -0
  284. package/src/release-manifest.ts +430 -0
  285. package/src/run-mode.ts +40 -0
  286. package/src/runtime-secrets.test.ts +627 -0
  287. package/src/runtime-secrets.ts +754 -0
  288. package/src/self-command.test.ts +14 -0
  289. package/src/self-command.ts +21 -0
  290. package/src/self-hosted-compose.test.ts +177 -0
  291. package/src/self-hosted-release.ts +240 -0
  292. package/src/self-hosted-roles.integration.test.ts +161 -0
  293. package/src/self-hosted-roles.test.ts +124 -0
  294. package/src/self-hosted-roles.ts +203 -0
  295. package/src/self-hosted-upgrade.test.ts +394 -0
  296. package/src/self-hosted-upgrade.ts +548 -0
  297. package/src/stack-contract-parity.test.ts +199 -0
  298. package/src/stack-contract.test.ts +391 -0
  299. package/src/stack-contract.ts +888 -0
  300. package/src/stack-converge-secrets.test.ts +421 -0
  301. package/src/stack-converge.test.ts +833 -0
  302. package/src/stack-converge.ts +909 -0
  303. package/src/staging-admin.test.ts +246 -0
  304. package/src/staging-admin.ts +324 -0
  305. package/src/state-attach.test.ts +211 -0
  306. package/src/state-attach.ts +132 -0
  307. package/src/state-backend.test.ts +145 -0
  308. package/src/state-backend.ts +262 -0
  309. package/src/state-command.ts +293 -0
  310. package/src/state-migrate.test.ts +479 -0
  311. package/src/state-migrate.ts +434 -0
  312. package/src/state-pulumi.ts +289 -0
  313. package/src/state-store.test.ts +212 -0
  314. package/src/state-store.ts +243 -0
  315. package/src/upgrade/app-roll.ts +486 -0
  316. package/src/upgrade/backup.ts +109 -0
  317. package/src/upgrade/converge.ts +437 -0
  318. package/src/upgrade/gcp-lookup.ts +167 -0
  319. package/src/upgrade/health.test.ts +279 -0
  320. package/src/upgrade/health.ts +305 -0
  321. package/src/upgrade/release-images.test.ts +119 -0
  322. package/src/upgrade/release-images.ts +391 -0
  323. package/src/upgrade/reporter.ts +46 -0
  324. package/src/upgrade/upgrade.test.ts +626 -0
  325. package/src/upgrade/upgrade.ts +249 -0
  326. package/src/upgrader-assess.test.ts +265 -0
  327. package/src/upgrader-assess.ts +233 -0
  328. package/src/upgrader-gcp.ts +123 -0
  329. package/src/upgrader-job.test.ts +408 -0
  330. package/src/upgrader-job.ts +499 -0
  331. package/src/upgrader-reporter.test.ts +46 -0
  332. package/src/upgrader-reporter.ts +105 -0
  333. package/src/upgrader-setup.test.ts +169 -0
  334. package/src/upgrader-setup.ts +355 -0
  335. package/src/wizard.ts +425 -0
  336. package/src/wizards/gcp-cost.ts +188 -0
  337. package/src/wizards/gcp-firebase.ts +72 -0
  338. package/src/wizards/gcp-install-job.ts +506 -0
  339. package/src/wizards/gcp-prompts.ts +26 -0
  340. package/src/wizards/gcp-pulumi.test.ts +246 -0
  341. package/src/wizards/gcp-pulumi.ts +516 -0
  342. package/src/wizards/gcp.ts +1935 -0
@@ -0,0 +1,283 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Keeps secret material out of the files the CLI writes under
5
+ * `~/.caelo-<install-id>/` (#656).
6
+ *
7
+ * Only `secrets/` may hold secrets (CLAUDE.md §7, §11.C); `progress.json`
8
+ * and `install.json` are plain bookkeeping that tooling, backups, sync
9
+ * clients and agents read. Installs from before #576 checkpointed the full
10
+ * Pulumi outputs, whose database URLs carried admin_role's and
11
+ * public_role's passwords, and every install so far checkpointed the Owner
12
+ * setup link with its token. This module
13
+ *
14
+ * - finds secret material in a JSON value ({@link findSecrets}), which
15
+ * `markStepDone` / `writeMetadata` use to refuse writing it;
16
+ * - removes it from an existing checkpoint ({@link scrubProgressFile}),
17
+ * which `upgrade` and wizard re-runs call, and names the secrets to
18
+ * rotate ({@link rotationHints});
19
+ * - masks credentials in text the CLI prints ({@link redactCredentials}).
20
+ *
21
+ * Detection is by shape (a URL with a password, a URL with a token-like
22
+ * query parameter) and by key name (password, secret, token, ...), so a
23
+ * future secret output is caught without being listed here.
24
+ */
25
+
26
+ import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
27
+ import { selfCommand } from "./self-command.js";
28
+ import { DATABASE_ROLE_SECRET, type DatabaseRole } from "./stack-contract.js";
29
+
30
+ /** One place a JSON value holds secret material. Never carries the secret itself. */
31
+ export interface SecretFinding {
32
+ /** Dotted path to the offending value, e.g. `steps.pulumi-up-acme.outputs.adminDatabaseUrlOut`. */
33
+ readonly path: string;
34
+ /**
35
+ * `credential-url`: a URL whose user-info carries a password.
36
+ * `token-url`: a URL with a token / secret / password query parameter.
37
+ * `secret-key`: a key whose name marks its value as secret.
38
+ */
39
+ readonly kind: "credential-url" | "token-url" | "secret-key";
40
+ /** For a credential URL: the user whose password it carries. */
41
+ readonly user?: string;
42
+ }
43
+
44
+ /** `scheme://user:password@` — the user-info of a URL that carries a password. */
45
+ const CREDENTIAL_URL = /[a-z][a-z0-9+.-]{0,31}:\/\/([^\s:@/]*):([^\s@/]+)@/gi;
46
+
47
+ /** A URL query parameter that carries a credential (the Owner setup link's `?token=`). */
48
+ const TOKEN_PARAM =
49
+ /[?&](?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|sig|signature)=[^&\s#]+/i;
50
+
51
+ /**
52
+ * Key names whose value is a secret, after lower-casing, dropping non
53
+ * alphanumerics and a Pulumi `Out` suffix: `postgresPassword`,
54
+ * `caeloSecretKek`, `bootstrap_token`, `anthropicApiKey`.
55
+ */
56
+ const SECRET_KEY = /(password|passwd|passphrase|secret|secretkek|kek|token|apikey|privatekey)$/;
57
+
58
+ function isSecretKey(key: string): boolean {
59
+ const k = key
60
+ .toLowerCase()
61
+ .replace(/[^a-z0-9]/g, "")
62
+ .replace(/out$/, "");
63
+ return SECRET_KEY.test(k);
64
+ }
65
+
66
+ function urlUser(raw: string): string {
67
+ try {
68
+ return decodeURIComponent(raw);
69
+ } catch {
70
+ return raw;
71
+ }
72
+ }
73
+
74
+ function stringFindings(value: string, path: string): SecretFinding[] {
75
+ const found: SecretFinding[] = [];
76
+ for (const m of value.matchAll(CREDENTIAL_URL)) {
77
+ found.push({ path, kind: "credential-url", user: urlUser(m[1] ?? "") });
78
+ }
79
+ if (found.length === 0 && /:\/\//.test(value) && TOKEN_PARAM.test(value)) {
80
+ found.push({ path, kind: "token-url" });
81
+ }
82
+ return found;
83
+ }
84
+
85
+ /**
86
+ * Every place `value` holds secret material.
87
+ *
88
+ * @param value any JSON-shaped value (a checkpoint payload, install.json)
89
+ * @param path the path prefix to report findings under
90
+ */
91
+ export function findSecrets(value: unknown, path = ""): SecretFinding[] {
92
+ if (typeof value === "string") return stringFindings(value, path || "(value)");
93
+ const join = (k: string | number) => (path ? `${path}.${k}` : String(k));
94
+ if (Array.isArray(value)) return value.flatMap((v, i) => findSecrets(v, join(i)));
95
+ if (value !== null && typeof value === "object") {
96
+ return Object.entries(value).flatMap(([k, v]) => {
97
+ // A secret-named key is only a finding when it holds something; a
98
+ // null / false / 0 marker is not secret material.
99
+ if (isSecretKey(k) && v !== null && v !== undefined && v !== "" && typeof v !== "boolean") {
100
+ return [{ path: join(k), kind: "secret-key" as const }];
101
+ }
102
+ return findSecrets(v, join(k));
103
+ });
104
+ }
105
+ return [];
106
+ }
107
+
108
+ /**
109
+ * Throw when `value` holds secret material. The message names the paths,
110
+ * never the values, so the error itself cannot leak them.
111
+ *
112
+ * @param what what was about to be written, for the message
113
+ */
114
+ export function assertNoSecrets(value: unknown, what: string): void {
115
+ refuseFindings(findSecrets(value), what);
116
+ }
117
+
118
+ /** {@link assertNoSecrets} for a whole checkpoint (see {@link findCheckpointSecrets}). */
119
+ export function assertNoCheckpointSecrets(checkpoint: unknown, what: string): void {
120
+ refuseFindings(findCheckpointSecrets(checkpoint), what);
121
+ }
122
+
123
+ function refuseFindings(found: readonly SecretFinding[], what: string): void {
124
+ if (found.length === 0) return;
125
+ throw new Error(
126
+ `Refusing to write secret material to ${what} (${found.map((f) => `${f.path}: ${f.kind}`).join(", ")}). ` +
127
+ "Secrets belong in Secret Manager or the install's secrets/ directory; read them at the moment of use instead (#656).",
128
+ );
129
+ }
130
+
131
+ function isRecord(v: unknown): v is Record<string, unknown> {
132
+ return v !== null && typeof v === "object" && !Array.isArray(v);
133
+ }
134
+
135
+ /**
136
+ * {@link findSecrets} for a progress.json checkpoint. Its step names are
137
+ * derived from operator input (`project-<project id>`), so they are
138
+ * identifiers, not field names: a project called `acme-token` must not read
139
+ * as a secret. Only the step payloads (and the other top-level fields) are
140
+ * inspected.
141
+ */
142
+ export function findCheckpointSecrets(checkpoint: unknown): SecretFinding[] {
143
+ if (!isRecord(checkpoint)) return findSecrets(checkpoint);
144
+ const { steps, ...rest } = checkpoint;
145
+ const inSteps = isRecord(steps)
146
+ ? Object.entries(steps).flatMap(([name, payload]) => findSecrets(payload, `steps.${name}`))
147
+ : findSecrets(steps, "steps");
148
+ return [...findSecrets(rest), ...inSteps];
149
+ }
150
+
151
+ /** {@link withoutSecrets} for a checkpoint: every step is kept, its payload cleaned. */
152
+ function checkpointWithoutSecrets(checkpoint: unknown): unknown {
153
+ if (!isRecord(checkpoint)) return withoutSecrets(checkpoint);
154
+ const { steps, ...rest } = checkpoint;
155
+ return {
156
+ ...(withoutSecrets(rest) as Record<string, unknown>),
157
+ steps: isRecord(steps)
158
+ ? Object.fromEntries(Object.entries(steps).map(([n, p]) => [n, withoutSecrets(p)]))
159
+ : withoutSecrets(steps),
160
+ };
161
+ }
162
+
163
+ /**
164
+ * `value` without its secret material: secret-named keys and strings that
165
+ * hold a credential or token URL are dropped. Everything else is kept.
166
+ */
167
+ export function withoutSecrets(value: unknown): unknown {
168
+ if (typeof value === "string") return value;
169
+ if (Array.isArray(value)) {
170
+ return value
171
+ .filter((v) => typeof v !== "string" || findSecrets(v).length === 0)
172
+ .map(withoutSecrets);
173
+ }
174
+ if (value !== null && typeof value === "object") {
175
+ const out: Record<string, unknown> = {};
176
+ for (const [k, v] of Object.entries(value)) {
177
+ if (findSecrets({ [k]: v }).length > 0 && (isSecretKey(k) || typeof v === "string")) continue;
178
+ out[k] = withoutSecrets(v);
179
+ }
180
+ return out;
181
+ }
182
+ return value;
183
+ }
184
+
185
+ /**
186
+ * The Pulumi outputs worth checkpointing: everything except outputs Pulumi
187
+ * marks secret and anything else that holds secret material (the Owner
188
+ * setup link). Nothing reads secrets back from the checkpoint; code that
189
+ * needs one reads the stack (`readStackOutput`) when it needs it.
190
+ *
191
+ * @param outputs the stack outputs as the Automation API returns them
192
+ */
193
+ export function checkpointableOutputs(
194
+ outputs: Readonly<Record<string, { value: unknown; secret?: boolean }>>,
195
+ ): Record<string, unknown> {
196
+ const kept: Record<string, unknown> = {};
197
+ for (const [k, o] of Object.entries(outputs)) {
198
+ if (o.secret) continue;
199
+ if (findSecrets({ [k]: o.value }).length > 0) continue;
200
+ kept[k] = o.value;
201
+ }
202
+ return kept;
203
+ }
204
+
205
+ /** Outcome of {@link scrubProgressFile}. */
206
+ export interface ScrubReport {
207
+ /** What was removed (paths + kinds, never values). Empty when the file was clean or absent. */
208
+ readonly removed: readonly SecretFinding[];
209
+ }
210
+
211
+ /**
212
+ * Remove secret material from a `progress.json` in place. The rewrite is
213
+ * atomic (temp file in the same directory, then rename) and the result is
214
+ * mode 0600, so an interrupted scrub never leaves a truncated checkpoint
215
+ * and wizard re-runs still find every completed step.
216
+ *
217
+ * @param path the checkpoint file; a missing file is a no-op
218
+ */
219
+ export function scrubProgressFile(path: string): ScrubReport {
220
+ if (!existsSync(path)) return { removed: [] };
221
+ const checkpoint: unknown = JSON.parse(readFileSync(path, "utf8"));
222
+ const removed = findCheckpointSecrets(checkpoint);
223
+ if (removed.length === 0) return { removed };
224
+ writeJsonAtomic(path, checkpointWithoutSecrets(checkpoint));
225
+ return { removed };
226
+ }
227
+
228
+ /**
229
+ * Write `value` as JSON to `path` atomically with mode 0600: a temp file in
230
+ * the same directory (so the rename stays on one filesystem), then rename.
231
+ */
232
+ export function writeJsonAtomic(path: string, value: unknown): void {
233
+ const tmp = `${path}.tmp-${process.pid}`;
234
+ try {
235
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
236
+ renameSync(tmp, path);
237
+ } catch (e) {
238
+ rmSync(tmp, { force: true });
239
+ throw e;
240
+ }
241
+ }
242
+
243
+ const ROLE_BY_USER = DATABASE_ROLE_SECRET as Readonly<Record<string, string | undefined>>;
244
+
245
+ /**
246
+ * What the operator should do about secrets a scrub removed: one line per
247
+ * affected secret, with the exact command where one exists.
248
+ *
249
+ * @param removed the scrub's findings
250
+ * @param installId the install, named in each command so it acts on the right one
251
+ */
252
+ export function rotationHints(removed: readonly SecretFinding[], installId: string): string[] {
253
+ const rotate = (secret: string) => selfCommand("rotate-secret", secret, "--install", installId);
254
+ const hints = new Set<string>();
255
+ for (const f of removed) {
256
+ if (f.kind === "credential-url") {
257
+ const secret = f.user ? ROLE_BY_USER[f.user as DatabaseRole] : undefined;
258
+ hints.add(
259
+ secret
260
+ ? `${f.user}'s password was stored in progress.json. Rotate it: ${rotate(secret)}`
261
+ : `A password for database user "${f.user ?? "?"}" was stored in progress.json (${f.path}); change it.`,
262
+ );
263
+ } else if (f.kind === "token-url") {
264
+ hints.add(
265
+ "The Owner setup link (with its one-time token) was stored in progress.json. It expires 24 hours after it was issued and stops working once the Owner account exists; nothing to rotate.",
266
+ );
267
+ } else {
268
+ hints.add(
269
+ `A secret value was stored in progress.json (${f.path}). Rotate whatever it belongs to.`,
270
+ );
271
+ }
272
+ }
273
+ return [...hints];
274
+ }
275
+
276
+ /**
277
+ * `text` with every URL password masked (`postgresql://admin_role:***@`),
278
+ * for anything the CLI prints that could carry one (errors from Pulumi or
279
+ * gcloud, for instance). Same masking as the API gateway's error log.
280
+ */
281
+ export function redactCredentials(text: string): string {
282
+ return text.replace(/([a-z][a-z0-9+.-]{0,31}:\/\/[^\s:@/]*):[^\s@/]+@/gi, "$1:***@");
283
+ }
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import { initDelegatesToWizard, resolveCliRoute } from "./cli-routing.js";
5
+
6
+ const HANDLERS = ["init", "upgrade", "wizard", "version", "--version", "-v"];
7
+ const argv = (...rest: string[]) => ["bun", "cli.js", ...rest];
8
+
9
+ describe("resolveCliRoute", () => {
10
+ it("routes the documented flags-only invocation to the wizard", () => {
11
+ expect(
12
+ resolveCliRoute(argv("--provider", "gcp-firebase", "--domain", "example.com"), HANDLERS),
13
+ ).toEqual({ kind: "wizard" });
14
+ });
15
+
16
+ it("routes a bare invocation to the wizard", () => {
17
+ expect(resolveCliRoute(argv(), HANDLERS)).toEqual({ kind: "wizard" });
18
+ });
19
+
20
+ it("dispatches known sub-commands, including flag-shaped ones", () => {
21
+ expect(resolveCliRoute(argv("upgrade", "--channel", "rc"), HANDLERS)).toEqual({
22
+ kind: "handler",
23
+ name: "upgrade",
24
+ });
25
+ expect(resolveCliRoute(argv("--version"), HANDLERS)).toEqual({
26
+ kind: "handler",
27
+ name: "--version",
28
+ });
29
+ });
30
+
31
+ it("prints usage for unknown sub-commands, --no-wizard and --help", () => {
32
+ expect(resolveCliRoute(argv("deploy"), HANDLERS)).toEqual({ kind: "usage" });
33
+ expect(resolveCliRoute(argv("--no-wizard"), HANDLERS)).toEqual({ kind: "usage" });
34
+ expect(resolveCliRoute(argv("--help"), HANDLERS)).toEqual({ kind: "usage" });
35
+ });
36
+ });
37
+
38
+ describe("initDelegatesToWizard", () => {
39
+ it("hands every cloud provider to the wizard and keeps self-hosted on init", () => {
40
+ for (const p of ["gcp", "gcp-firebase", "aws", "azure"]) {
41
+ expect(initDelegatesToWizard(p)).toBe(true);
42
+ }
43
+ expect(initDelegatesToWizard("self-hosted")).toBe(false);
44
+ });
45
+ });
@@ -0,0 +1,43 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * Argv → command routing for `cms-provision`, kept free of side effects so
5
+ * it can be unit-tested (cli.ts dispatches at module load).
6
+ *
7
+ * The documented one-command entry point (CLAUDE.md §11.C) is
8
+ * `bunx @caelo-cms/provisioning --provider gcp --domain …` — flags only,
9
+ * no sub-command. That shape must reach the wizard; treating the first
10
+ * flag as an unknown sub-command silently prints usage instead.
11
+ */
12
+
13
+ /** What the CLI should do for a given argv. */
14
+ export type CliRoute = { kind: "handler"; name: string } | { kind: "wizard" } | { kind: "usage" };
15
+
16
+ /**
17
+ * Resolve the route for `argv` (a full `process.argv`, so the sub-command
18
+ * sits at index 2).
19
+ *
20
+ * @param handlerNames - registered sub-commands; flag-shaped names such as
21
+ * `--version` win over the wizard fallback.
22
+ */
23
+ export function resolveCliRoute(argv: readonly string[], handlerNames: Iterable<string>): CliRoute {
24
+ const first = argv[2];
25
+ if (first !== undefined && new Set(handlerNames).has(first)) {
26
+ return { kind: "handler", name: first };
27
+ }
28
+ const flagsOnly = first === undefined || first.startsWith("-");
29
+ if (!flagsOnly) return { kind: "usage" };
30
+ if (argv.includes("--no-wizard") || argv.includes("--help") || argv.includes("-h")) {
31
+ return { kind: "usage" };
32
+ }
33
+ return { kind: "wizard" };
34
+ }
35
+
36
+ /**
37
+ * Whether `init --provider <p>` belongs to the wizard. Only self-hosted
38
+ * keeps the compose-file `init` flow; every cloud provider is provisioned
39
+ * end-to-end by the wizard rather than by printed manual Pulumi steps.
40
+ */
41
+ export function initDelegatesToWizard(provider: string): boolean {
42
+ return provider !== "self-hosted";
43
+ }