@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
@@ -16,6 +16,7 @@ import { describe, expect, it } from "bun:test";
16
16
  import { databaseUrlFromEnv } from "@caelo-cms/shared";
17
17
  import type { GcloudResult } from "./gcloud.js";
18
18
  import {
19
+ CLOUD_RUN_SLUGS,
19
20
  type CloudRunSlug,
20
21
  OPERATOR_OAUTH_CLIENT_FILE,
21
22
  RETIRED_SECRET_FILES,
@@ -32,7 +33,7 @@ import {
32
33
  } from "./stack-converge.js";
33
34
 
34
35
  const PW = "4f2a9c0d1e2b3a4f5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b";
35
- const HOST = "10.20.0.3";
36
+ const HOST = "db1.p-label.europe-west1.sql-psa.goog";
36
37
 
37
38
  /** Secret Manager as the fake revisions resolve it. */
38
39
  const SECRET_VALUES: Record<string, string> = {
@@ -141,7 +142,7 @@ function expectGatewayOnItsOwnLogins(env: Record<string, string>): void {
141
142
  expect(Object.values(env).join("\n")).not.toContain("g".repeat(64));
142
143
  expect(Object.values(env).join("\n")).not.toContain("cms_admin");
143
144
  expect(credentials(databaseUrlFromEnv(["PUBLIC_DATABASE_URL"], env))).toBe(
144
- `public_role:${"p".repeat(64)}@${HOST}:5432/cms_public?sslmode=require`,
145
+ `public_role:${"p".repeat(64)}@${HOST}:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`,
145
146
  );
146
147
  }
147
148
 
@@ -157,7 +158,7 @@ const serviceJson = (env: unknown[]) =>
157
158
  /** What `gcloud run services describe` showed on a live v0.10.29 gcp install. */
158
159
  function v01029Install(scheme: "postgresql" | "postgres") {
159
160
  const url = (role: string, db: string) =>
160
- `${scheme}://${role}:${PW}@${HOST}:5432/${db}?sslmode=require`;
161
+ `${scheme}://${role}:${PW}@${HOST}:5432/${db}?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`;
161
162
  const common = {
162
163
  CAELO_PROVIDER: "gcp",
163
164
  CAELO_ENV: "production",
@@ -200,20 +201,30 @@ const INSTALL = {
200
201
  stagingHost: "staging.acme.com",
201
202
  region: "europe-west1",
202
203
  };
204
+ /** The fixtures' services; the proxy is added by {@link upgrade} when absent. */
205
+ type InstallServices = Record<"admin" | "gateway", FakeService> & {
206
+ "mcp-auth-proxy"?: FakeService;
207
+ };
208
+
203
209
  const SERVICE_ACCOUNTS: Record<CloudRunSlug, string> = {
204
210
  admin: "caelo-production-run-sa@acme.iam.gserviceaccount.com",
205
211
  gateway: "caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
212
+ "mcp-auth-proxy": "caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com",
206
213
  };
207
214
 
208
215
  /** Plan + apply one upgrade roll on the fake services, as upgradeCommand does. */
209
- function upgrade(services: Record<CloudRunSlug, FakeService>) {
216
+ function upgrade(services: InstallServices) {
217
+ // #1328 — the gcp MCP-Auth-Proxy: created by the converge before the plan,
218
+ // with no env yet in these fixtures (they are about database logins).
219
+ services["mcp-auth-proxy"] ??= new FakeService(new Map(), SERVICE_ACCOUNTS["mcp-auth-proxy"]);
210
220
  const plan = planContractEnv(INSTALL, {
211
221
  admin: { serviceName: "adm", liveEnv: services.admin.env },
212
222
  gateway: { serviceName: "gw", liveEnv: services.gateway.env },
223
+ "mcp-auth-proxy": { serviceName: "proxy", liveEnv: services["mcp-auth-proxy"].env },
213
224
  });
214
225
  if (!plan.ok) throw new Error(plan.error);
215
- for (const slug of ["admin", "gateway"] as const) {
216
- services[slug].update(
226
+ for (const slug of CLOUD_RUN_SLUGS) {
227
+ (services[slug] as FakeService).update(
217
228
  serviceRollArgs({
218
229
  serviceName: slug,
219
230
  region: INSTALL.region,
@@ -297,7 +308,7 @@ describe.each(["postgresql", "postgres"] as const)(
297
308
  */
298
309
  function v01035Install() {
299
310
  const url = (role: string, db: string) =>
300
- `postgresql://${role}@${HOST}:5432/${db}?sslmode=require`;
311
+ `postgresql://${role}@${HOST}:5432/${db}?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`;
301
312
  const ref = (name: string, secret: string) => ({
302
313
  name,
303
314
  valueFrom: { secretKeyRef: { name: `caelo-production-${secret}`, key: "latest" } },
@@ -349,10 +360,9 @@ describe("upgrading a v0.10.35 install: the gateway leaves admin_role (#613)", (
349
360
  expect(plan.ok && plan.services.gateway.changes.map((c) => c.name).sort()).toEqual([
350
361
  "ADMIN_DATABASE_PASSWORD",
351
362
  "ADMIN_DATABASE_URL",
352
- // #806 — the remote-MCP broker's seal key and (#914) the Google
353
- // sign-in client file path.
354
- "CAELO_MCP_OAUTH_SEAL_KEY",
355
- "CAELO_OPERATOR_OAUTH_CLIENT_FILE",
363
+ // #1329 — the gateway says it is the live site's.
364
+ "CAELO_GATEWAY_ENVIRONMENT",
365
+ // No remote-MCP env: the broker is the MCP-Auth-Proxy's (#1328).
356
366
  // #650 — the staging gate key arrives in the same roll.
357
367
  "CAELO_STAGING_GATE_SECRET",
358
368
  // #845 — and which forwarded address is the visitor.
@@ -396,7 +406,7 @@ function v01050Install() {
396
406
  const env = new Map(services.gateway.env);
397
407
  env.set("GATEWAY_DATABASE_URL", {
398
408
  kind: "value",
399
- value: `postgresql://gateway_role@${HOST}:5432/cms_admin?sslmode=require`,
409
+ value: `postgresql://gateway_role@${HOST}:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`,
400
410
  });
401
411
  env.set("GATEWAY_DATABASE_PASSWORD", {
402
412
  kind: "secret",
@@ -488,7 +498,12 @@ describe("retiring a secret file removes its volume too (#954)", () => {
488
498
  },
489
499
  };
490
500
  const plan = (live: Parameters<typeof planSecretFiles>[0]) =>
491
- planSecretFiles(live, [OPERATOR_OAUTH_CLIENT_FILE], "production", RETIRED_SECRET_FILES.gateway);
501
+ planSecretFiles(
502
+ live,
503
+ [OPERATOR_OAUTH_CLIENT_FILE],
504
+ "production",
505
+ RETIRED_SECRET_FILES["mcp-auth-proxy"],
506
+ );
492
507
 
493
508
  it("a retired mount: removes the file, the mount and the volume behind it in the roll", () => {
494
509
  const r = plan({
@@ -13,9 +13,11 @@ import {
13
13
  type LiveEnvValue,
14
14
  liveContainerEnv,
15
15
  liveDatabaseHost,
16
+ liveDirectVpc,
16
17
  liveServiceAccount,
17
18
  planContractEnv,
18
19
  planEnvUpdate,
20
+ planNoVpc,
19
21
  policyGrants,
20
22
  removeRetiredIamBindings,
21
23
  rollService,
@@ -56,6 +58,7 @@ const policy = (bindings: { role: string; members: string[]; condition?: unknown
56
58
 
57
59
  const RUN_SA = "serviceAccount:caelo-production-run-sa@acme.iam.gserviceaccount.com";
58
60
  const GW_SA = "serviceAccount:caelo-production-gateway-sa@acme.iam.gserviceaccount.com";
61
+ const PROXY_SA = "serviceAccount:caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com";
59
62
  const GW_TELEMETRY = [
60
63
  { role: "roles/logging.logWriter", members: [GW_SA] },
61
64
  { role: "roles/monitoring.metricWriter", members: [GW_SA] },
@@ -67,7 +70,11 @@ const firebase: InstallTarget = {
67
70
  projectId: "acme",
68
71
  region: "europe-west1",
69
72
  env: "production",
70
- services: { admin: "caelo-production-admin-aaa", gateway: "caelo-production-gateway-bbb" },
73
+ services: {
74
+ admin: "caelo-production-admin-aaa",
75
+ gateway: "caelo-production-data-gateway",
76
+ "mcp-auth-proxy": "caelo-production-gateway-bbb",
77
+ },
71
78
  };
72
79
  const gcp: InstallTarget = { ...firebase, provider: "gcp" };
73
80
 
@@ -126,13 +133,13 @@ describe("liveContainerEnv / liveServiceAccount / liveDatabaseHost", () => {
126
133
 
127
134
  it("finds the Cloud SQL host in ADMIN_DATABASE_URL with or without an inline password", () => {
128
135
  for (const url of [
129
- "postgresql://admin_role:pw@10.20.0.3:5432/cms_admin?sslmode=require",
130
- "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
136
+ "postgresql://admin_role:pw@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
137
+ "postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
131
138
  ]) {
132
139
  const host = liveDatabaseHost(
133
140
  new Map([["ADMIN_DATABASE_URL", { kind: "value", value: url }]]),
134
141
  );
135
- expect(host).toEqual({ ok: true, host: "10.20.0.3" });
142
+ expect(host).toEqual({ ok: true, host: "db1.p-label.europe-west1.sql-psa.goog" });
136
143
  }
137
144
  expect(liveDatabaseHost(new Map()).ok).toBe(false);
138
145
  });
@@ -261,11 +268,13 @@ const secretRef = (name: string, secret: string) => ({
261
268
  const CONVERGED_ADMIN_DB = [
262
269
  {
263
270
  name: "ADMIN_DATABASE_URL",
264
- value: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
271
+ value:
272
+ "postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
265
273
  },
266
274
  {
267
275
  name: "PUBLIC_ADMIN_DATABASE_URL",
268
- value: "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
276
+ value:
277
+ "postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
269
278
  },
270
279
  secretRef("ADMIN_DATABASE_PASSWORD", "postgres-password"),
271
280
  secretRef("PUBLIC_ADMIN_DATABASE_PASSWORD", "postgres-password"),
@@ -276,7 +285,8 @@ const CONVERGED_ADMIN_DB = [
276
285
  const CONVERGED_GATEWAY_DB = [
277
286
  {
278
287
  name: "PUBLIC_DATABASE_URL",
279
- value: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
288
+ value:
289
+ "postgresql://public_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
280
290
  },
281
291
  secretRef("PUBLIC_DATABASE_PASSWORD", "public-role-password"),
282
292
  ];
@@ -315,21 +325,35 @@ describe("planContractEnv", () => {
315
325
  },
316
326
  {
317
327
  admin: { serviceName: "caelo-production-admin-aaa", liveEnv: adminLive },
318
- gateway: { serviceName: "caelo-production-gateway-bbb", liveEnv: gatewayLive },
328
+ // #1328 — the data gateway, a copy of the old gateway service.
329
+ gateway: { serviceName: "caelo-production-data-gateway", liveEnv: gatewayLive },
330
+ // The old gateway service, which becomes the MCP-Auth-Proxy (E13 a).
331
+ "mcp-auth-proxy": { serviceName: "caelo-production-gateway-bbb", liveEnv: gatewayLive },
319
332
  },
320
333
  );
321
334
  if (!plan.ok) throw new Error(plan.error);
322
335
  // #650 — the same roll converges the protected staging domain's host
323
336
  // and gate key onto an install that predates it.
324
337
  expect(plan.services.admin.flags).toEqual([
325
- "--update-env-vars=CAELO_SITE_URL=https://acme.com,MEDIA_ROOT_DIR=/app/apps/admin/data/media,CAELO_MCP_IAP_SERVICE_ACCOUNT=caelo-mcp@acme.iam.gserviceaccount.com,CAELO_OPERATOR_ACCESS_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-operator-access-sync,CAELO_UPGRADER_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-upgrader,CAELO_CONVERGE_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-converge,CAELO_AUDIT_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-audit,CAELO_AUDIT_RESULTS_BUCKET=acme-caelo-production-audit-results,CAELO_CAPTURE_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-capture,CAELO_CAPTURE_BUCKET=acme-caelo-production-capture,CAELO_MEDIA_BUCKET=acme-caelo-production-media,CAELO_OPERATOR_OAUTH_CLIENT_STORE=projects/acme/secrets/caelo-production-operator-oauth-client,CAELO_STAGING_HOST=staging.acme.com",
326
- "--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
338
+ // #1329 — and the staging database's pool onto one that predates it.
339
+ "--update-env-vars=PUBLIC_STAGING_ADMIN_DATABASE_URL=postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem,CAELO_SITE_URL=https://acme.com,MEDIA_ROOT_DIR=/app/apps/admin/data/media,CAELO_MCP_IAP_SERVICE_ACCOUNT=caelo-mcp@acme.iam.gserviceaccount.com,CAELO_OPERATOR_ACCESS_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-operator-access-sync,CAELO_UPGRADER_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-upgrader,CAELO_CONVERGE_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-converge,CAELO_AUDIT_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-audit,CAELO_AUDIT_RESULTS_BUCKET=acme-caelo-production-audit-results,CAELO_CAPTURE_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-capture,CAELO_CAPTURE_BUCKET=acme-caelo-production-capture,CAELO_MEDIA_BUCKET=acme-caelo-production-media,CAELO_SCHEDULER_SERVICE_ACCOUNT=caelo-production-scheduler@acme.iam.gserviceaccount.com,CAELO_OPERATOR_OAUTH_CLIENT_STORE=projects/acme/secrets/caelo-production-operator-oauth-client,CAELO_GATEWAY_SERVICE=caelo-production-data-gateway,CAELO_STAGING_HOST=staging.acme.com",
340
+ "--update-secrets=PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD=caelo-production-postgres-password:latest,CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
327
341
  ]);
342
+ // #1328 — the Firebase rewrite follows the data gateway (above), which
343
+ // gets no remote-MCP env.
328
344
  expect(plan.services.gateway.flags).toEqual([
329
- // The gateway learns which forwarded address is the visitor (#845).
330
- // #806 — and where the remote-MCP broker's client file and seal key are.
331
- "--update-env-vars=CAELO_TRUSTED_PROXY_HOPS=1,CAELO_OPERATOR_OAUTH_CLIENT_FILE=/secrets/operator-oauth-client/client.json",
332
- "--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest,CAELO_MCP_OAUTH_SEAL_KEY=caelo-production-mcp-oauth-seal-key:latest",
345
+ // The gateway learns which forwarded address is the visitor (#845),
346
+ // and (#1329) that it is the live site's.
347
+ "--update-env-vars=CAELO_GATEWAY_ENVIRONMENT=live,CAELO_TRUSTED_PROXY_HOPS=1",
348
+ "--update-secrets=CAELO_STAGING_GATE_SECRET=caelo-production-staging-gate-secret:latest",
349
+ ]);
350
+ // The old gateway service becomes the proxy in one revision: every
351
+ // database and site var goes, its own contract arrives.
352
+ expect(plan.services["mcp-auth-proxy"].flags).toEqual([
353
+ "--remove-env-vars=PUBLIC_DATABASE_URL,CAELO_ENV,MEDIA_STORAGE_URL",
354
+ "--update-env-vars=CAELO_REMOTE_MCP=available,CAELO_TRUSTED_PROXY_HOPS=1,CAELO_OPERATOR_OAUTH_CLIENT_FILE=/secrets/operator-oauth-client/client.json",
355
+ "--remove-secrets=PUBLIC_DATABASE_PASSWORD",
356
+ "--update-secrets=CAELO_MCP_OAUTH_SEAL_KEY=caelo-production-mcp-oauth-seal-key:latest",
333
357
  ]);
334
358
  });
335
359
 
@@ -352,9 +376,10 @@ describe("planContractEnv", () => {
352
376
  {
353
377
  admin: { serviceName: "caelo-production-admin-aaa", liveEnv: live },
354
378
  gateway: {
355
- serviceName: "caelo-production-gateway-bbb",
379
+ serviceName: "caelo-production-data-gateway",
356
380
  liveEnv: liveContainerEnv(serviceJson(CONVERGED_GATEWAY_DB)),
357
381
  },
382
+ "mcp-auth-proxy": { serviceName: "caelo-production-gateway-bbb", liveEnv: new Map() },
358
383
  },
359
384
  { leaveUntouched: ["CAELO_MCP_IAP_SERVICE_ACCOUNT", "CAELO_OPERATOR_ACCESS_JOB"] },
360
385
  );
@@ -375,18 +400,72 @@ describe("planContractEnv", () => {
375
400
  {
376
401
  admin: { serviceName: "adm", liveEnv: liveContainerEnv(serviceJson(CONVERGED_ADMIN_DB)) },
377
402
  gateway: { serviceName: "gw", liveEnv: new Map() },
403
+ "mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
378
404
  },
379
405
  );
380
406
  expect(plan.ok).toBe(false);
381
407
  expect(!plan.ok && plan.error).toContain("CAELO_FIREBASE_SITE");
382
408
  });
383
409
 
410
+ it("#1338: an install on the instance's IP plans its database URLs only with the converge's host", () => {
411
+ const onIp = (db: { name: string; value?: string }[]) =>
412
+ db.map((e) =>
413
+ e.value
414
+ ? {
415
+ ...e,
416
+ value: e.value
417
+ .replace("db1.p-label.europe-west1.sql-psa.goog", "10.20.0.3")
418
+ .replace(/\?.*$/, "?sslmode=require"),
419
+ }
420
+ : e,
421
+ );
422
+ const services = {
423
+ admin: {
424
+ serviceName: "adm",
425
+ liveEnv: liveContainerEnv(serviceJson(onIp(CONVERGED_ADMIN_DB) as never)),
426
+ },
427
+ gateway: {
428
+ serviceName: "gw",
429
+ liveEnv: liveContainerEnv(serviceJson(onIp(CONVERGED_GATEWAY_DB) as never)),
430
+ },
431
+ // #1328 — no database URL, so the TLS step has nothing to move on it.
432
+ "mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
433
+ };
434
+ const install = {
435
+ provider: "gcp" as const,
436
+ projectId: "a",
437
+ env: "production",
438
+ domain: "a.com",
439
+ stagingHost: "staging.a.com",
440
+ region: "r",
441
+ };
442
+ // Read-only plan: the URLs stay as they are and the plan says why.
443
+ const pending = planContractEnv(install, services);
444
+ if (!pending.ok) throw new Error(pending.error);
445
+ expect(pending.databaseHostPending).toBe(true);
446
+ for (const s of ["admin", "gateway"] as const) {
447
+ expect(pending.services[s].changes.map((c) => c.name)).not.toContainEqual(
448
+ expect.stringMatching(/DATABASE_URL$/),
449
+ );
450
+ }
451
+ // After the converge's TLS step: verify-full URLs on the DNS name.
452
+ const host = "db1.p-label.europe-west1.sql-psa.goog";
453
+ const moved = planContractEnv(install, services, { databaseHost: host });
454
+ if (!moved.ok) throw new Error(moved.error);
455
+ expect(moved.databaseHostPending).toBe(false);
456
+ const url = moved.services.gateway.changes.find((c) => c.name === "PUBLIC_DATABASE_URL");
457
+ expect(url?.to).toBe(
458
+ `postgresql://public_role@${host}:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem`,
459
+ );
460
+ });
461
+
384
462
  it("fails loudly when the Cloud SQL host can't be discovered", () => {
385
463
  const plan = planContractEnv(
386
464
  { provider: "gcp", projectId: "a", env: "production", domain: "a.com", region: "r" },
387
465
  {
388
466
  admin: { serviceName: "adm", liveEnv: new Map() },
389
467
  gateway: { serviceName: "gw", liveEnv: new Map() },
468
+ "mcp-auth-proxy": { serviceName: "px", liveEnv: new Map() },
390
469
  },
391
470
  );
392
471
  expect(plan.ok).toBe(false);
@@ -486,7 +565,7 @@ describe("ensureStackInvariants", () => {
486
565
  ],
487
566
  "secrets get-iam-policy": [
488
567
  policy([
489
- { role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA] },
568
+ { role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA, PROXY_SA] },
490
569
  // #806 — the admin adds remote-MCP OAuth client versions.
491
570
  { role: "roles/secretmanager.secretVersionAdder", members: [RUN_SA] },
492
571
  ]),
@@ -494,17 +573,23 @@ describe("ensureStackInvariants", () => {
494
573
  "storage buckets get-iam-policy": [
495
574
  policy([{ role: "roles/storage.objectAdmin", members: [RUN_SA] }]),
496
575
  ],
497
- "run services get-iam-policy caelo-production-gateway-bbb": [
576
+ // #1328 — the data gateway: the Firebase rewrite target.
577
+ "run services get-iam-policy caelo-production-data-gateway": [
498
578
  policy([
499
579
  { role: "roles/run.invoker", members: ["allUsers"] },
500
580
  { role: "roles/run.viewer", members: [RUN_SA] },
501
581
  ]),
502
582
  ],
583
+ // The MCP-Auth-Proxy: public for MCP clients.
584
+ "run services get-iam-policy caelo-production-gateway-bbb": [
585
+ policy([{ role: "roles/run.invoker", members: ["allUsers"] }]),
586
+ ],
503
587
  "run services get-iam-policy caelo-production-admin-aaa": [
504
588
  policy([{ role: "roles/run.invoker", members: [IAP_AGENT] }]),
505
589
  ],
506
590
  "compute firewall-rules list": [ok("[]")],
507
591
  "run services describe caelo-production-gateway-bbb": [ok(ingressJson("all"))],
592
+ "run services describe caelo-production-data-gateway": [ok(ingressJson("all"))],
508
593
  });
509
594
 
510
595
  it("is a read-only no-op on an install that is already in shape", async () => {
@@ -513,7 +598,8 @@ describe("ensureStackInvariants", () => {
513
598
  expect(report.mustAbort).toBe(false);
514
599
  expect(report.outcomes.every((o) => o.status === "present")).toBe(true);
515
600
  expect(report.outcomes).toHaveLength(
516
- stackIamInvariants("gcp-firebase").length + ABSENT_DEFAULT_FIREWALL_RULES.length + 1,
601
+ // The two public services' ingress (#732, #1328).
602
+ stackIamInvariants("gcp-firebase").length + ABSENT_DEFAULT_FIREWALL_RULES.length + 2,
517
603
  );
518
604
  expect(calls.some((c) => c.startsWith("compute firewall-rules delete"))).toBe(false);
519
605
  expect(calls.some((c) => c.includes("add-iam-policy-binding"))).toBe(false);
@@ -628,7 +714,7 @@ describe("ensureStackInvariants", () => {
628
714
  answers["projects get-iam-policy"] = [
629
715
  policy([{ role: "roles/firebasehosting.admin", members: [RUN_SA] }, ...GW_TELEMETRY]),
630
716
  ];
631
- answers["run services get-iam-policy caelo-production-gateway-bbb"] = [
717
+ answers["run services get-iam-policy caelo-production-data-gateway"] = [
632
718
  policy([{ role: "roles/run.invoker", members: ["allUsers"] }]),
633
719
  ];
634
720
  const { run, calls } = fakeGcloud(answers);
@@ -636,7 +722,7 @@ describe("ensureStackInvariants", () => {
636
722
 
637
723
  expect(report.mustAbort).toBe(false);
638
724
  expect(calls).toContain(
639
- `run services add-iam-policy-binding caelo-production-gateway-bbb --region=europe-west1 --project=acme --member=${RUN_SA} --role=roles/run.viewer --quiet --format=none`,
725
+ `run services add-iam-policy-binding caelo-production-data-gateway --region=europe-west1 --project=acme --member=${RUN_SA} --role=roles/run.viewer --quiet --format=none`,
640
726
  );
641
727
  for (const role of ["roles/logging.logWriter", "roles/monitoring.metricWriter"]) {
642
728
  expect(calls).toContain(
@@ -650,7 +736,7 @@ describe("ensureStackInvariants", () => {
650
736
 
651
737
  it("aborts when a binding the install needs can't be added", async () => {
652
738
  const answers = fullFirebase();
653
- answers["run services get-iam-policy caelo-production-gateway-bbb"] = [
739
+ answers["run services get-iam-policy caelo-production-data-gateway"] = [
654
740
  policy([{ role: "roles/run.invoker", members: ["allUsers"] }]),
655
741
  ];
656
742
  answers["run services add-iam-policy-binding"] = [
@@ -739,7 +825,7 @@ describe("ensureStackInvariants", () => {
739
825
  ],
740
826
  "secrets get-iam-policy": [
741
827
  policy([
742
- { role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA] },
828
+ { role: "roles/secretmanager.secretAccessor", members: [RUN_SA, GW_SA, PROXY_SA] },
743
829
  { role: "roles/secretmanager.secretVersionAdder", members: [RUN_SA] },
744
830
  ]),
745
831
  ],
@@ -871,10 +957,19 @@ describe("ensureStackInvariants", () => {
871
957
  expect(gatewayGrants).toEqual([
872
958
  `secrets add-iam-policy-binding caelo-production-public-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
873
959
  `secrets add-iam-policy-binding caelo-production-staging-gate-secret --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
874
- // #806 — the remote-MCP broker's seal key and OAuth client file.
875
- `secrets add-iam-policy-binding caelo-production-mcp-oauth-seal-key --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
876
- `secrets add-iam-policy-binding caelo-production-operator-oauth-client --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
877
960
  ]);
961
+ // #1328 — the remote-MCP broker's seal key and OAuth client file are
962
+ // the MCP-Auth-Proxy's, and those two secrets are all its SA reads.
963
+ expect(
964
+ calls.filter((c) => c.startsWith("secrets add-iam-policy-binding") && c.includes(PROXY_SA)),
965
+ ).toEqual([
966
+ `secrets add-iam-policy-binding caelo-production-mcp-oauth-seal-key --project=acme --member=${PROXY_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
967
+ `secrets add-iam-policy-binding caelo-production-operator-oauth-client --project=acme --member=${PROXY_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
968
+ ]);
969
+ // …and it holds no project role.
970
+ expect(
971
+ calls.some((c) => c.startsWith("projects add-iam-policy-binding") && c.includes(PROXY_SA)),
972
+ ).toBe(false);
878
973
  // #806 — the admin may add versions to the OAuth client secret, and to no other.
879
974
  const adders = calls.filter((c) => c.includes("roles/secretmanager.secretVersionAdder"));
880
975
  expect(adders).toEqual([
@@ -900,7 +995,13 @@ describe("removeRetiredIamBindings (#613)", () => {
900
995
  });
901
996
  const outcomes = await removeRetiredIamBindings(gcp, { run, sleep: async () => {} });
902
997
  // postgres-password removed; gateway-role-password (#635) already clean.
903
- expect(outcomes.map((o) => o.status)).toEqual(["removed", "absent"]);
998
+ expect(outcomes.map((o) => o.status)).toEqual([
999
+ "removed",
1000
+ "absent",
1001
+ "absent",
1002
+ "absent",
1003
+ "absent",
1004
+ ]);
904
1005
  expect(calls).toContain(REMOVE);
905
1006
  // The admin's own binding on the same secret is never touched.
906
1007
  expect(calls.some((c) => c.includes("remove") && c.includes(RUN_SA))).toBe(false);
@@ -913,7 +1014,13 @@ describe("removeRetiredIamBindings (#613)", () => {
913
1014
  ],
914
1015
  });
915
1016
  const outcomes = await removeRetiredIamBindings(firebase, { run, sleep: async () => {} });
916
- expect(outcomes.map((o) => o.status)).toEqual(["absent", "absent"]);
1017
+ expect(outcomes.map((o) => o.status)).toEqual([
1018
+ "absent",
1019
+ "absent",
1020
+ "absent",
1021
+ "absent",
1022
+ "absent",
1023
+ ]);
917
1024
  expect(calls.filter((c) => c.includes("remove-iam-policy-binding"))).toEqual([]);
918
1025
  });
919
1026
 
@@ -924,12 +1031,51 @@ describe("removeRetiredIamBindings (#613)", () => {
924
1031
  ],
925
1032
  });
926
1033
  const outcomes = await removeRetiredIamBindings(gcp, { run, sleep: async () => {} });
927
- expect(outcomes.map((o) => o.status)).toEqual(["absent", "removed"]);
1034
+ expect(outcomes.map((o) => o.status)).toEqual([
1035
+ "absent",
1036
+ "removed",
1037
+ "absent",
1038
+ "absent",
1039
+ "absent",
1040
+ ]);
928
1041
  expect(calls).toContain(
929
1042
  `secrets remove-iam-policy-binding caelo-production-gateway-role-password --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
930
1043
  );
931
1044
  });
932
1045
 
1046
+ it("#1328: takes the gateway SA off the broker's secrets and the admin's viewer off the proxy", async () => {
1047
+ const { run, calls } = fakeGcloud({
1048
+ "secrets get-iam-policy caelo-production-mcp-oauth-seal-key": [
1049
+ policy([{ role: "roles/secretmanager.secretAccessor", members: [GW_SA, PROXY_SA] }]),
1050
+ ],
1051
+ "secrets get-iam-policy caelo-production-operator-oauth-client": [
1052
+ policy([{ role: "roles/secretmanager.secretAccessor", members: [GW_SA, PROXY_SA] }]),
1053
+ ],
1054
+ "run services get-iam-policy caelo-production-gateway-bbb": [
1055
+ policy([
1056
+ { role: "roles/run.invoker", members: ["allUsers"] },
1057
+ { role: "roles/run.viewer", members: [RUN_SA] },
1058
+ ]),
1059
+ ],
1060
+ });
1061
+ const outcomes = await removeRetiredIamBindings(firebase, { run, sleep: async () => {} });
1062
+ expect(outcomes.map((o) => o.status)).toEqual([
1063
+ "absent",
1064
+ "absent",
1065
+ "removed",
1066
+ "removed",
1067
+ "removed",
1068
+ ]);
1069
+ for (const secret of ["mcp-oauth-seal-key", "operator-oauth-client"]) {
1070
+ expect(calls).toContain(
1071
+ `secrets remove-iam-policy-binding caelo-production-${secret} --project=acme --member=${GW_SA} --role=roles/secretmanager.secretAccessor --quiet --format=none`,
1072
+ );
1073
+ }
1074
+ // The proxy keeps its own access and its public invoker.
1075
+ expect(calls.some((c) => c.includes("remove") && c.includes(PROXY_SA))).toBe(false);
1076
+ expect(calls.some((c) => c.includes("remove") && c.includes("allUsers"))).toBe(false);
1077
+ });
1078
+
933
1079
  it("reports a failed removal instead of throwing", async () => {
934
1080
  const { run } = fakeGcloud({
935
1081
  "secrets get-iam-policy caelo-production-postgres-password": [
@@ -942,3 +1088,32 @@ describe("removeRetiredIamBindings (#613)", () => {
942
1088
  expect(o?.error).toContain("PERMISSION_DENIED");
943
1089
  });
944
1090
  });
1091
+
1092
+ describe("liveDirectVpc / planNoVpc (#1328)", () => {
1093
+ const withNetwork = JSON.stringify({
1094
+ spec: {
1095
+ template: {
1096
+ metadata: {
1097
+ annotations: {
1098
+ "run.googleapis.com/network-interfaces": '[{"network":"caelo-production-vpc"}]',
1099
+ "run.googleapis.com/vpc-access-egress": "private-ranges-only",
1100
+ },
1101
+ },
1102
+ },
1103
+ },
1104
+ });
1105
+
1106
+ it("reads Direct VPC egress from the revision template", () => {
1107
+ expect(liveDirectVpc(withNetwork)).toBe(true);
1108
+ expect(liveDirectVpc(JSON.stringify({ spec: { template: { metadata: {} } } }))).toBe(false);
1109
+ });
1110
+
1111
+ it("takes a service off the VPC in its roll, and leaves one that is off alone", () => {
1112
+ expect(planNoVpc(true)).toEqual({
1113
+ ok: true,
1114
+ flags: ["--clear-network"],
1115
+ changes: ["VPC egress (private ranges) → none"],
1116
+ });
1117
+ expect(planNoVpc(false)).toEqual({ ok: true, flags: [], changes: [] });
1118
+ });
1119
+ });