@caelo-cms/provisioning 0.11.9 → 0.11.11

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 (352) hide show
  1. package/dist/adapter.d.ts +6 -0
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/admin-domain.d.ts +10 -1
  4. package/dist/admin-domain.d.ts.map +1 -1
  5. package/dist/admin-domain.js +13 -3
  6. package/dist/admin-domain.js.map +1 -1
  7. package/dist/adopt-existing.d.ts +12 -3
  8. package/dist/adopt-existing.d.ts.map +1 -1
  9. package/dist/adopt-existing.js +143 -0
  10. package/dist/adopt-existing.js.map +1 -1
  11. package/dist/agent/command.d.ts +23 -0
  12. package/dist/agent/command.d.ts.map +1 -1
  13. package/dist/agent/command.js +32 -9
  14. package/dist/agent/command.js.map +1 -1
  15. package/dist/agent/run.d.ts +2 -0
  16. package/dist/agent/run.d.ts.map +1 -1
  17. package/dist/agent/run.js +5 -1
  18. package/dist/agent/run.js.map +1 -1
  19. package/dist/agent-db.js +69 -10
  20. package/dist/aws-task-roles.d.ts +4 -2
  21. package/dist/aws-task-roles.d.ts.map +1 -1
  22. package/dist/aws-task-roles.js +4 -1
  23. package/dist/aws-task-roles.js.map +1 -1
  24. package/dist/caddy.d.ts +21 -4
  25. package/dist/caddy.d.ts.map +1 -1
  26. package/dist/caddy.js +27 -3
  27. package/dist/caddy.js.map +1 -1
  28. package/dist/cli-routing.js +1 -1
  29. package/dist/cli-routing.js.map +1 -1
  30. package/dist/cli.js +68 -7
  31. package/dist/cli.js.map +1 -1
  32. package/dist/cloudsql-tls.d.ts +93 -0
  33. package/dist/cloudsql-tls.d.ts.map +1 -0
  34. package/dist/cloudsql-tls.js +295 -0
  35. package/dist/cloudsql-tls.js.map +1 -0
  36. package/dist/compose.d.ts +43 -6
  37. package/dist/compose.d.ts.map +1 -1
  38. package/dist/compose.js +148 -23
  39. package/dist/compose.js.map +1 -1
  40. package/dist/converge-checkpoint.d.ts +1 -0
  41. package/dist/converge-checkpoint.d.ts.map +1 -1
  42. package/dist/converge-checkpoint.js +8 -1
  43. package/dist/converge-checkpoint.js.map +1 -1
  44. package/dist/converge-db-trust.d.ts +44 -0
  45. package/dist/converge-db-trust.d.ts.map +1 -0
  46. package/dist/converge-db-trust.js +158 -0
  47. package/dist/converge-db-trust.js.map +1 -0
  48. package/dist/converge-job-gcp.d.ts +5 -2
  49. package/dist/converge-job-gcp.d.ts.map +1 -1
  50. package/dist/converge-job-gcp.js +11 -13
  51. package/dist/converge-job-gcp.js.map +1 -1
  52. package/dist/converge-job-preflight.d.ts.map +1 -1
  53. package/dist/converge-job-preflight.js +8 -0
  54. package/dist/converge-job-preflight.js.map +1 -1
  55. package/dist/converge-job.d.ts +17 -0
  56. package/dist/converge-job.d.ts.map +1 -1
  57. package/dist/converge-job.js +12 -0
  58. package/dist/converge-job.js.map +1 -1
  59. package/dist/deployed-release.d.ts +4 -1
  60. package/dist/deployed-release.d.ts.map +1 -1
  61. package/dist/deployed-release.js +18 -8
  62. package/dist/deployed-release.js.map +1 -1
  63. package/dist/gcp-names.d.ts +34 -0
  64. package/dist/gcp-names.d.ts.map +1 -1
  65. package/dist/gcp-names.js +45 -0
  66. package/dist/gcp-names.js.map +1 -1
  67. package/dist/gcp-url-map.d.ts.map +1 -1
  68. package/dist/gcp-url-map.js +6 -2
  69. package/dist/gcp-url-map.js.map +1 -1
  70. package/dist/index.d.ts +3 -2
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/infra-contract.d.ts +1 -1
  75. package/dist/infra-contract.d.ts.map +1 -1
  76. package/dist/infra-contract.js +45 -1
  77. package/dist/infra-contract.js.map +1 -1
  78. package/dist/install-job.d.ts +4 -2
  79. package/dist/install-job.d.ts.map +1 -1
  80. package/dist/install-job.js +4 -1
  81. package/dist/install-job.js.map +1 -1
  82. package/dist/install-state.d.ts +13 -0
  83. package/dist/install-state.d.ts.map +1 -1
  84. package/dist/install-state.js +17 -4
  85. package/dist/install-state.js.map +1 -1
  86. package/dist/lifecycle.d.ts.map +1 -1
  87. package/dist/lifecycle.js +20 -8
  88. package/dist/lifecycle.js.map +1 -1
  89. package/dist/mcp-auth-proxy-setup.d.ts +48 -0
  90. package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
  91. package/dist/mcp-auth-proxy-setup.js +280 -0
  92. package/dist/mcp-auth-proxy-setup.js.map +1 -0
  93. package/dist/migration-runner.d.ts.map +1 -1
  94. package/dist/migration-runner.js +8 -3
  95. package/dist/migration-runner.js.map +1 -1
  96. package/dist/migration-targets.d.ts +11 -0
  97. package/dist/migration-targets.d.ts.map +1 -0
  98. package/dist/migration-targets.js +12 -0
  99. package/dist/migration-targets.js.map +1 -0
  100. package/dist/operator-access.d.ts +10 -0
  101. package/dist/operator-access.d.ts.map +1 -1
  102. package/dist/operator-access.js +5 -0
  103. package/dist/operator-access.js.map +1 -1
  104. package/dist/pool-budget-stack.d.ts +13 -1
  105. package/dist/pool-budget-stack.d.ts.map +1 -1
  106. package/dist/pool-budget-stack.js +11 -1
  107. package/dist/pool-budget-stack.js.map +1 -1
  108. package/dist/public-role-isolation.d.ts +7 -3
  109. package/dist/public-role-isolation.d.ts.map +1 -1
  110. package/dist/public-role-isolation.js +25 -13
  111. package/dist/public-role-isolation.js.map +1 -1
  112. package/dist/release-contract.d.ts.map +1 -1
  113. package/dist/release-contract.js +3 -0
  114. package/dist/release-contract.js.map +1 -1
  115. package/dist/release-manifest.d.ts +25 -1
  116. package/dist/release-manifest.d.ts.map +1 -1
  117. package/dist/release-manifest.js +36 -3
  118. package/dist/release-manifest.js.map +1 -1
  119. package/dist/remote-mcp.d.ts +55 -18
  120. package/dist/remote-mcp.d.ts.map +1 -1
  121. package/dist/remote-mcp.js +307 -77
  122. package/dist/remote-mcp.js.map +1 -1
  123. package/dist/runtime-secrets.d.ts +11 -0
  124. package/dist/runtime-secrets.d.ts.map +1 -1
  125. package/dist/runtime-secrets.js +40 -25
  126. package/dist/runtime-secrets.js.map +1 -1
  127. package/dist/scheduler-setup.d.ts +75 -0
  128. package/dist/scheduler-setup.d.ts.map +1 -0
  129. package/dist/scheduler-setup.js +331 -0
  130. package/dist/scheduler-setup.js.map +1 -0
  131. package/dist/seal-key-rotation.d.ts +7 -7
  132. package/dist/seal-key-rotation.d.ts.map +1 -1
  133. package/dist/seal-key-rotation.js +22 -22
  134. package/dist/seal-key-rotation.js.map +1 -1
  135. package/dist/self-hosted-db-tls.d.ts +37 -0
  136. package/dist/self-hosted-db-tls.d.ts.map +1 -0
  137. package/dist/self-hosted-db-tls.js +220 -0
  138. package/dist/self-hosted-db-tls.js.map +1 -0
  139. package/dist/self-hosted-release.d.ts +3 -1
  140. package/dist/self-hosted-release.d.ts.map +1 -1
  141. package/dist/self-hosted-release.js +33 -9
  142. package/dist/self-hosted-release.js.map +1 -1
  143. package/dist/self-hosted-roles.d.ts +43 -2
  144. package/dist/self-hosted-roles.d.ts.map +1 -1
  145. package/dist/self-hosted-roles.js +87 -1
  146. package/dist/self-hosted-roles.js.map +1 -1
  147. package/dist/self-hosted-scheduler-interval.d.ts +21 -0
  148. package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
  149. package/dist/self-hosted-scheduler-interval.js +68 -0
  150. package/dist/self-hosted-scheduler-interval.js.map +1 -0
  151. package/dist/self-hosted-start.d.ts +0 -21
  152. package/dist/self-hosted-start.d.ts.map +1 -1
  153. package/dist/self-hosted-start.js +39 -1
  154. package/dist/self-hosted-start.js.map +1 -1
  155. package/dist/self-hosted-upgrade.d.ts +19 -7
  156. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  157. package/dist/self-hosted-upgrade.js +78 -25
  158. package/dist/self-hosted-upgrade.js.map +1 -1
  159. package/dist/stack-contract.d.ts +234 -28
  160. package/dist/stack-contract.d.ts.map +1 -1
  161. package/dist/stack-contract.js +397 -39
  162. package/dist/stack-contract.js.map +1 -1
  163. package/dist/stack-converge.d.ts +31 -0
  164. package/dist/stack-converge.d.ts.map +1 -1
  165. package/dist/stack-converge.js +71 -30
  166. package/dist/stack-converge.js.map +1 -1
  167. package/dist/staging-database.d.ts +22 -0
  168. package/dist/staging-database.d.ts.map +1 -0
  169. package/dist/staging-database.js +117 -0
  170. package/dist/staging-database.js.map +1 -0
  171. package/dist/upgrade/app-roll.d.ts +25 -4
  172. package/dist/upgrade/app-roll.d.ts.map +1 -1
  173. package/dist/upgrade/app-roll.js +63 -17
  174. package/dist/upgrade/app-roll.js.map +1 -1
  175. package/dist/upgrade/converge.d.ts +5 -0
  176. package/dist/upgrade/converge.d.ts.map +1 -1
  177. package/dist/upgrade/converge.js +207 -20
  178. package/dist/upgrade/converge.js.map +1 -1
  179. package/dist/upgrade/health.d.ts +31 -7
  180. package/dist/upgrade/health.d.ts.map +1 -1
  181. package/dist/upgrade/health.js +41 -17
  182. package/dist/upgrade/health.js.map +1 -1
  183. package/dist/upgrade/image-forms.d.ts +1 -1
  184. package/dist/upgrade/image-forms.d.ts.map +1 -1
  185. package/dist/upgrade/image-forms.js +24 -7
  186. package/dist/upgrade/image-forms.js.map +1 -1
  187. package/dist/upgrade/release-images.d.ts +12 -3
  188. package/dist/upgrade/release-images.d.ts.map +1 -1
  189. package/dist/upgrade/release-images.js +13 -4
  190. package/dist/upgrade/release-images.js.map +1 -1
  191. package/dist/upgrade/upgrade.d.ts +19 -3
  192. package/dist/upgrade/upgrade.d.ts.map +1 -1
  193. package/dist/upgrade/upgrade.js +37 -2
  194. package/dist/upgrade/upgrade.js.map +1 -1
  195. package/dist/upgrader-gcp.d.ts +13 -0
  196. package/dist/upgrader-gcp.d.ts.map +1 -1
  197. package/dist/upgrader-gcp.js +50 -1
  198. package/dist/upgrader-gcp.js.map +1 -1
  199. package/dist/upgrader-iam-preflight.d.ts.map +1 -1
  200. package/dist/upgrader-iam-preflight.js +6 -1
  201. package/dist/upgrader-iam-preflight.js.map +1 -1
  202. package/dist/upgrader-job.d.ts +1 -1
  203. package/dist/upgrader-job.d.ts.map +1 -1
  204. package/dist/upgrader-job.js +20 -14
  205. package/dist/upgrader-job.js.map +1 -1
  206. package/dist/upgrader-records.d.ts +1 -1
  207. package/dist/upgrader-records.d.ts.map +1 -1
  208. package/dist/upgrader-records.js +26 -7
  209. package/dist/upgrader-records.js.map +1 -1
  210. package/dist/upgrader-setup.d.ts.map +1 -1
  211. package/dist/upgrader-setup.js +10 -5
  212. package/dist/upgrader-setup.js.map +1 -1
  213. package/dist/wizard.d.ts +7 -0
  214. package/dist/wizard.d.ts.map +1 -1
  215. package/dist/wizard.js +11 -0
  216. package/dist/wizard.js.map +1 -1
  217. package/dist/wizards/gcp-cost.d.ts +31 -0
  218. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  219. package/dist/wizards/gcp-cost.js +103 -18
  220. package/dist/wizards/gcp-cost.js.map +1 -1
  221. package/dist/wizards/gcp.d.ts.map +1 -1
  222. package/dist/wizards/gcp.js +129 -27
  223. package/dist/wizards/gcp.js.map +1 -1
  224. package/package.json +2 -2
  225. package/src/adapter.ts +6 -0
  226. package/src/admin-domain.test.ts +9 -8
  227. package/src/admin-domain.ts +13 -3
  228. package/src/admin-memory.test.ts +2 -1
  229. package/src/adopt-existing.test.ts +85 -0
  230. package/src/adopt-existing.ts +141 -3
  231. package/src/agent/command.test.ts +49 -1
  232. package/src/agent/command.ts +39 -9
  233. package/src/agent/run.test.ts +19 -0
  234. package/src/agent/run.ts +11 -2
  235. package/src/aws-task-roles.ts +5 -2
  236. package/src/caddy-redirects.docker.test.ts +2 -0
  237. package/src/caddy-redirects.test.ts +4 -1
  238. package/src/caddy-validate.test.ts +7 -1
  239. package/src/caddy.test.ts +39 -1
  240. package/src/caddy.ts +36 -4
  241. package/src/checkpoint-secrets.test.ts +14 -6
  242. package/src/cli-routing.test.ts +1 -1
  243. package/src/cli-routing.ts +1 -1
  244. package/src/cli.ts +71 -5
  245. package/src/cloudsql-tls.test.ts +255 -0
  246. package/src/cloudsql-tls.ts +359 -0
  247. package/src/compose.ts +193 -24
  248. package/src/converge-checkpoint.ts +8 -1
  249. package/src/converge-db-trust.test.ts +237 -0
  250. package/src/converge-db-trust.ts +212 -0
  251. package/src/converge-job-gcp.ts +11 -12
  252. package/src/converge-job-preflight.test.ts +5 -0
  253. package/src/converge-job-preflight.ts +8 -0
  254. package/src/converge-job.test.ts +70 -0
  255. package/src/converge-job.ts +32 -0
  256. package/src/database-tls-builders.test.ts +56 -0
  257. package/src/deployed-release.test.ts +107 -40
  258. package/src/deployed-release.ts +30 -9
  259. package/src/gateway-env-isolation.test.ts +50 -12
  260. package/src/gcp-names.ts +56 -0
  261. package/src/gcp-url-map.test.ts +6 -8
  262. package/src/gcp-url-map.ts +6 -2
  263. package/src/index.test.ts +10 -4
  264. package/src/index.ts +7 -0
  265. package/src/infra-contract.test.ts +7 -3
  266. package/src/infra-contract.ts +45 -1
  267. package/src/install-job.ts +4 -1
  268. package/src/install-state.ts +33 -4
  269. package/src/lifecycle.ts +23 -11
  270. package/src/mcp-auth-proxy-setup.test.ts +305 -0
  271. package/src/mcp-auth-proxy-setup.ts +374 -0
  272. package/src/mcp-iap-stacks.test.ts +1 -1
  273. package/src/media-volume.test.ts +1 -1
  274. package/src/migration-runner.ts +14 -9
  275. package/src/migration-targets.ts +12 -0
  276. package/src/operator-access-stacks.test.ts +1 -1
  277. package/src/operator-access.test.ts +20 -4
  278. package/src/operator-access.ts +14 -0
  279. package/src/pool-budget-stack.ts +15 -0
  280. package/src/pool-budget-stacks.test.ts +39 -14
  281. package/src/public-role-isolation.test.ts +18 -8
  282. package/src/public-role-isolation.ts +31 -17
  283. package/src/release-contract.ts +3 -0
  284. package/src/release-manifest.test.ts +25 -0
  285. package/src/release-manifest.ts +59 -4
  286. package/src/remote-mcp-stack-config.test.ts +19 -9
  287. package/src/remote-mcp.test.ts +252 -57
  288. package/src/remote-mcp.ts +336 -77
  289. package/src/runtime-secrets.test.ts +37 -14
  290. package/src/runtime-secrets.ts +87 -27
  291. package/src/scheduler-setup.test.ts +201 -0
  292. package/src/scheduler-setup.ts +375 -0
  293. package/src/seal-key-rotation.test.ts +2 -2
  294. package/src/seal-key-rotation.ts +25 -25
  295. package/src/self-hosted-backup.test.ts +1 -0
  296. package/src/self-hosted-compose.test.ts +180 -6
  297. package/src/self-hosted-db-tls.test.ts +111 -0
  298. package/src/self-hosted-db-tls.ts +287 -0
  299. package/src/self-hosted-release-resolve.test.ts +71 -0
  300. package/src/self-hosted-release.ts +41 -11
  301. package/src/self-hosted-roles.integration.test.ts +2 -0
  302. package/src/self-hosted-roles.test.ts +87 -3
  303. package/src/self-hosted-roles.ts +101 -3
  304. package/src/self-hosted-scheduler-interval.test.ts +66 -0
  305. package/src/self-hosted-scheduler-interval.ts +79 -0
  306. package/src/self-hosted-start.test.ts +35 -2
  307. package/src/self-hosted-start.ts +20 -2
  308. package/src/self-hosted-upgrade.test.ts +104 -14
  309. package/src/self-hosted-upgrade.ts +123 -31
  310. package/src/stack-contract-parity.test.ts +79 -12
  311. package/src/stack-contract.test.ts +199 -31
  312. package/src/stack-contract.ts +472 -45
  313. package/src/stack-converge-secrets.test.ts +28 -13
  314. package/src/stack-converge.test.ts +203 -28
  315. package/src/stack-converge.ts +92 -30
  316. package/src/staging-database.test.ts +114 -0
  317. package/src/staging-database.ts +148 -0
  318. package/src/state-store.test.ts +2 -1
  319. package/src/upgrade/app-roll.ts +97 -19
  320. package/src/upgrade/converge.ts +249 -19
  321. package/src/upgrade/health.test.ts +63 -25
  322. package/src/upgrade/health.ts +54 -22
  323. package/src/upgrade/image-forms.test.ts +34 -0
  324. package/src/upgrade/image-forms.ts +23 -8
  325. package/src/upgrade/release-images.test.ts +5 -1
  326. package/src/upgrade/release-images.ts +22 -6
  327. package/src/upgrade/upgrade.test.ts +438 -46
  328. package/src/upgrade/upgrade.ts +64 -6
  329. package/src/upgrader-gcp.ts +66 -3
  330. package/src/upgrader-iam-preflight.test.ts +19 -0
  331. package/src/upgrader-iam-preflight.ts +6 -0
  332. package/src/upgrader-job.test.ts +101 -27
  333. package/src/upgrader-job.ts +27 -14
  334. package/src/upgrader-records.test.ts +97 -12
  335. package/src/upgrader-records.ts +28 -11
  336. package/src/upgrader-setup.test.ts +2 -0
  337. package/src/upgrader-setup.ts +10 -4
  338. package/src/wizard.ts +19 -0
  339. package/src/wizards/gcp-cost.test.ts +103 -0
  340. package/src/wizards/gcp-cost.ts +125 -18
  341. package/src/wizards/gcp.ts +164 -22
  342. package/stacks/aws/Pulumi.yaml +1 -1
  343. package/stacks/aws/index.ts +72 -14
  344. package/stacks/azure/index.ts +108 -21
  345. package/stacks/gcp/Pulumi.yaml +1 -1
  346. package/stacks/gcp/README.md +1 -0
  347. package/stacks/gcp/index.ts +133 -24
  348. package/stacks/gcp-firebase/Pulumi.yaml +1 -1
  349. package/stacks/gcp-firebase/index.ts +212 -34
  350. package/stacks/self-hosted/Pulumi.yaml +3 -0
  351. package/stacks/self-hosted/README.md +6 -1
  352. package/stacks/self-hosted/index.ts +40 -5
