@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 { readFileSync } from "node:fs";
16
16
  import { resolve } from "node:path";
17
17
  import {
18
18
  CLOUD_RUN_IGNORE_CHANGES,
19
+ type CloudRunSlug,
19
20
  type GcpProvider,
20
21
  type IamInvariant,
21
22
  runtimeSecretBindings,
@@ -61,11 +62,19 @@ const MEMBER_EXPR: Record<IamInvariant["member"], string> = {
61
62
  "run-sa": "runSa.email",
62
63
  // The secret loop resolves the SA per service through `serviceSa`.
63
64
  "gateway-sa": "gatewaySa.email",
65
+ "mcp-auth-proxy-sa": "mcpAuthProxySa.email",
64
66
  "static-publisher-sa": "staticPublisherSa.email",
65
67
  "iap-service-agent": "iapServiceIdentity.email",
66
68
  allUsers: 'member: "allUsers"',
67
69
  };
68
70
 
71
+ /** The variable each stack holds a Cloud Run service in. */
72
+ const SERVICE_VAR: Record<CloudRunSlug, string> = {
73
+ admin: "adminSvc",
74
+ gateway: "gatewaySvc",
75
+ "mcp-auth-proxy": "mcpAuthProxySvc",
76
+ };
77
+
69
78
  function targetExpr(inv: IamInvariant): { type: RegExp; ref?: string } {
70
79
  switch (inv.target.kind) {
71
80
  case "project":
@@ -77,7 +86,7 @@ function targetExpr(inv: IamInvariant): { type: RegExp; ref?: string } {
77
86
  case "run-service":
78
87
  return {
79
88
  type: /^cloudrunv2\.ServiceIamMember$/,
80
- ref: `name: ${inv.target.service}Svc.name`,
89
+ ref: `name: ${SERVICE_VAR[inv.target.service]}.name`,
81
90
  };
82
91
  }
83
92
  }
@@ -125,6 +134,19 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
125
134
  expect(unhandled).toEqual([]);
126
135
  });
127
136
 
137
+ it("#1328 — the MCP-Auth-Proxy's SA reads its two secrets and holds no other role", () => {
138
+ expect(src).toContain("accountId: mcpAuthProxyServiceAccountId(env)");
139
+ expect(src).toMatch(/"mcp-auth-proxy": mcpAuthProxySa,/);
140
+ // No binding but the secret loop names it: no project role, no bucket,
141
+ // no service — the loop gives it exactly serviceSecrets("mcp-auth-proxy").
142
+ expect(declared.filter((d) => d.body.includes("mcpAuthProxySa"))).toEqual([]);
143
+ expect(
144
+ runtimeSecretBindings()
145
+ .filter((b) => b.service === "mcp-auth-proxy")
146
+ .map((b) => b.secret),
147
+ ).toEqual(["mcp-oauth-seal-key", "operator-oauth-client"]);
148
+ });
149
+
128
150
  it("every invariant and exemption names a binding the stack still declares", () => {
129
151
  const names = new Set(declared.flatMap(expand));
130
152
  for (const inv of invariants) expect(names).toContain(inv.stackResource);
@@ -139,9 +161,20 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
139
161
  expect(src).toMatch(
140
162
  /serviceName: "admin",[\s\S]*?contractEnv: \[\s*\.\.\.adminEnvContract\([\s\S]*?\.\.\.remoteMcpEnv\.admin,\s*(\/\/[^\n]*\n\s*)?\.\.\.poolBudget\.env\.admin,?\s*\]/,
141
163
  );
164
+ // #1328 — the gateway carries no remote-MCP env any more.
142
165
  expect(src).toMatch(
143
- /serviceName: "gateway",[\s\S]*?contractEnv: \[\s*\.\.\.gatewayEnvContract\([\s\S]*?\.\.\.remoteMcpEnv\.gateway,\s*\.\.\.poolBudget\.env\.gateway,?\s*\]/,
166
+ /serviceName: "gateway",[\s\S]*?contractEnv: \[\s*\.\.\.gatewayEnvContract\(envContractInputs\),\s*\.\.\.poolBudget\.env\.gateway,?\s*\]/,
144
167
  );
168
+ if (provider === "gcp-firebase") {
169
+ // The proxy: its contract, its identity env, and (until #1331) the
170
+ // data gateway it forwards to — a stack output no contract can name.
171
+ expect(src).toMatch(
172
+ /serviceName: "mcp-auth-proxy",[\s\S]*?contractEnv: \[\s*\.\.\.mcpAuthProxyEnvContract\(\{ provider: "gcp-firebase", env \}\),\s*\.\.\.remoteMcpEnv\["mcp-auth-proxy"\],[\s\S]*?\{ name: INTERIM_FORWARD_ORIGIN_ENV, value: gatewaySvc\.uri \},\s*\]/,
173
+ );
174
+ } else {
175
+ // gcp: the proxy is CLI-owned (mcp-auth-proxy-setup.ts), not a stack service.
176
+ expect(src).not.toContain('serviceName: "mcp-auth-proxy"');
177
+ }
145
178
  expect(src).toContain("envs: [...args.contractEnv],");
146
179
  });
147
180
 
@@ -151,10 +184,11 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
151
184
  expect(src).toContain(
152
185
  "const remoteMcpEnv = remoteMcpIdentityStackEnv(cfg.get(REMOTE_MCP_IDENTITY_CONFIG));",
153
186
  );
154
- expect(src.match(/remoteMcpEnv\.(admin|gateway)/g)?.sort()).toEqual([
155
- "remoteMcpEnv.admin",
156
- "remoteMcpEnv.gateway",
157
- ]);
187
+ expect(src.match(/remoteMcpEnv(\.\w+|\["[\w-]+"\])/g)?.sort()).toEqual(
188
+ provider === "gcp-firebase"
189
+ ? ["remoteMcpEnv.admin", 'remoteMcpEnv["mcp-auth-proxy"]']
190
+ : ["remoteMcpEnv.admin"],
191
+ );
158
192
  });
