@caelo-cms/provisioning 0.11.9 → 0.11.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) hide show
  1. package/dist/adapter.d.ts +6 -0
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/admin-domain.d.ts +10 -1
  4. package/dist/admin-domain.d.ts.map +1 -1
  5. package/dist/admin-domain.js +13 -3
  6. package/dist/admin-domain.js.map +1 -1
  7. package/dist/adopt-existing.d.ts +12 -3
  8. package/dist/adopt-existing.d.ts.map +1 -1
  9. package/dist/adopt-existing.js +143 -0
  10. package/dist/adopt-existing.js.map +1 -1
  11. package/dist/agent/command.d.ts +23 -0
  12. package/dist/agent/command.d.ts.map +1 -1
  13. package/dist/agent/command.js +32 -9
  14. package/dist/agent/command.js.map +1 -1
  15. package/dist/agent/run.d.ts +2 -0
  16. package/dist/agent/run.d.ts.map +1 -1
  17. package/dist/agent/run.js +5 -1
  18. package/dist/agent/run.js.map +1 -1
  19. package/dist/agent-db.js +69 -10
  20. package/dist/aws-task-roles.d.ts +4 -2
  21. package/dist/aws-task-roles.d.ts.map +1 -1
  22. package/dist/aws-task-roles.js +4 -1
  23. package/dist/aws-task-roles.js.map +1 -1
  24. package/dist/caddy.d.ts +21 -4
  25. package/dist/caddy.d.ts.map +1 -1
  26. package/dist/caddy.js +27 -3
  27. package/dist/caddy.js.map +1 -1
  28. package/dist/cli-routing.js +1 -1
  29. package/dist/cli-routing.js.map +1 -1
  30. package/dist/cli.js +68 -7
  31. package/dist/cli.js.map +1 -1
  32. package/dist/cloudsql-tls.d.ts +93 -0
  33. package/dist/cloudsql-tls.d.ts.map +1 -0
  34. package/dist/cloudsql-tls.js +295 -0
  35. package/dist/cloudsql-tls.js.map +1 -0
  36. package/dist/compose.d.ts +43 -6
  37. package/dist/compose.d.ts.map +1 -1
  38. package/dist/compose.js +148 -23
  39. package/dist/compose.js.map +1 -1
  40. package/dist/converge-checkpoint.d.ts +1 -0
  41. package/dist/converge-checkpoint.d.ts.map +1 -1
  42. package/dist/converge-checkpoint.js +8 -1
  43. package/dist/converge-checkpoint.js.map +1 -1
  44. package/dist/converge-db-trust.d.ts +44 -0
  45. package/dist/converge-db-trust.d.ts.map +1 -0
  46. package/dist/converge-db-trust.js +158 -0
  47. package/dist/converge-db-trust.js.map +1 -0
  48. package/dist/converge-job-gcp.d.ts +5 -2
  49. package/dist/converge-job-gcp.d.ts.map +1 -1
  50. package/dist/converge-job-gcp.js +11 -13
  51. package/dist/converge-job-gcp.js.map +1 -1
  52. package/dist/converge-job-preflight.d.ts.map +1 -1
  53. package/dist/converge-job-preflight.js +8 -0
  54. package/dist/converge-job-preflight.js.map +1 -1
  55. package/dist/converge-job.d.ts +17 -0
  56. package/dist/converge-job.d.ts.map +1 -1
  57. package/dist/converge-job.js +12 -0
  58. package/dist/converge-job.js.map +1 -1
  59. package/dist/deployed-release.d.ts +4 -1
  60. package/dist/deployed-release.d.ts.map +1 -1
  61. package/dist/deployed-release.js +18 -8
  62. package/dist/deployed-release.js.map +1 -1
  63. package/dist/gcp-names.d.ts +34 -0
  64. package/dist/gcp-names.d.ts.map +1 -1
  65. package/dist/gcp-names.js +45 -0
  66. package/dist/gcp-names.js.map +1 -1
  67. package/dist/gcp-url-map.d.ts.map +1 -1
  68. package/dist/gcp-url-map.js +6 -2
  69. package/dist/gcp-url-map.js.map +1 -1
  70. package/dist/index.d.ts +3 -2
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/infra-contract.d.ts +1 -1
  75. package/dist/infra-contract.d.ts.map +1 -1
  76. package/dist/infra-contract.js +45 -1
  77. package/dist/infra-contract.js.map +1 -1
  78. package/dist/install-job.d.ts +4 -2
  79. package/dist/install-job.d.ts.map +1 -1
  80. package/dist/install-job.js +4 -1
  81. package/dist/install-job.js.map +1 -1
  82. package/dist/install-state.d.ts +13 -0
  83. package/dist/install-state.d.ts.map +1 -1
  84. package/dist/install-state.js +17 -4
  85. package/dist/install-state.js.map +1 -1
  86. package/dist/lifecycle.d.ts.map +1 -1
  87. package/dist/lifecycle.js +20 -8
  88. package/dist/lifecycle.js.map +1 -1
  89. package/dist/mcp-auth-proxy-setup.d.ts +48 -0
  90. package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
  91. package/dist/mcp-auth-proxy-setup.js +280 -0
  92. package/dist/mcp-auth-proxy-setup.js.map +1 -0
  93. package/dist/migration-runner.d.ts.map +1 -1
  94. package/dist/migration-runner.js +8 -3
  95. package/dist/migration-runner.js.map +1 -1
  96. package/dist/migration-targets.d.ts +11 -0
  97. package/dist/migration-targets.d.ts.map +1 -0
  98. package/dist/migration-targets.js +12 -0
  99. package/dist/migration-targets.js.map +1 -0
  100. package/dist/operator-access.d.ts +10 -0
  101. package/dist/operator-access.d.ts.map +1 -1
  102. package/dist/operator-access.js +5 -0
  103. package/dist/operator-access.js.map +1 -1
  104. package/dist/pool-budget-stack.d.ts +13 -1
  105. package/dist/pool-budget-stack.d.ts.map +1 -1
  106. package/dist/pool-budget-stack.js +11 -1
  107. package/dist/pool-budget-stack.js.map +1 -1
  108. package/dist/public-role-isolation.d.ts +7 -3
  109. package/dist/public-role-isolation.d.ts.map +1 -1
  110. package/dist/public-role-isolation.js +25 -13
  111. package/dist/public-role-isolation.js.map +1 -1
  112. package/dist/release-contract.d.ts.map +1 -1
  113. package/dist/release-contract.js +3 -0
  114. package/dist/release-contract.js.map +1 -1
  115. package/dist/release-manifest.d.ts +25 -1
  116. package/dist/release-manifest.d.ts.map +1 -1
  117. package/dist/release-manifest.js +36 -3
  118. package/dist/release-manifest.js.map +1 -1
  119. package/dist/remote-mcp.d.ts +55 -18
  120. package/dist/remote-mcp.d.ts.map +1 -1
  121. package/dist/remote-mcp.js +307 -77
  122. package/dist/remote-mcp.js.map +1 -1
  123. package/dist/runtime-secrets.d.ts +11 -0
  124. package/dist/runtime-secrets.d.ts.map +1 -1
  125. package/dist/runtime-secrets.js +40 -25
  126. package/dist/runtime-secrets.js.map +1 -1
  127. package/dist/scheduler-setup.d.ts +75 -0
  128. package/dist/scheduler-setup.d.ts.map +1 -0
  129. package/dist/scheduler-setup.js +331 -0
  130. package/dist/scheduler-setup.js.map +1 -0
  131. package/dist/seal-key-rotation.d.ts +7 -7
  132. package/dist/seal-key-rotation.d.ts.map +1 -1
  133. package/dist/seal-key-rotation.js +22 -22
  134. package/dist/seal-key-rotation.js.map +1 -1
  135. package/dist/self-hosted-db-tls.d.ts +37 -0
  136. package/dist/self-hosted-db-tls.d.ts.map +1 -0
  137. package/dist/self-hosted-db-tls.js +220 -0
  138. package/dist/self-hosted-db-tls.js.map +1 -0
  139. package/dist/self-hosted-release.d.ts +3 -1
  140. package/dist/self-hosted-release.d.ts.map +1 -1
  141. package/dist/self-hosted-release.js +33 -9
  142. package/dist/self-hosted-release.js.map +1 -1
  143. package/dist/self-hosted-roles.d.ts +43 -2
  144. package/dist/self-hosted-roles.d.ts.map +1 -1
  145. package/dist/self-hosted-roles.js +87 -1
  146. package/dist/self-hosted-roles.js.map +1 -1
  147. package/dist/self-hosted-scheduler-interval.d.ts +21 -0
  148. package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
  149. package/dist/self-hosted-scheduler-interval.js +68 -0
  150. package/dist/self-hosted-scheduler-interval.js.map +1 -0
  151. package/dist/self-hosted-start.d.ts +0 -21
  152. package/dist/self-hosted-start.d.ts.map +1 -1
  153. package/dist/self-hosted-start.js +39 -1
  154. package/dist/self-hosted-start.js.map +1 -1
  155. package/dist/self-hosted-upgrade.d.ts +19 -7
  156. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  157. package/dist/self-hosted-upgrade.js +78 -25
  158. package/dist/self-hosted-upgrade.js.map +1 -1
  159. package/dist/stack-contract.d.ts +234 -28
  160. package/dist/stack-contract.d.ts.map +1 -1
  161. package/dist/stack-contract.js +397 -39
  162. package/dist/stack-contract.js.map +1 -1
  163. package/dist/stack-converge.d.ts +31 -0
  164. package/dist/stack-converge.d.ts.map +1 -1
  165. package/dist/stack-converge.js +71 -30
  166. package/dist/stack-converge.js.map +1 -1
  167. package/dist/staging-database.d.ts +22 -0
  168. package/dist/staging-database.d.ts.map +1 -0
  169. package/dist/staging-database.js +117 -0
  170. package/dist/staging-database.js.map +1 -0
  171. package/dist/upgrade/app-roll.d.ts +25 -4
  172. package/dist/upgrade/app-roll.d.ts.map +1 -1
  173. package/dist/upgrade/app-roll.js +63 -17
  174. package/dist/upgrade/app-roll.js.map +1 -1
  175. package/dist/upgrade/converge.d.ts +5 -0
  176. package/dist/upgrade/converge.d.ts.map +1 -1
  177. package/dist/upgrade/converge.js +207 -20
  178. package/dist/upgrade/converge.js.map +1 -1
  179. package/dist/upgrade/health.d.ts +31 -7
  180. package/dist/upgrade/health.d.ts.map +1 -1
  181. package/dist/upgrade/health.js +41 -17
  182. package/dist/upgrade/health.js.map +1 -1
  183. package/dist/upgrade/image-forms.d.ts +1 -1
  184. package/dist/upgrade/image-forms.d.ts.map +1 -1
  185. package/dist/upgrade/image-forms.js +24 -7
  186. package/dist/upgrade/image-forms.js.map +1 -1
  187. package/dist/upgrade/release-images.d.ts +12 -3
  188. package/dist/upgrade/release-images.d.ts.map +1 -1
  189. package/dist/upgrade/release-images.js +13 -4
  190. package/dist/upgrade/release-images.js.map +1 -1
  191. package/dist/upgrade/upgrade.d.ts +19 -3
  192. package/dist/upgrade/upgrade.d.ts.map +1 -1
  193. package/dist/upgrade/upgrade.js +37 -2
  194. package/dist/upgrade/upgrade.js.map +1 -1
  195. package/dist/upgrader-gcp.d.ts +13 -0
  196. package/dist/upgrader-gcp.d.ts.map +1 -1
  197. package/dist/upgrader-gcp.js +50 -1
  198. package/dist/upgrader-gcp.js.map +1 -1
  199. package/dist/upgrader-iam-preflight.d.ts.map +1 -1
  200. package/dist/upgrader-iam-preflight.js +6 -1
  201. package/dist/upgrader-iam-preflight.js.map +1 -1
  202. package/dist/upgrader-job.d.ts +1 -1
  203. package/dist/upgrader-job.d.ts.map +1 -1
  204. package/dist/upgrader-job.js +20 -14
  205. package/dist/upgrader-job.js.map +1 -1
  206. package/dist/upgrader-records.d.ts +1 -1
  207. package/dist/upgrader-records.d.ts.map +1 -1
  208. package/dist/upgrader-records.js +26 -7
  209. package/dist/upgrader-records.js.map +1 -1
  210. package/dist/upgrader-setup.d.ts.map +1 -1
  211. package/dist/upgrader-setup.js +10 -5
  212. package/dist/upgrader-setup.js.map +1 -1
  213. package/dist/wizard.d.ts +7 -0
  214. package/dist/wizard.d.ts.map +1 -1
  215. package/dist/wizard.js +11 -0
  216. package/dist/wizard.js.map +1 -1
  217. package/dist/wizards/gcp-cost.d.ts +31 -0
  218. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  219. package/dist/wizards/gcp-cost.js +103 -18
  220. package/dist/wizards/gcp-cost.js.map +1 -1
  221. package/dist/wizards/gcp.d.ts.map +1 -1
  222. package/dist/wizards/gcp.js +129 -27
  223. package/dist/wizards/gcp.js.map +1 -1
  224. package/package.json +2 -2
  225. package/src/adapter.ts +6 -0
  226. package/src/admin-domain.test.ts +9 -8
  227. package/src/admin-domain.ts +13 -3
  228. package/src/admin-memory.test.ts +2 -1
  229. package/src/adopt-existing.test.ts +85 -0
  230. package/src/adopt-existing.ts +141 -3
  231. package/src/agent/command.test.ts +49 -1
  232. package/src/agent/command.ts +39 -9
  233. package/src/agent/run.test.ts +19 -0
  234. package/src/agent/run.ts +11 -2
  235. package/src/aws-task-roles.ts +5 -2
  236. package/src/caddy-redirects.docker.test.ts +2 -0
  237. package/src/caddy-redirects.test.ts +4 -1
  238. package/src/caddy-validate.test.ts +7 -1
  239. package/src/caddy.test.ts +39 -1
  240. package/src/caddy.ts +36 -4
  241. package/src/checkpoint-secrets.test.ts +14 -6
  242. package/src/cli-routing.test.ts +1 -1
  243. package/src/cli-routing.ts +1 -1
  244. package/src/cli.ts +71 -5
  245. package/src/cloudsql-tls.test.ts +255 -0
  246. package/src/cloudsql-tls.ts +359 -0
  247. package/src/compose.ts +193 -24
  248. package/src/converge-checkpoint.ts +8 -1
  249. package/src/converge-db-trust.test.ts +237 -0
  250. package/src/converge-db-trust.ts +212 -0
  251. package/src/converge-job-gcp.ts +11 -12
  252. package/src/converge-job-preflight.test.ts +5 -0
  253. package/src/converge-job-preflight.ts +8 -0
  254. package/src/converge-job.test.ts +70 -0
  255. package/src/converge-job.ts +32 -0
  256. package/src/database-tls-builders.test.ts +56 -0
  257. package/src/deployed-release.test.ts +107 -40
  258. package/src/deployed-release.ts +30 -9
  259. package/src/gateway-env-isolation.test.ts +50 -12
  260. package/src/gcp-names.ts +56 -0
  261. package/src/gcp-url-map.test.ts +6 -8
  262. package/src/gcp-url-map.ts +6 -2
  263. package/src/index.test.ts +10 -4
  264. package/src/index.ts +7 -0
  265. package/src/infra-contract.test.ts +7 -3
  266. package/src/infra-contract.ts +45 -1
  267. package/src/install-job.ts +4 -1
  268. package/src/install-state.ts +33 -4
  269. package/src/lifecycle.ts +23 -11
  270. package/src/mcp-auth-proxy-setup.test.ts +305 -0
  271. package/src/mcp-auth-proxy-setup.ts +374 -0
  272. package/src/mcp-iap-stacks.test.ts +1 -1
  273. package/src/media-volume.test.ts +1 -1
  274. package/src/migration-runner.ts +14 -9
  275. package/src/migration-targets.ts +12 -0
  276. package/src/operator-access-stacks.test.ts +1 -1
  277. package/src/operator-access.test.ts +20 -4
  278. package/src/operator-access.ts +14 -0
  279. package/src/pool-budget-stack.ts +15 -0
  280. package/src/pool-budget-stacks.test.ts +39 -14
  281. package/src/public-role-isolation.test.ts +18 -8
  282. package/src/public-role-isolation.ts +31 -17
  283. package/src/release-contract.ts +3 -0
  284. package/src/release-manifest.test.ts +25 -0
  285. package/src/release-manifest.ts +59 -4
  286. package/src/remote-mcp-stack-config.test.ts +19 -9
  287. package/src/remote-mcp.test.ts +252 -57
  288. package/src/remote-mcp.ts +336 -77
  289. package/src/runtime-secrets.test.ts +37 -14
  290. package/src/runtime-secrets.ts +87 -27
  291. package/src/scheduler-setup.test.ts +201 -0
  292. package/src/scheduler-setup.ts +375 -0
  293. package/src/seal-key-rotation.test.ts +2 -2
  294. package/src/seal-key-rotation.ts +25 -25
  295. package/src/self-hosted-backup.test.ts +1 -0
  296. package/src/self-hosted-compose.test.ts +180 -6
  297. package/src/self-hosted-db-tls.test.ts +111 -0
  298. package/src/self-hosted-db-tls.ts +287 -0
  299. package/src/self-hosted-release-resolve.test.ts +71 -0
  300. package/src/self-hosted-release.ts +41 -11
  301. package/src/self-hosted-roles.integration.test.ts +2 -0
  302. package/src/self-hosted-roles.test.ts +87 -3
  303. package/src/self-hosted-roles.ts +101 -3
  304. package/src/self-hosted-scheduler-interval.test.ts +66 -0
  305. package/src/self-hosted-scheduler-interval.ts +79 -0
  306. package/src/self-hosted-start.test.ts +35 -2
  307. package/src/self-hosted-start.ts +20 -2
  308. package/src/self-hosted-upgrade.test.ts +104 -14
  309. package/src/self-hosted-upgrade.ts +123 -31
  310. package/src/stack-contract-parity.test.ts +79 -12
  311. package/src/stack-contract.test.ts +199 -31
  312. package/src/stack-contract.ts +472 -45
  313. package/src/stack-converge-secrets.test.ts +28 -13
  314. package/src/stack-converge.test.ts +203 -28
  315. package/src/stack-converge.ts +92 -30
  316. package/src/staging-database.test.ts +114 -0
  317. package/src/staging-database.ts +148 -0
  318. package/src/state-store.test.ts +2 -1
  319. package/src/upgrade/app-roll.ts +97 -19
  320. package/src/upgrade/converge.ts +249 -19
  321. package/src/upgrade/health.test.ts +63 -25
  322. package/src/upgrade/health.ts +54 -22
  323. package/src/upgrade/image-forms.test.ts +34 -0
  324. package/src/upgrade/image-forms.ts +23 -8
  325. package/src/upgrade/release-images.test.ts +5 -1
  326. package/src/upgrade/release-images.ts +22 -6
  327. package/src/upgrade/upgrade.test.ts +438 -46
  328. package/src/upgrade/upgrade.ts +64 -6
  329. package/src/upgrader-gcp.ts +66 -3
  330. package/src/upgrader-iam-preflight.test.ts +19 -0
  331. package/src/upgrader-iam-preflight.ts +6 -0
  332. package/src/upgrader-job.test.ts +101 -27
  333. package/src/upgrader-job.ts +27 -14
  334. package/src/upgrader-records.test.ts +97 -12
  335. package/src/upgrader-records.ts +28 -11
  336. package/src/upgrader-setup.test.ts +2 -0
  337. package/src/upgrader-setup.ts +10 -4
  338. package/src/wizard.ts +19 -0
  339. package/src/wizards/gcp-cost.test.ts +103 -0
  340. package/src/wizards/gcp-cost.ts +125 -18
  341. package/src/wizards/gcp.ts +164 -22
  342. package/stacks/aws/Pulumi.yaml +1 -1
  343. package/stacks/aws/index.ts +72 -14
  344. package/stacks/azure/index.ts +108 -21
  345. package/stacks/gcp/Pulumi.yaml +1 -1
  346. package/stacks/gcp/README.md +1 -0
  347. package/stacks/gcp/index.ts +133 -24
  348. package/stacks/gcp-firebase/Pulumi.yaml +1 -1
  349. package/stacks/gcp-firebase/index.ts +212 -34
  350. package/stacks/self-hosted/Pulumi.yaml +3 -0
  351. package/stacks/self-hosted/README.md +6 -1
  352. package/stacks/self-hosted/index.ts +40 -5
