@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
@@ -29,13 +29,21 @@ import {
29
29
  } from "./upgrader-records.js";
30
30
 
31
31
  const D = (c: string) => `sha256:${c.repeat(64)}`;
32
+ // #1328 — the MCP-Auth-Proxy's index; serving revisions name it as is here,
33
+ // so it matches without a registry lookup.
34
+ const PROXY_INDEX = `sha256:${"e".repeat(64)}`;
32
35
  const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
33
36
 
34
37
  const V064: ReleaseManifest = {
35
38
  manifestVersion: 1,
36
39
  version: "0.10.64",
37
40
  channel: "stable",
38
- digests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX, ops: D("c") },
41
+ digests: {
42
+ admin: ADMIN_INDEX,
43
+ gateway: GATEWAY_INDEX,
44
+ ops: D("c"),
45
+ "mcp-auth-proxy": PROXY_INDEX,
46
+ },
39
47
  minFromVersion: "0.10.0",
40
48
  infraContractVersion: 13,
41
49
  migrations: { contract: false, lastContractVersion: null, head: "0338_x" },
@@ -51,13 +59,17 @@ const META: InstallMetadata = {
51
59
  ownerEmail: "owner@acme.com",
52
60
  region: "europe-west1",
53
61
  createdAt: "2026-01-01T00:00:00Z",
54
- imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX },
62
+ imageDigests: { admin: ADMIN_INDEX, gateway: GATEWAY_INDEX, "mcp-auth-proxy": PROXY_INDEX },
55
63
  releaseVersion: "0.10.64",
56
64
  infraContractVersion: 13,
57
65
  };
58
66
 
59
67
  /** What the serving revisions read back as on searchviu-website (spec = status). */
60
- const LIVE_SERVING: ServingDigests = { admin: [ADMIN_AMD64], gateway: [GATEWAY_AMD64] };
68
+ const LIVE_SERVING: ServingDigests = {
69
+ admin: [ADMIN_AMD64],
70
+ gateway: [GATEWAY_AMD64],
71
+ "mcp-auth-proxy": [PROXY_INDEX],
72
+ };
61
73
 
62
74
  function deps(
63
75
  opts: {
@@ -112,16 +124,38 @@ describe("recordMatchesServing", () => {
112
124
  const both = { ...META, platformDigests: { admin: ADMIN_AMD64, gateway: GATEWAY_AMD64 } };
113
125
  expect(recordMatchesServing(both, LIVE_SERVING)).toBe(true);
114
126
  // Still by the index when a revision names it.
115
- expect(recordMatchesServing(META, { admin: [ADMIN_INDEX], gateway: [GATEWAY_INDEX] })).toBe(
116
- true,
117
- );
127
+ expect(
128
+ recordMatchesServing(META, {
129
+ admin: [ADMIN_INDEX],
130
+ gateway: [GATEWAY_INDEX],
131
+ "mcp-auth-proxy": [PROXY_INDEX],
132
+ }),
133
+ ).toBe(true);
118
134
  });
119
135
 
120
136
  it("both services must match, and no record never matches", () => {
121
137
  const both = { ...META, platformDigests: { admin: ADMIN_AMD64, gateway: GATEWAY_AMD64 } };
122
- expect(recordMatchesServing(both, { admin: [ADMIN_AMD64], gateway: [D("6")] })).toBe(false);
138
+ expect(
139
+ recordMatchesServing(both, {
140
+ admin: [ADMIN_AMD64],
141
+ gateway: [D("6")],
142
+ "mcp-auth-proxy": [PROXY_INDEX],
143
+ }),
144
+ ).toBe(false);
123
145
  expect(recordMatchesServing({}, LIVE_SERVING)).toBe(false);
124
146
  });
147
+
148
+ it("#1328: an install.json from before the MCP-Auth-Proxy (no proxy digest) never matches", () => {
149
+ const { "mcp-auth-proxy": _none, ...old } = META.imageDigests as NonNullable<
150
+ InstallMetadata["imageDigests"]
151
+ >;
152
+ const both = {
153
+ ...META,
154
+ imageDigests: old,
155
+ platformDigests: { admin: ADMIN_AMD64, gateway: GATEWAY_AMD64 },
156
+ };
157
+ expect(recordMatchesServing(both, LIVE_SERVING)).toBe(false);
158
+ });
125
159
  });
