@caelo-cms/provisioning 0.11.8 → 0.11.10

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 (261) hide show
  1. package/dist/admin-domain.d.ts +10 -1
  2. package/dist/admin-domain.d.ts.map +1 -1
  3. package/dist/admin-domain.js +13 -3
  4. package/dist/admin-domain.js.map +1 -1
  5. package/dist/adopt-existing.d.ts +6 -1
  6. package/dist/adopt-existing.d.ts.map +1 -1
  7. package/dist/adopt-existing.js +86 -0
  8. package/dist/adopt-existing.js.map +1 -1
  9. package/dist/agent/command.d.ts +20 -0
  10. package/dist/agent/command.d.ts.map +1 -1
  11. package/dist/agent/command.js +32 -9
  12. package/dist/agent/command.js.map +1 -1
  13. package/dist/agent/run.d.ts +2 -0
  14. package/dist/agent/run.d.ts.map +1 -1
  15. package/dist/agent/run.js +5 -1
  16. package/dist/agent/run.js.map +1 -1
  17. package/dist/agent-db.js +65 -3
  18. package/dist/aws-task-roles.d.ts +4 -2
  19. package/dist/aws-task-roles.d.ts.map +1 -1
  20. package/dist/aws-task-roles.js +4 -1
  21. package/dist/aws-task-roles.js.map +1 -1
  22. package/dist/caddy.d.ts +21 -4
  23. package/dist/caddy.d.ts.map +1 -1
  24. package/dist/caddy.js +27 -3
  25. package/dist/caddy.js.map +1 -1
  26. package/dist/cli.js +12 -3
  27. package/dist/cli.js.map +1 -1
  28. package/dist/cloudsql-tls.d.ts +63 -0
  29. package/dist/cloudsql-tls.d.ts.map +1 -0
  30. package/dist/cloudsql-tls.js +289 -0
  31. package/dist/cloudsql-tls.js.map +1 -0
  32. package/dist/compose.d.ts +29 -6
  33. package/dist/compose.d.ts.map +1 -1
  34. package/dist/compose.js +95 -23
  35. package/dist/compose.js.map +1 -1
  36. package/dist/converge-checkpoint.d.ts +1 -0
  37. package/dist/converge-checkpoint.d.ts.map +1 -1
  38. package/dist/converge-checkpoint.js +8 -1
  39. package/dist/converge-checkpoint.js.map +1 -1
  40. package/dist/converge-job-gcp.d.ts.map +1 -1
  41. package/dist/converge-job-gcp.js +2 -11
  42. package/dist/converge-job-gcp.js.map +1 -1
  43. package/dist/deployed-release.d.ts +4 -1
  44. package/dist/deployed-release.d.ts.map +1 -1
  45. package/dist/deployed-release.js +18 -8
  46. package/dist/deployed-release.js.map +1 -1
  47. package/dist/gcp-names.d.ts +21 -0
  48. package/dist/gcp-names.d.ts.map +1 -1
  49. package/dist/gcp-names.js +24 -0
  50. package/dist/gcp-names.js.map +1 -1
  51. package/dist/gcp-url-map.d.ts.map +1 -1
  52. package/dist/gcp-url-map.js +6 -2
  53. package/dist/gcp-url-map.js.map +1 -1
  54. package/dist/index.d.ts +1 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/infra-contract.d.ts +1 -1
  59. package/dist/infra-contract.d.ts.map +1 -1
  60. package/dist/infra-contract.js +24 -1
  61. package/dist/infra-contract.js.map +1 -1
  62. package/dist/install-state.d.ts +6 -0
  63. package/dist/install-state.d.ts.map +1 -1
  64. package/dist/install-state.js +17 -4
  65. package/dist/install-state.js.map +1 -1
  66. package/dist/lifecycle.d.ts.map +1 -1
  67. package/dist/lifecycle.js +17 -8
  68. package/dist/lifecycle.js.map +1 -1
  69. package/dist/mcp-auth-proxy-setup.d.ts +48 -0
  70. package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
  71. package/dist/mcp-auth-proxy-setup.js +280 -0
  72. package/dist/mcp-auth-proxy-setup.js.map +1 -0
  73. package/dist/release-manifest.d.ts +25 -1
  74. package/dist/release-manifest.d.ts.map +1 -1
  75. package/dist/release-manifest.js +36 -3
  76. package/dist/release-manifest.js.map +1 -1
  77. package/dist/remote-mcp.d.ts +55 -18
  78. package/dist/remote-mcp.d.ts.map +1 -1
  79. package/dist/remote-mcp.js +307 -77
  80. package/dist/remote-mcp.js.map +1 -1
  81. package/dist/runtime-secrets.d.ts +9 -0
  82. package/dist/runtime-secrets.d.ts.map +1 -1
  83. package/dist/runtime-secrets.js +21 -10
  84. package/dist/runtime-secrets.js.map +1 -1
  85. package/dist/seal-key-rotation.d.ts +7 -7
  86. package/dist/seal-key-rotation.d.ts.map +1 -1
  87. package/dist/seal-key-rotation.js +22 -22
  88. package/dist/seal-key-rotation.js.map +1 -1
  89. package/dist/self-hosted-db-tls.d.ts +37 -0
  90. package/dist/self-hosted-db-tls.d.ts.map +1 -0
  91. package/dist/self-hosted-db-tls.js +220 -0
  92. package/dist/self-hosted-db-tls.js.map +1 -0
  93. package/dist/self-hosted-release.d.ts +3 -1
  94. package/dist/self-hosted-release.d.ts.map +1 -1
  95. package/dist/self-hosted-release.js +33 -9
  96. package/dist/self-hosted-release.js.map +1 -1
  97. package/dist/self-hosted-roles.d.ts +34 -0
  98. package/dist/self-hosted-roles.d.ts.map +1 -1
  99. package/dist/self-hosted-roles.js +52 -0
  100. package/dist/self-hosted-roles.js.map +1 -1
  101. package/dist/self-hosted-start.d.ts +0 -21
  102. package/dist/self-hosted-start.d.ts.map +1 -1
  103. package/dist/self-hosted-start.js +39 -1
  104. package/dist/self-hosted-start.js.map +1 -1
  105. package/dist/self-hosted-upgrade.d.ts +15 -6
  106. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  107. package/dist/self-hosted-upgrade.js +72 -23
  108. package/dist/self-hosted-upgrade.js.map +1 -1
  109. package/dist/stack-contract.d.ts +170 -25
  110. package/dist/stack-contract.d.ts.map +1 -1
  111. package/dist/stack-contract.js +314 -37
  112. package/dist/stack-contract.js.map +1 -1
  113. package/dist/stack-converge.d.ts +31 -0
  114. package/dist/stack-converge.d.ts.map +1 -1
  115. package/dist/stack-converge.js +71 -30
  116. package/dist/stack-converge.js.map +1 -1
  117. package/dist/upgrade/app-roll.d.ts +25 -4
  118. package/dist/upgrade/app-roll.d.ts.map +1 -1
  119. package/dist/upgrade/app-roll.js +63 -17
  120. package/dist/upgrade/app-roll.js.map +1 -1
  121. package/dist/upgrade/converge.d.ts.map +1 -1
  122. package/dist/upgrade/converge.js +99 -12
  123. package/dist/upgrade/converge.js.map +1 -1
  124. package/dist/upgrade/health.d.ts +31 -7
  125. package/dist/upgrade/health.d.ts.map +1 -1
  126. package/dist/upgrade/health.js +41 -17
  127. package/dist/upgrade/health.js.map +1 -1
  128. package/dist/upgrade/image-forms.d.ts +1 -1
  129. package/dist/upgrade/image-forms.d.ts.map +1 -1
  130. package/dist/upgrade/image-forms.js +24 -7
  131. package/dist/upgrade/image-forms.js.map +1 -1
  132. package/dist/upgrade/release-images.d.ts +12 -3
  133. package/dist/upgrade/release-images.d.ts.map +1 -1
  134. package/dist/upgrade/release-images.js +13 -4
  135. package/dist/upgrade/release-images.js.map +1 -1
  136. package/dist/upgrade/upgrade.d.ts +13 -3
  137. package/dist/upgrade/upgrade.d.ts.map +1 -1
  138. package/dist/upgrade/upgrade.js +34 -2
  139. package/dist/upgrade/upgrade.js.map +1 -1
  140. package/dist/upgrader-gcp.d.ts +12 -0
  141. package/dist/upgrader-gcp.d.ts.map +1 -1
  142. package/dist/upgrader-gcp.js +49 -1
  143. package/dist/upgrader-gcp.js.map +1 -1
  144. package/dist/upgrader-iam-preflight.d.ts.map +1 -1
  145. package/dist/upgrader-iam-preflight.js +6 -1
  146. package/dist/upgrader-iam-preflight.js.map +1 -1
  147. package/dist/upgrader-job.d.ts +1 -1
  148. package/dist/upgrader-job.d.ts.map +1 -1
  149. package/dist/upgrader-job.js +20 -14
  150. package/dist/upgrader-job.js.map +1 -1
  151. package/dist/upgrader-records.d.ts +1 -1
  152. package/dist/upgrader-records.d.ts.map +1 -1
  153. package/dist/upgrader-records.js +26 -7
  154. package/dist/upgrader-records.js.map +1 -1
  155. package/dist/upgrader-setup.d.ts.map +1 -1
  156. package/dist/upgrader-setup.js +10 -5
  157. package/dist/upgrader-setup.js.map +1 -1
  158. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  159. package/dist/wizards/gcp-cost.js +22 -7
  160. package/dist/wizards/gcp-cost.js.map +1 -1
  161. package/dist/wizards/gcp.d.ts.map +1 -1
  162. package/dist/wizards/gcp.js +43 -17
  163. package/dist/wizards/gcp.js.map +1 -1
  164. package/package.json +2 -2
  165. package/src/admin-domain.test.ts +9 -8
  166. package/src/admin-domain.ts +13 -3
  167. package/src/admin-memory.test.ts +1 -0
  168. package/src/adopt-existing.test.ts +39 -0
  169. package/src/adopt-existing.ts +83 -1
  170. package/src/agent/command.test.ts +49 -1
  171. package/src/agent/command.ts +36 -9
  172. package/src/agent/run.test.ts +19 -0
  173. package/src/agent/run.ts +11 -2
  174. package/src/aws-task-roles.ts +5 -2
  175. package/src/caddy-redirects.docker.test.ts +2 -0
  176. package/src/caddy-redirects.test.ts +3 -0
  177. package/src/caddy-validate.test.ts +7 -1
  178. package/src/caddy.test.ts +39 -1
  179. package/src/caddy.ts +36 -4
  180. package/src/checkpoint-secrets.test.ts +14 -6
  181. package/src/cli.ts +12 -1
  182. package/src/cloudsql-tls.test.ts +225 -0
  183. package/src/cloudsql-tls.ts +352 -0
  184. package/src/compose.ts +125 -24
  185. package/src/converge-checkpoint.ts +8 -1
  186. package/src/converge-job-gcp.ts +2 -11
  187. package/src/database-tls-builders.test.ts +55 -0
  188. package/src/deployed-release.test.ts +104 -40
  189. package/src/deployed-release.ts +30 -9
  190. package/src/gateway-env-isolation.test.ts +10 -5
  191. package/src/gcp-names.ts +31 -0
  192. package/src/gcp-url-map.test.ts +6 -8
  193. package/src/gcp-url-map.ts +6 -2
  194. package/src/index.test.ts +7 -1
  195. package/src/index.ts +1 -0
  196. package/src/infra-contract.test.ts +5 -3
  197. package/src/infra-contract.ts +24 -1
  198. package/src/install-state.ts +26 -4
  199. package/src/lifecycle.ts +20 -11
  200. package/src/mcp-auth-proxy-setup.test.ts +305 -0
  201. package/src/mcp-auth-proxy-setup.ts +374 -0
  202. package/src/mcp-iap-stacks.test.ts +1 -1
  203. package/src/media-volume.test.ts +1 -1
  204. package/src/operator-access-stacks.test.ts +1 -1
  205. package/src/operator-access.test.ts +5 -4
  206. package/src/pool-budget-stacks.test.ts +1 -1
  207. package/src/release-manifest.test.ts +25 -0
  208. package/src/release-manifest.ts +59 -4
  209. package/src/remote-mcp-stack-config.test.ts +19 -9
  210. package/src/remote-mcp.test.ts +252 -57
  211. package/src/remote-mcp.ts +336 -77
  212. package/src/runtime-secrets.test.ts +20 -7
  213. package/src/runtime-secrets.ts +50 -9
  214. package/src/seal-key-rotation.test.ts +2 -2
  215. package/src/seal-key-rotation.ts +25 -25
  216. package/src/self-hosted-compose.test.ts +108 -5
  217. package/src/self-hosted-db-tls.test.ts +111 -0
  218. package/src/self-hosted-db-tls.ts +287 -0
  219. package/src/self-hosted-release-resolve.test.ts +71 -0
  220. package/src/self-hosted-release.ts +41 -11
  221. package/src/self-hosted-roles.test.ts +54 -0
  222. package/src/self-hosted-roles.ts +58 -0
  223. package/src/self-hosted-start.test.ts +34 -2
  224. package/src/self-hosted-start.ts +20 -2
  225. package/src/self-hosted-upgrade.test.ts +103 -14
  226. package/src/self-hosted-upgrade.ts +115 -29
  227. package/src/stack-contract-parity.test.ts +79 -12
  228. package/src/stack-contract.test.ts +169 -29
  229. package/src/stack-contract.ts +362 -42
  230. package/src/stack-converge-secrets.test.ts +26 -13
  231. package/src/stack-converge.test.ts +199 -26
  232. package/src/stack-converge.ts +92 -30
  233. package/src/state-store.test.ts +2 -1
  234. package/src/upgrade/app-roll.ts +97 -19
  235. package/src/upgrade/converge.ts +120 -11
  236. package/src/upgrade/health.test.ts +63 -25
  237. package/src/upgrade/health.ts +54 -22
  238. package/src/upgrade/image-forms.test.ts +34 -0
  239. package/src/upgrade/image-forms.ts +23 -8
  240. package/src/upgrade/release-images.test.ts +5 -1
  241. package/src/upgrade/release-images.ts +22 -6
  242. package/src/upgrade/upgrade.test.ts +353 -44
  243. package/src/upgrade/upgrade.ts +55 -6
  244. package/src/upgrader-gcp.ts +65 -3
  245. package/src/upgrader-iam-preflight.test.ts +19 -0
  246. package/src/upgrader-iam-preflight.ts +6 -0
  247. package/src/upgrader-job.test.ts +101 -27
  248. package/src/upgrader-job.ts +27 -14
  249. package/src/upgrader-records.test.ts +97 -12
  250. package/src/upgrader-records.ts +28 -11
  251. package/src/upgrader-setup.test.ts +2 -0
  252. package/src/upgrader-setup.ts +10 -4
  253. package/src/wizards/gcp-cost.test.ts +50 -0
  254. package/src/wizards/gcp-cost.ts +24 -7
  255. package/src/wizards/gcp.ts +55 -14
  256. package/stacks/aws/index.ts +32 -14
  257. package/stacks/azure/index.ts +57 -18
  258. package/stacks/gcp/index.ts +83 -23
  259. package/stacks/gcp-firebase/index.ts +162 -33
  260. package/stacks/self-hosted/README.md +2 -1
  261. package/stacks/self-hosted/index.ts +11 -1