@@ -15,7 +15,7 @@ import {
15
15
  adminEnvContract,
16
16
  type CloudRunEnvVar,
17
17
  databaseUrls,
18
- gatewayEnvContract,
18
+ mcpAuthProxyEnvContract,
19
19
  parseRemoteMcpIdentityConfig,
20
20
  REMOTE_MCP_ENV,
21
21
  REMOTE_MCP_IDENTITY_CONFIG,
@@ -41,24 +41,28 @@ const contractInputs = {
41
41
  domain: "viu.one",
42
42
  region: "europe-west1",
43
43
  stagingHost: "staging.viu.one",
44
- databaseUrls: databaseUrls("10.153.0.3"),
44
+ databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
45
45
  } as const;
46
46
  const contract = {
47
47
  admin: adminEnvContract({
48
48
  ...contractInputs,
49
49
  firebaseSiteId: "caelo-production-site-eb4697",
50
- gatewayService: "caelo-production-gateway-6da3bb0",
50
+ gatewayService: "caelo-production-data-gateway",
51
51
  }),
52
- gateway: gatewayEnvContract(contractInputs),
52
+ // #1328 — the broker's identity env is the MCP-Auth-Proxy's, the service
53
+ // that was the gateway (E13 a), so its URL is the old gateway's.
54
+ "mcp-auth-proxy": mcpAuthProxyEnvContract(contractInputs),
53
55
  };
54
56
 
57
+ type IdentitySlug = keyof typeof contract;
58
+
55
59
  /** The env a stack declares for one service, given the config value. */
56
- function declared(slug: "admin" | "gateway", config: string | undefined): CloudRunEnvVar[] {
60
+ function declared(slug: IdentitySlug, config: string | undefined): CloudRunEnvVar[] {
57
61
  return [...contract[slug], ...remoteMcpIdentityStackEnv(config)[slug]];
58
62
  }
59
63
 
60
64
  /** The live services after an installer run plus its old gcloud post-step: gcloud reorders. */
61
- function live(slug: "admin" | "gateway"): CloudRunEnvVar[] {
65
+ function live(slug: IdentitySlug): CloudRunEnvVar[] {
62
66
  return [...contract[slug], ...remoteMcpIdentityEnv(identity)[slug]].reverse();
63
67
  }
64
68
 
@@ -89,7 +93,11 @@ describe("remote-MCP identity as stack config (#1082)", () => {
89
93
 
90
94
  it("declares nothing while the config is unset (a fresh install's first up)", () => {
91
95
  expect(parseRemoteMcpIdentityConfig(undefined)).toBeNull();
92
- expect(remoteMcpIdentityStackEnv(undefined)).toEqual({ admin: [], gateway: [] });
96
+ expect(remoteMcpIdentityStackEnv(undefined)).toEqual({
97
+ admin: [],
98
+ gateway: [],
99
+ "mcp-auth-proxy": [],
100
+ });
93
101
  });
94
102
 
95
103
  it("refuses a corrupt or foreign value loudly instead of dropping the vars", () => {
@@ -111,9 +119,11 @@ describe("remote-MCP identity as stack config (#1082)", () => {
111
119
  .admin.map((v) => v.name)
112
120
  .sort(),
113
121
  ).toEqual(Object.values(REMOTE_MCP_ENV).sort());
122
+ // #1328 — the visitor gateway carries none of it.
123
+ expect(remoteMcpIdentityStackEnv(serializeRemoteMcpIdentity(identity)).gateway).toEqual([]);
114
124
  });
115
125
 
116
- it.each(["admin", "gateway"] as const)(
126
+ it.each(["admin", "mcp-auth-proxy"] as const)(
117
127
  "%s: with the config the CLI writes, a refreshed plan removes and changes nothing",
118
128
  (slug) => {
119
129
  const plan = envPlan(declared(slug, serializeRemoteMcpIdentity(identity)), live(slug));
@@ -128,7 +138,7 @@ describe("remote-MCP identity as stack config (#1082)", () => {
128
138
  "CAELO_IAP_AUDIENCE",
129
139
  "CAELO_MCP_PUBLIC_URL",
130
140
  ]);
131
- expect(envPlan(declared("gateway", undefined), live("gateway")).removed).toEqual([
141
+ expect(envPlan(declared("mcp-auth-proxy", undefined), live("mcp-auth-proxy")).removed).toEqual([
132
142
  "CAELO_ADMIN_URL",
133
143
  "CAELO_MCP_PUBLIC_URL",
134
144
  ]);
@@ -7,12 +7,14 @@
7
7
  */
8
8
 
9
9
  import { describe, expect, it } from "bun:test";
10
- import { GCP_PUBLIC_BASE_PATHS } from "@caelo-cms/shared";
10
+ import { GCP_GATEWAY_PATHS, GCP_MCP_AUTH_PROXY_PATHS } from "@caelo-cms/shared";
11
11
  import type { GcloudResult } from "./gcloud.js";
12
12
  import type { GcloudRunner } from "./gcloud-retry.js";
13
13
  import {
14
14
  checkRemoteMcpIdentityEnv,
15
15
  ensureMcpArmorRule,
16
+ ensureMcpAuthProxyBackend,
17
+ ensureRemoteMcpEdge,
16
18
  findRemoteMcpTarget,
17
19
  type LiveUrlMap,
18
20
  planUrlMapPaths,
@@ -20,6 +22,7 @@ import {
20
22
  resolveGoogleOrgDomain,
21
23
  resolveRemoteMcpIdentity,
22
24
  resolveStackRemoteMcpIdentity,
25
+ retireGatewayMcpArmorRule,
23
26
  } from "./remote-mcp.js";
24
27
  import {
25
28
  GENERAL_RATE_LIMIT_PRIORITY,
@@ -33,9 +36,10 @@ import {
33
36
  import { initialSecretValue } from "./runtime-secrets.js";
34
37
  import {
35
38
  CLI_GENERATED_SECRETS,
36
- GATEWAY_TIMEOUT_SECONDS,
37
39
  GATEWAY_TRUSTED_PROXY_HOPS,
38
40
  GENERATED_SECRET_FORMAT,
41
+ MCP_AUTH_PROXY_TIMEOUT_SECONDS,
42
+ MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS,
39
43
  OPERATOR_OAUTH_CLIENT_FILE,
40
44
  REMOTE_MCP_ENV,
41
45
  RETIRED_RUNTIME_SECRETS,
@@ -82,7 +86,7 @@ describe("remote-MCP identity (contract)", () => {
82
86
  });
83
87
  });
84
88
 
85
- it("gcp-firebase: the gateway's and admin's Cloud Run URLs, the service's audience", () => {
89
+ it("gcp-firebase: the MCP-Auth-Proxy's (the old gateway service, E13 a) and admin's Cloud Run URLs, the service's audience", () => {
86
90
  expect(
87
91
  remoteMcpIdentity({
88
92
  provider: "gcp-firebase",
@@ -90,7 +94,7 @@ describe("remote-MCP identity (contract)", () => {
90
94
  region: "europe-west1",
91
95
  adminServiceName: "caelo-production-admin-aaa",
92
96
  adminServiceUrl: "https://caelo-production-admin-aaa-xyz.a.run.app/",
93
- gatewayServiceUrl: "https://caelo-production-gateway-bbb-xyz.a.run.app",
97
+ mcpAuthProxyServiceUrl: "https://caelo-production-gateway-bbb-xyz.a.run.app",
94
98
  }),
95
99
  ).toEqual({
96
100
  adminUrl: "https://caelo-production-admin-aaa-xyz.a.run.app",
@@ -105,8 +109,10 @@ describe("remote-MCP identity (contract)", () => {
105
109
  // broker answers on the site host, through the LB (client + LB = 2).
106
110
  // gcp-firebase: on the gateway's own run.app URL, never through the
107
111
  // Firebase Hosting rewrite, so Cloud Run's front end appends the real
108
- // client (1). A broker URL on the Hosting path would pool every client
109
- // behind one CDN edge address.
112
+ // client (1). A broker URL on the Hosting path would key every client by
113
+ // a Google proxy address that changes per request
114
+ // (docs/dev/firebase-hosting-behaviour.md).
115
+ expect(MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS.gcp).toBe(2);
110
116
  expect(GATEWAY_TRUSTED_PROXY_HOPS.gcp).toBe(2);
111
117
  expect(
112
118
  remoteMcpIdentity({
@@ -116,19 +122,19 @@ describe("remote-MCP identity (contract)", () => {
116
122
  adminBackendServiceId: "1",
117
123
  }).mcpPublicUrl,
118
124
  ).toBe("https://acme.com");
119
- expect(GATEWAY_TRUSTED_PROXY_HOPS["gcp-firebase"]).toBe(1);
125
+ expect(MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS["gcp-firebase"]).toBe(1);
120
126
  const firebase = remoteMcpIdentity({
121
127
  provider: "gcp-firebase",
122
128
  projectNumber: "42",
123
129
  region: "europe-west1",
124
130
  adminServiceName: "a",
125
131
  adminServiceUrl: "https://a-xyz.a.run.app",
126
- gatewayServiceUrl: "https://g-xyz.a.run.app",
132
+ mcpAuthProxyServiceUrl: "https://g-xyz.a.run.app",
127
133
  });
128
134
  expect(new URL(firebase.mcpPublicUrl).hostname.endsWith(".run.app")).toBe(true);
129
135
  });
130
136
 
131
- it("gives the admin the audience, public URL and its own URL, and the gateway the admin + public URL", () => {
137
+ it("gives the admin the audience, public URL and its own URL, the proxy the admin + public URL, the gateway nothing", () => {
132
138
  const env = remoteMcpIdentityEnv({ adminUrl: "A", mcpPublicUrl: "P", iapAudience: "AUD" });
133
139
  expect(env.admin).toEqual([
134
140
  { name: REMOTE_MCP_ENV.iapAudience, value: "AUD" },
@@ -136,10 +142,12 @@ describe("remote-MCP identity (contract)", () => {
136
142
  // #914 — Google sign-in's self-check opens the admin signed out.
137
143
  { name: REMOTE_MCP_ENV.adminUrl, value: "A" },
138
144
  ]);
139
- expect(env.gateway).toEqual([
145
+ expect(env["mcp-auth-proxy"]).toEqual([
140
146
  { name: REMOTE_MCP_ENV.adminUrl, value: "A" },
141
147
  { name: REMOTE_MCP_ENV.mcpPublicUrl, value: "P" },
142
148
  ]);
149
+ // #1328 — the visitor gateway holds none of remote MCP.
150
+ expect(env.gateway).toEqual([]);
143
151
  expect(REMOTE_MCP_ENV).toEqual({
144
152
  adminUrl: "CAELO_ADMIN_URL",
145
153
  mcpPublicUrl: "CAELO_MCP_PUBLIC_URL",
@@ -156,7 +164,7 @@ describe("remote-MCP identity (contract)", () => {
156
164
  googleOrgDomain: "acme.com",
157
165
  });
158
166
  expect(known.admin).toContainEqual({ name: "CAELO_GOOGLE_ORG_DOMAIN", value: "acme.com" });
159
- expect(known.gateway.map((e) => e.name)).not.toContain("CAELO_GOOGLE_ORG_DOMAIN");
167
+ expect(known["mcp-auth-proxy"].map((e) => e.name)).not.toContain("CAELO_GOOGLE_ORG_DOMAIN");
160
168
  const unknown = remoteMcpIdentityEnv({ adminUrl: "A", mcpPublicUrl: "P", iapAudience: "AUD" });
161
169
  expect(unknown.admin.map((e) => e.name)).not.toContain("CAELO_GOOGLE_ORG_DOMAIN");
162
170
  });
@@ -199,10 +207,11 @@ describe("remote-MCP secrets + IAM (contract)", () => {
199
207
  }
200
208
  });
201
209
 
202
- it("mounts the OAuth client on the gateway only, at latest, as the file its env names", () => {
210
+ it("mounts the OAuth client on the MCP-Auth-Proxy only, at latest, as the file its env names", () => {
203
211
  expect(SERVICE_SECRET_FILES.admin).toEqual([]);
204
- expect(SERVICE_SECRET_FILES.gateway).toEqual([OPERATOR_OAUTH_CLIENT_FILE]);
205
- expect(secretFileVolumesTemplate("gateway", "production")).toEqual({
212
+ expect(SERVICE_SECRET_FILES.gateway).toEqual([]);
213
+ expect(SERVICE_SECRET_FILES["mcp-auth-proxy"]).toEqual([OPERATOR_OAUTH_CLIENT_FILE]);
214
+ expect(secretFileVolumesTemplate("mcp-auth-proxy", "production")).toEqual({
206
215
  volumes: [
207
216
  {
208
217
  name: "operator-oauth-client",
@@ -261,7 +270,7 @@ describe("planSecretFiles / planRequestTimeout (upgrade)", () => {
261
270
  }
262
271
  });
263
272
 
264
- it("#914 — moves the gateway from the retired mcp-oauth-client mount to the renamed secret in one roll", () => {
273
+ it("#914 — moves the MCP-Auth-Proxy (the old gateway service) from the retired mcp-oauth-client mount to the renamed secret in one roll", () => {
265
274
  const live = {
266
275
  executionEnvironment: null,
267
276
  volumes: [
@@ -275,14 +284,19 @@ describe("planSecretFiles / planRequestTimeout (upgrade)", () => {
275
284
  ],
276
285
  mounts: [{ name: "mcp-oauth-client", mountPath: "/secrets/mcp-oauth-client" }],
277
286
  };
287
+ expect(RETIRED_SECRET_FILES["mcp-auth-proxy"].map((f) => f.mountDir)).toEqual([
288
+ "/secrets/mcp-oauth-client",
289
+ ]);
290
+ // #1328 — and the gateway gives the client file up altogether.
278
291
  expect(RETIRED_SECRET_FILES.gateway.map((f) => f.mountDir)).toEqual([
279
292
  "/secrets/mcp-oauth-client",
293
+ "/secrets/operator-oauth-client",
280
294
  ]);
281
295
  const plan = planSecretFiles(
282
296
  live,
283
297
  [OPERATOR_OAUTH_CLIENT_FILE],
284
298
  "production",
285
- RETIRED_SECRET_FILES.gateway,
299
+ RETIRED_SECRET_FILES["mcp-auth-proxy"],
286
300
  );
287
301
  expect(plan.ok && plan.flags).toEqual([
288
302
  "--remove-secrets=/secrets/mcp-oauth-client/client.json",
@@ -309,7 +323,7 @@ describe("planSecretFiles / planRequestTimeout (upgrade)", () => {
309
323
  after,
310
324
  [OPERATOR_OAUTH_CLIENT_FILE],
311
325
  "production",
312
- RETIRED_SECRET_FILES.gateway,
326
+ RETIRED_SECRET_FILES["mcp-auth-proxy"],
313
327
  ),
314
328
  ).toEqual({ ok: true, flags: [], changes: [] });
315
329
  });
@@ -378,8 +392,8 @@ describe("planSecretFiles / planRequestTimeout (upgrade)", () => {
378
392
  expect(plan.error).toContain("--remove-volume-mount=/secrets/operator-oauth-client");
379
393
  });
380
394
 
381
- it("raises the gateway's request timeout to the contract's", () => {
382
- expect(GATEWAY_TIMEOUT_SECONDS).toBe(3600);
395
+ it("raises the MCP-Auth-Proxy's request timeout to the contract's", () => {
396
+ expect(MCP_AUTH_PROXY_TIMEOUT_SECONDS).toBe(3600);
383
397
  expect(planRequestTimeout(null, 3600).ok && planRequestTimeout(null, 3600)).toEqual({
384
398
  ok: true,
385
399
  flags: ["--timeout=3600"],
@@ -396,7 +410,11 @@ const firebase: RemoteMcpTarget = {
396
410
  region: "europe-west1",
397
411
  env: "production",
398
412
  domain: "acme.com",
399
- services: { admin: "caelo-production-admin-aaa", gateway: "caelo-production-gateway-bbb" },
413
+ services: {
414
+ admin: "caelo-production-admin-aaa",
415
+ gateway: "caelo-production-data-gateway",
416
+ "mcp-auth-proxy": "caelo-production-gateway-bbb",
417
+ },
400
418
  };
401
419
 
402
420
  const ANCESTORS_IN_ORG = ok(
@@ -408,7 +426,7 @@ const ANCESTORS_IN_ORG = ok(
408
426
  );
409
427
 
410
428
  describe("resolveRemoteMcpIdentity", () => {
411
- it("gcp-firebase: reads the project number, both services' URLs and the organisation", async () => {
429
+ it("gcp-firebase: reads the project number, the admin's and the MCP-Auth-Proxy's URLs and the organisation", async () => {
412
430
  const { run } = fakeGcloud({
413
431
  "projects describe acme": ok("42\n"),
414
432
  "projects get-ancestors acme": ANCESTORS_IN_ORG,
@@ -504,7 +522,7 @@ const serviceJson = (env: { name: string; value: string }[]) =>
504
522
  const DESCRIBE_JSON = "--region=europe-west1 --project=acme --format=json";
505
523
 
506
524
  describe("findRemoteMcpTarget (#1082)", () => {
507
- it("finds the Pulumi-suffixed services by prefix", async () => {
525
+ it("finds the Pulumi-suffixed services by prefix and names the fixed one (#1328)", async () => {
508
526
  const { run } = fakeGcloud({
509
527
  "run services list --region europe-west1 --project acme --filter metadata.name~^caelo-production-admin":
510
528
  ok("caelo-production-admin-aaa\n"),
@@ -582,12 +600,13 @@ describe("checkRemoteMcpIdentityEnv (#1082)", () => {
582
600
  [`run services describe caelo-production-admin-aaa ${DESCRIBE_JSON}`]: serviceJson(
583
601
  plain(full.admin),
584
602
  ),
603
+ [`run services describe caelo-production-data-gateway ${DESCRIBE_JSON}`]: serviceJson([]),
585
604
  [`run services describe caelo-production-gateway-bbb ${DESCRIBE_JSON}`]: serviceJson(
586
- plain(full.gateway),
605
+ plain(full["mcp-auth-proxy"]),
587
606
  ),
588
607
  });
589
608
  const r = await checkRemoteMcpIdentityEnv(firebase, identity, { run });
590
- expect(r).toEqual({ ok: true, drift: { admin: [], gateway: [] } });
609
+ expect(r).toEqual({ ok: true, drift: { admin: [], gateway: [], "mcp-auth-proxy": [] } });
591
610
  expect(calls.some((c) => c.startsWith("run services update"))).toBe(false);
592
611
  });
593
612
 
@@ -596,10 +615,12 @@ describe("checkRemoteMcpIdentityEnv (#1082)", () => {
596
615
  [`run services describe caelo-production-admin-aaa ${DESCRIBE_JSON}`]: serviceJson(
597
616
  plain(full.admin),
598
617
  ),
618
+ [`run services describe caelo-production-data-gateway ${DESCRIBE_JSON}`]: serviceJson([]),
599
619
  [`run services describe caelo-production-gateway-bbb ${DESCRIBE_JSON}`]: serviceJson([]),
600
620
  });
601
621
  const r = await checkRemoteMcpIdentityEnv(firebase, identity, { run });
602
- expect(r.ok && r.drift.gateway.map((c) => c.name)).toEqual([
622
+ expect(r.ok && r.drift.gateway).toEqual([]);
623
+ expect(r.ok && r.drift["mcp-auth-proxy"].map((c) => c.name)).toEqual([
603
624
  "CAELO_ADMIN_URL",
604
625
  "CAELO_MCP_PUBLIC_URL",
605
626
  ]);
@@ -646,7 +667,7 @@ describe("gcp load balancer (remote-mcp-edge)", () => {
646
667
  // Every /api/mcp/ route the gateway documents — the consent page's
647
668
  // POST /api/mcp/oauth/authorize included — is covered.
648
669
  const routes = await Bun.file(
649
- new URL("../../../apps/api-gateway/src/mcp/routes.ts", import.meta.url),
670
+ new URL("../../../apps/mcp-auth-proxy/src/routes.ts", import.meta.url),
650
671
  ).text();
651
672
  const documented = [...routes.matchAll(/^ \* {3}[A-Z|]+ +(\/api\/mcp\/[^\s]+)/gm)]
652
673
  .map((m) => m[1] as string)
@@ -665,7 +686,7 @@ describe("gcp load balancer (remote-mcp-edge)", () => {
665
686
  // gateway: CAELO_TRUSTED_PROXY_HOPS). Neither may be the tighter one by
666
687
  // accident — a lower edge limit would 429 traffic the broker allows.
667
688
  const src = await Bun.file(
668
- new URL("../../../apps/api-gateway/src/mcp/rate-limit.ts", import.meta.url),
689
+ new URL("../../../apps/mcp-auth-proxy/src/rate-limit.ts", import.meta.url),
669
690
  ).text();
670
691
  const proxy = /proxy: \{ capacity: (\d+), windowSeconds: (\d+) \}/.exec(src);
671
692
  expect(proxy).not.toBeNull();
@@ -692,14 +713,13 @@ describe("gcp load balancer (remote-mcp-edge)", () => {
692
713
  ]);
693
714
  });
694
715
 
695
- it("the gcp stack declares the rule from the shared module", async () => {
716
+ it("#1328 — the gcp stack's own WAF policy no longer carries the rule (the proxy's policy does)", async () => {
696
717
  const src = await Bun.file(new URL("../stacks/gcp/index.ts", import.meta.url)).text();
697
- expect(src).toContain("mcpArmorRuleArgs(),");
698
- expect(src.indexOf("mcpArmorRuleArgs(),")).toBeLessThan(
699
- src.indexOf("evaluatePreconfiguredWaf"),
700
- );
718
+ expect(src).not.toContain("mcpArmorRuleArgs");
701
719
  });
702
720
 
721
+ const PROXY_BACKEND =
722
+ "https://www.googleapis.com/compute/v1/projects/acme/global/backendServices/caelo-production-mcp-auth-proxy-backend";
703
723
  const map = (paths: string[], extra: object[] = []): LiveUrlMap => ({
704
724
  name: "caelo-production-url-map-abc",
705
725
  fingerprint: "fp",
@@ -718,46 +738,87 @@ describe("gcp load balancer (remote-mcp-edge)", () => {
718
738
  { name: "admin", pathRules: [] },
719
739
  ],
720
740
  });
741
+ const redirect = { paths: ["/old"], urlRedirect: { pathRedirect: "/new" } };
721
742
 
722
- it("adds the discovery paths to the /api/* rule and keeps the admin's redirects", () => {
723
- const redirect = { paths: ["/old"], urlRedirect: { pathRedirect: "/new" } };
724
- const plan = planUrlMapPaths([map(["/api/*"], [redirect])], "acme.com");
743
+ it("#1328 — moves the remote-MCP paths off the gateway's rule onto the proxy's, keeping redirects", () => {
744
+ // What #806 left: the discovery paths on the gateway's /api/* rule.
745
+ const plan = planUrlMapPaths(
746
+ [map(["/api/*", ...GCP_MCP_AUTH_PROXY_PATHS.slice(1)], [redirect])],
747
+ "acme.com",
748
+ PROXY_BACKEND,
749
+ );
725
750
  if (!plan.ok) throw new Error(plan.error);
726
- expect(plan.added).toEqual(GCP_PUBLIC_BASE_PATHS.filter((p) => p !== "/api/*"));
727
751
  const rules = plan.map.pathMatchers?.[0]?.pathRules ?? [];
728
- expect(rules[0]?.paths).toEqual([...GCP_PUBLIC_BASE_PATHS]);
729
- expect(rules[1]).toEqual(redirect);
752
+ expect(rules).toEqual([
753
+ { paths: [...GCP_GATEWAY_PATHS], service: "projects/acme/global/backendServices/gw" },
754
+ { paths: [...GCP_MCP_AUTH_PROXY_PATHS], service: PROXY_BACKEND },
755
+ redirect,
756
+ ]);
757
+ expect(plan.changes).toHaveLength(2);
730
758
  expect(plan.map.fingerprint).toBe("fp");
759
+ // The admin host's matcher is not touched.
760
+ expect(plan.map.pathMatchers?.[1]).toEqual({ name: "admin", pathRules: [] });
731
761
  });
732
762
 
733
- it("changes nothing on a map that already routes them", () => {
734
- const plan = planUrlMapPaths([map([...GCP_PUBLIC_BASE_PATHS])], "acme.com");
735
- expect(plan.ok && plan.added).toEqual([]);
763
+ it("#1328 — is idempotent, and brings a drifted proxy rule back to its paths", () => {
764
+ const converged = map(
765
+ [...GCP_GATEWAY_PATHS],
766
+ [{ paths: [...GCP_MCP_AUTH_PROXY_PATHS], service: PROXY_BACKEND }, redirect],
767
+ );
768
+ const again = planUrlMapPaths([converged], "acme.com", PROXY_BACKEND);
769
+ expect(again.ok && again.changes).toEqual([]);
770
+ expect(again.ok && again.map).toBe(converged);
771
+
772
+ const drifted = map(
773
+ [...GCP_GATEWAY_PATHS],
774
+ [{ paths: ["/api/mcp/*"], service: PROXY_BACKEND }],
775
+ );
776
+ const fixed = planUrlMapPaths([drifted], "acme.com", PROXY_BACKEND);
777
+ if (!fixed.ok) throw new Error(fixed.error);
778
+ expect(fixed.map.pathMatchers?.[0]?.pathRules?.[1]).toEqual({
779
+ paths: [...GCP_MCP_AUTH_PROXY_PATHS],
780
+ service: PROXY_BACKEND,
781
+ });
736
782
  });
737
783
 
738
784
  it("refuses a map without the gateway rule instead of inventing one", () => {
739
- const plan = planUrlMapPaths([map(["/other/*"])], "acme.com");
785
+ const plan = planUrlMapPaths([map(["/other/*"])], "acme.com", PROXY_BACKEND);
740
786
  expect(plan.ok).toBe(false);
741
- expect(planUrlMapPaths([], "acme.com").ok).toBe(false);
742
- });
743
-
744
- it("creates the Cloud Armor rule when missing, leaves it alone when in shape", async () => {
745
- const policyJson = (rules: object[]) => ok(JSON.stringify({ rules }));
746
- const missing = fakeGcloud({
747
- "compute security-policies list": ok("caelo-production-waf-1a2b\n"),
748
- "compute security-policies describe": policyJson([{ priority: 1000 }]),
749
- "compute security-policies rules create 900": ok(),
750
- });
751
- expect(await ensureMcpArmorRule({ projectId: "acme", env: "production" }, missing)).toEqual({
787
+ expect(planUrlMapPaths([], "acme.com", PROXY_BACKEND).ok).toBe(false);
788
+ });
789
+
790
+ const policyJson = (rules: object[]) => ok(JSON.stringify({ rules }));
791
+ const PROXY_POLICY = "caelo-production-mcp-auth-proxy-armor";
792
+
793
+ it("#1328 — creates the proxy's own policy and its rule when missing", async () => {
794
+ let created = false;
795
+ const calls: string[] = [];
796
+ const run: GcloudRunner = async (args) => {
797
+ const line = args.join(" ");
798
+ calls.push(line);
799
+ if (line.startsWith(`compute security-policies describe ${PROXY_POLICY}`)) {
800
+ return created ? policyJson([{ priority: 2147483647 }]) : fail("NOT_FOUND: not found");
801
+ }
802
+ if (line.startsWith(`compute security-policies create ${PROXY_POLICY}`)) {
803
+ created = true;
804
+ return ok();
805
+ }
806
+ if (line.startsWith("compute security-policies rules create 900")) return ok();
807
+ return fail(`unexpected: ${line}`);
808
+ };
809
+ expect(await ensureMcpArmorRule({ projectId: "acme", env: "production" }, { run })).toEqual({
752
810
  ok: true,
753
- policy: "caelo-production-waf-1a2b",
811
+ policy: PROXY_POLICY,
754
812
  action: "created",
755
813
  });
756
- expect(missing.calls.at(-1)).toContain("--security-policy=caelo-production-waf-1a2b");
814
+ expect(calls.at(-1)).toContain(`--security-policy=${PROXY_POLICY}`);
815
+ // Never the gateway's policy.
816
+ expect(calls.some((c) => c.includes("-waf"))).toBe(false);
817
+ });
757
818
 
819
+ it("leaves a rule in shape alone and brings a drifted one back", async () => {
758
820
  const args = mcpArmorRuleArgs();
759
821
  const present = fakeGcloud({
760
- "compute security-policies list": ok("caelo-production-waf-1a2b\n"),
761
822
  "compute security-policies describe": policyJson([{ ...args }]),
762
823
  });
763
824
  const r = await ensureMcpArmorRule({ projectId: "acme", env: "production" }, present);
@@ -765,11 +826,145 @@ describe("gcp load balancer (remote-mcp-edge)", () => {
765
826
  expect(present.calls.some((c) => c.includes("rules "))).toBe(false);
766
827
 
767
828
  const drifted = fakeGcloud({
768
- "compute security-policies list": ok("caelo-production-waf-1a2b\n"),
769
829
  "compute security-policies describe": policyJson([{ ...args, action: "allow" }]),
770
830
  "compute security-policies rules update 900": ok(),
771
831
  });
772
832
  const d = await ensureMcpArmorRule({ projectId: "acme", env: "production" }, drifted);
773
833
  expect(d.ok && d.action).toBe("updated");
774
834
  });
835
+
836
+ it("#1328 — takes the MCP rule off the gateway's policy, and only exactly that rule", async () => {
837
+ const args = mcpArmorRuleArgs();
838
+ const withRule = fakeGcloud({
839
+ "compute security-policies list": ok("caelo-production-waf-1a2b\n"),
840
+ "compute security-policies describe caelo-production-waf-1a2b": policyJson([{ ...args }]),
841
+ "compute security-policies rules delete 900": ok(),
842
+ });
843
+ expect(
844
+ await retireGatewayMcpArmorRule({ projectId: "acme", env: "production" }, withRule),
845
+ ).toEqual({ ok: true, removed: "caelo-production-waf-1a2b" });
846
+ expect(withRule.calls.at(-1)).toContain("--security-policy=caelo-production-waf-1a2b");
847
+
848
+ // Someone's own rule at the priority stays.
849
+ const own = fakeGcloud({
850
+ "compute security-policies list": ok("caelo-production-waf-1a2b\n"),
851
+ "compute security-policies describe caelo-production-waf-1a2b": policyJson([
852
+ { priority: 900, action: "deny(403)" },
853
+ ]),
854
+ });
855
+ expect(await retireGatewayMcpArmorRule({ projectId: "acme", env: "production" }, own)).toEqual({
856
+ ok: true,
857
+ removed: null,
858
+ });
859
+ expect(own.calls.some((c) => c.includes("rules delete"))).toBe(false);
860
+ });
861
+
862
+ it("#1328 — creates the proxy's NEG and backend with its policy, then is a no-op", async () => {
863
+ const target = { projectId: "acme", region: "europe-west1", env: "production" };
864
+ const missing = fakeGcloud({
865
+ "compute network-endpoint-groups describe": fail("NOT_FOUND: was not found"),
866
+ "compute network-endpoint-groups create": ok(),
867
+ "compute backend-services describe": fail("NOT_FOUND: was not found"),
868
+ "compute backend-services create": ok(JSON.stringify([{ selfLink: PROXY_BACKEND }])),
869
+ "compute backend-services add-backend": ok(),
870
+ });
871
+ const r = await ensureMcpAuthProxyBackend(target, missing);
872
+ expect(r.ok && r.backend).toBe(PROXY_BACKEND);
873
+ expect(missing.calls).toContain(
874
+ "compute network-endpoint-groups create caelo-production-mcp-auth-proxy-neg --region=europe-west1 --project=acme --network-endpoint-type=serverless --cloud-run-service=caelo-production-mcp-auth-proxy --quiet",
875
+ );
876
+ expect(missing.calls.find((c) => c.startsWith("compute backend-services create"))).toContain(
877
+ `--security-policy=${PROXY_POLICY}`,
878
+ );
879
+ expect(
880
+ missing.calls.find((c) => c.startsWith("compute backend-services add-backend")),
881
+ ).toContain("--network-endpoint-group=caelo-production-mcp-auth-proxy-neg");
882
+
883
+ const present = fakeGcloud({
884
+ "compute network-endpoint-groups describe": ok("caelo-production-mcp-auth-proxy-neg\n"),
885
+ "compute backend-services describe": ok(
886
+ JSON.stringify({
887
+ selfLink: PROXY_BACKEND,
888
+ securityPolicy: `https://www.googleapis.com/compute/v1/projects/acme/global/securityPolicies/${PROXY_POLICY}`,
889
+ backends: [
890
+ {
891
+ group:
892
+ "https://www.googleapis.com/compute/v1/projects/acme/regions/europe-west1/networkEndpointGroups/caelo-production-mcp-auth-proxy-neg",
893
+ },
894
+ ],
895
+ }),
896
+ ),
897
+ });
898
+ const again = await ensureMcpAuthProxyBackend(target, present);
899
+ expect(again).toEqual({ ok: true, backend: PROXY_BACKEND, done: [] });
900
+ });
901
+
902
+ it("#1328 — ensureRemoteMcpEdge: service, policy, backend, URL map, then the gateway rule; no-op on gcp-firebase", async () => {
903
+ expect(
904
+ await ensureRemoteMcpEdge({
905
+ provider: "gcp-firebase",
906
+ projectId: "acme",
907
+ region: "europe-west1",
908
+ env: "production",
909
+ domain: "acme.com",
910
+ mcpAuthProxyImageRef: "img",
911
+ }),
912
+ ).toEqual({ ok: true, done: [] });
913
+
914
+ const order: string[] = [];
915
+ const run: GcloudRunner = async (args) => {
916
+ const line = args.join(" ");
917
+ const step = [
918
+ ["run services describe caelo-production-mcp-auth-proxy", "service"],
919
+ [`compute security-policies describe ${PROXY_POLICY}`, "policy"],
920
+ ["compute network-endpoint-groups describe", "neg"],
921
+ ["compute backend-services describe", "backend"],
922
+ ["compute url-maps list", "url-map"],
923
+ ["compute security-policies list", "gateway-policy"],
924
+ ].find(([prefix]) => line.startsWith(prefix as string))?.[1];
925
+ if (step) order.push(step);
926
+ if (line.startsWith("run services describe")) return ok("caelo-production-mcp-auth-proxy\n");
927
+ if (line.startsWith(`compute security-policies describe ${PROXY_POLICY}`)) {
928
+ return policyJson([{ ...mcpArmorRuleArgs() }]);
929
+ }
930
+ if (line.startsWith("compute network-endpoint-groups describe")) return ok("neg\n");
931
+ if (line.startsWith("compute backend-services describe")) {
932
+ return ok(
933
+ JSON.stringify({
934
+ selfLink: PROXY_BACKEND,
935
+ securityPolicy: `x/securityPolicies/${PROXY_POLICY}`,
936
+ backends: [{ group: "x/networkEndpointGroups/caelo-production-mcp-auth-proxy-neg" }],
937
+ }),
938
+ );
939
+ }
940
+ if (line.startsWith("compute url-maps list")) {
941
+ return ok(
942
+ JSON.stringify([
943
+ map(
944
+ [...GCP_GATEWAY_PATHS],
945
+ [{ paths: [...GCP_MCP_AUTH_PROXY_PATHS], service: PROXY_BACKEND }],
946
+ ),
947
+ ]),
948
+ );
949
+ }
950
+ if (line.startsWith("compute security-policies list")) return ok("caelo-production-waf-1\n");
951
+ if (line.startsWith("compute security-policies describe caelo-production-waf-1")) {
952
+ return policyJson([]);
953
+ }
954
+ return fail(`unexpected: ${line}`);
955
+ };
956
+ const r = await ensureRemoteMcpEdge(
957
+ {
958
+ provider: "gcp",
959
+ projectId: "acme",
960
+ region: "europe-west1",
961
+ env: "production",
962
+ domain: "acme.com",
963
+ mcpAuthProxyImageRef: "img",
964
+ },
965
+ { run },
966
+ );
967
+ expect(r.ok).toBe(true);
968
+ expect(order).toEqual(["service", "policy", "neg", "backend", "url-map", "gateway-policy"]);
969
+ });
775
970
  });