126
160
 
127
161
  describe("findServingRelease", () => {
@@ -141,12 +175,12 @@ describe("findServingRelease", () => {
141
175
  it("an attestation manifest of the index is not a serving image of the release", async () => {
142
176
  const r = await findServingRelease(
143
177
  META,
144
- { admin: [ADMIN_ATTESTATION], gateway: [GATEWAY_AMD64] },
178
+ { admin: [ADMIN_ATTESTATION], gateway: [GATEWAY_AMD64], "mcp-auth-proxy": [PROXY_INDEX] },
145
179
  deps(),
146
180
  );
147
181
  expect(r.ok).toBe(false);
148
182
  expect(r.ok ? "" : r.reason).toContain(
149
- "The gateway serves v0.10.64's image, the admin does not.",
183
+ "The gateway and the MCP-Auth-Proxy serve v0.10.64's image, the admin does not.",
150
184
  );
151
185
  });
152
186
 
@@ -166,7 +200,11 @@ describe("findServingRelease", () => {
166
200
  });
167
201
 
168
202
  it("a digest in no signed index is still refused with the current message", async () => {
169
- const r = await findServingRelease(META, { admin: [D("7")], gateway: [D("6")] }, deps());
203
+ const r = await findServingRelease(
204
+ META,
205
+ { admin: [D("7")], gateway: [D("6")], "mcp-auth-proxy": [PROXY_INDEX] },
206
+ deps(),
207
+ );
170
208
  expect(r).toMatchObject({ ok: false, retryable: false });
171
209
  expect(r.ok ? "" : r.reason).toContain(
172
210
  "are not the ones install.json names, and they do not belong to one signed Caelo release",
@@ -190,13 +228,60 @@ describe("findServingRelease", () => {
190
228
 
191
229
  it("finds the release by the template's index when install.json names none", async () => {
192
230
  const { releaseVersion: _none, ...unversioned } = META;
193
- const meta = { ...unversioned, imageDigests: { admin: D("9"), gateway: D("8") } };
231
+ const meta = {
232
+ ...unversioned,
233
+ imageDigests: { admin: D("9"), gateway: D("8"), "mcp-auth-proxy": D("5") },
234
+ };
194
235
  const d = deps({
195
236
  tags: { admin: { "0.10.64": ADMIN_INDEX } },
196
- hints: { admin: [ADMIN_INDEX], gateway: [GATEWAY_INDEX] },
237
+ hints: { admin: [ADMIN_INDEX], gateway: [GATEWAY_INDEX], "mcp-auth-proxy": [PROXY_INDEX] },
197
238
  });
198
239
  const r = await findServingRelease(meta, LIVE_SERVING, d);
199
240
  expect(r.ok && r.manifest.version).toBe("0.10.64");
200
241
  expect(d.fetched).toEqual(["0.10.64"]);
201
242
  });
202
243
  });
244
+
245
+ describe("findServingRelease — the MCP-Auth-Proxy (#1328)", () => {
246
+ it("records the proxy's signed platform form with the others", async () => {
247
+ const PROXY_AMD64 = `sha256:${"f".repeat(64)}`;
248
+ const signed: ReleaseManifest = {
249
+ ...V064,
250
+ platformDigests: {
251
+ admin: ADMIN_AMD64,
252
+ gateway: GATEWAY_AMD64,
253
+ ops: D("3"),
254
+ "mcp-auth-proxy": PROXY_AMD64,
255
+ },
256
+ };
257
+ const r = await findServingRelease(
258
+ META,
259
+ { ...LIVE_SERVING, "mcp-auth-proxy": [PROXY_AMD64] },
260
+ deps({ manifests: { "0.10.64": signed } }),
261
+ );
262
+ expect(r).toMatchObject({
263
+ ok: true,
264
+ platformDigests: {
265
+ admin: ADMIN_AMD64,
266
+ gateway: GATEWAY_AMD64,
267
+ "mcp-auth-proxy": PROXY_AMD64,
268
+ },
269
+ });
270
+ });
271
+
272
+ it("a release from before the proxy cannot be what serves (its manifest names no proxy)", async () => {
273
+ const { "mcp-auth-proxy": _none, ...digests } = V064.digests;
274
+ const old: ReleaseManifest = { ...V064, digests };
275
+ const r = await findServingRelease(META, LIVE_SERVING, deps({ manifests: { "0.10.64": old } }));
276
+ expect(r.ok).toBe(false);
277
+ });
278
+
279
+ it("a proxy of another release is refused", async () => {
280
+ const r = await findServingRelease(
281
+ META,
282
+ { ...LIVE_SERVING, "mcp-auth-proxy": [D("4")] },
283
+ deps(),
284
+ );
285
+ expect(r.ok).toBe(false);
286
+ });
287
+ });
@@ -30,14 +30,16 @@
30
30
  import type { GcloudRunner } from "./gcloud-retry.js";
31
31
  import type { ImageDigests, InstallMetadata } from "./install-state.js";
32
32
  import type { ReleaseManifest, ReleaseManifestResult } from "./release-manifest.js";
33
- import type { RollSlug } from "./upgrade/app-roll.js";
33
+ import { ROLL_ORDER, type RollSlug } from "./upgrade/app-roll.js";
34
34
  import type { ReleaseImageForms } from "./upgrade/image-forms.js";
35
35
  import { releaseVersionForDigest } from "./upgrade/release-images.js";
36
36
 
37
37
  /** Every digest each serving image is known by (gcp-lookup.ts servingImageDigests). */
38
38
  export type ServingDigests = Readonly<Record<RollSlug, readonly string[]>>;
39
39
 
40
- const SLUGS: readonly RollSlug[] = ["admin", "gateway"];
40
+ // #1328 — the MCP-Auth-Proxy serves on every install this code runs on (the
41
+ // upgrader runs only on installs converged to its contract).
42
+ const SLUGS: readonly RollSlug[] = ROLL_ORDER;
41
43
 
42
44
  /**
43
45
  * Whether install.json names the images that serve: per service, the
@@ -50,11 +52,14 @@ export function recordMatchesServing(
50
52
  ): boolean {
51
53
  const { imageDigests, platformDigests } = recorded;
52
54
  if (!imageDigests) return false;
53
- return SLUGS.every(
54
- (slug) =>
55
- serving[slug].includes(imageDigests[slug]) ||
56
- (platformDigests !== undefined && serving[slug].includes(platformDigests[slug])),
57
- );
55
+ return SLUGS.every((slug) => {
56
+ const index = imageDigests[slug];
57
+ const platform = platformDigests?.[slug];
58
+ return (
59
+ (index !== undefined && serving[slug].includes(index)) ||
60
+ (platform !== undefined && serving[slug].includes(platform))
61
+ );
62
+ });
58
63
  }
59
64
 
60
65
  /** What {@link findServingRelease} learned. */
@@ -139,7 +144,7 @@ export async function findServingRelease(
139
144
  if (judged.matching.length > 0) {
140
145
  const other = SLUGS.filter((s) => !judged.matching.includes(s));
141
146
  partial.push(
142
- `the ${judged.matching.join(", ")} serves v${m.version}'s image, the ${other.join(", ")} does not`,
147
+ `${serviceList(judged.matching)} ${judged.matching.length > 1 ? "serve" : "serves"} v${m.version}'s image, ${serviceList(other)} ${other.length > 1 ? "do" : "does"} not`,
143
148
  );
144
149
  }
145
150
  }
@@ -162,7 +167,7 @@ export async function findServingRelease(
162
167
  ...(unverified.length > 0
163
168
  ? [`These release records could not be verified: ${unverified.join("; ")}.`]
164
169
  : []),
165
- "Nothing was changed. Check who deployed these images (the Cloud Run revision history of both services); an upgrade with Cloud Shell puts a signed release back.",
170
+ "Nothing was changed. Check who deployed these images (the Cloud Run revision history of the services); an upgrade with Cloud Shell puts a signed release back.",
166
171
  ].join(" "),
167
172
  };
168
173
  }
@@ -189,7 +194,10 @@ async function judgeRelease(
189
194
  const matching: RollSlug[] = [];
190
195
  const platform: Partial<Record<RollSlug, string>> = {};
191
196
  for (const slug of SLUGS) {
192
- if (serving[slug].includes(m.digests[slug])) {
197
+ const index = m.digests[slug];
198
+ // #1328 — a release from before the proxy cannot be what serves here.
199
+ if (!index) continue;
200
+ if (serving[slug].includes(index)) {
193
201
  matching.push(slug);
194
202
  const signed = m.platformDigests?.[slug];
195
203
  if (signed) platform[slug] = signed;
@@ -205,10 +213,12 @@ async function judgeRelease(
205
213
  }
206
214
  }
207
215
  const { admin, gateway } = platform;
216
+ const proxy = platform["mcp-auth-proxy"];
208
217
  return {
209
218
  kind: "judged",
210
219
  matching,
211
- platformDigests: admin && gateway ? { admin, gateway } : null,
220
+ platformDigests:
221
+ admin && gateway ? { admin, gateway, ...(proxy ? { "mcp-auth-proxy": proxy } : {}) } : null,
212
222
  };
213
223
  }
214
224
 
@@ -245,3 +255,10 @@ async function* candidateVersions(
245
255
  function capitalize(s: string): string {
246
256
  return s.charAt(0).toUpperCase() + s.slice(1);
247
257
  }
258
+
259
+ /** `the admin`, `the gateway and the MCP-Auth-Proxy`, … for the refusal text. */
260
+ function serviceList(slugs: readonly RollSlug[]): string {
261
+ const names = slugs.map((s) => (s === "mcp-auth-proxy" ? "the MCP-Auth-Proxy" : `the ${s}`));
262
+ if (names.length <= 1) return names[0] ?? "";
263
+ return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
264
+ }
@@ -81,6 +81,8 @@ describe("ensureUpgraderJob", () => {
81
81
  "projects add-iam-policy-binding acme → projects/acme/roles/caeloUpgraderSqlBackup",
82
82
  "iam service-accounts add-iam-policy-binding caelo-production-run-sa@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
83
83
  "iam service-accounts add-iam-policy-binding caelo-production-gateway-sa@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
84
+ // #1328 — the MCP-Auth-Proxy's revisions run as its own account.
85
+ "iam service-accounts add-iam-policy-binding caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
84
86
  "iam service-accounts add-iam-policy-binding caelo-production-upgrader@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
85
87
  "iam service-accounts add-iam-policy-binding caelo-mcp@acme.iam.gserviceaccount.com → roles/iam.serviceAccountTokenCreator",
86
88
  "iam service-accounts add-iam-policy-binding caelo-production-audit@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
@@ -27,11 +27,11 @@
27
27
  * PULUMI_*) and so act as the provisioner (#883 review). The custom role
28
28
  * has no `runWithOverrides` and no `*.setIamPolicy`. Updating another
29
29
  * job (the converge job's image, say) additionally needs `actAs` on its
30
- * service account, which the upgrader holds only on the admin, gateway
31
- * and its own. A grant of `roles/run.developer` left by an earlier
30
+ * service account, which the upgrader holds only on the admin, gateway,
31
+ * MCP-Auth-Proxy and its own. A grant of `roles/run.developer` left by an earlier
32
32
  * setup is removed.
33
- * - `roles/iam.serviceAccountUser` on exactly the admin's and the
34
- * gateway's runtime service accounts (a revision runs as them), on
33
+ * - `roles/iam.serviceAccountUser` on exactly the admin's, the
34
+ * gateway's and the MCP-Auth-Proxy's (#1328) runtime service accounts (a revision runs as them), on
35
35
  * itself (its own job) and on the quality-audit and capture jobs'
36
36
  * accounts (the app-roll moves those jobs' images, #875, #1217) — not
37
37
  * on the project.
@@ -79,6 +79,7 @@ import {
79
79
  auditServiceAccountEmail,
80
80
  captureServiceAccountEmail,
81
81
  gatewayServiceAccountEmail,
82
+ mcpAuthProxyServiceAccountEmail,
82
83
  mcpIapServiceAccountEmail,
83
84
  runServiceAccountEmail,
84
85
  upgraderJobName,
@@ -367,6 +368,11 @@ export async function ensureUpgraderJob(
367
368
  role: "roles/iam.serviceAccountUser",
368
369
  why: "gateway revisions run as it",
369
370
  },
371
+ {
372
+ account: mcpAuthProxyServiceAccountEmail(projectId, env),
373
+ role: "roles/iam.serviceAccountUser",
374
+ why: "MCP-Auth-Proxy revisions run as it (#1328)",
375
+ },
370
376
  { account: sa, role: "roles/iam.serviceAccountUser", why: "the job updates its own image" },
371
377
  {
372
378
  account: auditServiceAccountEmail(projectId, env),
@@ -0,0 +1,50 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * #1328 — the pre-flight cost table shows the MCP-Auth-Proxy on both GCP
5
+ * variants, its own Cloud Armor policy on `gcp`, and the gcp-firebase data
6
+ * gateway (CLAUDE.md §11.C: every billable resource has a cost line).
7
+ */
8
+
9
+ import { describe, expect, it } from "bun:test";
10
+ import { type CostEstimateInputs, estimateGcpCost } from "./gcp-cost.js";
11
+
12
+ const inputs: CostEstimateInputs = {
13
+ cloudSqlTier: "db-f1-micro",
14
+ cloudSqlHa: false,
15
+ adminMinInstances: 0,
16
+ gatewayMinInstances: 0,
17
+ wafAdaptiveProtection: false,
18
+ region: "europe-west1",
19
+ };
20
+
21
+ function names(provider: "gcp" | "gcp-firebase"): string[] {
22
+ return estimateGcpCost({ ...inputs, provider }).lines.map((l) => l.name);
23
+ }
24
+
25
+ describe("cost table: MCP-Auth-Proxy (#1328)", () => {
26
+ it("lists the proxy's Cloud Run service at $0 idle on both variants", () => {
27
+ for (const provider of ["gcp", "gcp-firebase"] as const) {
28
+ const line = estimateGcpCost({ ...inputs, provider }).lines.find(
29
+ (l) => l.name === "Cloud Run MCP-Auth-Proxy",
30
+ );
31
+ expect(line?.monthlyUsd).toBe(0);
32
+ expect(line?.notes).toContain("scale-to-zero");
33
+ }
34
+ });
35
+
36
+ it("prices the proxy's own Cloud Armor policy on gcp only (policy + 2 rules)", () => {
37
+ const gcp = estimateGcpCost({ ...inputs, provider: "gcp" }).lines.find((l) =>
38
+ l.name.startsWith("MCP-Auth-Proxy backend"),
39
+ );
40
+ expect(gcp?.monthlyUsd).toBe(7);
41
+ expect(names("gcp-firebase").some((n) => n.startsWith("MCP-Auth-Proxy backend"))).toBe(false);
42
+ // The throttle moved off the gateway's policy: no separate rule line.
43
+ expect(names("gcp")).not.toContain("Cloud Armor rule: remote MCP throttle");
44
+ });
45
+
46
+ it("names the gcp-firebase visitor gateway as the data gateway", () => {
47
+ expect(names("gcp-firebase")).toContain("Cloud Run data gateway");
48
+ expect(names("gcp")).toContain("Cloud Run gateway");
49
+ });
50
+ });
@@ -116,13 +116,24 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
116
116
  "one short container per server-side screenshot or page render (~20-40 s at 1 vCPU / 2 GiB): ~$0.001 each, a 50-page site import ~$0.05; mostly in the free tier; capture bucket ~$0",
117
117
  },
118
118
  {
119
- name: "Cloud Run gateway",
119
+ // #1328 — on gcp-firebase the visitor gateway is the new
120
+ // `caelo-<env>-data-gateway` service; same billing either way.
121
+ name: provider === "gcp" ? "Cloud Run gateway" : "Cloud Run data gateway",
120
122
  monthlyUsd: inputs.gatewayMinInstances === 0 ? 1 : 1 + inputs.gatewayMinInstances * 15,
121
123
  notes:
122
124
  inputs.gatewayMinInstances === 0
123
125
  ? "scale-to-zero; ~$1/mo light traffic"
124
126
  : `${inputs.gatewayMinInstances} min-instance${inputs.gatewayMinInstances > 1 ? "s" : ""}`,
125
127
  },
128
+ {
129
+ // #1328 — remote MCP's sign-in service, its own service on every
130
+ // install (E14 a of #763): always provisioned, zero minimum instances,
131
+ // CPU only during requests, so it costs nothing while nobody connects.
132
+ name: "Cloud Run MCP-Auth-Proxy",
133
+ monthlyUsd: 0,
134
+ notes:
135
+ "scale-to-zero, request-based CPU; only external AI apps' sign-in and MCP calls wake it, mostly in the free tier",
136
+ },
126
137
  // v0.3.3 — LB / Cloud CDN / Cloud Armor lines apply only to the
127
138
  // gcp variant. gcp-firebase has no LB (Firebase Hosting is the
128
139
  // edge); no Cloud CDN line (Firebase Hosting has its own CDN
@@ -148,12 +159,18 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
148
159
  : "Free tier — rate limit + OWASP basic rules",
149
160
  },
150
161
  {
151
- // #806 — the remote-MCP throttle (src/remote-mcp-edge.ts) is one
152
- // more rule on the gateway's policy; Cloud Armor Standard bills
153
- // each rule at about $1/mo.
154
- name: "Cloud Armor rule: remote MCP throttle",
155
- monthlyUsd: 1,
156
- notes: "Per-IP throttle for /api/mcp/* (remote MCP clients)",
162
+ // #1328 — the MCP-Auth-Proxy's own backend (serverless NEG +
163
+ // backend service, no extra LB fee: same URL map) with its own
164
+ // Cloud Armor policy carrying the remote-MCP throttle
165
+ // (src/remote-mcp-edge.ts) and the default rule. Cloud Armor
166
+ // Standard: $0.006849315/policy-hour (~$5/mo) and
167
+ // $0.001369863/rule-hour (~$1/mo), plus $0.75 per million
168
+ // requests on a global policy — cloud.google.com/armor/pricing,
169
+ // checked 2026-11-02.
170
+ name: "MCP-Auth-Proxy backend + Cloud Armor policy",
171
+ monthlyUsd: 7,
172
+ notes:
173
+ "own policy ($5) with 2 rules ($2): per-IP throttle for remote MCP clients; requests ~$0",
157
174
  },
158
175
  ] satisfies CostLine[])
159
176
  : ([
@@ -30,6 +30,7 @@ import { randomBytes } from "node:crypto";
30
30
  import { existsSync, readFileSync } from "node:fs";
31
31
  import { homedir } from "node:os";
32
32
  import { join } from "node:path";
33
+ import { CLOUD_SQL_CA_BUNDLE, verifyFullQuery } from "@caelo-cms/shared";
33
34
  import { cancel, confirm, isCancel, log, note, select, spinner } from "@clack/prompts";
34
35
  import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
35
36
  import { checkAdcImpersonation } from "../adc-impersonation.js";
@@ -118,6 +119,7 @@ import type { RunMode } from "../run-mode.js";
118
119
  import { selfCommand } from "../self-command.js";
119
120
  import {
120
121
  ABSENT_DEFAULT_FIREWALL_RULES,
122
+ cloudSqlPrivateDnsName,
121
123
  type RemoteMcpIdentity,
122
124
  sameRemoteMcpIdentity,
123
125
  } from "../stack-contract.js";
@@ -134,6 +136,7 @@ import {
134
136
  import { acquireStateLock, GcsStateStore } from "../state-store.js";
135
137
  import { createReleaseImageForms, platformDigestsOf } from "../upgrade/image-forms.js";
136
138
  import {
139
+ releaseImageRef,
137
140
  releaseVersionForDigest,
138
141
  resolveOpsImageRef,
139
142
  verifyCosignAll,
@@ -357,7 +360,7 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
357
360
  const deployed = isStepDone(installId, `pulumi-up-${projectId}`);
358
361
  if (deployed) {
359
362
  // The kept release must be able to use the env the stack deploys.
360
- const runtimeEnv = await checkDeployedRuntimeEnv({ projectId, region });
363
+ const runtimeEnv = await checkDeployedRuntimeEnv({ provider, projectId, region });
361
364
  if (!runtimeEnv.ok) {
362
365
  cancel(runtimeEnv.error);
363
366
  process.exit(1);
@@ -366,7 +369,7 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
366
369
  const choice = await chooseImageDigests({
367
370
  recorded: recordedImageDigests(meta),
368
371
  deployed,
369
- readLive: () => readDeployedImageDigests({ projectId, region }),
372
+ readLive: () => readDeployedImageDigests({ provider, projectId, region }),
370
373
  resolveLatest: resolveImageDigests,
371
374
  });
372
375
  if (!choice.ok) {
@@ -531,7 +534,14 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
531
534
 
532
535
  // === 11.6. Remote MCP (#806): LB rules (gcp) + a check of the identity env ===
533
536
  // The env itself came with `pulumi up` (step 10.05, #1082).
534
- const remoteMcpReady = await stepRemoteMcp(projectId, provider, domain, identity);
537
+ const remoteMcpReady = await stepRemoteMcp(
538
+ projectId,
539
+ region,
540
+ provider,
541
+ domain,
542
+ identity,
543
+ imageDigests,
544
+ );
535
545
 
536
546
  // A fresh install ran every converge step above, so it is at this CLI's
537
547
  // infrastructure contract — recorded only when the upgrader and the
@@ -603,8 +613,12 @@ async function resolveImageDigests(): Promise<ImageDigests> {
603
613
  const project = "caelo-website";
604
614
  const registryRegion = "europe-west1";
605
615
  const repo = "caelo-cms-images";
606
- const out = { admin: "", gateway: "" };
607
- for (const service of ["admin", "gateway"] as const) {
616
+ const out: Record<"admin" | "gateway" | "mcp-auth-proxy", string> = {
617
+ admin: "",
618
+ gateway: "",
619
+ "mcp-auth-proxy": "",
620
+ };
621
+ for (const service of ["admin", "gateway", "mcp-auth-proxy"] as const) {
608
622
  const s = spinner();
609
623
  s.start(`Resolving ${service}:latest → digest...`);
610
624
  // gcloud's --filter='tag=latest' is in a transitional state (warns +
@@ -1309,9 +1323,11 @@ async function stepResolveRemoteMcpIdentity(
1309
1323
  */
1310
1324
  async function stepRemoteMcp(
1311
1325
  projectId: string,
1326
+ region: string,
1312
1327
  provider: "gcp" | "gcp-firebase",
1313
1328
  domain: string,
1314
1329
  identity: ResolvedRemoteMcpIdentity | null,
1330
+ imageDigests: ImageDigests,
1315
1331
  ): Promise<boolean> {
1316
1332
  const s = spinner();
1317
1333
  s.start(
@@ -1325,11 +1341,22 @@ async function stepRemoteMcp(
1325
1341
  return false;
1326
1342
  };
1327
1343
  if (!identity) return failed("its URLs and IAP audience could not be read (see above)");
1328
- const edge = await ensureRemoteMcpEdge({ provider, projectId, env: GCP_STACK_ENV, domain });
1344
+ const proxyDigest = imageDigests["mcp-auth-proxy"];
1345
+ if (!proxyDigest) return failed("the release records no MCP-Auth-Proxy image");
1346
+ const edge = await ensureRemoteMcpEdge({
1347
+ provider,
1348
+ projectId,
1349
+ region,
1350
+ env: GCP_STACK_ENV,
1351
+ domain,
1352
+ mcpAuthProxyImageRef: releaseImageRef("mcp-auth-proxy", proxyDigest),
1353
+ });
1329
1354
  if (!edge.ok) return failed(edge.error);
1330
1355
  const check = await checkRemoteMcpIdentityEnv(identity.target, identity.identity);
1331
1356
  if (!check.ok) return failed(check.error);
1332
- const missing = [...check.drift.admin, ...check.drift.gateway].map((c) => c.name);
1357
+ const missing = Object.values(check.drift)
1358
+ .flat()
1359
+ .map((c) => c.name);
1333
1360
  if (missing.length > 0) {
1334
1361
  return failed(`pulumi up left ${[...new Set(missing)].join(", ")} out of the services' env`);
1335
1362
  }
@@ -1905,8 +1932,10 @@ async function stepDrainLegacyCaeloAdminUser(projectId: string, region: string):
1905
1932
  pgPass,
1906
1933
  ]);
1907
1934
 
1908
- // Resolve the SQL instance's private IP from gcloud (avoids reading
1909
- // Pulumi outputs from inside the wizard).
1935
+ // Resolve the SQL instance's DNS name from gcloud (avoids reading
1936
+ // Pulumi outputs from inside the wizard). #1338: the job connects with
1937
+ // verify-full, and the shared-CA certificate names the DNS name, not the
1938
+ // private IP.
1910
1939
  const sqlDescr = await gcloud([
1911
1940
  "sql",
1912
1941
  "instances",
@@ -1914,16 +1943,28 @@ async function stepDrainLegacyCaeloAdminUser(projectId: string, region: string):
1914
1943
  "caelo-production-pg-1d65811",
1915
1944
  "--project",
1916
1945
  projectId,
1917
- "--format=value(ipAddresses[0].ipAddress)",
1946
+ "--format=json(dnsNames)",
1918
1947
  ]);
1919
- const pgHost = (sqlDescr.stdout || "").trim();
1948
+ let pgHost: string | null = null;
1949
+ try {
1950
+ pgHost = cloudSqlPrivateDnsName(
1951
+ (
1952
+ JSON.parse(sqlDescr.stdout || "{}") as {
1953
+ dnsNames?: { name?: string; connectionType?: string }[];
1954
+ }
1955
+ ).dnsNames,
1956
+ );
1957
+ } catch {
1958
+ pgHost = null;
1959
+ }
1920
1960
  if (!pgHost) {
1921
- s.stop(yellow("Could not resolve Cloud SQL private IP — skipping drain"));
1961
+ s.stop(yellow("Could not resolve the Cloud SQL instance's DNS name — skipping drain"));
1922
1962
  return;
1923
1963
  }
1924
1964
 
1925
- const adminPg = `postgres://postgres:${pgPass}@${pgHost}:5432/cms_admin`;
1926
- const publicPg = `postgres://postgres:${pgPass}@${pgHost}:5432/cms_public`;
1965
+ const tls = verifyFullQuery(CLOUD_SQL_CA_BUNDLE);
1966
+ const adminPg = `postgres://postgres:${pgPass}@${pgHost}:5432/cms_admin?${tls}`;
1967
+ const publicPg = `postgres://postgres:${pgPass}@${pgHost}:5432/cms_public?${tls}`;
1927
1968
 
1928
1969
  // The cleanup script — uses bun's globalThis.Bun.SQL (already proven
1929
1970
  // pattern in hooks.server.ts + the migration runner). DO block makes