@@ -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,26 @@ 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,
18
26
  SERVICE_SECRET_ENV,
27
+ SERVICE_SECRET_FILES,
28
+ SERVICE_VPC_EGRESS,
19
29
  STACK_IAM_NOT_ENSURED,
20
30
  serviceSecrets,
21
31
  stackIamInvariants,
@@ -27,7 +37,7 @@ const base = {
27
37
  domain: "acme.com",
28
38
  stagingHost: "staging.acme.com",
29
39
  region: "europe-west1",
30
- databaseUrls: databaseUrls("10.20.0.3"),
40
+ databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
31
41
  };
32
42
  const asRecord = (vars: CloudRunEnvVar[]) =>
33
43
  Object.fromEntries(
@@ -50,10 +60,13 @@ const ADMIN_SECRETS = {
50
60
 
51
61
  describe("databaseUrls", () => {
52
62
  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",
63
+ expect(databaseUrls("db1.p-label.europe-west1.sql-psa.goog")).toEqual({
64
+ admin:
65
+ "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",
66
+ publicAdmin:
67
+ "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",
68
+ public:
69
+ "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",
57
70
  });
58
71
  });
59
72
  });
@@ -64,9 +77,10 @@ describe("adminEnvContract", () => {
64
77
  CAELO_PROVIDER: "gcp",
65
78
  CAELO_ENV: "production",
66
79
  MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
67
- ADMIN_DATABASE_URL: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
80
+ ADMIN_DATABASE_URL:
81
+ "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
82
  PUBLIC_ADMIN_DATABASE_URL:
69
- "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
83
+ "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
84
  CAELO_SITE_URL: "https://acme.com",
71
85
  CAELO_GENERATOR_CLI: "/app/apps/static-generator/src/cli.ts",
72
86
  MEDIA_ROOT_DIR: "/app/apps/admin/data/media",
@@ -103,9 +117,10 @@ describe("adminEnvContract", () => {
103
117
  CAELO_PROVIDER: "gcp-firebase",
104
118
  CAELO_ENV: "production",
105
119
  MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
106
- ADMIN_DATABASE_URL: "postgresql://admin_role@10.20.0.3:5432/cms_admin?sslmode=require",
120
+ ADMIN_DATABASE_URL:
121
+ "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
122
  PUBLIC_ADMIN_DATABASE_URL:
108
- "postgresql://admin_role@10.20.0.3:5432/cms_public?sslmode=require",
123
+ "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",
109
124
  CAELO_SITE_URL: "https://acme.com",
110
125
  CAELO_GENERATOR_CLI: "/app/apps/static-generator/src/cli.ts",
111
126
  MEDIA_ROOT_DIR: "/app/apps/admin/data/media",
@@ -178,12 +193,11 @@ describe("gatewayEnvContract", () => {
178
193
  MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
179
194
  // #845 — gcp: the LB appends <client>,<LB IP>.
180
195
  CAELO_TRUSTED_PROXY_HOPS: "2",
181
- PUBLIC_DATABASE_URL: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
196
+ PUBLIC_DATABASE_URL:
197
+ "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
198
  PUBLIC_DATABASE_PASSWORD: "secret:caelo-production-public-role-password:latest",
183
199
  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",
200
+ // No remote-MCP env: the broker is the MCP-Auth-Proxy's (#1328).
187
201
  });
188
202
  });
189
203
 
@@ -215,13 +229,7 @@ describe("gatewayEnvContract", () => {
215
229
  });
216
230
 
217
231
  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
- ]);
232
+ expect(serviceSecrets("gateway")).toEqual(["public-role-password", "staging-gate-secret"]);
225
233
  for (const v of gateway) {
226
234
  expect(v.name).not.toMatch(/KEK|INTERNAL|TOOL_APPROVAL|PUBLIC_ADMIN/);
227
235
  }
