@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
@@ -170,9 +170,9 @@ describe("enableStagingDomain (#650)", () => {
170
170
  },
171
171
  });
172
172
 
173
- it("opens the gateway's ingress, maps staging.<domain> to the gateway and prints the record", async () => {
173
+ it("#1328 — maps staging.<domain> to the data gateway (it runs the gate), opening its ingress", async () => {
174
174
  const { run, calls } = fakeGcloud({
175
- "run services list": [ok("caelo-production-gateway-xyz\n")],
175
+ "run services list": [ok("caelo-production-data-gateway\n")],
176
176
  "run services describe": [ok("internal-and-cloud-load-balancing\n")],
177
177
  "beta run domain-mappings describe": [fail("NOT_FOUND"), ok(STAGING_MAPPING)],
178
178
  "domains list-user-verified": [ok("example.com\n")],
@@ -189,21 +189,22 @@ describe("enableStagingDomain (#650)", () => {
189
189
  "run",
190
190
  "services",
191
191
  "update",
192
- "caelo-production-gateway-xyz",
192
+ "caelo-production-data-gateway",
193
193
  "--region=europe-west1",
194
194
  "--project=p",
195
195
  "--ingress=all",
196
196
  "--quiet",
197
197
  ]);
198
198
  expect(calls.find((c) => c.includes("create"))).toContain(
199
- "--service=caelo-production-gateway-xyz",
199
+ "--service=caelo-production-data-gateway",
200
200
  );
201
- expect(calls[0]).toContain("--filter=metadata.name~^caelo-production-gateway");
201
+ // Never the old gateway service, which is the MCP-Auth-Proxy now.
202
+ expect(calls[0]).toContain("--filter=metadata.name~^caelo-production-data-gateway");
202
203
  });
203
204
 
204
205
  it("is idempotent: ingress already open and mapping present ⇒ no update, no create", async () => {
205
206
  const { run, calls } = fakeGcloud({
206
- "run services list": [ok("caelo-production-gateway-xyz\n")],
207
+ "run services list": [ok("caelo-production-data-gateway\n")],
207
208
  "run services describe": [ok("all\n")],
208
209
  "beta run domain-mappings describe": [ok(STAGING_MAPPING)],
209
210
  });
@@ -215,7 +216,7 @@ describe("enableStagingDomain (#650)", () => {
215
216
 
216
217
  it("maps a host the operator chose on another domain, verifying that host itself", async () => {
217
218
  const { run, calls } = fakeGcloud({
218
- "run services list": [ok("caelo-production-gateway-xyz\n")],
219
+ "run services list": [ok("caelo-production-data-gateway\n")],
219
220
  "run services describe": [ok("all\n")],
220
221
  "beta run domain-mappings describe": [fail("NOT_FOUND")],
221
222
  "domains list-user-verified": [ok("example.com\n")],
@@ -240,7 +241,7 @@ describe("enableStagingDomain (#650)", () => {
240
241
  it("installs the missing beta component before its first beta command", async () => {
241
242
  const { run, calls } = fakeGcloud({
242
243
  "components list": [ok("")],
243
- "run services list": [ok("caelo-production-gateway-xyz\n")],
244
+ "run services list": [ok("caelo-production-data-gateway\n")],
244
245
  "run services describe": [ok("all\n")],
245
246
  "beta run domain-mappings describe": [ok(STAGING_MAPPING)],
246
247
  });
@@ -31,7 +31,8 @@
31
31
 
32
32
  import { gcloud as defaultGcloud, type GcloudResult } from "./gcloud.js";
33
33
  import { ensureGcloudComponent } from "./gcloud-components.js";
34
- import { STAGING_HOST_ENV } from "./stack-contract.js";
34
+ import { GCP_STACK_ENV } from "./gcp-names.js";
35
+ import { cloudRunServicePrefix, STAGING_HOST_ENV } from "./stack-contract.js";
35
36
  import { liveContainerEnv } from "./stack-converge.js";
36
37
 
37
38
  type Run = (args: string[]) => Promise<GcloudResult>;
@@ -76,13 +77,17 @@ function logOf(opts: DomainOpts): (message: string) => void {
76
77
  return opts.log ?? ((message: string) => console.log(message));
77
78
  }
78
79
 
79
- /** The one deployed Cloud Run service of `slug` (Pulumi-suffixed name); also used by staging-admin.ts. */
80
+ /**
81
+ * The one deployed Cloud Run service playing `slug` on a gcp-firebase
82
+ * install (stack-contract.ts cloudRunServicePrefix: since #1328 the visitor
83
+ * gateway is `caelo-production-data-gateway`); also used by staging-admin.ts.
84
+ */
80
85
  export async function findService(
81
86
  run: Run,
82
87
  scope: string[],
83
88
  slug: "admin" | "gateway",
84
89
  ): Promise<{ ok: true; service: string } | { ok: false; error: string }> {
85
- const prefix = `caelo-production-${slug}`;
90
+ const prefix = cloudRunServicePrefix("gcp-firebase", GCP_STACK_ENV, slug);
86
91
  const services = await run([
87
92
  "run",
88
93
  "services",
@@ -128,6 +133,11 @@ export async function enableAdminDomain(opts: DomainOpts): Promise<AdminDomainRe
128
133
  * ingress is internal-only, so the gateway's ingress is opened first (the
129
134
  * stack declares the same since #650; installs from before keep the old
130
135
  * value until this runs). Idempotent like the admin mapping.
136
+ *
137
+ * #1328 — a new mapping goes to the data gateway, which runs the gate. A
138
+ * mapping made before stays on the service that was the gateway (now the
139
+ * MCP-Auth-Proxy, which forwards it there) until the staging cutover moves
140
+ * it (#1330, #1331); an existing mapping is only reported here.
131
141
  */
132
142
  export async function enableStagingDomain(
133
143
  opts: DomainOpts & { stagingHost: string },
@@ -76,6 +76,7 @@ describe("self-hosted compose", () => {
76
76
  postgresPassword: "pw",
77
77
  rolePasswords: { admin: "a", public: "b" },
78
78
  caeloSecretKek: "0".repeat(64),
79
+ dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
79
80
  internalSecret: "i".repeat(48),
80
81
  diskSize: "20Gi",
81
82
  ports: { http: 80, https: 443 },
@@ -341,6 +341,45 @@ describe("adoptionCandidates — only declared creates of an adoptable kind", ()
341
341
  );
342
342
  expect(c?.label).toBe("service account");
343
343
  });
344
+
345
+ it("#1328 — names an explicitly named Cloud Run service (the data gateway) and only such", () => {
346
+ const SERVICE = "gcp:cloudrunv2/service:Service";
347
+ const [c, ...rest] = adoptionCandidates(
348
+ [
349
+ step({
350
+ type: SERVICE,
351
+ name: "caelo-production-data-gateway",
352
+ inputs: { name: "caelo-production-data-gateway", location: "europe-west1" },
353
+ }),
354
+ // Auto-named services (the admin, the proxy) have no name to look up.
355
+ step({
356
+ type: SERVICE,
357
+ name: "caelo-production-admin",
358
+ inputs: { location: "europe-west1" },
359
+ }),
360
+ step({
361
+ type: SERVICE,
362
+ name: "elsewhere",
363
+ inputs: { name: "x-svc", location: "europe-west1", project: "other-project" },
364
+ }),
365
+ ],
366
+ PROJECT,
367
+ );
368
+ expect(rest).toEqual([]);
369
+ expect(c?.label).toBe("Cloud Run service");
370
+ expect(c?.importId).toBe(
371
+ `projects/${PROJECT}/locations/europe-west1/services/caelo-production-data-gateway`,
372
+ );
373
+ expect(c?.describeArgs).toEqual([
374
+ "run",
375
+ "services",
376
+ "describe",
377
+ "caelo-production-data-gateway",
378
+ "--region=europe-west1",
379
+ `--project=${PROJECT}`,
380
+ "--format=value(metadata.name)",
381
+ ]);
382
+ });
344
383
  });
345
384
 
346
385
  describe("preview steps carry a create's inputs for adoption", () => {
@@ -25,7 +25,9 @@
25
25
  * 4. A second preview: what the review shows and `up` applies. An adopted
26
26
  * resource that is still planned as a create stops the run.
27
27
  *
28
- * Why only service accounts ({@link ADOPTABLE_KINDS}). A kind is adopted
28
+ * Why service accounts, since #1338 the Cloud SQL private DNS zone and its
29
+ * record (cloudsql-tls.ts creates both on upgrade), and since #1328
30
+ * explicitly named Cloud Run services — {@link ADOPTABLE_KINDS}. A kind is adopted
29
31
  * when the stack declares it AND the CLI also creates it through gcloud, and
30
32
  * its name is unique in the project. Checked per kind against both stacks:
31
33
  *
@@ -34,6 +36,9 @@
34
36
  * (stack-contract.ts CLI_GENERATED_SECRETS) the stack only references by
35
37
  * id. Adopting a secret without its value resource would also make `up`
36
38
  * add a version with a freshly generated value — a new KEK.
39
+ * - Cloud Run services: only the ones the stack names explicitly (#1328:
40
+ * the gcp-firebase data gateway, which the converge creates on older
41
+ * installs); the auto-named admin and proxy are Pulumi's alone.
37
42
  * - Buckets, Cloud Run jobs, custom roles: the stack's buckets are made by
38
43
  * Pulumi alone; the CLI's own (ops, audit results, the upgrader, converge,
39
44
  * audit and operator-access jobs, their custom roles) are not declared.
@@ -102,6 +107,83 @@ export const ADOPTABLE_KINDS: Readonly<Record<string, AdoptableKind>> = {
102
107
  };
103
108
  },
104
109
  },
110
+ // #1338 — the private zone + A record that resolve the Cloud SQL
111
+ // instance's DNS name in the VPC. `cms-provision upgrade` creates both
112
+ // through gcloud (cloudsql-tls.ts) on installs that predate them; the
113
+ // stacks declare them with literal names so the preview pins them down.
114
+ "gcp:dns/managedZone:ManagedZone": {
115
+ label: "private DNS zone",
116
+ target(inputs, projectId) {
117
+ const name = inputs.name;
118
+ if (typeof name !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(name)) return null;
119
+ if ((inputs.project ?? projectId) !== projectId) return null;
120
+ return {
121
+ display: name,
122
+ describeArgs: [
123
+ "dns",
124
+ "managed-zones",
125
+ "describe",
126
+ name,
127
+ `--project=${projectId}`,
128
+ "--format=value(name)",
129
+ ],
130
+ importId: `projects/${projectId}/managedZones/${name}`,
131
+ };
132
+ },
133
+ },
134
+ "gcp:dns/recordSet:RecordSet": {
135
+ label: "DNS record",
136
+ target(inputs, projectId) {
137
+ const { managedZone: zone, name, type } = inputs;
138
+ if (typeof zone !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(zone)) return null;
139
+ if (typeof name !== "string" || !/^[a-z0-9.-]{1,253}\.$/.test(name)) return null;
140
+ if (typeof type !== "string" || !/^[A-Z]{1,10}$/.test(type)) return null;
141
+ if ((inputs.project ?? projectId) !== projectId) return null;
142
+ return {
143
+ display: `${name} ${type}`,
144
+ describeArgs: [
145
+ "dns",
146
+ "record-sets",
147
+ "describe",
148
+ name,
149
+ `--zone=${zone}`,
150
+ `--type=${type}`,
151
+ `--project=${projectId}`,
152
+ "--format=value(name)",
153
+ ],
154
+ importId: `projects/${projectId}/managedZones/${zone}/rrsets/${name}/${type}`,
155
+ };
156
+ },
157
+ },
158
+ // #1328 — the gcp-firebase data gateway: the stack declares it under a
159
+ // fixed name (stack-contract.ts fixedCloudRunServiceName), and the converge
160
+ // creates the same service on installs whose stack predates it. Only a
161
+ // service the stack names explicitly is a candidate: an auto-named one has
162
+ // no name until Pulumi picks it.
163
+ "gcp:cloudrunv2/service:Service": {
164
+ label: "Cloud Run service",
165
+ target(inputs, projectId) {
166
+ const name = inputs.name;
167
+ const location = inputs.location;
168
+ if (typeof name !== "string" || !/^[a-z][a-z0-9-]{0,47}[a-z0-9]$/.test(name)) return null;
169
+ if (typeof location !== "string" || !/^[a-z]+-[a-z]+\d+$/.test(location)) return null;
170
+ const project = inputs.project ?? projectId;
171
+ if (project !== projectId) return null;
172
+ return {
173
+ display: `${name} (${location})`,
174
+ describeArgs: [
175
+ "run",
176
+ "services",
177
+ "describe",
178
+ name,
179
+ `--region=${location}`,
180
+ `--project=${projectId}`,
181
+ "--format=value(metadata.name)",
182
+ ],
183
+ importId: `projects/${projectId}/locations/${location}/services/${name}`,
184
+ };
185
+ },
186
+ },
105
187
  };
106
188
 
107
189
  /** A planned create the installer may adopt instead. */
@@ -8,7 +8,12 @@
8
8
 
9
9
  import { describe, expect, it } from "bun:test";
10
10
  import type { CommandResult, HostRunner } from "../self-hosted-release.js";
11
- import { applyDatabaseSide, POSTGRES_RESTART_NOTICE } from "./command.js";
11
+ import {
12
+ applyArgv,
13
+ applyDatabaseSide,
14
+ POSTGRES_RESTART_NOTICE,
15
+ parseApplyArgs,
16
+ } from "./command.js";
12
17
 
13
18
  function runner(published: string | null): HostRunner & { calls: string[] } {
14
19
  const calls: string[] = [];
@@ -75,3 +80,46 @@ describe("applyDatabaseSide", () => {
75
80
  expect(r.calls).toEqual([]);
76
81
  });
77
82
  });
83
+
84
+ describe("agent apply argv (#1328)", () => {
85
+ const D = (c: string) => `sha256:${c.repeat(64)}`;
86
+ const request = {
87
+ requestId: "11111111-1111-4111-8111-111111111111",
88
+ targetVersion: "0.12.0",
89
+ trigger: "manual" as const,
90
+ cliPath: "/srv/.caelo/agent/cli/caelo-0.12.0/node_modules/@caelo-cms/provisioning/dist/cli.js",
91
+ };
92
+
93
+ it("passes the MCP-Auth-Proxy digest when the manifest names one, and the apply reads it back", () => {
94
+ const argv = applyArgv("/usr/bin/bun", {
95
+ ...request,
96
+ digests: { admin: D("a"), gateway: D("b"), "mcp-auth-proxy": D("d") },
97
+ });
98
+ expect(argv.slice(argv.indexOf("--mcp-auth-proxy-digest"))).toEqual([
99
+ "--mcp-auth-proxy-digest",
100
+ D("d"),
101
+ ]);
102
+ const parsed = parseApplyArgs(argv);
103
+ expect(parsed.success && parsed.data.mcpAuthProxyDigest).toBe(D("d"));
104
+ });
105
+
106
+ it("an agent of a release from before the proxy starts the apply without it", () => {
107
+ const argv = applyArgv("/usr/bin/bun", {
108
+ ...request,
109
+ digests: { admin: D("a"), gateway: D("b") },
110
+ });
111
+ expect(argv).not.toContain("--mcp-auth-proxy-digest");
112
+ const parsed = parseApplyArgs(argv);
113
+ expect(parsed.success).toBe(true);
114
+ expect(parsed.success && parsed.data.mcpAuthProxyDigest).toBeUndefined();
115
+ });
116
+
117
+ it("refuses a malformed proxy digest", () => {
118
+ const argv = [
119
+ ...applyArgv("/usr/bin/bun", { ...request, digests: { admin: D("a"), gateway: D("b") } }),
120
+ "--mcp-auth-proxy-digest",
121
+ "latest",
122
+ ];
123
+ expect(parseApplyArgs(argv).success).toBe(false);
124
+ });
125
+ });
@@ -57,6 +57,8 @@ export interface AgentCliContext {
57
57
  emitConfig(): void;
58
58
  /** The role convergence as the superuser, including the agent's login. */
59
59
  convergeRoles(): Promise<ConvergeOutcome>;
60
+ /** #1338 — public_role's limits as the superuser, before an upgrade's migrations. */
61
+ applyPublicRoleLimits(): Promise<ConvergeOutcome>;
60
62
  /** The Compose file for `images` from config.json. */
61
63
  renderCompose(images: ReleaseImages): string;
62
64
  /** Re-write the Caddyfile (config + domains table) and reload Caddy (#891). */
@@ -417,12 +419,15 @@ function pruneVerifiedClis(caeloDir: string, keep: readonly (string | undefined)
417
419
  }
418
420
  }
419
421
 
420
- /** Start the target release's `agent apply` and wait for it. */
421
- async function spawnApply(request: AgentApplyRequest): Promise<AgentApplyOutcome> {
422
- // The verified install (agent/cli-provenance.ts) — never `bun x`, which
423
- // would fetch an unverified copy.
424
- const argv = [
425
- process.execPath,
422
+ /**
423
+ * The argv that starts the target release's `agent apply` with `bun` at
424
+ * `execPath`: the verified install (agent/cli-provenance.ts) — never
425
+ * `bun x`, which would fetch an unverified copy — and the digests the signed
426
+ * manifest names, the MCP-Auth-Proxy's only when it names one (#1328).
427
+ */
428
+ export function applyArgv(execPath: string, request: AgentApplyRequest): string[] {
429
+ return [
430
+ execPath,
426
431
  request.cliPath,
427
432
  "agent",
428
433
  "apply",
@@ -436,7 +441,15 @@ async function spawnApply(request: AgentApplyRequest): Promise<AgentApplyOutcome
436
441
  request.digests.admin,
437
442
  "--gateway-digest",
438
443
  request.digests.gateway,
444
+ ...(request.digests["mcp-auth-proxy"]
445
+ ? ["--mcp-auth-proxy-digest", request.digests["mcp-auth-proxy"]]
446
+ : []),
439
447
  ];
448
+ }
449
+
450
+ /** Start the target release's `agent apply` and wait for it. */
451
+ async function spawnApply(request: AgentApplyRequest): Promise<AgentApplyOutcome> {
452
+ const argv = applyArgv(process.execPath, request);
440
453
  try {
441
454
  const proc = Bun.spawn(argv, {
442
455
  stdout: "pipe",
@@ -504,16 +517,25 @@ const applyArgs = z.object({
504
517
  trigger: z.enum(PLATFORM_UPDATE_TRIGGERS),
505
518
  adminDigest: digest,
506
519
  gatewayDigest: digest,
520
+ // #1328 — an agent of a release before the MCP-Auth-Proxy starts this
521
+ // without it; the signed manifest the apply reads names it anyway.
522
+ mcpAuthProxyDigest: digest.optional(),
507
523
  });
508
524
 
509
- async function agentApply(argv: readonly string[], ctx: AgentCliContext): Promise<number> {
510
- const parsed = applyArgs.safeParse({
525
+ /** The `agent apply` flags {@link applyArgv} writes, validated. */
526
+ export function parseApplyArgs(argv: readonly string[]) {
527
+ return applyArgs.safeParse({
511
528
  request: flag(argv, "request"),
512
529
  version: flag(argv, "version"),
513
530
  trigger: flag(argv, "trigger"),
514
531
  adminDigest: flag(argv, "admin-digest"),
515
532
  gatewayDigest: flag(argv, "gateway-digest"),
533
+ mcpAuthProxyDigest: flag(argv, "mcp-auth-proxy-digest"),
516
534
  });
535
+ }
536
+
537
+ async function agentApply(argv: readonly string[], ctx: AgentCliContext): Promise<number> {
538
+ const parsed = parseApplyArgs(argv);
517
539
  if (!parsed.success) {
518
540
  console.error(
519
541
  `agent apply is started by the update agent, not by hand (${parsed.error.issues.map((i) => i.path.join(".")).join(", ")}).`,
@@ -539,13 +561,18 @@ async function agentApply(argv: readonly string[], ctx: AgentCliContext): Promis
539
561
  upgradeSelfHosted(
540
562
  {
541
563
  version: args.version,
542
- expectedDigests: { admin: args.adminDigest, gateway: args.gatewayDigest },
564
+ expectedDigests: {
565
+ admin: args.adminDigest,
566
+ gateway: args.gatewayDigest,
567
+ ...(args.mcpAuthProxyDigest ? { "mcp-auth-proxy": args.mcpAuthProxyDigest } : {}),
568
+ },
543
569
  },
544
570
  {
545
571
  caeloDir: ctx.caeloDir,
546
572
  runner: bunHostRunner,
547
573
  renderCompose: (images) => ctx.renderCompose(images),
548
574
  convergeRoles: () => ctx.convergeRoles(),
575
+ applyPublicRoleLimits: () => ctx.applyPublicRoleLimits(),
549
576
  refreshCaddy: () => ctx.refreshCaddy(),
550
577
  log,
551
578
  },
@@ -198,6 +198,25 @@ describe("runAgentOnce", () => {
198
198
  expect(locks).toEqual({ taken: 1, released: 1 });
199
199
  });
200
200
 
201
+ it("#1328: hands over the MCP-Auth-Proxy digest the signed manifest names", async () => {
202
+ const withProxy: ReleaseManifest = {
203
+ ...MANIFEST,
204
+ digests: { ...MANIFEST.digests, "mcp-auth-proxy": D("d") },
205
+ };
206
+ const { deps, applied } = setup({
207
+ fetchManifest: async (): Promise<ReleaseManifestResult> => ({
208
+ ok: true,
209
+ manifest: withProxy,
210
+ }),
211
+ });
212
+ await runAgentOnce(deps);
213
+ expect(applied[0]?.digests).toEqual({
214
+ admin: D("a"),
215
+ gateway: D("b"),
216
+ "mcp-auth-proxy": D("d"),
217
+ });
218
+ });
219
+
201
220
  interface RefusalRow {
202
221
  readonly name: string;
203
222
  readonly over: Partial<AgentRunDeps>;
package/src/agent/run.ts CHANGED
@@ -48,7 +48,12 @@ export interface AgentApplyRequest {
48
48
  readonly targetVersion: string;
49
49
  readonly trigger: PlatformUpdateTrigger;
50
50
  /** From the verified manifest: the images the target must resolve to. */
51
- readonly digests: { readonly admin: string; readonly gateway: string };
51
+ readonly digests: {
52
+ readonly admin: string;
53
+ readonly gateway: string;
54
+ /** #1328 — absent in manifests of releases from before the MCP-Auth-Proxy. */
55
+ readonly "mcp-auth-proxy"?: string;
56
+ };
52
57
  /** The target's CLI, installed and provenance-verified (agent/cli-provenance.ts). */
53
58
  readonly cliPath: string;
54
59
  }
@@ -254,7 +259,11 @@ export async function runAgentOnce(deps: AgentRunDeps): Promise<AgentRunResult>
254
259
  requestId: request.id,
255
260
  targetVersion: m.version,
256
261
  trigger: request.trigger,
257
- digests: { admin: m.digests.admin, gateway: m.digests.gateway },
262
+ digests: {
263
+ admin: m.digests.admin,
264
+ gateway: m.digests.gateway,
265
+ ...(m.digests["mcp-auth-proxy"] ? { "mcp-auth-proxy": m.digests["mcp-auth-proxy"] } : {}),
266
+ },
258
267
  cliPath: cli.cliPath,
259
268
  });
260
269
  } finally {
@@ -20,12 +20,14 @@ export type AwsSecret =
20
20
  | "resend-api-key";
21
21
 
22
22
  /** A service with its own ECS task role. */
23
- export type AwsTaskService = "admin" | "gateway";
23
+ export type AwsTaskService = "admin" | "gateway" | "mcp-auth-proxy";
24
24
 
25
25
  /**
26
26
  * The secrets each service's task role may read. The admin side (admin and
27
27
  * its in-process workers) reads all of them; the gateway reads only
28
- * public_role's password — its one database login.
28
+ * public_role's password — its one database login; the MCP-Auth-Proxy
29
+ * (#1328) reads none: remote MCP is not available on AWS, so it holds
30
+ * neither a database login nor the broker's keys.
29
31
  */
30
32
  export const AWS_SERVICE_SECRETS: Readonly<Record<AwsTaskService, readonly AwsSecret[]>> = {
31
33
  admin: [
@@ -37,6 +39,7 @@ export const AWS_SERVICE_SECRETS: Readonly<Record<AwsTaskService, readonly AwsSe
37
39
  "resend-api-key",
38
40
  ],
39
41
  gateway: ["public-role-password"],
42
+ "mcp-auth-proxy": [],
40
43
  };
41
44
 
42
45
  /**
@@ -31,6 +31,7 @@ import {
31
31
  CADDY_IMAGE,
32
32
  COMPOSE_ADMIN_UPSTREAM,
33
33
  COMPOSE_GATEWAY_UPSTREAM,
34
+ COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
34
35
  COMPOSE_REDIRECT_SNIPPETS,
35
36
  SITE_OUTPUT_ROOT,
36
37
  } from "./compose.js";
@@ -149,6 +150,7 @@ describe.skipIf(!dockerUp)(`self-hosted redirects through ${CADDY_IMAGE}`, () =>
149
150
  stagingSiteRoot: `${SITE_OUTPUT_ROOT}/output/staging/current`,
150
151
  adminUpstream: COMPOSE_ADMIN_UPSTREAM,
151
152
  gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
153
+ mcpAuthProxyUpstream: COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
152
154
  domains: selfHostedSeedDomains(LIVE, STAGING),
153
155
  }),
154
156
  );
@@ -15,6 +15,7 @@ import { caddyRedirectReloadCommand, caddyRedirectReloadScript } from "./caddy-r
15
15
  import {
16
16
  COMPOSE_ADMIN_UPSTREAM,
17
17
  COMPOSE_GATEWAY_UPSTREAM,
18
+ COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
18
19
  COMPOSE_REDIRECT_SNIPPETS,
19
20
  generateDockerCompose,
20
21
  } from "./compose.js";
@@ -26,6 +27,7 @@ const caddyfile = generateCaddyfile({
26
27
  stagingSiteRoot: "/srv/caelo/output/staging/current",
27
28
  adminUpstream: COMPOSE_ADMIN_UPSTREAM,
28
29
  gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
30
+ mcpAuthProxyUpstream: COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
29
31
  domains: selfHostedSeedDomains("example.com", "staging.example.com"),
30
32
  });
31
33
 
@@ -63,6 +65,7 @@ describe("Compose — Caddy picks up new redirects (#587)", () => {
63
65
  postgresPassword: "pgpw",
64
66
  rolePasswords: { admin: "a", public: "p" },
65
67
  caeloSecretKek: "0".repeat(64),
68
+ dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
66
69
  internalSecret: "i".repeat(48),
67
70
  diskSize: "20Gi",
68
71
  ports: { http: 80, https: 443 },
@@ -16,7 +16,12 @@ import { describe, expect, it } from "bun:test";
16
16
  import { readFileSync } from "node:fs";
17
17
  import { join } from "node:path";
18
18
  import { type CaddyfileSpec, generateCaddyfile, selfHostedSeedDomains } from "./caddy.js";
19
- import { CADDY_IMAGE, COMPOSE_ADMIN_UPSTREAM, COMPOSE_GATEWAY_UPSTREAM } from "./compose.js";
19
+ import {
20
+ CADDY_IMAGE,
21
+ COMPOSE_ADMIN_UPSTREAM,
22
+ COMPOSE_GATEWAY_UPSTREAM,
23
+ COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
24
+ } from "./compose.js";
20
25
 
21
26
  const REQUIRED = process.env.CAELO_CADDY_VALIDATE === "required";
22
27
 
@@ -62,6 +67,7 @@ const base: CaddyfileSpec = {
62
67
  stagingSiteRoot: "/srv/caelo/output/staging/current",
63
68
  adminUpstream: COMPOSE_ADMIN_UPSTREAM,
64
69
  gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
70
+ mcpAuthProxyUpstream: COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
65
71
  domains: selfHostedSeedDomains("example.com", "staging.example.com"),
66
72
  };
67
73
 
package/src/caddy.test.ts CHANGED
@@ -6,9 +6,14 @@ import {
6
6
  caddyTokenProblem,
7
7
  generateCaddyfile,
8
8
  selfHostedAdminHost,
9
+ selfHostedMcpHost,
9
10
  selfHostedSeedDomains,
10
11
  } from "./caddy.js";
11
- import { COMPOSE_ADMIN_UPSTREAM, COMPOSE_GATEWAY_UPSTREAM } from "./compose.js";
12
+ import {
13
+ COMPOSE_ADMIN_UPSTREAM,
14
+ COMPOSE_GATEWAY_UPSTREAM,
15
+ COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
16
+ } from "./compose.js";
12
17
 
13
18
  /** The inputs `cms-provision init` passes for example.com. */
14
19
  const spec: CaddyfileSpec = {
@@ -18,6 +23,7 @@ const spec: CaddyfileSpec = {
18
23
  stagingSiteRoot: "/srv/caelo/output/staging/current",
19
24
  adminUpstream: COMPOSE_ADMIN_UPSTREAM,
20
25
  gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
26
+ mcpAuthProxyUpstream: COMPOSE_MCP_AUTH_PROXY_UPSTREAM,
21
27
  domains: selfHostedSeedDomains("example.com", "staging.example.com"),
22
28
  };
23
29
 
@@ -35,7 +41,39 @@ describe("self-hosted seed vhosts", () => {
35
41
  { hostname: "example.com", kind: "public", env: "production" },
36
42
  { hostname: "staging.example.com", kind: "public", env: "staging" },
37
43
  ]);
44
+ // #1328 — no MCP vhost until remote MCP exists on self-hosted: Caddy would
45
+ // otherwise chase an ACME certificate for a host without a DNS record.
46
+ expect(
47
+ selfHostedSeedDomains("example.com", "staging.example.com").some((d) => d.kind === "mcp"),
48
+ ).toBe(false);
38
49
  expect(selfHostedAdminHost("example.com")).toBe("admin.example.com");
50
+ expect(selfHostedMcpHost("example.com")).toBe("mcp.example.com");
51
+ });
52
+
53
+ // #1328 — the MCP-Auth-Proxy never shares an origin with the site's module JS.
54
+ it("serves the MCP-Auth-Proxy on its own vhost, nothing else of the site", () => {
55
+ const out = generateCaddyfile({
56
+ ...spec,
57
+ domains: [
58
+ ...selfHostedSeedDomains("example.com", "staging.example.com"),
59
+ { hostname: selfHostedMcpHost("example.com"), kind: "mcp", env: "production" },
60
+ ],
61
+ });
62
+ const block = out.slice(out.indexOf("mcp.example.com {"));
63
+ const mcp = block.slice(0, block.indexOf("\n}\n") + 3);
64
+ expect(mcp).toContain(`reverse_proxy ${COMPOSE_MCP_AUTH_PROXY_UPSTREAM}`);
65
+ expect(mcp).toContain('X-Robots-Tag "noindex"');
66
+ expect(mcp).not.toContain("file_server");
67
+ expect(mcp).not.toContain(COMPOSE_GATEWAY_UPSTREAM);
68
+ // ... and the site's vhost does not route the proxy.
69
+ const site = out.slice(out.indexOf("\nexample.com {"));
70
+ expect(site.slice(0, site.indexOf("\n}\n"))).not.toContain(COMPOSE_MCP_AUTH_PROXY_UPSTREAM);
71
+ });
72
+
73
+ it("refuses an MCP-Auth-Proxy upstream that would break out of its token", () => {
74
+ expect(() => generateCaddyfile({ ...spec, mcpAuthProxyUpstream: "x }\nevil {" })).toThrow(
75
+ /mcpAuthProxyUpstream/,
76
+ );
39
77
  });
40
78
 
41
79
  it("refuses two vhosts for one host — Caddy would reject the whole file", () => {