159
193
 
160
194
  it("#1082 — leaves client / clientVersion (who deployed last) to gcloud on both services", () => {
@@ -165,6 +199,17 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
165
199
  expect(CLOUD_RUN_IGNORE_CHANGES).toEqual(["client", "clientVersion"]);
166
200
  });
167
201
 
202
+ it("#1328 E13 a — the proxy keeps the gateway's logical resource names, so its service and URL stay", () => {
203
+ if (!src.includes('serviceName: "mcp-auth-proxy"')) return; // the gcp stack: CLI-owned proxy
204
+ // Same URN as before → Pulumi updates the existing service (keeping its
205
+ // auto-generated name and run.app URL) instead of creating a new one.
206
+ expect(src).toMatch(
207
+ /serviceName: "mcp-auth-proxy",\s*resourceName: `\$\{namePrefix\}-gateway`,/,
208
+ );
209
+ expect(src).toMatch(/`\$\{namePrefix\}-gateway-public-invoker`/);
210
+ expect(src).not.toContain("aliases");
211
+ });
212
+
168
213
  it("mounts the media bucket on the admin, and only there, from the contract upgrade applies", () => {
169
214
  // The stack's admin gets adminMediaVolume() — the same volume
170
215
  // stack-converge.ts planMediaVolume adds to installs that lack it.
@@ -193,10 +238,17 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
193
238
  expect(src.match(/secretFileVolumesTemplate\(args\.serviceName, env\)/g)).toHaveLength(2);
194
239
  });
195
240
 