@@ -318,23 +326,24 @@ describe("runtime secrets", () => {
318
326
  secret: "staging-gate-secret",
319
327
  stackResource: "gateway-staging-gate-secret-binding",
320
328
  },
329
+ // #1328 — the broker's seal key (env) and Google sign-in client (a
330
+ // file, which counts like an env secret: the SA reads it).
321
331
  {
322
- service: "gateway",
332
+ service: "mcp-auth-proxy",
323
333
  secret: "mcp-oauth-seal-key",
324
- stackResource: "gateway-mcp-oauth-seal-key-binding",
334
+ stackResource: "mcp-auth-proxy-mcp-oauth-seal-key-binding",
325
335
  },
326
- // The secret file counts like an env secret: the SA reads it.
327
336
  {
328
- service: "gateway",
337
+ service: "mcp-auth-proxy",
329
338
  secret: "operator-oauth-client",
330
- stackResource: "gateway-operator-oauth-client-binding",
339
+ stackResource: "mcp-auth-proxy-operator-oauth-client-binding",
331
340
  },
332
341
  ]);
333
342
  });
334
343
 
335
344
  it("every CLI-generated secret is read by some service", () => {
336
345
  for (const s of CLI_GENERATED_SECRETS) {
337
- expect([...serviceSecrets("admin"), ...serviceSecrets("gateway")]).toContain(s);
346
+ expect(CLOUD_RUN_SLUGS.flatMap((slug) => serviceSecrets(slug))).toContain(s);
338
347
  }
339
348
  });
