@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
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * #1338 — every database URL a provider adapter builds asks for
5
+ * `sslmode=verify-full`, and no adapter still builds a weaker one
6
+ * (CLAUDE.md §2 no fallbacks: a downgrade is never silent).
7
+ */
8
+
9
+ import { describe, expect, it } from "bun:test";
10
+ import { readFileSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { databaseTlsProblem } from "@caelo-cms/shared";
13
+ import { generateDockerCompose } from "./compose.js";
14
+ import { databaseUrls } from "./stack-contract.js";
15
+
16
+ const DNS = "abc123.p-label.europe-west1.sql-psa.goog";
17
+
18
+ describe("database URL builders (#1338)", () => {
19
+ it("Cloud SQL: verify-full on the instance's DNS name, never on its IP", () => {
20
+ for (const url of Object.values(databaseUrls(DNS))) {
21
+ expect(databaseTlsProblem(url)).toBeNull();
22
+ expect(new URL(url).hostname).toBe(DNS);
23
+ }
24
+ expect(() => databaseUrls("10.20.0.3")).toThrow(/DNS name/);
25
+ });
26
+
27
+ it("self-hosted Compose: every service URL is verify-full against the install's CA", () => {
28
+ const yaml = generateDockerCompose({
29
+ domain: "example.com",
30
+ postgresPassword: "superpw",
31
+ rolePasswords: { admin: "adminpw", public: "publicpw" },
32
+ caeloSecretKek: "0".repeat(64),
33
+ dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
34
+ internalSecret: "i".repeat(48),
35
+ diskSize: "20Gi",
36
+ images: {
37
+ mode: "release",
38
+ admin: `ghcr.io/caelo-cms/admin@sha256:${"a".repeat(64)}`,
39
+ gateway: `ghcr.io/caelo-cms/gateway@sha256:${"b".repeat(64)}`,
40
+ },
41
+ ports: { http: 80, https: 443 },
42
+ project: "caelo-example-com",
43
+ });
44
+ const urls = [...yaml.matchAll(/_DATABASE_URL: "([^"]+)"/g)].map((m) => m[1] as string);
45
+ expect(urls).toHaveLength(3);
46
+ for (const url of urls) expect(databaseTlsProblem(url)).toBeNull();
47
+ });
48
+
49
+ for (const stack of ["gcp", "gcp-firebase", "aws", "azure"]) {
50
+ it(`stacks/${stack} builds no URL weaker than verify-full`, () => {
51
+ const src = readFileSync(resolve(import.meta.dir, `../stacks/${stack}/index.ts`), "utf8");
52
+ expect(src).not.toMatch(/sslmode=(disable|allow|prefer|require|verify-ca)\b/);
53
+ });
54
+ }
55
+ });
@@ -29,6 +29,17 @@ function fakeGcloud(answers: Record<string, GcloudResult[]>) {
29
29
  return { run, calls };
30
30
  }
31
31
 
32
+ /**
33
+ * A gcp-firebase install since #1328: the admin, the data gateway and the
34
+ * MCP-Auth-Proxy — the service that was the gateway (E13 a).
35
+ */
36
+ const FIREBASE = { provider: "gcp-firebase" as const, projectId: "p", region: "europe-west1" };
37
+ const firebaseLists = () => ({
38
+ "metadata.name~^caelo-production-admin": [ok("caelo-production-admin-abc\n")],
39
+ "metadata.name~^caelo-production-data-gateway": [ok("caelo-production-data-gateway\n")],
40
+ "metadata.name~^caelo-production-gateway": [ok("caelo-production-gateway-def\n")],
41
+ });
42
+
32
43
  describe("digestFromImageRef", () => {
33
44
  it("extracts the pinned digest", () => {
34
45
  expect(digestFromImageRef(`${REG}/admin@${A}\n`)).toBe(A);
@@ -39,16 +50,43 @@ describe("digestFromImageRef", () => {
39
50
  });
40
51
 
41
52
  describe("readDeployedImageDigests", () => {
42
- it("reads both services' digests", async () => {
53
+ it("reads the three services' digests, each from the service playing its role", async () => {
43
54
  const { run, calls } = fakeGcloud({
55
+ ...firebaseLists(),
56
+ "describe caelo-production-admin-abc": [ok(`${REG}/admin@${A}`)],
57
+ "describe caelo-production-data-gateway": [ok(`${REG}/gateway@${B}`)],
58
+ "describe caelo-production-gateway-def": [ok(`${REG}/mcp-auth-proxy@${C}`)],
59
+ });
60
+ const r = await readDeployedImageDigests({ ...FIREBASE, run });
61
+ expect(r).toEqual({ ok: true, digests: { admin: A, gateway: B, "mcp-auth-proxy": C } });
62
+ expect(calls.every((c) => c.includes("--project=p"))).toBe(true);
63
+ });
64
+
65
+ it("finds the gcp proxy under its own name", async () => {
66
+ const { run } = fakeGcloud({
44
67
  "metadata.name~^caelo-production-admin": [ok("caelo-production-admin-abc\n")],
45
68
  "metadata.name~^caelo-production-gateway": [ok("caelo-production-gateway-def\n")],
69
+ "metadata.name~^caelo-production-mcp-auth-proxy": [ok("caelo-production-mcp-auth-proxy\n")],
46
70
  "describe caelo-production-admin-abc": [ok(`${REG}/admin@${A}`)],
47
71
  "describe caelo-production-gateway-def": [ok(`${REG}/gateway@${B}`)],
72
+ "describe caelo-production-mcp-auth-proxy": [ok(`${REG}/mcp-auth-proxy@${C}`)],
48
73
  });
49
- const r = await readDeployedImageDigests({ projectId: "p", region: "europe-west1", run });
50
- expect(r).toEqual({ ok: true, digests: { admin: A, gateway: B } });
51
- expect(calls.every((c) => c.includes("--project=p"))).toBe(true);
74
+ const r = await readDeployedImageDigests({ ...FIREBASE, provider: "gcp", run });
75
+ expect(r).toEqual({ ok: true, digests: { admin: A, gateway: B, "mcp-auth-proxy": C } });
76
+ });
77
+
78
+ it("sends an install without its #1328 services to upgrade", async () => {
79
+ const { run } = fakeGcloud({
80
+ "metadata.name~^caelo-production-admin": [ok("caelo-production-admin-abc\n")],
81
+ "metadata.name~^caelo-production-data-gateway": [ok("")],
82
+ "describe caelo-production-admin-abc": [ok(`${REG}/admin@${A}`)],
83
+ });
84
+ const r = await readDeployedImageDigests({ ...FIREBASE, run });
85
+ expect(r.ok).toBe(false);
86
+ if (!r.ok) {
87
+ expect(r.error).toContain("no caelo-production-data-gateway* Cloud Run service");
88
+ expect(r.error).toContain("Run `upgrade` first");
89
+ }
52
90
  });
53
91
 
54
92
  it("refuses an ambiguous service match", async () => {
@@ -57,7 +95,7 @@ describe("readDeployedImageDigests", () => {
57
95
  ok("caelo-production-admin-a\ncaelo-production-admin-b\n"),
58
96
  ],
59
97
  });
60
- const r = await readDeployedImageDigests({ projectId: "p", region: "r", run });
98
+ const r = await readDeployedImageDigests({ ...FIREBASE, run });
61
99
  expect(r.ok).toBe(false);
62
100
  if (!r.ok)
63
101
  expect(r.error).toContain("found caelo-production-admin-a, caelo-production-admin-b");
@@ -68,14 +106,14 @@ describe("readDeployedImageDigests", () => {
68
106
  "metadata.name~^caelo-production-admin": [ok("caelo-production-admin-abc\n")],
69
107
  "describe caelo-production-admin-abc": [ok(`${REG}/admin:latest`)],
70
108
  });
71
- const r = await readDeployedImageDigests({ projectId: "p", region: "r", run });
109
+ const r = await readDeployedImageDigests({ ...FIREBASE, run });
72
110
  expect(r.ok).toBe(false);
73
111
  if (!r.ok) expect(r.error).toContain("not pinned to a digest");
74
112
  });
75
113
 
76
114
  it("surfaces a gcloud failure", async () => {
77
115
  const { run } = fakeGcloud({ "services list": [fail("PERMISSION_DENIED")] });
78
- const r = await readDeployedImageDigests({ projectId: "p", region: "r", run });
116
+ const r = await readDeployedImageDigests({ ...FIREBASE, run });
79
117
  expect(r).toEqual({ ok: false, error: "list Cloud Run services: PERMISSION_DENIED" });
80
118
  });
81
119
  });
@@ -87,40 +125,50 @@ describe("checkDeployedRuntimeEnv", () => {
87
125
  name,
88
126
  valueFrom: { secretKeyRef: { name: `caelo-production-${id}`, key: "latest" } },
89
127
  });
90
- const lists = () => ({
91
- "metadata.name~^caelo-production-admin": [ok("caelo-production-admin-abc\n")],
92
- "metadata.name~^caelo-production-gateway": [ok("caelo-production-gateway-def\n")],
93
- });
128
+ const adminOk = svc([
129
+ secret("ADMIN_DATABASE_PASSWORD"),
130
+ secret("PUBLIC_ADMIN_DATABASE_PASSWORD"),
131
+ ]);
132
+ const gatewayOk = svc([secret("PUBLIC_DATABASE_PASSWORD", "public-role-password")]);
133
+ const proxyOk = svc([secret("CAELO_MCP_OAUTH_SEAL_KEY", "mcp-oauth-seal-key")]);
94
134
 
95
- it("passes once both services read the password from Secret Manager", async () => {
135
+ it("passes once every service runs the current env", async () => {
96
136
  const { run } = fakeGcloud({
97
- ...lists(),
98
- "describe caelo-production-admin-abc": [
99
- svc([secret("ADMIN_DATABASE_PASSWORD"), secret("PUBLIC_ADMIN_DATABASE_PASSWORD")]),
100
- ],
101
- "describe caelo-production-gateway-def": [
102
- svc([secret("PUBLIC_DATABASE_PASSWORD", "public-role-password")]),
103
- ],
137
+ ...firebaseLists(),
138
+ "describe caelo-production-admin-abc": [adminOk],
139
+ "describe caelo-production-data-gateway": [gatewayOk],
140
+ "describe caelo-production-gateway-def": [proxyOk],
104
141
  });
105
- expect(await checkDeployedRuntimeEnv({ projectId: "p", region: "r", run })).toEqual({
106
- ok: true,
142
+ expect(await checkDeployedRuntimeEnv({ ...FIREBASE, run })).toEqual({ ok: true });
143
+ });
144
+
145
+ it("#1328 — stops a wizard re-run while the proxy is still the old gateway (database env)", async () => {
146
+ const { run } = fakeGcloud({
147
+ ...firebaseLists(),
148
+ "describe caelo-production-admin-abc": [adminOk],
149
+ "describe caelo-production-data-gateway": [gatewayOk],
150
+ "describe caelo-production-gateway-def": [gatewayOk],
107
151
  });
152
+ const r = await checkDeployedRuntimeEnv({ ...FIREBASE, run });
153
+ expect(r.ok).toBe(false);
154
+ if (!r.ok) {
155
+ expect(r.error).toContain("PUBLIC_DATABASE_PASSWORD");
156
+ expect(r.error).toContain("Run `upgrade` first");
157
+ }
108
158
  });
109
159
 
110
160
  it("stops a wizard re-run on a gateway from before #635 (it needs its gateway_role pool)", async () => {
111
161
  const { run } = fakeGcloud({
112
- ...lists(),
113
- "describe caelo-production-admin-abc": [
114
- svc([secret("ADMIN_DATABASE_PASSWORD"), secret("PUBLIC_ADMIN_DATABASE_PASSWORD")]),
115
- ],
116
- "describe caelo-production-gateway-def": [
162
+ ...firebaseLists(),
163
+ "describe caelo-production-admin-abc": [adminOk],
164
+ "describe caelo-production-data-gateway": [
117
165
  svc([
118
166
  secret("GATEWAY_DATABASE_PASSWORD", "gateway-role-password"),
119
167
  secret("PUBLIC_DATABASE_PASSWORD", "public-role-password"),
120
168
  ]),
121
169
  ],
122
170
  });
123
- const r = await checkDeployedRuntimeEnv({ projectId: "p", region: "r", run });
171
+ const r = await checkDeployedRuntimeEnv({ ...FIREBASE, run });
124
172
  expect(r.ok).toBe(false);
125
173
  if (!r.ok) {
126
174
  expect(r.error).toContain("GATEWAY_DATABASE_PASSWORD");
@@ -130,15 +178,13 @@ describe("checkDeployedRuntimeEnv", () => {
130
178
 
131
179
  it("stops a wizard re-run on a gateway from before #613 (it needs the admin_role pool)", async () => {
132
180
  const { run } = fakeGcloud({
133
- ...lists(),
134
- "describe caelo-production-admin-abc": [
135
- svc([secret("ADMIN_DATABASE_PASSWORD"), secret("PUBLIC_ADMIN_DATABASE_PASSWORD")]),
136
- ],
137
- "describe caelo-production-gateway-def": [
181
+ ...firebaseLists(),
182
+ "describe caelo-production-admin-abc": [adminOk],
183
+ "describe caelo-production-data-gateway": [
138
184
  svc([secret("ADMIN_DATABASE_PASSWORD"), secret("PUBLIC_DATABASE_PASSWORD")]),
139
185
  ],
140
186
  });
141
- const r = await checkDeployedRuntimeEnv({ projectId: "p", region: "r", run });
187
+ const r = await checkDeployedRuntimeEnv({ ...FIREBASE, run });
142
188
  expect(r.ok).toBe(false);
143
189
  if (!r.ok) {
144
190
  expect(r.error).toContain("ADMIN_DATABASE_PASSWORD");
@@ -148,14 +194,17 @@ describe("checkDeployedRuntimeEnv", () => {
148
194
 
149
195
  it("stops a wizard re-run on an install upgrade hasn't moved over (password in the URL)", async () => {
150
196
  const { run } = fakeGcloud({
151
- ...lists(),
197
+ ...firebaseLists(),
152
198
  "describe caelo-production-admin-abc": [
153
199
  svc([
154
- { name: "ADMIN_DATABASE_URL", value: "postgres://admin_role:pw@10.0.0.3:5432/cms_admin" },
200
+ {
201
+ name: "ADMIN_DATABASE_URL",
202
+ value: "postgres://admin_role:pw@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin",
203
+ },
155
204
  ]),
156
205
  ],
157
206
  });
158
- const r = await checkDeployedRuntimeEnv({ projectId: "p", region: "r", run });
207
+ const r = await checkDeployedRuntimeEnv({ ...FIREBASE, run });
159
208
  expect(r.ok).toBe(false);
160
209
  if (!r.ok) {
161
210
  expect(r.error).toContain("Run `upgrade` first");
@@ -165,29 +214,44 @@ describe("checkDeployedRuntimeEnv", () => {
165
214
  });
166
215
 
167
216
  describe("chooseImageDigests", () => {
168
- const latest = { admin: C, gateway: C };
217
+ const latest = { admin: C, gateway: C, "mcp-auth-proxy": C };
218
+ const recorded = { admin: A, gateway: B, "mcp-auth-proxy": C };
169
219
  const never = () => {
170
220
  throw new Error("must not be called");
171
221
  };
172
222
 
173
223
  it("keeps the recorded release", async () => {
224
+ const r = await chooseImageDigests({
225
+ recorded,
226
+ deployed: true,
227
+ readLive: never,
228
+ resolveLatest: never,
229
+ });
230
+ expect(r).toEqual({ ok: true, source: "recorded", digests: recorded });
231
+ });
232
+
233
+ it("#1328 — sends a record from before the MCP-Auth-Proxy to upgrade instead of mixing releases", async () => {
174
234
  const r = await chooseImageDigests({
175
235
  recorded: { admin: A, gateway: B },
176
236
  deployed: true,
177
237
  readLive: never,
178
238
  resolveLatest: never,
179
239
  });
180
- expect(r).toEqual({ ok: true, source: "recorded", digests: { admin: A, gateway: B } });
240
+ expect(r.ok).toBe(false);
241
+ if (!r.ok) {
242
+ expect(r.error).toContain("records no mcp-auth-proxy image");
243
+ expect(r.error).toContain("Run `upgrade` first");
244
+ }
181
245
  });
182
246
 
183
247
  it("keeps what a provisioned install without a record runs (no :latest roll)", async () => {
184
248
  const r = await chooseImageDigests({
185
249
  recorded: null,
186
250
  deployed: true,
187
- readLive: async () => ({ ok: true, digests: { admin: A, gateway: B } }),
251
+ readLive: async () => ({ ok: true, digests: recorded }),
188
252
  resolveLatest: never,
189
253
  });
190
- expect(r).toEqual({ ok: true, source: "live", digests: { admin: A, gateway: B } });
254
+ expect(r).toEqual({ ok: true, source: "live", digests: recorded });
191
255
  });
192
256
 
193
257
  it("stops and points at upgrade when the running release can't be read", async () => {
@@ -15,7 +15,14 @@ import { gcloud as defaultGcloud } from "./gcloud.js";
15
15
  import type { GcloudRunner } from "./gcloud-retry.js";
16
16
  import { GCP_STACK_ENV, gcpNamePrefix } from "./gcp-names.js";
17
17
  import type { ImageDigests } from "./install-state.js";
18
- import { type CloudRunSlug, RETIRED_SERVICE_ENV, SERVICE_SECRET_ENV } from "./stack-contract.js";
18
+ import {
19
+ CLOUD_RUN_SLUGS,
20
+ type CloudRunSlug,
21
+ cloudRunServicePrefix,
22
+ type GcpProvider,
23
+ RETIRED_SERVICE_ENV,
24
+ SERVICE_SECRET_ENV,
25
+ } from "./stack-contract.js";
19
26
  import { liveContainerEnv } from "./stack-converge.js";
20
27
 
21
28
  /** The `sha256:…` digest an image reference pins, or null for a tag reference. */
@@ -23,13 +30,13 @@ export function digestFromImageRef(ref: string): string | null {
23
30
  return ref.trim().match(/@(sha256:[0-9a-f]{64})$/)?.[1] ?? null;
24
31
  }
25
32
 
26
- /** The single deployed (Pulumi-suffixed) Cloud Run service for `slug`. */
33
+ /** The single deployed Cloud Run service playing `slug` (stack-contract.ts cloudRunServicePrefix). */
27
34
  async function findDeployedService(
28
35
  run: GcloudRunner,
29
- opts: { projectId: string; region: string },
36
+ opts: { provider: GcpProvider; projectId: string; region: string },
30
37
  slug: CloudRunSlug,
31
38
  ): Promise<{ ok: true; name: string } | { ok: false; error: string }> {
32
- const prefix = `${gcpNamePrefix(GCP_STACK_ENV)}-${slug}`;
39
+ const prefix = cloudRunServicePrefix(opts.provider, GCP_STACK_ENV, slug);
33
40
  const list = await run([
34
41
  "run",
35
42
  "services",
@@ -47,7 +54,10 @@ async function findDeployedService(
47
54
  if (names.length !== 1) {
48
55
  return {
49
56
  ok: false,
50
- error: `expected one ${prefix}* Cloud Run service, found ${names.length === 0 ? "none" : names.join(", ")}`,
57
+ error:
58
+ names.length === 0
59
+ ? `no ${prefix}* Cloud Run service: the install runs a release from before it (the ${slug} became its own service in #1328). Run \`upgrade\` first: it creates the service and moves the release over.`
60
+ : `expected one ${prefix}* Cloud Run service, found ${names.join(", ")}`,
51
61
  };
52
62
  }
53
63
  return { ok: true, name: names[0] as string };
@@ -68,12 +78,13 @@ async function findDeployedService(
68
78
  * and the env in one revision.
69
79
  */
70
80
  export async function checkDeployedRuntimeEnv(opts: {
81
+ provider: GcpProvider;
71
82
  projectId: string;
72
83
  region: string;
73
84
  run?: GcloudRunner;
74
85
  }): Promise<{ ok: true } | { ok: false; error: string }> {
75
86
  const run = opts.run ?? defaultGcloud;
76
- for (const slug of ["admin", "gateway"] as const) {
87
+ for (const slug of CLOUD_RUN_SLUGS) {
77
88
  const found = await findDeployedService(run, opts, slug);
78
89
  if (!found.ok) return found;
79
90
  const describe = await run([
@@ -109,18 +120,19 @@ export async function checkDeployedRuntimeEnv(opts: {
109
120
  }
110
121
 
111
122
  /**
112
- * Read the image digests the admin and gateway services run. Fails, naming
123
+ * Read the image digests the Cloud Run services run. Fails, naming
113
124
  * what is missing, when a service can't be found unambiguously or doesn't
114
125
  * run a digest-pinned image.
115
126
  */
116
127
  export async function readDeployedImageDigests(opts: {
128
+ provider: GcpProvider;
117
129
  projectId: string;
118
130
  region: string;
119
131
  run?: GcloudRunner;
120
132
  }): Promise<{ ok: true; digests: ImageDigests } | { ok: false; error: string }> {
121
133
  const run = opts.run ?? defaultGcloud;
122
- const digests = { admin: "", gateway: "" };
123
- for (const slug of ["admin", "gateway"] as const) {
134
+ const digests: Record<CloudRunSlug, string> = { admin: "", gateway: "", "mcp-auth-proxy": "" };
135
+ for (const slug of CLOUD_RUN_SLUGS) {
124
136
  const found = await findDeployedService(run, opts, slug);
125
137
  if (!found.ok) return found;
126
138
  const image = await run([
@@ -170,6 +182,15 @@ export async function chooseImageDigests(opts: {
170
182
  readonly readLive: () => ReturnType<typeof readDeployedImageDigests>;
171
183
  readonly resolveLatest: () => Promise<ImageDigests>;
172
184
  }): Promise<DigestChoice> {
185
+ if (opts.recorded && !opts.recorded["mcp-auth-proxy"]) {
186
+ // #1328 — the release it records predates the MCP-Auth-Proxy: this
187
+ // installer's stack would deploy a proxy image of another release.
188
+ return {
189
+ ok: false,
190
+ error:
191
+ "This install runs a release from before the MCP-Auth-Proxy (its install.json records no mcp-auth-proxy image). Run `upgrade` first — it sets the proxy up and records the release — then re-run the installer.",
192
+ };
193
+ }
173
194
  if (opts.recorded) return { ok: true, source: "recorded", digests: opts.recorded };
174
195
  if (!opts.deployed) return { ok: true, source: "latest", digests: await opts.resolveLatest() };
175
196
  const live = await opts.readLive();
@@ -65,7 +65,7 @@ describe("gcp + gcp-firebase: the gateway env contract (#635)", () => {
65
65
  domain: "example.com",
66
66
  region: "europe-west1",
67
67
  stagingHost: "staging.example.com",
68
- databaseUrls: databaseUrls("10.20.0.3"),
68
+ databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
69
69
  });
70
70
 
71
71
  it("carries no cms_admin URL, role or credential name", () => {
@@ -75,7 +75,8 @@ describe("gcp + gcp-firebase: the gateway env contract (#635)", () => {
75
75
  }
76
76
  expect(env.find((v) => v.name === "PUBLIC_DATABASE_URL")).toEqual({
77
77
  name: "PUBLIC_DATABASE_URL",
78
- value: "postgresql://public_role@10.20.0.3:5432/cms_public?sslmode=require",
78
+ value:
79
+ "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",
79
80
  });
80
81
  });
81
82
 
@@ -135,6 +136,7 @@ describe("self-hosted: the generated compose gateway service (#635)", () => {
135
136
  postgresPassword: "superpw",
136
137
  rolePasswords: { admin: "adminpw", public: "publicpw" },
137
138
  caeloSecretKek: "0".repeat(64),
139
+ dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
138
140
  internalSecret: "i".repeat(48),
139
141
  diskSize: "20Gi",
140
142
  images: {
@@ -151,9 +153,12 @@ describe("self-hosted: the generated compose gateway service (#635)", () => {
151
153
  expect(gateway).toContain("PUBLIC_DATABASE_URL:");
152
154
  expect(gateway).not.toMatch(ADMIN_ACCESS);
153
155
  expect(gateway).not.toMatch(/superpw|adminpw|gatewaypw/);
154
- // No other way in: no Compose secrets, no env file, no mounts (a
155
- // mounted file is read by no env check), and exactly its own env.
156
- expect(gateway).not.toMatch(/^ {4}(secrets|env_file|volumes):/m);
156
+ // No other way in: no Compose secrets, no env file, and exactly one
157
+ // mount — the database CA certificate (#1338), public, no credential.
158
+ expect(gateway).not.toMatch(/^ {4}(secrets|env_file):/m);
159
+ expect(gateway).toContain(
160
+ " volumes:\n - ./postgres-tls/ca.crt:/etc/caelo/db-ca/self-hosted-ca.pem:ro\n restart:",
161
+ );
157
162
  const env = gateway.slice(gateway.indexOf(" environment:"));
158
163
  const keys = [...env.matchAll(/^ {6}([A-Z_][A-Z0-9_]*):/gm)].map((m) => m[1]);
159
164
  expect(keys.sort()).toEqual([
package/src/gcp-names.ts CHANGED
@@ -56,6 +56,37 @@ export function gatewayServiceAccountEmail(projectId: string, env: string): stri
56
56
  return `${gatewayServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
57
57
  }
58
58
 
59
+ /**
60
+ * #1328 — account id of the service account the MCP-Auth-Proxy runs as. It
61
+ * may read the Google sign-in client and the broker's seal key and nothing
62
+ * else: no database password, no project role (stack-contract.ts
63
+ * `SERVICE_SECRET_ENV` / `SERVICE_SECRET_FILES`). Shortened from the
64
+ * service's name: account ids max out at 30 characters.
65
+ */
66
+ export function mcpAuthProxyServiceAccountId(env: string): string {
67
+ return `${gcpNamePrefix(env)}-mcp-proxy-sa`;
68
+ }
69
+
70
+ /**
71
+ * #1328 — the `gcp` MCP-Auth-Proxy's load-balancer resources, CLI-owned
72
+ * (remote-mcp.ts ensureRemoteMcpEdge): its serverless NEG, its backend
73
+ * service and its own Cloud Armor policy. None matches a prefix the other
74
+ * lookups use (`…-waf`, `…-admin-backend`, `…-gateway-backend`).
75
+ */
76
+ export function mcpAuthProxyEdgeNames(env: string): {
77
+ readonly neg: string;
78
+ readonly backend: string;
79
+ readonly policy: string;
80
+ } {
81
+ const prefix = `${gcpNamePrefix(env)}-mcp-auth-proxy`;
82
+ return { neg: `${prefix}-neg`, backend: `${prefix}-backend`, policy: `${prefix}-armor` };
83
+ }
84
+
85
+ /** Email of the service account the MCP-Auth-Proxy runs as. */
86
+ export function mcpAuthProxyServiceAccountEmail(projectId: string, env: string): string {
87
+ return `${mcpAuthProxyServiceAccountId(env)}@${projectId}.iam.gserviceaccount.com`;
88
+ }
89
+
59
90
  /**
60
91
  * Account id of the `gcp` stack's static-publisher service account. Account
61
92
  * ids max out at 30 chars, so the env is shortened.
@@ -9,7 +9,7 @@
9
9
  import { beforeAll, describe, expect, it } from "bun:test";
10
10
  import { readFileSync } from "node:fs";
11
11
  import { resolve } from "node:path";
12
- import { GCP_PUBLIC_BASE_PATHS, isGcpRedirectRule, withGcpRedirects } from "@caelo-cms/shared";
12
+ import { GCP_GATEWAY_PATHS, isGcpRedirectRule, withGcpRedirects } from "@caelo-cms/shared";
13
13
  import * as pulumi from "@pulumi/pulumi";
14
14
  import { URL_MAP_REDIRECTS_PERMISSIONS, urlMapRedirectsRoleId } from "./gcp-names.js";
15
15
 
@@ -66,7 +66,8 @@ describe("gcp stack URL map (Pulumi mocks)", () => {
66
66
  {
67
67
  name: PUBLIC_PATH_MATCHER,
68
68
  defaultService: "bucket-id",
69
- pathRules: [{ paths: [...GCP_PUBLIC_BASE_PATHS], service: "gateway-id" }],
69
+ // #1328 — only /api/* here: the MCP-Auth-Proxy's rule is the CLI's.
70
+ pathRules: [{ paths: [...GCP_GATEWAY_PATHS], service: "gateway-id" }],
70
71
  },
71
72
  { name: "admin", defaultService: "admin-id" },
72
73
  ]);
@@ -102,12 +103,9 @@ describe("gcp stack URL map (Pulumi mocks)", () => {
102
103
  );
103
104
  expect(withRedirects.pathRules?.filter((r) => !isGcpRedirectRule(r))).toEqual([
104
105
  {
105
- paths: [
106
- "/api/*",
107
- // #806 — the remote-MCP broker's discovery documents.
108
- "/.well-known/oauth-protected-resource/*",
109
- "/.well-known/oauth-authorization-server/*",
110
- ],
106
+ // #1328 — the remote-MCP discovery paths go to the MCP-Auth-Proxy's
107
+ // own rule, which the CLI adds (remote-mcp.ts planUrlMapPaths).
108
+ paths: ["/api/*"],
111
109
  service: "gateway-id",
112
110
  },
113
111
  ]);
@@ -19,7 +19,7 @@
19
19
  * Imported by the stack from `dist/`; keep it free of CLI dependencies.
20
20
  */
21
21
 
22
- import { GCP_PUBLIC_BASE_PATHS } from "@caelo-cms/shared";
22
+ import { GCP_GATEWAY_PATHS } from "@caelo-cms/shared";
23
23
  import * as gcp from "@pulumi/gcp";
24
24
  import type * as pulumi from "@pulumi/pulumi";
25
25
 
@@ -58,7 +58,11 @@ export function siteUrlMapArgs(i: SiteUrlMapInputs): gcp.compute.URLMapArgs {
58
58
  {
59
59
  name: PUBLIC_PATH_MATCHER,
60
60
  defaultService: i.staticBackend,
61
- pathRules: [{ paths: [...GCP_PUBLIC_BASE_PATHS], service: i.gatewayBackend }],
61
+ // #1328 — the MCP-Auth-Proxy's rule (GCP_MCP_AUTH_PROXY_PATHS) is the
62
+ // CLI's: it creates the proxy's backend after the first `up`
63
+ // (remote-mcp.ts ensureRemoteMcpEdge), and the stack ignores
64
+ // `pathRules` after creation anyway.
65
+ pathRules: [{ paths: [...GCP_GATEWAY_PATHS], service: i.gatewayBackend }],
62
66
  },
63
67
  {
64
68
  name: "admin",
package/src/index.test.ts CHANGED
@@ -22,6 +22,7 @@ describe("Caddyfile generator", () => {
22
22
  tls: "acme",
23
23
  adminUpstream: "caelo-admin:5173",
24
24
  gatewayUpstream: "caelo-gateway:8090",
25
+ mcpAuthProxyUpstream: "caelo-mcp-auth-proxy:8095",
25
26
  domains: [
26
27
  { hostname: "example.com", kind: "public", env: "production" },
27
28
  { hostname: "staging.example.com", kind: "public", env: "staging" },
@@ -49,6 +50,7 @@ describe("Caddyfile generator", () => {
49
50
  tls: "acme",
50
51
  adminUpstream: "caelo-admin:5173",
51
52
  gatewayUpstream: "caelo-gateway:8090",
53
+ mcpAuthProxyUpstream: "caelo-mcp-auth-proxy:8095",
52
54
  domains: [],
53
55
  });
54
56
  expect(out).toContain(":8081 {");
@@ -64,6 +66,7 @@ describe("Caddy cache policy for content-hashed assets", () => {
64
66
  tls: "acme",
65
67
  adminUpstream: "caelo-admin:5173",
66
68
  gatewayUpstream: "caelo-gateway:8090",
69
+ mcpAuthProxyUpstream: "caelo-mcp-auth-proxy:8095",
67
70
  domains: [
68
71
  { hostname: "example.com", kind: "public", env: "production" },
69
72
  { hostname: "admin.example.com", kind: "admin", env: "production" },
@@ -151,6 +154,7 @@ describe("docker-compose generator", () => {
151
154
  postgresPassword: "supersecret",
152
155
  rolePasswords: { admin: "adminpw", public: "publicpw" },
153
156
  caeloSecretKek: "0".repeat(64),
157
+ dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
154
158
  internalSecret: "i".repeat(48),
155
159
  anthropicApiKey: "sk-ant-…",
156
160
  diskSize: "20Gi",
@@ -173,6 +177,7 @@ describe("docker-compose generator", () => {
173
177
  postgresPassword: "supersecret",
174
178
  rolePasswords: { admin: "adminpw", public: "publicpw" },
175
179
  caeloSecretKek: "0".repeat(64),
180
+ dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
176
181
  internalSecret: "i".repeat(48),
177
182
  diskSize: "20Gi",
178
183
  images: RELEASE_IMAGES,
@@ -181,7 +186,7 @@ describe("docker-compose generator", () => {
181
186
  });
182
187
  const gateway = out.slice(out.indexOf("caelo-gateway:"), out.indexOf("\nvolumes:"));
183
188
  expect(gateway).toContain(
184
- 'PUBLIC_DATABASE_URL: "postgres://public_role:publicpw@postgres:5432/cms_public"',
189
+ 'PUBLIC_DATABASE_URL: "postgres://public_role:publicpw@postgres:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/self-hosted-ca.pem"',
185
190
  );
186
191
  expect(gateway).not.toMatch(
187
192
  /ADMIN_DATABASE|GATEWAY_DATABASE|cms_admin|admin_role|gateway_role|adminpw|gatewaypw|supersecret|CAELO_SECRET_KEK/,
@@ -198,6 +203,7 @@ describe("docker-compose generator", () => {
198
203
  postgresPassword: 'pa"ss',
199
204
  rolePasswords: { admin: "a", public: "b" },
200
205
  caeloSecretKek: "0".repeat(64),
206
+ dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
201
207
  internalSecret: "i".repeat(48),
202
208
  diskSize: "1Gi",
203
209
  images: RELEASE_IMAGES,
package/src/index.ts CHANGED
@@ -96,6 +96,7 @@ export {
96
96
  type RequestDecision,
97
97
  } from "./upgrader-assess.js";
98
98
  export {
99
+ convergeDatabaseHost,
99
100
  gcpUpgraderDeps,
100
101
  type UpgraderPlacement,
101
102
  upgraderDatabaseHost,
@@ -20,8 +20,8 @@ describe("infrastructure contract steps", () => {
20
20
  );
21
21
  });
22
22
 
23
- it("is at 14: public_role revoke (3), URL-map redirects role (4), no gateway cms_admin login (5), ops-bucket list (6), gateway proxy hops (7), quality-audit job (8), converge job (9), remote MCP (10), Google sign-in (11), project-level sign-in client (12), edge security headers (13), capture job (14)", () => {
24
- expect(INFRA_CONTRACT_VERSION).toBe(14);
23
+ it("is at 16: public_role revoke (3), URL-map redirects role (4), no gateway cms_admin login (5), ops-bucket list (6), gateway proxy hops (7), quality-audit job (8), converge job (9), remote MCP (10), Google sign-in (11), project-level sign-in client (12), edge security headers (13), capture job (14), database verify-full TLS (15), MCP-Auth-Proxy (16)", () => {
24
+ expect(INFRA_CONTRACT_VERSION).toBe(16);
25
25
  expect(INFRA_CONTRACT_STEPS[3]).toContain("#633");
26
26
  expect(INFRA_CONTRACT_STEPS[4]).toContain("#587");
27
27
  expect(INFRA_CONTRACT_STEPS[5]).toContain("#635");
@@ -34,6 +34,8 @@ describe("infrastructure contract steps", () => {
34
34
  expect(INFRA_CONTRACT_STEPS[12]).toContain("project's IAP settings");
35
35
  expect(INFRA_CONTRACT_STEPS[13]).toContain("#891");
36
36
  expect(INFRA_CONTRACT_STEPS[14]).toContain("#1217");
37
+ expect(INFRA_CONTRACT_STEPS[15]).toContain("#1338");
38
+ expect(INFRA_CONTRACT_STEPS[16]).toContain("#1328");
37
39
  });
38
40
 
39
41
  it("lists the pending steps oldest first", () => {
@@ -44,6 +46,6 @@ describe("infrastructure contract steps", () => {
44
46
  });
45
47
 
46
48
  it("names a version a newer release added by number", () => {
47
- expect(infraStepsText(14, 15)).toBe("infrastructure step 15 (see that release's notes)");
49
+ expect(infraStepsText(16, 17)).toBe("infrastructure step 17 (see that release's notes)");
48
50
  });
49
51
  });