196
- it("#806 — gives the gateway the contract's request timeout upgrade applies", () => {
197
- expect(src).toMatch(
198
- /serviceName: "gateway",[\s\S]*?timeout: `\$\{GATEWAY_TIMEOUT_SECONDS\}s`,[\s\S]*?contractEnv: \[\s*\.\.\.gatewayEnvContract\(/,
199
- );
241
+ it("#806 — gives the MCP-Auth-Proxy the contract's request timeout upgrade applies", () => {
242
+ // #1328 — the long remote-MCP streams are the proxy's; the gateway keeps
243
+ // Cloud Run's default. On gcp the proxy is deployed by the CLI with the
244
+ // same constant (mcp-auth-proxy-setup.ts gcpProxyDeployArgs).
245
+ expect(src).toMatch(/serviceName: "gateway",[\s\S]*?timeout: "300s",/);
246
+ expect(src).not.toContain("GATEWAY_TIMEOUT_SECONDS");
247
+ if (provider === "gcp-firebase") {
248
+ expect(src).toMatch(
249
+ /serviceName: "mcp-auth-proxy",[\s\S]*?timeout: `\$\{MCP_AUTH_PROXY_TIMEOUT_SECONDS\}s`,/,
250
+ );
251
+ }
200
252
  });
201
253
 
202
254
  it("sets no env var outside the contract (C1: no secret value in a plain var)", () => {
@@ -207,7 +259,17 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
207
259
 
208
260
  it("builds no database URL with a password in it", () => {
209
261
  expect(src).not.toMatch(/:\$\{postgresPassword\}@|\$\{pw\}@/);
210
- expect(src).toContain("sqlInstance.privateIpAddress.apply(databaseUrls)");
262
+ // #1338 — from the instance's DNS name, once its private zone record exists.
263
+ expect(src).toContain(".apply(([host]) => databaseUrls(host))");
264
+ });
265
+
266
+ it("serves verify-full TLS: shared server CA, encrypted only, DNS name in the VPC (#1338)", () => {
267
+ expect(src).toContain("serverCaMode: CLOUD_SQL_SERVER_CA_MODE");
268
+ expect(src).toContain("sslMode: CLOUD_SQL_SSL_MODE");
269
+ // The names cloudsql-tls.ts converges and adopt-existing.ts adopts.
270
+ expect(src).toContain("name: cloudSqlDnsZoneName(env)");
271
+ expect(src).toContain("managedZone: cloudSqlDnsZoneName(env)");
272
+ expect(src).toContain('visibility: "private"');
211
273
  });
212
274
 
213
275
  it("references the CLI-generated secrets by id instead of creating them", () => {
@@ -225,11 +287,16 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
225
287
 
226
288
  it("pins images by the config keys upgrade writes back", () => {
227
289
  const keys = Object.keys(
228
- imageDigestConfig(provider, { admin: "sha256:a", gateway: "sha256:b" }),
290
+ imageDigestConfig(provider, {
291
+ admin: "sha256:a",
292
+ gateway: "sha256:b",
293
+ "mcp-auth-proxy": "sha256:c",
294
+ }),
229
295
  );
230
296
  expect(keys).toEqual([
231
297
  `caelo-${provider}:image-digest-admin`,
232
298
  `caelo-${provider}:image-digest-gateway`,
299
+ `caelo-${provider}:image-digest-mcp-auth-proxy`,
233
300
  ]);
234
301
  // The stack reads `<ns>:image-digest-<service>` through pulumi.Config.
235
302
  expect(src).toMatch(/cfg\.get\(`image-digest-\$\{service\}`\)/);
@@ -6,16 +6,27 @@ import { generateDockerCompose } from "./compose.js";
6
6
  import {
7
7
  adminEnvContract,
8
8
  CLI_GENERATED_SECRETS,
9
+ CLOUD_RUN_SLUGS,
9
10
  type CloudRunEnvVar,
11
+ cloudRunServicePrefix,
10
12
  DATABASE_ROLE_SECRET,
11
13
  databaseUrls,
12
14
  defaultStagingHost,
15
+ fixedCloudRunServiceName,
13
16
  gatewayEnvContract,
17
+ INTERIM_FORWARD_ORIGIN_ENV,
14
18
  iamMember,
19
+ MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS,
20
+ mcpAuthProxyEnvContract,
21
+ REMOTE_MCP_MODE,
15
22
  RETIRED_IAM_BINDINGS,
23
+ RETIRED_SECRET_FILES,
16
24
  RETIRED_SERVICE_ENV,
17
25
  runtimeSecretBindings,
26
+ SECRETS_SET_UP_AHEAD_OF_THEIR_READER,
18
27
  SERVICE_SECRET_ENV,
28
+ SERVICE_SECRET_FILES,
29
+ SERVICE_VPC_EGRESS,
19
30
  STACK_IAM_NOT_ENSURED,
20
31
  serviceSecrets,
21
32
  stackIamInvariants,
@@ -27,7 +38,7 @@ const base = {
27
38
  domain: "acme.com",
28
39
  stagingHost: "staging.acme.com",
29
40
  region: "europe-west1",
30
- databaseUrls: databaseUrls("10.20.0.3"),
41
+ databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
31
42
  };
32
43
  const asRecord = (vars: CloudRunEnvVar[]) =>
33
44
  Object.fromEntries(
@@ -42,6 +53,7 @@ const asRecord = (vars: CloudRunEnvVar[]) =>
42
53
  const ADMIN_SECRETS = {
43
54
  ADMIN_DATABASE_PASSWORD: "secret:caelo-production-postgres-password:latest",
44
55
  PUBLIC_ADMIN_DATABASE_PASSWORD: "secret:caelo-production-postgres-password:latest",
56
+ PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD: "secret:caelo-production-postgres-password:latest",
45
57
  CAELO_SECRET_KEK: "secret:caelo-production-secret-kek:latest",
46
58
  CAELO_INTERNAL_SECRET: "secret:caelo-production-internal-secret:latest",
47
59
  CAELO_TOOL_APPROVAL_SECRET: "secret:caelo-production-tool-approval-secret:latest",
@@ -50,10 +62,17 @@ const ADMIN_SECRETS = {
50
62
 
51
63
  describe("databaseUrls", () => {
52
64
  it("builds password-less URLs for the three role/database pairs", () => {
53
- expect(databaseUrls("10.20.0.3")).toEqual({
54
- admin: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
55
- publicAdmin: "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
56
- public: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
65
+ expect(databaseUrls("db1.p-label.europe-west1.sql-psa.goog")).toEqual({
66
+ admin:
67
+ "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",
68
+ publicAdmin:
69
+ "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",
70
+ public:
71
+ "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",
72
+ publicStagingAdmin:
73
+ "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",
74
+ publicStaging:
75
+ "postgresql://public_staging_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
57
76
  });
58
77
  });
59
78
  });
@@ -64,14 +83,18 @@ describe("adminEnvContract", () => {
64
83
  CAELO_PROVIDER: "gcp",
65
84
  CAELO_ENV: "production",
66
85
  MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
67
- ADMIN_DATABASE_URL: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
86
+ ADMIN_DATABASE_URL:
87
+ "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",
68
88
  PUBLIC_ADMIN_DATABASE_URL:
69
- "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
89
+ "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",
90
+ PUBLIC_STAGING_ADMIN_DATABASE_URL:
91
+ "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",
70
92
  CAELO_SITE_URL: "https://acme.com",
71
93
  CAELO_GENERATOR_CLI: "/app/apps/static-generator/src/cli.ts",
72
94
  MEDIA_ROOT_DIR: "/app/apps/admin/data/media",
73
95
  CAELO_MCP_IAP_SERVICE_ACCOUNT: "caelo-mcp@acme.iam.gserviceaccount.com",
74
96
  CAELO_MEDIA_BUCKET: "acme-caelo-production-media",
97
+ CAELO_SCHEDULER_SERVICE_ACCOUNT: "caelo-production-scheduler@acme.iam.gserviceaccount.com",
75
98
  // #806 — where Settings → MCP → Remote writes the OAuth client.
76
99
  CAELO_OPERATOR_OAUTH_CLIENT_STORE:
77
100
  "projects/acme/secrets/caelo-production-operator-oauth-client",
@@ -103,14 +126,18 @@ describe("adminEnvContract", () => {
103
126
  CAELO_PROVIDER: "gcp-firebase",
104
127
  CAELO_ENV: "production",
105
128
  MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
106
- ADMIN_DATABASE_URL: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
129
+ ADMIN_DATABASE_URL:
130
+ "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",
107
131
  PUBLIC_ADMIN_DATABASE_URL:
108
- "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
132
+ "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",
133
+ PUBLIC_STAGING_ADMIN_DATABASE_URL:
134
+ "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",
109
135
  CAELO_SITE_URL: "https://acme.com",
110
136
  CAELO_GENERATOR_CLI: "/app/apps/static-generator/src/cli.ts",
111
137
  MEDIA_ROOT_DIR: "/app/apps/admin/data/media",
112
138
  CAELO_MCP_IAP_SERVICE_ACCOUNT: "caelo-mcp@acme.iam.gserviceaccount.com",
113
139
  CAELO_MEDIA_BUCKET: "acme-caelo-production-media",
140
+ CAELO_SCHEDULER_SERVICE_ACCOUNT: "caelo-production-scheduler@acme.iam.gserviceaccount.com",
114
141
  // #806 — where Settings → MCP → Remote writes the OAuth client.
115
142
  CAELO_OPERATOR_OAUTH_CLIENT_STORE:
116
143
  "projects/acme/secrets/caelo-production-operator-oauth-client",
@@ -176,14 +203,15 @@ describe("gatewayEnvContract", () => {
176
203
  CAELO_PROVIDER: "gcp",
177
204
  CAELO_ENV: "production",
178
205
  MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
206
+ // #1329 — the live site's gateway.
207
+ CAELO_GATEWAY_ENVIRONMENT: "live",
179
208
  // #845 — gcp: the LB appends <client>,<LB IP>.
180
209
  CAELO_TRUSTED_PROXY_HOPS: "2",
181
- PUBLIC_DATABASE_URL: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
210
+ PUBLIC_DATABASE_URL:
211
+ "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",
182
212
  PUBLIC_DATABASE_PASSWORD: "secret:caelo-production-public-role-password:latest",
183
213
  CAELO_STAGING_GATE_SECRET: "secret:caelo-production-staging-gate-secret:latest",
184
- // #806 — the remote-MCP broker: its client file and its seal key.
185
- CAELO_OPERATOR_OAUTH_CLIENT_FILE: "/secrets/operator-oauth-client/client.json",
186
- CAELO_MCP_OAUTH_SEAL_KEY: "secret:caelo-production-mcp-oauth-seal-key:latest",
214
+ // No remote-MCP env: the broker is the MCP-Auth-Proxy's (#1328).
187
215
  });
188
216
  });
189
217
 
@@ -215,13 +243,7 @@ describe("gatewayEnvContract", () => {
215
243
  });
216
244
 
217
245
  it("the gateway's run SA reads no admin-only secret (admin password, KEK, internal, tool approval)", () => {
218
- expect(serviceSecrets("gateway")).toEqual([
219
- "public-role-password",
220
- "staging-gate-secret",
221
- // #806 — the broker's seal key (env) and OAuth client (file).
222
- "mcp-oauth-seal-key",
223
- "operator-oauth-client",
224
- ]);
246
+ expect(serviceSecrets("gateway")).toEqual(["public-role-password", "staging-gate-secret"]);
225
247
  for (const v of gateway) {
226
248
  expect(v.name).not.toMatch(/KEK|INTERNAL|TOOL_APPROVAL|PUBLIC_ADMIN/);
227
249
  }
@@ -259,6 +281,7 @@ describe("database role secrets (#613)", () => {
259
281
  expect(DATABASE_ROLE_SECRET).toEqual({
260
282
  admin_role: "postgres-password",
261
283
  public_role: "public-role-password",
284
+ public_staging_role: "public-staging-role-password",
262
285
  });
263
286
  });
264
287
 
@@ -318,25 +341,39 @@ describe("runtime secrets", () => {
318
341
  secret: "staging-gate-secret",
319
342
  stackResource: "gateway-staging-gate-secret-binding",
320
343
  },
344
+ // #1328 — the broker's seal key (env) and Google sign-in client (a
345
+ // file, which counts like an env secret: the SA reads it).
321
346
  {
322
- service: "gateway",
347
+ service: "mcp-auth-proxy",
323
348
  secret: "mcp-oauth-seal-key",
324
- stackResource: "gateway-mcp-oauth-seal-key-binding",
349
+ stackResource: "mcp-auth-proxy-mcp-oauth-seal-key-binding",
325
350
  },
326
- // The secret file counts like an env secret: the SA reads it.
327
351
  {
328
- service: "gateway",
352
+ service: "mcp-auth-proxy",
329
353
  secret: "operator-oauth-client",
330
- stackResource: "gateway-operator-oauth-client-binding",
354
+ stackResource: "mcp-auth-proxy-operator-oauth-client-binding",
331
355
  },
332
356
  ]);
333
357
  });
334
358
 
335
- it("every CLI-generated secret is read by some service", () => {
359
+ it("every CLI-generated secret is read by some service, or says what reads it until one does", () => {
360
+ const read = CLOUD_RUN_SLUGS.flatMap((slug) => serviceSecrets(slug));
336
361
  for (const s of CLI_GENERATED_SECRETS) {
337
- expect([...serviceSecrets("admin"), ...serviceSecrets("gateway")]).toContain(s);
362
+ if (SECRETS_SET_UP_AHEAD_OF_THEIR_READER[s]) {
363
+ // Set up ahead: no service may read it yet, or the entry is stale.
364
+ expect(read).not.toContain(s);
365
+ continue;
366
+ }
367
+ expect(read).toContain(s);
338
368
  }
339
369
  });
370
+
371
+ it("the two gateway logins share no password secret, and no service reads the staging one yet (#1329)", () => {
372
+ expect(DATABASE_ROLE_SECRET.public_staging_role).not.toBe(DATABASE_ROLE_SECRET.public_role);
373
+ expect(CLI_GENERATED_SECRETS).toContain("public-staging-role-password");
374
+ expect(serviceSecrets("gateway")).not.toContain("public-staging-role-password");
375
+ expect(serviceSecrets("admin")).not.toContain("public-staging-role-password");
376
+ });
340
377
  });
341
378
 
342
379
  describe("self-hosted compose", () => {
@@ -344,8 +381,9 @@ describe("self-hosted compose", () => {
344
381
  const yaml = generateDockerCompose({
345
382
  domain: "acme.com",
346
383
  postgresPassword: "p",
347
- rolePasswords: { admin: "a", public: "b" },
384
+ rolePasswords: { admin: "a", public: "b", publicStaging: "ps" },
348
385
  caeloSecretKek: "k",
386
+ dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
349
387
  internalSecret: "i".repeat(48),
350
388
  diskSize: "10G",
351
389
  ports: { http: 80, https: 443 },
@@ -386,14 +424,12 @@ describe("stackIamInvariants", () => {
386
424
  }
387
425
  });
388
426
 
389
- it("grants the gateway SA only its own role password, the staging gate key, the sign-in client, the seal key + telemetry roles", () => {
427
+ it("grants the gateway SA only its own role password, the staging gate key + telemetry roles", () => {
390
428
  for (const provider of ["gcp", "gcp-firebase"] as const) {
391
429
  const gateway = stackIamInvariants(provider).filter((i) => i.member === "gateway-sa");
392
430
  expect(gateway.map((i) => `${i.role} ${JSON.stringify(i.target)}`).sort()).toEqual([
393
431
  'roles/logging.logWriter {"kind":"project"}',
394
432
  'roles/monitoring.metricWriter {"kind":"project"}',
395
- 'roles/secretmanager.secretAccessor {"kind":"secret","name":"mcp-oauth-seal-key"}',
396
- 'roles/secretmanager.secretAccessor {"kind":"secret","name":"operator-oauth-client"}',
397
433
  'roles/secretmanager.secretAccessor {"kind":"secret","name":"public-role-password"}',
398
434
  'roles/secretmanager.secretAccessor {"kind":"secret","name":"staging-gate-secret"}',
399
435
  ]);
@@ -407,6 +443,135 @@ describe("stackIamInvariants", () => {
407
443
  });
408
444
  });
409
445
 
446
+ describe("MCP-Auth-Proxy contract (#1328)", () => {
447
+ it("names one service per role; no prefix is a prefix of another", () => {
448
+ for (const provider of ["gcp", "gcp-firebase"] as const) {
449
+ const prefixes = CLOUD_RUN_SLUGS.map((s) => cloudRunServicePrefix(provider, "production", s));
450
+ expect(new Set(prefixes).size).toBe(prefixes.length);
451
+ for (const a of prefixes) {
452
+ for (const b of prefixes) if (a !== b) expect(b.startsWith(a)).toBe(false);
453
+ }
454
+ }
455
+ // E13 (a): on gcp-firebase the proxy keeps the old gateway service.
456
+ expect(cloudRunServicePrefix("gcp-firebase", "production", "mcp-auth-proxy")).toBe(
457
+ "caelo-production-gateway",
458
+ );
459
+ expect(cloudRunServicePrefix("gcp-firebase", "production", "gateway")).toBe(
460
+ "caelo-production-data-gateway",
461
+ );
462
+ expect(cloudRunServicePrefix("gcp", "production", "gateway")).toBe("caelo-production-gateway");
463
+ expect(cloudRunServicePrefix("gcp", "production", "mcp-auth-proxy")).toBe(
464
+ "caelo-production-mcp-auth-proxy",
465
+ );
466
+ });
467
+
468
+ it("fixes the names of exactly the services the converge creates", () => {
469
+ expect(fixedCloudRunServiceName("gcp-firebase", "production", "gateway")).toBe(
470
+ "caelo-production-data-gateway",
471
+ );
472
+ expect(fixedCloudRunServiceName("gcp", "production", "mcp-auth-proxy")).toBe(
473
+ "caelo-production-mcp-auth-proxy",
474
+ );
475
+ for (const [provider, slug] of [
476
+ ["gcp-firebase", "admin"],
477
+ ["gcp-firebase", "mcp-auth-proxy"],
478
+ ["gcp", "admin"],
479
+ ["gcp", "gateway"],
480
+ ] as const) {
481
+ expect(fixedCloudRunServiceName(provider, "production", slug)).toBeNull();
482
+ }
483
+ });
484
+
485
+ it("gives the proxy no database, site or pool env — only its mode, hops, file and seal key", () => {
486
+ for (const provider of ["gcp", "gcp-firebase"] as const) {
487
+ const env = asRecord(mcpAuthProxyEnvContract({ provider, env: "production" }));
488
+ expect(env).toEqual({
489
+ CAELO_PROVIDER: provider,
490
+ CAELO_REMOTE_MCP: "available",
491
+ CAELO_TRUSTED_PROXY_HOPS: provider === "gcp" ? "2" : "1",
492
+ CAELO_OPERATOR_OAUTH_CLIENT_FILE: "/secrets/operator-oauth-client/client.json",
493
+ CAELO_MCP_OAUTH_SEAL_KEY: "secret:caelo-production-mcp-oauth-seal-key:latest",
494
+ });
495
+ for (const name of Object.keys(env)) expect(name).not.toMatch(/DATABASE|POOL|KEK|STAGING/);
496
+ }
497
+ // Remote MCP needs Google IAP: everywhere else the proxy answers its 404.
498
+ expect(REMOTE_MCP_MODE).toEqual({
499
+ gcp: "available",
500
+ "gcp-firebase": "available",
501
+ "self-hosted": "unavailable",
502
+ aws: "unavailable",
503
+ azure: "unavailable",
504
+ });
505
+ expect(
506
+ asRecord(mcpAuthProxyEnvContract({ provider: "azure", env: "production" })),
507
+ ).toMatchObject({
508
+ CAELO_REMOTE_MCP: "unavailable",
509
+ CAELO_TRUSTED_PROXY_HOPS: String(MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS.azure),
510
+ });
511
+ // The interim forward origin is the stack's / converge's, never the contract's.
512
+ expect(
513
+ mcpAuthProxyEnvContract({ provider: "gcp-firebase", env: "production" }).map((v) => v.name),
514
+ ).not.toContain(INTERIM_FORWARD_ORIGIN_ENV);
515
+ });
516
+
517
+ it("the proxy's SA reads exactly its two secrets and holds no other invariant", () => {
518
+ expect(serviceSecrets("mcp-auth-proxy")).toEqual([
519
+ "mcp-oauth-seal-key",
520
+ "operator-oauth-client",
521
+ ]);
522
+ for (const provider of ["gcp", "gcp-firebase"] as const) {
523
+ const proxy = stackIamInvariants(provider).filter((i) => i.member === "mcp-auth-proxy-sa");
524
+ expect(proxy.map((i) => `${i.role} ${JSON.stringify(i.target)}`).sort()).toEqual([
525
+ 'roles/secretmanager.secretAccessor {"kind":"secret","name":"mcp-oauth-seal-key"}',
526
+ 'roles/secretmanager.secretAccessor {"kind":"secret","name":"operator-oauth-client"}',
527
+ ]);
528
+ }
529
+ expect(SERVICE_VPC_EGRESS["mcp-auth-proxy"]).toBe(false);
530
+ expect(SERVICE_SECRET_FILES["mcp-auth-proxy"]).toHaveLength(1);
531
+ expect(SERVICE_SECRET_FILES.gateway).toEqual([]);
532
+ });
533
+
534
+ it("retires the broker's env, file and secret access on the gateway", () => {
535
+ for (const name of [
536
+ "CAELO_OPERATOR_OAUTH_CLIENT_FILE",
537
+ "CAELO_MCP_OAUTH_SEAL_KEY",
538
+ "CAELO_MCP_OAUTH_SEAL_KEY_PREVIOUS",
539
+ "CAELO_MCP_PUBLIC_URL",
540
+ "CAELO_ADMIN_URL",
541
+ ]) {
542
+ expect(RETIRED_SERVICE_ENV.gateway).toContain(name);
543
+ }
544
+ expect(RETIRED_SECRET_FILES.gateway.map((f) => f.secret)).toContain("operator-oauth-client");
545
+ for (const secret of ["mcp-oauth-seal-key", "operator-oauth-client"]) {
546
+ expect(RETIRED_IAM_BINDINGS).toContainEqual(
547
+ expect.objectContaining({ member: "gateway-sa", target: { kind: "secret", name: secret } }),
548
+ );
549
+ }
550
+ // On gcp-firebase the proxy is the old gateway service: its visitor env goes.
551
+ for (const name of [
552
+ "PUBLIC_DATABASE_URL",
553
+ "PUBLIC_DATABASE_PASSWORD",
554
+ "CAELO_STAGING_GATE_SECRET",
555
+ ]) {
556
+ expect(RETIRED_SERVICE_ENV["mcp-auth-proxy"]).toContain(name);
557
+ }
558
+ });
559
+
560
+ it("gcp-firebase: the data gateway is the rewrite target, the proxy public", () => {
561
+ const inv = stackIamInvariants("gcp-firebase");
562
+ const on = (service: string) =>
563
+ inv
564
+ .filter((i) => i.target.kind === "run-service" && i.target.service === service)
565
+ .map((i) => `${i.stackResource} ${i.role} ${i.member}`)
566
+ .sort();
567
+ expect(on("gateway")).toEqual([
568
+ "data-gateway-public-invoker roles/run.invoker allUsers",
569
+ "data-gateway-viewer-for-admin-sa roles/run.viewer run-sa",
570
+ ]);
571
+ expect(on("mcp-auth-proxy")).toEqual(["gateway-public-invoker roles/run.invoker allUsers"]);
572
+ });
573
+ });
574
+
410
575
  describe("iamMember", () => {
411
576
  const install = { projectId: "acme", projectNumber: "123", env: "production" };
412
577
  it("resolves every principal to the name the stack gives it", () => {
@@ -416,6 +581,9 @@ describe("iamMember", () => {
416
581
  expect(iamMember("gateway-sa", install)).toBe(
417
582
  "serviceAccount:caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
418
583
  );
584
+ expect(iamMember("mcp-auth-proxy-sa", install)).toBe(
585
+ "serviceAccount:caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com",
586
+ );
419
587
  expect(iamMember("static-publisher-sa", install)).toBe(
420
588
  "serviceAccount:caelo-prod-publisher@acme.iam.gserviceaccount.com",
421
589
  );