340
349
  });
@@ -346,6 +355,7 @@ describe("self-hosted compose", () => {
346
355
  postgresPassword: "p",
347
356
  rolePasswords: { admin: "a", public: "b" },
348
357
  caeloSecretKek: "k",
358
+ dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
349
359
  internalSecret: "i".repeat(48),
350
360
  diskSize: "10G",
351
361
  ports: { http: 80, https: 443 },
@@ -386,14 +396,12 @@ describe("stackIamInvariants", () => {
386
396
  }
387
397
  });
388
398
 
389
- it("grants the gateway SA only its own role password, the staging gate key, the sign-in client, the seal key + telemetry roles", () => {
399
+ it("grants the gateway SA only its own role password, the staging gate key + telemetry roles", () => {
390
400
  for (const provider of ["gcp", "gcp-firebase"] as const) {
391
401
  const gateway = stackIamInvariants(provider).filter((i) => i.member === "gateway-sa");
392
402
  expect(gateway.map((i) => `${i.role} ${JSON.stringify(i.target)}`).sort()).toEqual([
393
403
  'roles/logging.logWriter {"kind":"project"}',
394
404
  '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
405
  'roles/secretmanager.secretAccessor {"kind":"secret","name":"public-role-password"}',
398
406
  'roles/secretmanager.secretAccessor {"kind":"secret","name":"staging-gate-secret"}',
399
407
  ]);
@@ -407,6 +415,135 @@ describe("stackIamInvariants", () => {
407
415
  });
408
416
  });
409
417
 
418
+ describe("MCP-Auth-Proxy contract (#1328)", () => {
419
+ it("names one service per role; no prefix is a prefix of another", () => {
420
+ for (const provider of ["gcp", "gcp-firebase"] as const) {
421
+ const prefixes = CLOUD_RUN_SLUGS.map((s) => cloudRunServicePrefix(provider, "production", s));
422
+ expect(new Set(prefixes).size).toBe(prefixes.length);
423
+ for (const a of prefixes) {
424
+ for (const b of prefixes) if (a !== b) expect(b.startsWith(a)).toBe(false);
425
+ }
426
+ }
427
+ // E13 (a): on gcp-firebase the proxy keeps the old gateway service.
428
+ expect(cloudRunServicePrefix("gcp-firebase", "production", "mcp-auth-proxy")).toBe(
429
+ "caelo-production-gateway",
430
+ );
431
+ expect(cloudRunServicePrefix("gcp-firebase", "production", "gateway")).toBe(
432
+ "caelo-production-data-gateway",
433
+ );
434
+ expect(cloudRunServicePrefix("gcp", "production", "gateway")).toBe("caelo-production-gateway");
435
+ expect(cloudRunServicePrefix("gcp", "production", "mcp-auth-proxy")).toBe(
436
+ "caelo-production-mcp-auth-proxy",
437
+ );
438
+ });
439
+
440
+ it("fixes the names of exactly the services the converge creates", () => {
441
+ expect(fixedCloudRunServiceName("gcp-firebase", "production", "gateway")).toBe(
442
+ "caelo-production-data-gateway",
443
+ );
444
+ expect(fixedCloudRunServiceName("gcp", "production", "mcp-auth-proxy")).toBe(
445
+ "caelo-production-mcp-auth-proxy",
446
+ );
447
+ for (const [provider, slug] of [
448
+ ["gcp-firebase", "admin"],
449
+ ["gcp-firebase", "mcp-auth-proxy"],
450
+ ["gcp", "admin"],
451
+ ["gcp", "gateway"],
452
+ ] as const) {
453
+ expect(fixedCloudRunServiceName(provider, "production", slug)).toBeNull();
454
+ }
455
+ });
456
+
457
+ it("gives the proxy no database, site or pool env — only its mode, hops, file and seal key", () => {
458
+ for (const provider of ["gcp", "gcp-firebase"] as const) {
459
+ const env = asRecord(mcpAuthProxyEnvContract({ provider, env: "production" }));
460
+ expect(env).toEqual({
461
+ CAELO_PROVIDER: provider,
462
+ CAELO_REMOTE_MCP: "available",
463
+ CAELO_TRUSTED_PROXY_HOPS: provider === "gcp" ? "2" : "1",
464
+ CAELO_OPERATOR_OAUTH_CLIENT_FILE: "/secrets/operator-oauth-client/client.json",
465
+ CAELO_MCP_OAUTH_SEAL_KEY: "secret:caelo-production-mcp-oauth-seal-key:latest",
466
+ });
467
+ for (const name of Object.keys(env)) expect(name).not.toMatch(/DATABASE|POOL|KEK|STAGING/);
468
+ }
469
+ // Remote MCP needs Google IAP: everywhere else the proxy answers its 404.
470
+ expect(REMOTE_MCP_MODE).toEqual({
471
+ gcp: "available",
472
+ "gcp-firebase": "available",
473
+ "self-hosted": "unavailable",
474
+ aws: "unavailable",
475
+ azure: "unavailable",
476
+ });
477
+ expect(
478
+ asRecord(mcpAuthProxyEnvContract({ provider: "azure", env: "production" })),
479
+ ).toMatchObject({
480
+ CAELO_REMOTE_MCP: "unavailable",
481
+ CAELO_TRUSTED_PROXY_HOPS: String(MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS.azure),
482
+ });
483
+ // The interim forward origin is the stack's / converge's, never the contract's.
484
+ expect(
485
+ mcpAuthProxyEnvContract({ provider: "gcp-firebase", env: "production" }).map((v) => v.name),
486
+ ).not.toContain(INTERIM_FORWARD_ORIGIN_ENV);
487
+ });
488
+
489
+ it("the proxy's SA reads exactly its two secrets and holds no other invariant", () => {
490
+ expect(serviceSecrets("mcp-auth-proxy")).toEqual([
491
+ "mcp-oauth-seal-key",
492
+ "operator-oauth-client",
493
+ ]);
494
+ for (const provider of ["gcp", "gcp-firebase"] as const) {
495
+ const proxy = stackIamInvariants(provider).filter((i) => i.member === "mcp-auth-proxy-sa");
496
+ expect(proxy.map((i) => `${i.role} ${JSON.stringify(i.target)}`).sort()).toEqual([
497
+ 'roles/secretmanager.secretAccessor {"kind":"secret","name":"mcp-oauth-seal-key"}',
498
+ 'roles/secretmanager.secretAccessor {"kind":"secret","name":"operator-oauth-client"}',
499
+ ]);
500
+ }
501
+ expect(SERVICE_VPC_EGRESS["mcp-auth-proxy"]).toBe(false);
502
+ expect(SERVICE_SECRET_FILES["mcp-auth-proxy"]).toHaveLength(1);
503
+ expect(SERVICE_SECRET_FILES.gateway).toEqual([]);
504
+ });
505
+
506
+ it("retires the broker's env, file and secret access on the gateway", () => {
507
+ for (const name of [
508
+ "CAELO_OPERATOR_OAUTH_CLIENT_FILE",
509
+ "CAELO_MCP_OAUTH_SEAL_KEY",
510
+ "CAELO_MCP_OAUTH_SEAL_KEY_PREVIOUS",
511
+ "CAELO_MCP_PUBLIC_URL",
512
+ "CAELO_ADMIN_URL",
513
+ ]) {
514
+ expect(RETIRED_SERVICE_ENV.gateway).toContain(name);
515
+ }
516
+ expect(RETIRED_SECRET_FILES.gateway.map((f) => f.secret)).toContain("operator-oauth-client");
517
+ for (const secret of ["mcp-oauth-seal-key", "operator-oauth-client"]) {
518
+ expect(RETIRED_IAM_BINDINGS).toContainEqual(
519
+ expect.objectContaining({ member: "gateway-sa", target: { kind: "secret", name: secret } }),
520
+ );
521
+ }
522
+ // On gcp-firebase the proxy is the old gateway service: its visitor env goes.
523
+ for (const name of [
524
+ "PUBLIC_DATABASE_URL",
525
+ "PUBLIC_DATABASE_PASSWORD",
526
+ "CAELO_STAGING_GATE_SECRET",
527
+ ]) {
528
+ expect(RETIRED_SERVICE_ENV["mcp-auth-proxy"]).toContain(name);
529
+ }
530
+ });
531
+
532
+ it("gcp-firebase: the data gateway is the rewrite target, the proxy public", () => {
533
+ const inv = stackIamInvariants("gcp-firebase");
534
+ const on = (service: string) =>
535
+ inv
536
+ .filter((i) => i.target.kind === "run-service" && i.target.service === service)
537
+ .map((i) => `${i.stackResource} ${i.role} ${i.member}`)
538
+ .sort();
539
+ expect(on("gateway")).toEqual([
540
+ "data-gateway-public-invoker roles/run.invoker allUsers",
541
+ "data-gateway-viewer-for-admin-sa roles/run.viewer run-sa",
542
+ ]);
543
+ expect(on("mcp-auth-proxy")).toEqual(["gateway-public-invoker roles/run.invoker allUsers"]);
544
+ });
545
+ });
546
+
410
547
  describe("iamMember", () => {
411
548
  const install = { projectId: "acme", projectNumber: "123", env: "production" };
412
549
  it("resolves every principal to the name the stack gives it", () => {
@@ -416,6 +553,9 @@ describe("iamMember", () => {
416
553
  expect(iamMember("gateway-sa", install)).toBe(
417
554
  "serviceAccount:caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
418
555
  );
556
+ expect(iamMember("mcp-auth-proxy-sa", install)).toBe(
557
+ "serviceAccount:caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com",
558
+ );
419
559
  expect(iamMember("static-publisher-sa", install)).toBe(
420
560
  "serviceAccount:caelo-prod-publisher@acme.iam.gserviceaccount.com",
421
561
  );