@@ -23,12 +23,13 @@
23
23
  * Storage volume + mount), added in the same `services update` when
24
24
  * missing.
25
25
  * - {@link planSecretFiles} / {@link planRequestTimeout} (#806): the
26
- * gateway's secret file (the remote-MCP OAuth client) and its request
26
+ * MCP-Auth-Proxy.s secret file (the remote-MCP OAuth client) and its request
27
27
  * timeout, in the same `services update`.
28
28
  * - {@link planCpuIdle}: request-based CPU where the contract wants it
29
29
  * (`SERVICE_CPU_IDLE`), in the same `services update`.
30
30
  */
31
31
 
32
+ import { isIpLiteral } from "@caelo-cms/shared";
32
33
  import { removeDefaultFirewallRules } from "./default-firewall.js";
33
34
  import { gcloud as defaultGcloud, type GcloudResult } from "./gcloud.js";
34
35
  import { type GcloudRunner, realSleep, runWithRetry, type Sleep } from "./gcloud-retry.js";
@@ -38,8 +39,10 @@ import {
38
39
  ADMIN_MEMORY_DEFAULT,
39
40
  type AdminEnvInputs,
40
41
  adminEnvContract,
42
+ CLOUD_RUN_SLUGS,
41
43
  type CloudRunEnvVar,
42
44
  type CloudRunSlug,
45
+ DATABASE_URL_VARS,
43
46
  databaseUrls,
44
47
  type GcpProvider,
45
48
  gatewayEnvContract,
@@ -47,6 +50,7 @@ import {
47
50
  type IamTarget,
48
51
  iamMember,
49
52
  type MediaVolumeContract,
53
+ mcpAuthProxyEnvContract,
50
54
  memoryQuantityMiB,
51
55
  RETIRED_IAM_BINDINGS,
52
56
  RETIRED_RUNTIME_SECRETS,
@@ -167,7 +171,7 @@ function maskUrlPassword(value: string): string {
167
171
  * gcloud's list flags split on `,`; a value containing one needs the
168
172
  * `^<delim>^` escape syntax (`gcloud topic escaping`).
169
173
  */
170
- function listFlag(flag: string, pairs: string[]): string {
174
+ export function listFlag(flag: string, pairs: string[]): string {
171
175
  const joined = pairs.join(",");
172
176
  if (!pairs.some((p) => p.includes(","))) return `${flag}=${joined}`;
173
177
  const delim = ["@", "|", "#", "~", ";"].find((d) => !pairs.some((p) => p.includes(d)));
@@ -306,21 +310,42 @@ export function planContractEnv(
306
310
  * refused rather than silently doubled.
307
311
  */
308
312
  readonly extraEnv?: Readonly<Partial<Record<CloudRunSlug, readonly CloudRunEnvVar[]>>>;
313
+ /**
314
+ * #1338 — the database host the converge just set up (the instance's
315
+ * DNS name, cloudsql-tls.ts), in place of the one the services dialled
316
+ * when the plan was read.
317
+ */
318
+ readonly databaseHost?: string;
309
319
  } = {},
310
320
  ):
311
321
  | {
312
322
  readonly ok: true;
313
323
  readonly services: Record<CloudRunSlug, { flags: string[]; changes: EnvChange[] }>;
324
+ /**
325
+ * #1338 — the services still dial the instance's IP, which no
326
+ * verify-full connection can use; their database URLs were left as
327
+ * they are. Only a converge moves them (cloudsql-tls.ts); a roll must
328
+ * not start while this is true.
329
+ */
330
+ readonly databaseHostPending: boolean;
314
331
  }
315
332
  | { readonly ok: false; readonly error: string } {
316
- const host = liveDatabaseHost(services.admin.liveEnv);
317
- if (!host.ok) {
333
+ const live = liveDatabaseHost(services.admin.liveEnv);
334
+ if (!opts.databaseHost && !live.ok) {
318
335
  return {
319
336
  ok: false,
320
- error: `the Cloud SQL host is unknown: the admin service (${services.admin.serviceName}) ${host.error}`,
337
+ error: `the Cloud SQL host is unknown: the admin service (${services.admin.serviceName}) ${live.error}`,
321
338
  };
322
339
  }
323
- const runtime: RuntimeEnvInputs = { ...install, databaseUrls: databaseUrls(host.host) };
340
+ const host = opts.databaseHost ?? (live.ok ? live.host : "");
341
+ const databaseHostPending = isIpLiteral(host);
342
+ // An IP cannot carry a verify-full URL (databaseUrls refuses it): the URLs
343
+ // are planned from a placeholder name and then left untouched, so the
344
+ // rest of the env still plans and the caller can see what is pending.
345
+ const runtime: RuntimeEnvInputs = {
346
+ ...install,
347
+ databaseUrls: databaseUrls(databaseHostPending ? "pending.invalid" : host),
348
+ };
324
349
  let inputs: AdminEnvInputs;
325
350
  if (install.provider === "gcp-firebase") {
326
351
  // The site id carries a random suffix Pulumi generated at install time.
@@ -340,36 +365,33 @@ export function planContractEnv(
340
365
  } else {
341
366
  inputs = { ...runtime, provider: "gcp" };
342
367
  }
343
- const skip = new Set(opts.leaveUntouched ?? []);
368
+ const skip = new Set([
369
+ ...(opts.leaveUntouched ?? []),
370
+ ...(databaseHostPending ? [...DATABASE_URL_VARS.admin, ...DATABASE_URL_VARS.gateway] : []),
371
+ ]);
344
372
  const desired = (slug: CloudRunSlug, contract: readonly CloudRunEnvVar[]) => {
345
373
  const extra = opts.extraEnv?.[slug] ?? [];
346
374
  const clash = extra.find((e) => contract.some((c) => c.name === e.name));
347
375
  if (clash) throw new Error(`${slug}: ${clash.name} is both in the contract and extra env`);
348
376
  return [...contract, ...extra].filter((e) => !skip.has(e.name));
349
377
  };
350
- let adminDesired: CloudRunEnvVar[];
351
- let gatewayDesired: CloudRunEnvVar[];
378
+ let contracts: Record<CloudRunSlug, CloudRunEnvVar[]>;
352
379
  try {
353
- adminDesired = desired("admin", adminEnvContract(inputs));
354
- gatewayDesired = desired("gateway", gatewayEnvContract(runtime));
380
+ contracts = {
381
+ admin: desired("admin", adminEnvContract(inputs)),
382
+ gateway: desired("gateway", gatewayEnvContract(runtime)),
383
+ "mcp-auth-proxy": desired("mcp-auth-proxy", mcpAuthProxyEnvContract(install)),
384
+ };
355
385
  } catch (e) {
356
386
  return { ok: false, error: e instanceof Error ? e.message : String(e) };
357
387
  }
358
- const admin = planEnvUpdate(services.admin.liveEnv, adminDesired, RETIRED_SERVICE_ENV.admin);
359
- if (!admin.ok) return { ok: false, error: `admin: ${admin.error}` };
360
- const gateway = planEnvUpdate(
361
- services.gateway.liveEnv,
362
- gatewayDesired,
363
- RETIRED_SERVICE_ENV.gateway,
364
- );
365
- if (!gateway.ok) return { ok: false, error: `gateway: ${gateway.error}` };
366
- return {
367
- ok: true,
368
- services: {
369
- admin: { flags: admin.flags, changes: admin.changes },
370
- gateway: { flags: gateway.flags, changes: gateway.changes },
371
- },
372
- };
388
+ const out = {} as Record<CloudRunSlug, { flags: string[]; changes: EnvChange[] }>;
389
+ for (const slug of CLOUD_RUN_SLUGS) {
390
+ const plan = planEnvUpdate(services[slug].liveEnv, contracts[slug], RETIRED_SERVICE_ENV[slug]);
391
+ if (!plan.ok) return { ok: false, error: `${slug}: ${plan.error}` };
392
+ out[slug] = { flags: plan.flags, changes: plan.changes };
393
+ }
394
+ return { ok: true, services: out, databaseHostPending };
373
395
  }
374
396
 
375
397
  // ===========================================================================
@@ -618,16 +640,24 @@ export function liveIngress(serviceJson: string): string | null {
618
640
  * the serving one and the app-roll's rollback target stay as they are. On
619
641
  * `gcp` the stack declares load-balancer-only and `staging-domain enable`
620
642
  * (admin-domain.ts) opens it deliberately, so this never runs there.
643
+ *
644
+ * #1328 — both public services of a gcp-firebase install: the data gateway
645
+ * (the Firebase rewrite, the health probe) and the MCP-Auth-Proxy (remote-MCP
646
+ * clients, and until #1331 the staging mapping it forwards).
621
647
  */
622
- async function ensureGatewayIngress(
648
+ async function ensurePublicIngress(
623
649
  install: InstallTarget,
650
+ slug: "gateway" | "mcp-auth-proxy",
624
651
  run: GcloudRunner,
625
652
  ): Promise<InvariantOutcome> {
626
- const service = install.services.gateway;
653
+ const service = install.services[slug];
627
654
  const base = {
628
655
  id: `ingress all on ${service}`,
629
656
  onFailure: "abort" as const,
630
- why: "the gcp-firebase stack's gateway ingress (#650); the health check, the staging domain and remote MCP reach its run.app URL",
657
+ why:
658
+ slug === "gateway"
659
+ ? "the gcp-firebase stack's gateway ingress (#650); Firebase Hosting and the health check reach its run.app URL"
660
+ : "the gcp-firebase stack's MCP-Auth-Proxy ingress (#1328); remote-MCP clients, the health check and the staging domain reach its run.app URL",
631
661
  };
632
662
  const where = [`--region=${install.region}`, `--project=${install.projectId}`];
633
663
  const describe = await run(["run", "services", "describe", service, ...where, "--format=json"]);
@@ -697,7 +727,10 @@ export async function ensureStackInvariants(
697
727
  outcomes.push(await ensureCdnPolicy(install, run));
698
728
  outcomes.push(await ensureSvgDownloads(install, run));
699
729
  }
700
- if (install.provider === "gcp-firebase") outcomes.push(await ensureGatewayIngress(install, run));
730
+ if (install.provider === "gcp-firebase") {
731
+ outcomes.push(await ensurePublicIngress(install, "gateway", run));
732
+ outcomes.push(await ensurePublicIngress(install, "mcp-auth-proxy", run));
733
+ }
701
734
  outcomes.push(...(await removeDefaultFirewallRules(install.projectId, { run })));
702
735
 
703
736
  return {
@@ -1183,6 +1216,35 @@ export function planCpuIdle(live: boolean, desired: boolean): RollAdditionsPlan
1183
1216
  };
1184
1217
  }
1185
1218
 
1219
+ /**
1220
+ * Whether the service's revision template egresses into a VPC (Direct VPC
1221
+ * egress, the stacks' `vpcAccess.networkInterfaces`).
1222
+ */
1223
+ export function liveDirectVpc(serviceJson: string): boolean {
1224
+ const svc = JSON.parse(serviceJson) as {
1225
+ spec?: { template?: { metadata?: { annotations?: Record<string, string> } } };
1226
+ };
1227
+ return Boolean(
1228
+ svc.spec?.template?.metadata?.annotations?.["run.googleapis.com/network-interfaces"],
1229
+ );
1230
+ }
1231
+
1232
+ /**
1233
+ * #1328 — `--clear-network` when a service the contract keeps off the VPC
1234
+ * still egresses into it. The MCP-Auth-Proxy on gcp-firebase is the service
1235
+ * that was the gateway, which reached Cloud SQL's private IP that way; the
1236
+ * proxy talks to the admin's public URL only, so it loses the route to the
1237
+ * database's network together with its database credentials.
1238
+ */
1239
+ export function planNoVpc(live: boolean): RollAdditionsPlan {
1240
+ if (!live) return { ok: true, flags: [], changes: [] };
1241
+ return {
1242
+ ok: true,
1243
+ flags: ["--clear-network"],
1244
+ changes: ["VPC egress (private ranges) → none"],
1245
+ };
1246
+ }
1247
+
1186
1248
  /**
1187
1249
  * The single `gcloud run services update` that rolls a service to a new
1188
1250
  * image, its run SA and its env changes, so all land in one new revision.
@@ -0,0 +1,114 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import type { GcloudResult } from "./gcloud.js";
5
+ import type { GcloudRunner } from "./gcloud-retry.js";
6
+ import type { HttpFetch } from "./runtime-secrets.js";
7
+ import { ensureStagingDatabase } from "./staging-database.js";
8
+
9
+ const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
10
+ const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
11
+ const noSleep = async () => {};
12
+ const install = { projectId: "acme", env: "production", credentials: "operator" } as const;
13
+
14
+ /** gcloud with an instance `pg-1`, the databases and users given, and a staging password secret. */
15
+ function fakeGcloud(state: { databases: string[]; users: string[] }) {
16
+ const calls: string[] = [];
17
+ const run: GcloudRunner = async (args) => {
18
+ const line = args.join(" ");
19
+ calls.push(line);
20
+ if (line.startsWith("sql instances list")) return ok("pg-1\n");
21
+ if (line.startsWith("sql databases list")) return ok(`${state.databases.join("\n")}\n`);
22
+ if (line.startsWith("sql users list")) return ok(`${state.users.join("\n")}\n`);
23
+ if (line.startsWith("sql databases create")) return ok();
24
+ if (line.startsWith("secrets versions access latest")) return ok("s3cret-staging");
25
+ if (line.startsWith("auth print-access-token")) return ok("tok\n");
26
+ return fail(`unexpected: ${line}`);
27
+ };
28
+ return { run, calls };
29
+ }
30
+
31
+ /** The Cloud SQL Admin API, finishing every operation at once. */
32
+ function fakeHttp() {
33
+ const requests: { method: string; url: string; body?: string }[] = [];
34
+ const http: HttpFetch = async (url, init) => {
35
+ requests.push({ method: init.method, url, ...(init.body ? { body: init.body } : {}) });
36
+ return { ok: true, status: 200, text: async () => JSON.stringify({ status: "DONE" }) };
37
+ };
38
+ return { http, requests };
39
+ }
40
+
41
+ describe("ensureStagingDatabase (#1329)", () => {
42
+ it("creates the database and the login on an install from before them", async () => {
43
+ const { run, calls } = fakeGcloud({
44
+ databases: ["cms_admin", "cms_public"],
45
+ users: ["admin_role", "public_role"],
46
+ });
47
+ const { http, requests } = fakeHttp();
48
+ const outcomes = await ensureStagingDatabase(install, { run, http, sleep: noSleep });
49
+ expect(outcomes.map((o) => [o.id, o.status])).toEqual([
50
+ ["database cms_public_staging", "applied"],
51
+ ["database role public_staging_role", "applied"],
52
+ ]);
53
+ expect(calls).toContain(
54
+ "sql databases create cms_public_staging --instance=pg-1 --project=acme --quiet",
55
+ );
56
+ // The password goes in the request body, read from its own secret.
57
+ expect(calls).toContain(
58
+ "secrets versions access latest --secret=caelo-production-public-staging-role-password --project=acme",
59
+ );
60
+ expect(requests).toEqual([
61
+ {
62
+ method: "POST",
63
+ url: "https://sqladmin.googleapis.com/v1/projects/acme/instances/pg-1/users",
64
+ body: JSON.stringify({ name: "public_staging_role", password: "s3cret-staging" }),
65
+ },
66
+ ]);
67
+ expect(calls.some((c) => c.includes("s3cret-staging"))).toBe(false);
68
+ });
69
+
70
+ it("leaves an install that has both alone, re-applying the password", async () => {
71
+ const { run, calls } = fakeGcloud({
72
+ databases: ["cms_admin", "cms_public", "cms_public_staging"],
73
+ users: ["admin_role", "public_role", "public_staging_role"],
74
+ });
75
+ const { http, requests } = fakeHttp();
76
+ const outcomes = await ensureStagingDatabase(install, { run, http, sleep: noSleep });
77
+ expect(outcomes.map((o) => o.status)).toEqual(["present", "present"]);
78
+ expect(calls.some((c) => c.startsWith("sql databases create"))).toBe(false);
79
+ expect(requests.map((r) => [r.method, r.url])).toEqual([
80
+ [
81
+ "PUT",
82
+ "https://sqladmin.googleapis.com/v1/projects/acme/instances/pg-1/users?name=public_staging_role",
83
+ ],
84
+ ]);
85
+ });
86
+
87
+ it("reports a refused create with the permission it needs", async () => {
88
+ const calls: string[] = [];
89
+ const run: GcloudRunner = async (args) => {
90
+ const line = args.join(" ");
91
+ calls.push(line);
92
+ if (line.startsWith("sql instances list")) return ok("pg-1\n");
93
+ if (line.startsWith("sql databases list")) return ok("cms_admin\n");
94
+ if (line.startsWith("sql databases create")) return fail("PERMISSION_DENIED: nope");
95
+ return ok("public_staging_role\n");
96
+ };
97
+ const { http } = fakeHttp();
98
+ const [db] = await ensureStagingDatabase(install, { run, http, sleep: noSleep });
99
+ expect(db?.status).toBe("failed");
100
+ expect(db?.error).toContain("cloudsql.databases.create");
101
+ });
102
+
103
+ it("fails without an instance and creates nothing", async () => {
104
+ const calls: string[] = [];
105
+ const run: GcloudRunner = async (args) => {
106
+ calls.push(args.join(" "));
107
+ return ok("");
108
+ };
109
+ const outcomes = await ensureStagingDatabase(install, { run, sleep: noSleep });
110
+ expect(outcomes).toHaveLength(1);
111
+ expect(outcomes[0]?.status).toBe("failed");
112
+ expect(calls).toHaveLength(1);
113
+ });
114
+ });
@@ -0,0 +1,148 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * The staging database on an existing GCP install (issue #1329, gateway
5
+ * hardening P3b-1, epic #763 E10).
6
+ *
7
+ * A new install gets `cms_public_staging` and its gateway login
8
+ * `public_staging_role` from the stack (`gcp.sql.Database` / `gcp.sql.User`).
9
+ * An install from before has neither, and an update must bring them
10
+ * without a terminal (CLAUDE.md §11.C): the converge — `cms-provision
11
+ * upgrade` or the in-cloud converge job behind Settings → Updates — runs
12
+ * {@link ensureStagingDatabase} before the migrations, which need the
13
+ * database (migrate.ts `public-staging`, cms_admin migration 0530).
14
+ *
15
+ * Both go through the Cloud SQL Admin API, the way the stack creates them,
16
+ * so the next `pulumi up` adopts the same objects. The database is created
17
+ * with gcloud (no secret involved); the user through the Admin API with the
18
+ * password in the request body (runtime-secrets.ts `ensureDatabaseRolePassword`),
19
+ * read from its CLI-generated secret `public-staging-role-password`, never
20
+ * on argv. The Admin API makes the new user a member of cloudsqlsuperuser;
21
+ * the revoke that follows (public-role-isolation.ts) and the role audit
22
+ * take care of it, as for public_role. Idempotent: an install that has both
23
+ * only gets the password re-applied.
24
+ */
25
+
26
+ import { findSqlInstance } from "./gateway-credentials.js";
27
+ import { gcloud as defaultGcloud } from "./gcloud.js";
28
+ import { type GcloudRunner, realSleep, runWithRetry, type Sleep } from "./gcloud-retry.js";
29
+ import {
30
+ type EnsureOutcome,
31
+ ensureDatabaseRolePassword,
32
+ type HttpFetch,
33
+ } from "./runtime-secrets.js";
34
+ import { type GcloudCredentials, lookupFailure } from "./upgrade/gcp-lookup.js";
35
+
36
+ /** The staging site's database (#1329). */
37
+ export const STAGING_DATABASE = "cms_public_staging";
38
+
39
+ /** The staging gateway's login (#1329). */
40
+ export const STAGING_DATABASE_ROLE = "public_staging_role";
41
+
42
+ /** The names gcloud lists, one per line. */
43
+ function names(stdout: string): string[] {
44
+ return stdout
45
+ .split("\n")
46
+ .map((l) => l.trim())
47
+ .filter(Boolean);
48
+ }
49
+
50
+ /**
51
+ * Create `cms_public_staging` and `public_staging_role` where the install
52
+ * lacks them, and set the role's password to its secret. Reports instead of
53
+ * throwing; a `failed` outcome names the next step.
54
+ */
55
+ export async function ensureStagingDatabase(
56
+ install: {
57
+ readonly projectId: string;
58
+ readonly env: string;
59
+ readonly credentials: GcloudCredentials;
60
+ },
61
+ deps: { readonly run?: GcloudRunner; readonly sleep?: Sleep; readonly http?: HttpFetch } = {},
62
+ ): Promise<EnsureOutcome[]> {
63
+ const run = deps.run ?? defaultGcloud;
64
+ const sleep = deps.sleep ?? realSleep;
65
+ const lookup = await findSqlInstance(install, { run });
66
+ if (lookup.status !== "found") {
67
+ return [
68
+ {
69
+ id: `database ${STAGING_DATABASE}`,
70
+ status: "failed",
71
+ error: lookupFailure(lookup, "Cloud SQL instance", install.credentials),
72
+ },
73
+ ];
74
+ }
75
+ const instance = lookup.name;
76
+ const where = [`--instance=${instance}`, `--project=${install.projectId}`];
77
+ return [
78
+ await ensureDatabase(run, sleep, instance, where),
79
+ await ensureRole(run, deps, install, instance, where),
80
+ ];
81
+ }
82
+
83
+ async function ensureDatabase(
84
+ run: GcloudRunner,
85
+ sleep: Sleep,
86
+ instance: string,
87
+ where: readonly string[],
88
+ ): Promise<EnsureOutcome> {
89
+ const id = `database ${STAGING_DATABASE}`;
90
+ const listed = await run(["sql", "databases", "list", ...where, "--format=value(name)"]);
91
+ if (!listed.ok) {
92
+ return {
93
+ id,
94
+ status: "failed",
95
+ error: `gcloud sql databases list on ${instance}: ${listed.stderr.trim()}`,
96
+ };
97
+ }
98
+ if (names(listed.stdout).includes(STAGING_DATABASE)) return { id, status: "present" };
99
+ const created = await runWithRetry(run, sleep, [
100
+ "sql",
101
+ "databases",
102
+ "create",
103
+ STAGING_DATABASE,
104
+ ...where,
105
+ "--quiet",
106
+ ]);
107
+ if (!created.ok) {
108
+ return {
109
+ id,
110
+ status: "failed",
111
+ error: `gcloud sql databases create ${STAGING_DATABASE} on ${instance}: ${created.stderr.trim()}. The account running this needs cloudsql.databases.create on the project; re-run once it has.`,
112
+ };
113
+ }
114
+ return { id, status: "applied", detail: `created on ${instance}` };
115
+ }
116
+
117
+ async function ensureRole(
118
+ run: GcloudRunner,
119
+ deps: { readonly sleep?: Sleep; readonly http?: HttpFetch },
120
+ install: { readonly projectId: string; readonly env: string },
121
+ instance: string,
122
+ where: readonly string[],
123
+ ): Promise<EnsureOutcome> {
124
+ const id = `database role ${STAGING_DATABASE_ROLE}`;
125
+ const listed = await run(["sql", "users", "list", ...where, "--format=value(name)"]);
126
+ if (!listed.ok) {
127
+ return {
128
+ id,
129
+ status: "failed",
130
+ error: `gcloud sql users list on ${instance}: ${listed.stderr.trim()}`,
131
+ };
132
+ }
133
+ const exists = names(listed.stdout).includes(STAGING_DATABASE_ROLE);
134
+ const outcome = await ensureDatabaseRolePassword(
135
+ { projectId: install.projectId, env: install.env, sqlInstance: instance },
136
+ STAGING_DATABASE_ROLE,
137
+ {
138
+ run,
139
+ ...(deps.sleep ? { sleep: deps.sleep } : {}),
140
+ ...(deps.http ? { http: deps.http } : {}),
141
+ create: !exists,
142
+ },
143
+ );
144
+ if (outcome.status === "failed") return { ...outcome, id };
145
+ return exists
146
+ ? { id, status: "present", detail: "password re-applied" }
147
+ : { id, status: "applied", detail: `created on ${instance}` };
148
+ }
@@ -36,7 +36,8 @@ function fakeBucket() {
36
36
  return { objects, calls, run };
37
37
  }
38
38
 
39
- const PASSWORD_URL = "postgres://admin_role:hunter2hunter2@10.0.0.3:5432/cms_admin";
39
+ const PASSWORD_URL =
40
+ "postgres://admin_role:hunter2hunter2@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin";
40
41
 
41
42
  let root = "";
42
43
  beforeEach(() => {