@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.
- package/dist/adapter.d.ts +6 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin-domain.d.ts +10 -1
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +13 -3
- package/dist/admin-domain.js.map +1 -1
- package/dist/adopt-existing.d.ts +12 -3
- package/dist/adopt-existing.d.ts.map +1 -1
- package/dist/adopt-existing.js +143 -0
- package/dist/adopt-existing.js.map +1 -1
- package/dist/agent/command.d.ts +23 -0
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +32 -9
- package/dist/agent/command.js.map +1 -1
- package/dist/agent/run.d.ts +2 -0
- package/dist/agent/run.d.ts.map +1 -1
- package/dist/agent/run.js +5 -1
- package/dist/agent/run.js.map +1 -1
- package/dist/agent-db.js +69 -10
- package/dist/aws-task-roles.d.ts +4 -2
- package/dist/aws-task-roles.d.ts.map +1 -1
- package/dist/aws-task-roles.js +4 -1
- package/dist/aws-task-roles.js.map +1 -1
- package/dist/caddy.d.ts +21 -4
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +27 -3
- package/dist/caddy.js.map +1 -1
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +68 -7
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-tls.d.ts +93 -0
- package/dist/cloudsql-tls.d.ts.map +1 -0
- package/dist/cloudsql-tls.js +295 -0
- package/dist/cloudsql-tls.js.map +1 -0
- package/dist/compose.d.ts +43 -6
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +148 -23
- package/dist/compose.js.map +1 -1
- package/dist/converge-checkpoint.d.ts +1 -0
- package/dist/converge-checkpoint.d.ts.map +1 -1
- package/dist/converge-checkpoint.js +8 -1
- package/dist/converge-checkpoint.js.map +1 -1
- package/dist/converge-db-trust.d.ts +44 -0
- package/dist/converge-db-trust.d.ts.map +1 -0
- package/dist/converge-db-trust.js +158 -0
- package/dist/converge-db-trust.js.map +1 -0
- package/dist/converge-job-gcp.d.ts +5 -2
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +11 -13
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/converge-job-preflight.d.ts.map +1 -1
- package/dist/converge-job-preflight.js +8 -0
- package/dist/converge-job-preflight.js.map +1 -1
- package/dist/converge-job.d.ts +17 -0
- package/dist/converge-job.d.ts.map +1 -1
- package/dist/converge-job.js +12 -0
- package/dist/converge-job.js.map +1 -1
- package/dist/deployed-release.d.ts +4 -1
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +18 -8
- package/dist/deployed-release.js.map +1 -1
- package/dist/gcp-names.d.ts +34 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +45 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts.map +1 -1
- package/dist/gcp-url-map.js +6 -2
- package/dist/gcp-url-map.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +45 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +4 -2
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js +4 -1
- package/dist/install-job.js.map +1 -1
- package/dist/install-state.d.ts +13 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +17 -4
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +20 -8
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-auth-proxy-setup.d.ts +48 -0
- package/dist/mcp-auth-proxy-setup.d.ts.map +1 -0
- package/dist/mcp-auth-proxy-setup.js +280 -0
- package/dist/mcp-auth-proxy-setup.js.map +1 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +8 -3
- package/dist/migration-runner.js.map +1 -1
- package/dist/migration-targets.d.ts +11 -0
- package/dist/migration-targets.d.ts.map +1 -0
- package/dist/migration-targets.js +12 -0
- package/dist/migration-targets.js.map +1 -0
- package/dist/operator-access.d.ts +10 -0
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +5 -0
- package/dist/operator-access.js.map +1 -1
- package/dist/pool-budget-stack.d.ts +13 -1
- package/dist/pool-budget-stack.d.ts.map +1 -1
- package/dist/pool-budget-stack.js +11 -1
- package/dist/pool-budget-stack.js.map +1 -1
- package/dist/public-role-isolation.d.ts +7 -3
- package/dist/public-role-isolation.d.ts.map +1 -1
- package/dist/public-role-isolation.js +25 -13
- package/dist/public-role-isolation.js.map +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +3 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/release-manifest.d.ts +25 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +36 -3
- package/dist/release-manifest.js.map +1 -1
- package/dist/remote-mcp.d.ts +55 -18
- package/dist/remote-mcp.d.ts.map +1 -1
- package/dist/remote-mcp.js +307 -77
- package/dist/remote-mcp.js.map +1 -1
- package/dist/runtime-secrets.d.ts +11 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +40 -25
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/scheduler-setup.d.ts +75 -0
- package/dist/scheduler-setup.d.ts.map +1 -0
- package/dist/scheduler-setup.js +331 -0
- package/dist/scheduler-setup.js.map +1 -0
- package/dist/seal-key-rotation.d.ts +7 -7
- package/dist/seal-key-rotation.d.ts.map +1 -1
- package/dist/seal-key-rotation.js +22 -22
- package/dist/seal-key-rotation.js.map +1 -1
- package/dist/self-hosted-db-tls.d.ts +37 -0
- package/dist/self-hosted-db-tls.d.ts.map +1 -0
- package/dist/self-hosted-db-tls.js +220 -0
- package/dist/self-hosted-db-tls.js.map +1 -0
- package/dist/self-hosted-release.d.ts +3 -1
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +33 -9
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +43 -2
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +87 -1
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-scheduler-interval.d.ts +21 -0
- package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
- package/dist/self-hosted-scheduler-interval.js +68 -0
- package/dist/self-hosted-scheduler-interval.js.map +1 -0
- package/dist/self-hosted-start.d.ts +0 -21
- package/dist/self-hosted-start.d.ts.map +1 -1
- package/dist/self-hosted-start.js +39 -1
- package/dist/self-hosted-start.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +19 -7
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +78 -25
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +234 -28
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +397 -39
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +31 -0
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +71 -30
- package/dist/stack-converge.js.map +1 -1
- package/dist/staging-database.d.ts +22 -0
- package/dist/staging-database.d.ts.map +1 -0
- package/dist/staging-database.js +117 -0
- package/dist/staging-database.js.map +1 -0
- package/dist/upgrade/app-roll.d.ts +25 -4
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +63 -17
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts +5 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +207 -20
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/health.d.ts +31 -7
- package/dist/upgrade/health.d.ts.map +1 -1
- package/dist/upgrade/health.js +41 -17
- package/dist/upgrade/health.js.map +1 -1
- package/dist/upgrade/image-forms.d.ts +1 -1
- package/dist/upgrade/image-forms.d.ts.map +1 -1
- package/dist/upgrade/image-forms.js +24 -7
- package/dist/upgrade/image-forms.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -3
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +13 -4
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +19 -3
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +37 -2
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +13 -0
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +50 -1
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +6 -1
- package/dist/upgrader-iam-preflight.js.map +1 -1
- package/dist/upgrader-job.d.ts +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +20 -14
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-records.d.ts +1 -1
- package/dist/upgrader-records.d.ts.map +1 -1
- package/dist/upgrader-records.js +26 -7
- package/dist/upgrader-records.js.map +1 -1
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +10 -5
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizard.d.ts +7 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +11 -0
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts +31 -0
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +103 -18
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +129 -27
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +6 -0
- package/src/admin-domain.test.ts +9 -8
- package/src/admin-domain.ts +13 -3
- package/src/admin-memory.test.ts +2 -1
- package/src/adopt-existing.test.ts +85 -0
- package/src/adopt-existing.ts +141 -3
- package/src/agent/command.test.ts +49 -1
- package/src/agent/command.ts +39 -9
- package/src/agent/run.test.ts +19 -0
- package/src/agent/run.ts +11 -2
- package/src/aws-task-roles.ts +5 -2
- package/src/caddy-redirects.docker.test.ts +2 -0
- package/src/caddy-redirects.test.ts +4 -1
- package/src/caddy-validate.test.ts +7 -1
- package/src/caddy.test.ts +39 -1
- package/src/caddy.ts +36 -4
- package/src/checkpoint-secrets.test.ts +14 -6
- package/src/cli-routing.test.ts +1 -1
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +71 -5
- package/src/cloudsql-tls.test.ts +255 -0
- package/src/cloudsql-tls.ts +359 -0
- package/src/compose.ts +193 -24
- package/src/converge-checkpoint.ts +8 -1
- package/src/converge-db-trust.test.ts +237 -0
- package/src/converge-db-trust.ts +212 -0
- package/src/converge-job-gcp.ts +11 -12
- package/src/converge-job-preflight.test.ts +5 -0
- package/src/converge-job-preflight.ts +8 -0
- package/src/converge-job.test.ts +70 -0
- package/src/converge-job.ts +32 -0
- package/src/database-tls-builders.test.ts +56 -0
- package/src/deployed-release.test.ts +107 -40
- package/src/deployed-release.ts +30 -9
- package/src/gateway-env-isolation.test.ts +50 -12
- package/src/gcp-names.ts +56 -0
- package/src/gcp-url-map.test.ts +6 -8
- package/src/gcp-url-map.ts +6 -2
- package/src/index.test.ts +10 -4
- package/src/index.ts +7 -0
- package/src/infra-contract.test.ts +7 -3
- package/src/infra-contract.ts +45 -1
- package/src/install-job.ts +4 -1
- package/src/install-state.ts +33 -4
- package/src/lifecycle.ts +23 -11
- package/src/mcp-auth-proxy-setup.test.ts +305 -0
- package/src/mcp-auth-proxy-setup.ts +374 -0
- package/src/mcp-iap-stacks.test.ts +1 -1
- package/src/media-volume.test.ts +1 -1
- package/src/migration-runner.ts +14 -9
- package/src/migration-targets.ts +12 -0
- package/src/operator-access-stacks.test.ts +1 -1
- package/src/operator-access.test.ts +20 -4
- package/src/operator-access.ts +14 -0
- package/src/pool-budget-stack.ts +15 -0
- package/src/pool-budget-stacks.test.ts +39 -14
- package/src/public-role-isolation.test.ts +18 -8
- package/src/public-role-isolation.ts +31 -17
- package/src/release-contract.ts +3 -0
- package/src/release-manifest.test.ts +25 -0
- package/src/release-manifest.ts +59 -4
- package/src/remote-mcp-stack-config.test.ts +19 -9
- package/src/remote-mcp.test.ts +252 -57
- package/src/remote-mcp.ts +336 -77
- package/src/runtime-secrets.test.ts +37 -14
- package/src/runtime-secrets.ts +87 -27
- package/src/scheduler-setup.test.ts +201 -0
- package/src/scheduler-setup.ts +375 -0
- package/src/seal-key-rotation.test.ts +2 -2
- package/src/seal-key-rotation.ts +25 -25
- package/src/self-hosted-backup.test.ts +1 -0
- package/src/self-hosted-compose.test.ts +180 -6
- package/src/self-hosted-db-tls.test.ts +111 -0
- package/src/self-hosted-db-tls.ts +287 -0
- package/src/self-hosted-release-resolve.test.ts +71 -0
- package/src/self-hosted-release.ts +41 -11
- package/src/self-hosted-roles.integration.test.ts +2 -0
- package/src/self-hosted-roles.test.ts +87 -3
- package/src/self-hosted-roles.ts +101 -3
- package/src/self-hosted-scheduler-interval.test.ts +66 -0
- package/src/self-hosted-scheduler-interval.ts +79 -0
- package/src/self-hosted-start.test.ts +35 -2
- package/src/self-hosted-start.ts +20 -2
- package/src/self-hosted-upgrade.test.ts +104 -14
- package/src/self-hosted-upgrade.ts +123 -31
- package/src/stack-contract-parity.test.ts +79 -12
- package/src/stack-contract.test.ts +199 -31
- package/src/stack-contract.ts +472 -45
- package/src/stack-converge-secrets.test.ts +28 -13
- package/src/stack-converge.test.ts +203 -28
- package/src/stack-converge.ts +92 -30
- package/src/staging-database.test.ts +114 -0
- package/src/staging-database.ts +148 -0
- package/src/state-store.test.ts +2 -1
- package/src/upgrade/app-roll.ts +97 -19
- package/src/upgrade/converge.ts +249 -19
- package/src/upgrade/health.test.ts +63 -25
- package/src/upgrade/health.ts +54 -22
- package/src/upgrade/image-forms.test.ts +34 -0
- package/src/upgrade/image-forms.ts +23 -8
- package/src/upgrade/release-images.test.ts +5 -1
- package/src/upgrade/release-images.ts +22 -6
- package/src/upgrade/upgrade.test.ts +438 -46
- package/src/upgrade/upgrade.ts +64 -6
- package/src/upgrader-gcp.ts +66 -3
- package/src/upgrader-iam-preflight.test.ts +19 -0
- package/src/upgrader-iam-preflight.ts +6 -0
- package/src/upgrader-job.test.ts +101 -27
- package/src/upgrader-job.ts +27 -14
- package/src/upgrader-records.test.ts +97 -12
- package/src/upgrader-records.ts +28 -11
- package/src/upgrader-setup.test.ts +2 -0
- package/src/upgrader-setup.ts +10 -4
- package/src/wizard.ts +19 -0
- package/src/wizards/gcp-cost.test.ts +103 -0
- package/src/wizards/gcp-cost.ts +125 -18
- package/src/wizards/gcp.ts +164 -22
- package/stacks/aws/Pulumi.yaml +1 -1
- package/stacks/aws/index.ts +72 -14
- package/stacks/azure/index.ts +108 -21
- package/stacks/gcp/Pulumi.yaml +1 -1
- package/stacks/gcp/README.md +1 -0
- package/stacks/gcp/index.ts +133 -24
- package/stacks/gcp-firebase/Pulumi.yaml +1 -1
- package/stacks/gcp-firebase/index.ts +212 -34
- package/stacks/self-hosted/Pulumi.yaml +3 -0
- package/stacks/self-hosted/README.md +6 -1
- package/stacks/self-hosted/index.ts +40 -5
|
@@ -16,6 +16,7 @@ import { readFileSync } from "node:fs";
|
|
|
16
16
|
import { resolve } from "node:path";
|
|
17
17
|
import {
|
|
18
18
|
CLOUD_RUN_IGNORE_CHANGES,
|
|
19
|
+
type CloudRunSlug,
|
|
19
20
|
type GcpProvider,
|
|
20
21
|
type IamInvariant,
|
|
21
22
|
runtimeSecretBindings,
|
|
@@ -61,11 +62,19 @@ const MEMBER_EXPR: Record<IamInvariant["member"], string> = {
|
|
|
61
62
|
"run-sa": "runSa.email",
|
|
62
63
|
// The secret loop resolves the SA per service through `serviceSa`.
|
|
63
64
|
"gateway-sa": "gatewaySa.email",
|
|
65
|
+
"mcp-auth-proxy-sa": "mcpAuthProxySa.email",
|
|
64
66
|
"static-publisher-sa": "staticPublisherSa.email",
|
|
65
67
|
"iap-service-agent": "iapServiceIdentity.email",
|
|
66
68
|
allUsers: 'member: "allUsers"',
|
|
67
69
|
};
|
|
68
70
|
|
|
71
|
+
/** The variable each stack holds a Cloud Run service in. */
|
|
72
|
+
const SERVICE_VAR: Record<CloudRunSlug, string> = {
|
|
73
|
+
admin: "adminSvc",
|
|
74
|
+
gateway: "gatewaySvc",
|
|
75
|
+
"mcp-auth-proxy": "mcpAuthProxySvc",
|
|
76
|
+
};
|
|
77
|
+
|
|
69
78
|
function targetExpr(inv: IamInvariant): { type: RegExp; ref?: string } {
|
|
70
79
|
switch (inv.target.kind) {
|
|
71
80
|
case "project":
|
|
@@ -77,7 +86,7 @@ function targetExpr(inv: IamInvariant): { type: RegExp; ref?: string } {
|
|
|
77
86
|
case "run-service":
|
|
78
87
|
return {
|
|
79
88
|
type: /^cloudrunv2\.ServiceIamMember$/,
|
|
80
|
-
ref: `name: ${inv.target.service}
|
|
89
|
+
ref: `name: ${SERVICE_VAR[inv.target.service]}.name`,
|
|
81
90
|
};
|
|
82
91
|
}
|
|
83
92
|
}
|
|
@@ -125,6 +134,19 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
|
|
|
125
134
|
expect(unhandled).toEqual([]);
|
|
126
135
|
});
|
|
127
136
|
|
|
137
|
+
it("#1328 — the MCP-Auth-Proxy's SA reads its two secrets and holds no other role", () => {
|
|
138
|
+
expect(src).toContain("accountId: mcpAuthProxyServiceAccountId(env)");
|
|
139
|
+
expect(src).toMatch(/"mcp-auth-proxy": mcpAuthProxySa,/);
|
|
140
|
+
// No binding but the secret loop names it: no project role, no bucket,
|
|
141
|
+
// no service — the loop gives it exactly serviceSecrets("mcp-auth-proxy").
|
|
142
|
+
expect(declared.filter((d) => d.body.includes("mcpAuthProxySa"))).toEqual([]);
|
|
143
|
+
expect(
|
|
144
|
+
runtimeSecretBindings()
|
|
145
|
+
.filter((b) => b.service === "mcp-auth-proxy")
|
|
146
|
+
.map((b) => b.secret),
|
|
147
|
+
).toEqual(["mcp-oauth-seal-key", "operator-oauth-client"]);
|
|
148
|
+
});
|
|
149
|
+
|
|
128
150
|
it("every invariant and exemption names a binding the stack still declares", () => {
|
|
129
151
|
const names = new Set(declared.flatMap(expand));
|
|
130
152
|
for (const inv of invariants) expect(names).toContain(inv.stackResource);
|
|
@@ -139,9 +161,20 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
|
|
|
139
161
|
expect(src).toMatch(
|
|
140
162
|
/serviceName: "admin",[\s\S]*?contractEnv: \[\s*\.\.\.adminEnvContract\([\s\S]*?\.\.\.remoteMcpEnv\.admin,\s*(\/\/[^\n]*\n\s*)?\.\.\.poolBudget\.env\.admin,?\s*\]/,
|
|
141
163
|
);
|
|
164
|
+
// #1328 — the gateway carries no remote-MCP env any more.
|
|
142
165
|
expect(src).toMatch(
|
|
143
|
-
/serviceName: "gateway",[\s\S]*?contractEnv: \[\s*\.\.\.gatewayEnvContract\(
|
|
166
|
+
/serviceName: "gateway",[\s\S]*?contractEnv: \[\s*\.\.\.gatewayEnvContract\(envContractInputs\),\s*\.\.\.poolBudget\.env\.gateway,?\s*\]/,
|
|
144
167
|
);
|
|
168
|
+
if (provider === "gcp-firebase") {
|
|
169
|
+
// The proxy: its contract, its identity env, and (until #1331) the
|
|
170
|
+
// data gateway it forwards to — a stack output no contract can name.
|
|
171
|
+
expect(src).toMatch(
|
|
172
|
+
/serviceName: "mcp-auth-proxy",[\s\S]*?contractEnv: \[\s*\.\.\.mcpAuthProxyEnvContract\(\{ provider: "gcp-firebase", env \}\),\s*\.\.\.remoteMcpEnv\["mcp-auth-proxy"\],[\s\S]*?\{ name: INTERIM_FORWARD_ORIGIN_ENV, value: gatewaySvc\.uri \},\s*\]/,
|
|
173
|
+
);
|
|
174
|
+
} else {
|
|
175
|
+
// gcp: the proxy is CLI-owned (mcp-auth-proxy-setup.ts), not a stack service.
|
|
176
|
+
expect(src).not.toContain('serviceName: "mcp-auth-proxy"');
|
|
177
|
+
}
|
|
145
178
|
expect(src).toContain("envs: [...args.contractEnv],");
|
|
146
179
|
});
|
|
147
180
|
|
|
@@ -151,10 +184,11 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
|
|
|
151
184
|
expect(src).toContain(
|
|
152
185
|
"const remoteMcpEnv = remoteMcpIdentityStackEnv(cfg.get(REMOTE_MCP_IDENTITY_CONFIG));",
|
|
153
186
|
);
|
|
154
|
-
expect(src.match(/remoteMcpEnv
|
|
155
|
-
"
|
|
156
|
-
|
|
157
|
-
|
|
187
|
+
expect(src.match(/remoteMcpEnv(\.\w+|\["[\w-]+"\])/g)?.sort()).toEqual(
|
|
188
|
+
provider === "gcp-firebase"
|
|
189
|
+
? ["remoteMcpEnv.admin", 'remoteMcpEnv["mcp-auth-proxy"]']
|
|
190
|
+
: ["remoteMcpEnv.admin"],
|
|
191
|
+
);
|
|
158
192
|
});
|
|
159
193
|
|
|
160
194
|
it("#1082 — leaves client / clientVersion (who deployed last) to gcloud on both services", () => {
|
|
@@ -165,6 +199,17 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
|
|
|
165
199
|
expect(CLOUD_RUN_IGNORE_CHANGES).toEqual(["client", "clientVersion"]);
|
|
166
200
|
});
|
|
167
201
|
|
|
202
|
+
it("#1328 E13 a — the proxy keeps the gateway's logical resource names, so its service and URL stay", () => {
|
|
203
|
+
if (!src.includes('serviceName: "mcp-auth-proxy"')) return; // the gcp stack: CLI-owned proxy
|
|
204
|
+
// Same URN as before → Pulumi updates the existing service (keeping its
|
|
205
|
+
// auto-generated name and run.app URL) instead of creating a new one.
|
|
206
|
+
expect(src).toMatch(
|
|
207
|
+
/serviceName: "mcp-auth-proxy",\s*resourceName: `\$\{namePrefix\}-gateway`,/,
|
|
208
|
+
);
|
|
209
|
+
expect(src).toMatch(/`\$\{namePrefix\}-gateway-public-invoker`/);
|
|
210
|
+
expect(src).not.toContain("aliases");
|
|
211
|
+
});
|
|
212
|
+
|
|
168
213
|
it("mounts the media bucket on the admin, and only there, from the contract upgrade applies", () => {
|
|
169
214
|
// The stack's admin gets adminMediaVolume() — the same volume
|
|
170
215
|
// stack-converge.ts planMediaVolume adds to installs that lack it.
|
|
@@ -193,10 +238,17 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
|
|
|
193
238
|
expect(src.match(/secretFileVolumesTemplate\(args\.serviceName, env\)/g)).toHaveLength(2);
|
|
194
239
|
});
|
|
195
240
|
|
|
196
|
-
it("#806 — gives the
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
241
|
+
it("#806 — gives the MCP-Auth-Proxy the contract's request timeout upgrade applies", () => {
|
|
242
|
+
// #1328 — the long remote-MCP streams are the proxy's; the gateway keeps
|
|
243
|
+
// Cloud Run's default. On gcp the proxy is deployed by the CLI with the
|
|
244
|
+
// same constant (mcp-auth-proxy-setup.ts gcpProxyDeployArgs).
|
|
245
|
+
expect(src).toMatch(/serviceName: "gateway",[\s\S]*?timeout: "300s",/);
|
|
246
|
+
expect(src).not.toContain("GATEWAY_TIMEOUT_SECONDS");
|
|
247
|
+
if (provider === "gcp-firebase") {
|
|
248
|
+
expect(src).toMatch(
|
|
249
|
+
/serviceName: "mcp-auth-proxy",[\s\S]*?timeout: `\$\{MCP_AUTH_PROXY_TIMEOUT_SECONDS\}s`,/,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
200
252
|
});
|
|
201
253
|
|
|
202
254
|
it("sets no env var outside the contract (C1: no secret value in a plain var)", () => {
|
|
@@ -207,7 +259,17 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
|
|
|
207
259
|
|
|
208
260
|
it("builds no database URL with a password in it", () => {
|
|
209
261
|
expect(src).not.toMatch(/:\$\{postgresPassword\}@|\$\{pw\}@/);
|
|
210
|
-
|
|
262
|
+
// #1338 — from the instance's DNS name, once its private zone record exists.
|
|
263
|
+
expect(src).toContain(".apply(([host]) => databaseUrls(host))");
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("serves verify-full TLS: shared server CA, encrypted only, DNS name in the VPC (#1338)", () => {
|
|
267
|
+
expect(src).toContain("serverCaMode: CLOUD_SQL_SERVER_CA_MODE");
|
|
268
|
+
expect(src).toContain("sslMode: CLOUD_SQL_SSL_MODE");
|
|
269
|
+
// The names cloudsql-tls.ts converges and adopt-existing.ts adopts.
|
|
270
|
+
expect(src).toContain("name: cloudSqlDnsZoneName(env)");
|
|
271
|
+
expect(src).toContain("managedZone: cloudSqlDnsZoneName(env)");
|
|
272
|
+
expect(src).toContain('visibility: "private"');
|
|
211
273
|
});
|
|
212
274
|
|
|
213
275
|
it("references the CLI-generated secrets by id instead of creating them", () => {
|
|
@@ -225,11 +287,16 @@ describe.each(["gcp", "gcp-firebase"] as const)("%s stack ↔ upgrade parity", (
|
|
|
225
287
|
|
|
226
288
|
it("pins images by the config keys upgrade writes back", () => {
|
|
227
289
|
const keys = Object.keys(
|
|
228
|
-
imageDigestConfig(provider, {
|
|
290
|
+
imageDigestConfig(provider, {
|
|
291
|
+
admin: "sha256:a",
|
|
292
|
+
gateway: "sha256:b",
|
|
293
|
+
"mcp-auth-proxy": "sha256:c",
|
|
294
|
+
}),
|
|
229
295
|
);
|
|
230
296
|
expect(keys).toEqual([
|
|
231
297
|
`caelo-${provider}:image-digest-admin`,
|
|
232
298
|
`caelo-${provider}:image-digest-gateway`,
|
|
299
|
+
`caelo-${provider}:image-digest-mcp-auth-proxy`,
|
|
233
300
|
]);
|
|
234
301
|
// The stack reads `<ns>:image-digest-<service>` through pulumi.Config.
|
|
235
302
|
expect(src).toMatch(/cfg\.get\(`image-digest-\$\{service\}`\)/);
|
|
@@ -6,16 +6,27 @@ import { generateDockerCompose } from "./compose.js";
|
|
|
6
6
|
import {
|
|
7
7
|
adminEnvContract,
|
|
8
8
|
CLI_GENERATED_SECRETS,
|
|
9
|
+
CLOUD_RUN_SLUGS,
|
|
9
10
|
type CloudRunEnvVar,
|
|
11
|
+
cloudRunServicePrefix,
|
|
10
12
|
DATABASE_ROLE_SECRET,
|
|
11
13
|
databaseUrls,
|
|
12
14
|
defaultStagingHost,
|
|
15
|
+
fixedCloudRunServiceName,
|
|
13
16
|
gatewayEnvContract,
|
|
17
|
+
INTERIM_FORWARD_ORIGIN_ENV,
|
|
14
18
|
iamMember,
|
|
19
|
+
MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS,
|
|
20
|
+
mcpAuthProxyEnvContract,
|
|
21
|
+
REMOTE_MCP_MODE,
|
|
15
22
|
RETIRED_IAM_BINDINGS,
|
|
23
|
+
RETIRED_SECRET_FILES,
|
|
16
24
|
RETIRED_SERVICE_ENV,
|
|
17
25
|
runtimeSecretBindings,
|
|
26
|
+
SECRETS_SET_UP_AHEAD_OF_THEIR_READER,
|
|
18
27
|
SERVICE_SECRET_ENV,
|
|
28
|
+
SERVICE_SECRET_FILES,
|
|
29
|
+
SERVICE_VPC_EGRESS,
|
|
19
30
|
STACK_IAM_NOT_ENSURED,
|
|
20
31
|
serviceSecrets,
|
|
21
32
|
stackIamInvariants,
|
|
@@ -27,7 +38,7 @@ const base = {
|
|
|
27
38
|
domain: "acme.com",
|
|
28
39
|
stagingHost: "staging.acme.com",
|
|
29
40
|
region: "europe-west1",
|
|
30
|
-
databaseUrls: databaseUrls("
|
|
41
|
+
databaseUrls: databaseUrls("db1.p-label.europe-west1.sql-psa.goog"),
|
|
31
42
|
};
|
|
32
43
|
const asRecord = (vars: CloudRunEnvVar[]) =>
|
|
33
44
|
Object.fromEntries(
|
|
@@ -42,6 +53,7 @@ const asRecord = (vars: CloudRunEnvVar[]) =>
|
|
|
42
53
|
const ADMIN_SECRETS = {
|
|
43
54
|
ADMIN_DATABASE_PASSWORD: "secret:caelo-production-postgres-password:latest",
|
|
44
55
|
PUBLIC_ADMIN_DATABASE_PASSWORD: "secret:caelo-production-postgres-password:latest",
|
|
56
|
+
PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD: "secret:caelo-production-postgres-password:latest",
|
|
45
57
|
CAELO_SECRET_KEK: "secret:caelo-production-secret-kek:latest",
|
|
46
58
|
CAELO_INTERNAL_SECRET: "secret:caelo-production-internal-secret:latest",
|
|
47
59
|
CAELO_TOOL_APPROVAL_SECRET: "secret:caelo-production-tool-approval-secret:latest",
|
|
@@ -50,10 +62,17 @@ const ADMIN_SECRETS = {
|
|
|
50
62
|
|
|
51
63
|
describe("databaseUrls", () => {
|
|
52
64
|
it("builds password-less URLs for the three role/database pairs", () => {
|
|
53
|
-
expect(databaseUrls("
|
|
54
|
-
admin:
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
expect(databaseUrls("db1.p-label.europe-west1.sql-psa.goog")).toEqual({
|
|
66
|
+
admin:
|
|
67
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
68
|
+
publicAdmin:
|
|
69
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
70
|
+
public:
|
|
71
|
+
"postgresql://public_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
72
|
+
publicStagingAdmin:
|
|
73
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
74
|
+
publicStaging:
|
|
75
|
+
"postgresql://public_staging_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
57
76
|
});
|
|
58
77
|
});
|
|
59
78
|
});
|
|
@@ -64,14 +83,18 @@ describe("adminEnvContract", () => {
|
|
|
64
83
|
CAELO_PROVIDER: "gcp",
|
|
65
84
|
CAELO_ENV: "production",
|
|
66
85
|
MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
|
|
67
|
-
ADMIN_DATABASE_URL:
|
|
86
|
+
ADMIN_DATABASE_URL:
|
|
87
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
68
88
|
PUBLIC_ADMIN_DATABASE_URL:
|
|
69
|
-
"postgresql://admin_role@
|
|
89
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
90
|
+
PUBLIC_STAGING_ADMIN_DATABASE_URL:
|
|
91
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
70
92
|
CAELO_SITE_URL: "https://acme.com",
|
|
71
93
|
CAELO_GENERATOR_CLI: "/app/apps/static-generator/src/cli.ts",
|
|
72
94
|
MEDIA_ROOT_DIR: "/app/apps/admin/data/media",
|
|
73
95
|
CAELO_MCP_IAP_SERVICE_ACCOUNT: "caelo-mcp@acme.iam.gserviceaccount.com",
|
|
74
96
|
CAELO_MEDIA_BUCKET: "acme-caelo-production-media",
|
|
97
|
+
CAELO_SCHEDULER_SERVICE_ACCOUNT: "caelo-production-scheduler@acme.iam.gserviceaccount.com",
|
|
75
98
|
// #806 — where Settings → MCP → Remote writes the OAuth client.
|
|
76
99
|
CAELO_OPERATOR_OAUTH_CLIENT_STORE:
|
|
77
100
|
"projects/acme/secrets/caelo-production-operator-oauth-client",
|
|
@@ -103,14 +126,18 @@ describe("adminEnvContract", () => {
|
|
|
103
126
|
CAELO_PROVIDER: "gcp-firebase",
|
|
104
127
|
CAELO_ENV: "production",
|
|
105
128
|
MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
|
|
106
|
-
ADMIN_DATABASE_URL:
|
|
129
|
+
ADMIN_DATABASE_URL:
|
|
130
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_admin?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
107
131
|
PUBLIC_ADMIN_DATABASE_URL:
|
|
108
|
-
"postgresql://admin_role@
|
|
132
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
133
|
+
PUBLIC_STAGING_ADMIN_DATABASE_URL:
|
|
134
|
+
"postgresql://admin_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public_staging?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
109
135
|
CAELO_SITE_URL: "https://acme.com",
|
|
110
136
|
CAELO_GENERATOR_CLI: "/app/apps/static-generator/src/cli.ts",
|
|
111
137
|
MEDIA_ROOT_DIR: "/app/apps/admin/data/media",
|
|
112
138
|
CAELO_MCP_IAP_SERVICE_ACCOUNT: "caelo-mcp@acme.iam.gserviceaccount.com",
|
|
113
139
|
CAELO_MEDIA_BUCKET: "acme-caelo-production-media",
|
|
140
|
+
CAELO_SCHEDULER_SERVICE_ACCOUNT: "caelo-production-scheduler@acme.iam.gserviceaccount.com",
|
|
114
141
|
// #806 — where Settings → MCP → Remote writes the OAuth client.
|
|
115
142
|
CAELO_OPERATOR_OAUTH_CLIENT_STORE:
|
|
116
143
|
"projects/acme/secrets/caelo-production-operator-oauth-client",
|
|
@@ -176,14 +203,15 @@ describe("gatewayEnvContract", () => {
|
|
|
176
203
|
CAELO_PROVIDER: "gcp",
|
|
177
204
|
CAELO_ENV: "production",
|
|
178
205
|
MEDIA_STORAGE_URL: "gs://acme-caelo-production-media",
|
|
206
|
+
// #1329 — the live site's gateway.
|
|
207
|
+
CAELO_GATEWAY_ENVIRONMENT: "live",
|
|
179
208
|
// #845 — gcp: the LB appends <client>,<LB IP>.
|
|
180
209
|
CAELO_TRUSTED_PROXY_HOPS: "2",
|
|
181
|
-
PUBLIC_DATABASE_URL:
|
|
210
|
+
PUBLIC_DATABASE_URL:
|
|
211
|
+
"postgresql://public_role@db1.p-label.europe-west1.sql-psa.goog:5432/cms_public?sslmode=verify-full&sslrootcert=/etc/caelo/db-ca/cloudsql-global.pem",
|
|
182
212
|
PUBLIC_DATABASE_PASSWORD: "secret:caelo-production-public-role-password:latest",
|
|
183
213
|
CAELO_STAGING_GATE_SECRET: "secret:caelo-production-staging-gate-secret:latest",
|
|
184
|
-
//
|
|
185
|
-
CAELO_OPERATOR_OAUTH_CLIENT_FILE: "/secrets/operator-oauth-client/client.json",
|
|
186
|
-
CAELO_MCP_OAUTH_SEAL_KEY: "secret:caelo-production-mcp-oauth-seal-key:latest",
|
|
214
|
+
// No remote-MCP env: the broker is the MCP-Auth-Proxy's (#1328).
|
|
187
215
|
});
|
|
188
216
|
});
|
|
189
217
|
|
|
@@ -215,13 +243,7 @@ describe("gatewayEnvContract", () => {
|
|
|
215
243
|
});
|
|
216
244
|
|
|
217
245
|
it("the gateway's run SA reads no admin-only secret (admin password, KEK, internal, tool approval)", () => {
|
|
218
|
-
expect(serviceSecrets("gateway")).toEqual([
|
|
219
|
-
"public-role-password",
|
|
220
|
-
"staging-gate-secret",
|
|
221
|
-
// #806 — the broker's seal key (env) and OAuth client (file).
|
|
222
|
-
"mcp-oauth-seal-key",
|
|
223
|
-
"operator-oauth-client",
|
|
224
|
-
]);
|
|
246
|
+
expect(serviceSecrets("gateway")).toEqual(["public-role-password", "staging-gate-secret"]);
|
|
225
247
|
for (const v of gateway) {
|
|
226
248
|
expect(v.name).not.toMatch(/KEK|INTERNAL|TOOL_APPROVAL|PUBLIC_ADMIN/);
|
|
227
249
|
}
|
|
@@ -259,6 +281,7 @@ describe("database role secrets (#613)", () => {
|
|
|
259
281
|
expect(DATABASE_ROLE_SECRET).toEqual({
|
|
260
282
|
admin_role: "postgres-password",
|
|
261
283
|
public_role: "public-role-password",
|
|
284
|
+
public_staging_role: "public-staging-role-password",
|
|
262
285
|
});
|
|
263
286
|
});
|
|
264
287
|
|
|
@@ -318,25 +341,39 @@ describe("runtime secrets", () => {
|
|
|
318
341
|
secret: "staging-gate-secret",
|
|
319
342
|
stackResource: "gateway-staging-gate-secret-binding",
|
|
320
343
|
},
|
|
344
|
+
// #1328 — the broker's seal key (env) and Google sign-in client (a
|
|
345
|
+
// file, which counts like an env secret: the SA reads it).
|
|
321
346
|
{
|
|
322
|
-
service: "
|
|
347
|
+
service: "mcp-auth-proxy",
|
|
323
348
|
secret: "mcp-oauth-seal-key",
|
|
324
|
-
stackResource: "
|
|
349
|
+
stackResource: "mcp-auth-proxy-mcp-oauth-seal-key-binding",
|
|
325
350
|
},
|
|
326
|
-
// The secret file counts like an env secret: the SA reads it.
|
|
327
351
|
{
|
|
328
|
-
service: "
|
|
352
|
+
service: "mcp-auth-proxy",
|
|
329
353
|
secret: "operator-oauth-client",
|
|
330
|
-
stackResource: "
|
|
354
|
+
stackResource: "mcp-auth-proxy-operator-oauth-client-binding",
|
|
331
355
|
},
|
|
332
356
|
]);
|
|
333
357
|
});
|
|
334
358
|
|
|
335
|
-
it("every CLI-generated secret is read by some service", () => {
|
|
359
|
+
it("every CLI-generated secret is read by some service, or says what reads it until one does", () => {
|
|
360
|
+
const read = CLOUD_RUN_SLUGS.flatMap((slug) => serviceSecrets(slug));
|
|
336
361
|
for (const s of CLI_GENERATED_SECRETS) {
|
|
337
|
-
|
|
362
|
+
if (SECRETS_SET_UP_AHEAD_OF_THEIR_READER[s]) {
|
|
363
|
+
// Set up ahead: no service may read it yet, or the entry is stale.
|
|
364
|
+
expect(read).not.toContain(s);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
expect(read).toContain(s);
|
|
338
368
|
}
|
|
339
369
|
});
|
|
370
|
+
|
|
371
|
+
it("the two gateway logins share no password secret, and no service reads the staging one yet (#1329)", () => {
|
|
372
|
+
expect(DATABASE_ROLE_SECRET.public_staging_role).not.toBe(DATABASE_ROLE_SECRET.public_role);
|
|
373
|
+
expect(CLI_GENERATED_SECRETS).toContain("public-staging-role-password");
|
|
374
|
+
expect(serviceSecrets("gateway")).not.toContain("public-staging-role-password");
|
|
375
|
+
expect(serviceSecrets("admin")).not.toContain("public-staging-role-password");
|
|
376
|
+
});
|
|
340
377
|
});
|
|
341
378
|
|
|
342
379
|
describe("self-hosted compose", () => {
|
|
@@ -344,8 +381,9 @@ describe("self-hosted compose", () => {
|
|
|
344
381
|
const yaml = generateDockerCompose({
|
|
345
382
|
domain: "acme.com",
|
|
346
383
|
postgresPassword: "p",
|
|
347
|
-
rolePasswords: { admin: "a", public: "b" },
|
|
384
|
+
rolePasswords: { admin: "a", public: "b", publicStaging: "ps" },
|
|
348
385
|
caeloSecretKek: "k",
|
|
386
|
+
dbTlsServerCertSha256: "abababababababababababababababababababababababababababababababab",
|
|
349
387
|
internalSecret: "i".repeat(48),
|
|
350
388
|
diskSize: "10G",
|
|
351
389
|
ports: { http: 80, https: 443 },
|
|
@@ -386,14 +424,12 @@ describe("stackIamInvariants", () => {
|
|
|
386
424
|
}
|
|
387
425
|
});
|
|
388
426
|
|
|
389
|
-
it("grants the gateway SA only its own role password, the staging gate key
|
|
427
|
+
it("grants the gateway SA only its own role password, the staging gate key + telemetry roles", () => {
|
|
390
428
|
for (const provider of ["gcp", "gcp-firebase"] as const) {
|
|
391
429
|
const gateway = stackIamInvariants(provider).filter((i) => i.member === "gateway-sa");
|
|
392
430
|
expect(gateway.map((i) => `${i.role} ${JSON.stringify(i.target)}`).sort()).toEqual([
|
|
393
431
|
'roles/logging.logWriter {"kind":"project"}',
|
|
394
432
|
'roles/monitoring.metricWriter {"kind":"project"}',
|
|
395
|
-
'roles/secretmanager.secretAccessor {"kind":"secret","name":"mcp-oauth-seal-key"}',
|
|
396
|
-
'roles/secretmanager.secretAccessor {"kind":"secret","name":"operator-oauth-client"}',
|
|
397
433
|
'roles/secretmanager.secretAccessor {"kind":"secret","name":"public-role-password"}',
|
|
398
434
|
'roles/secretmanager.secretAccessor {"kind":"secret","name":"staging-gate-secret"}',
|
|
399
435
|
]);
|
|
@@ -407,6 +443,135 @@ describe("stackIamInvariants", () => {
|
|
|
407
443
|
});
|
|
408
444
|
});
|
|
409
445
|
|
|
446
|
+
describe("MCP-Auth-Proxy contract (#1328)", () => {
|
|
447
|
+
it("names one service per role; no prefix is a prefix of another", () => {
|
|
448
|
+
for (const provider of ["gcp", "gcp-firebase"] as const) {
|
|
449
|
+
const prefixes = CLOUD_RUN_SLUGS.map((s) => cloudRunServicePrefix(provider, "production", s));
|
|
450
|
+
expect(new Set(prefixes).size).toBe(prefixes.length);
|
|
451
|
+
for (const a of prefixes) {
|
|
452
|
+
for (const b of prefixes) if (a !== b) expect(b.startsWith(a)).toBe(false);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// E13 (a): on gcp-firebase the proxy keeps the old gateway service.
|
|
456
|
+
expect(cloudRunServicePrefix("gcp-firebase", "production", "mcp-auth-proxy")).toBe(
|
|
457
|
+
"caelo-production-gateway",
|
|
458
|
+
);
|
|
459
|
+
expect(cloudRunServicePrefix("gcp-firebase", "production", "gateway")).toBe(
|
|
460
|
+
"caelo-production-data-gateway",
|
|
461
|
+
);
|
|
462
|
+
expect(cloudRunServicePrefix("gcp", "production", "gateway")).toBe("caelo-production-gateway");
|
|
463
|
+
expect(cloudRunServicePrefix("gcp", "production", "mcp-auth-proxy")).toBe(
|
|
464
|
+
"caelo-production-mcp-auth-proxy",
|
|
465
|
+
);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("fixes the names of exactly the services the converge creates", () => {
|
|
469
|
+
expect(fixedCloudRunServiceName("gcp-firebase", "production", "gateway")).toBe(
|
|
470
|
+
"caelo-production-data-gateway",
|
|
471
|
+
);
|
|
472
|
+
expect(fixedCloudRunServiceName("gcp", "production", "mcp-auth-proxy")).toBe(
|
|
473
|
+
"caelo-production-mcp-auth-proxy",
|
|
474
|
+
);
|
|
475
|
+
for (const [provider, slug] of [
|
|
476
|
+
["gcp-firebase", "admin"],
|
|
477
|
+
["gcp-firebase", "mcp-auth-proxy"],
|
|
478
|
+
["gcp", "admin"],
|
|
479
|
+
["gcp", "gateway"],
|
|
480
|
+
] as const) {
|
|
481
|
+
expect(fixedCloudRunServiceName(provider, "production", slug)).toBeNull();
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("gives the proxy no database, site or pool env — only its mode, hops, file and seal key", () => {
|
|
486
|
+
for (const provider of ["gcp", "gcp-firebase"] as const) {
|
|
487
|
+
const env = asRecord(mcpAuthProxyEnvContract({ provider, env: "production" }));
|
|
488
|
+
expect(env).toEqual({
|
|
489
|
+
CAELO_PROVIDER: provider,
|
|
490
|
+
CAELO_REMOTE_MCP: "available",
|
|
491
|
+
CAELO_TRUSTED_PROXY_HOPS: provider === "gcp" ? "2" : "1",
|
|
492
|
+
CAELO_OPERATOR_OAUTH_CLIENT_FILE: "/secrets/operator-oauth-client/client.json",
|
|
493
|
+
CAELO_MCP_OAUTH_SEAL_KEY: "secret:caelo-production-mcp-oauth-seal-key:latest",
|
|
494
|
+
});
|
|
495
|
+
for (const name of Object.keys(env)) expect(name).not.toMatch(/DATABASE|POOL|KEK|STAGING/);
|
|
496
|
+
}
|
|
497
|
+
// Remote MCP needs Google IAP: everywhere else the proxy answers its 404.
|
|
498
|
+
expect(REMOTE_MCP_MODE).toEqual({
|
|
499
|
+
gcp: "available",
|
|
500
|
+
"gcp-firebase": "available",
|
|
501
|
+
"self-hosted": "unavailable",
|
|
502
|
+
aws: "unavailable",
|
|
503
|
+
azure: "unavailable",
|
|
504
|
+
});
|
|
505
|
+
expect(
|
|
506
|
+
asRecord(mcpAuthProxyEnvContract({ provider: "azure", env: "production" })),
|
|
507
|
+
).toMatchObject({
|
|
508
|
+
CAELO_REMOTE_MCP: "unavailable",
|
|
509
|
+
CAELO_TRUSTED_PROXY_HOPS: String(MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS.azure),
|
|
510
|
+
});
|
|
511
|
+
// The interim forward origin is the stack's / converge's, never the contract's.
|
|
512
|
+
expect(
|
|
513
|
+
mcpAuthProxyEnvContract({ provider: "gcp-firebase", env: "production" }).map((v) => v.name),
|
|
514
|
+
).not.toContain(INTERIM_FORWARD_ORIGIN_ENV);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("the proxy's SA reads exactly its two secrets and holds no other invariant", () => {
|
|
518
|
+
expect(serviceSecrets("mcp-auth-proxy")).toEqual([
|
|
519
|
+
"mcp-oauth-seal-key",
|
|
520
|
+
"operator-oauth-client",
|
|
521
|
+
]);
|
|
522
|
+
for (const provider of ["gcp", "gcp-firebase"] as const) {
|
|
523
|
+
const proxy = stackIamInvariants(provider).filter((i) => i.member === "mcp-auth-proxy-sa");
|
|
524
|
+
expect(proxy.map((i) => `${i.role} ${JSON.stringify(i.target)}`).sort()).toEqual([
|
|
525
|
+
'roles/secretmanager.secretAccessor {"kind":"secret","name":"mcp-oauth-seal-key"}',
|
|
526
|
+
'roles/secretmanager.secretAccessor {"kind":"secret","name":"operator-oauth-client"}',
|
|
527
|
+
]);
|
|
528
|
+
}
|
|
529
|
+
expect(SERVICE_VPC_EGRESS["mcp-auth-proxy"]).toBe(false);
|
|
530
|
+
expect(SERVICE_SECRET_FILES["mcp-auth-proxy"]).toHaveLength(1);
|
|
531
|
+
expect(SERVICE_SECRET_FILES.gateway).toEqual([]);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("retires the broker's env, file and secret access on the gateway", () => {
|
|
535
|
+
for (const name of [
|
|
536
|
+
"CAELO_OPERATOR_OAUTH_CLIENT_FILE",
|
|
537
|
+
"CAELO_MCP_OAUTH_SEAL_KEY",
|
|
538
|
+
"CAELO_MCP_OAUTH_SEAL_KEY_PREVIOUS",
|
|
539
|
+
"CAELO_MCP_PUBLIC_URL",
|
|
540
|
+
"CAELO_ADMIN_URL",
|
|
541
|
+
]) {
|
|
542
|
+
expect(RETIRED_SERVICE_ENV.gateway).toContain(name);
|
|
543
|
+
}
|
|
544
|
+
expect(RETIRED_SECRET_FILES.gateway.map((f) => f.secret)).toContain("operator-oauth-client");
|
|
545
|
+
for (const secret of ["mcp-oauth-seal-key", "operator-oauth-client"]) {
|
|
546
|
+
expect(RETIRED_IAM_BINDINGS).toContainEqual(
|
|
547
|
+
expect.objectContaining({ member: "gateway-sa", target: { kind: "secret", name: secret } }),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
// On gcp-firebase the proxy is the old gateway service: its visitor env goes.
|
|
551
|
+
for (const name of [
|
|
552
|
+
"PUBLIC_DATABASE_URL",
|
|
553
|
+
"PUBLIC_DATABASE_PASSWORD",
|
|
554
|
+
"CAELO_STAGING_GATE_SECRET",
|
|
555
|
+
]) {
|
|
556
|
+
expect(RETIRED_SERVICE_ENV["mcp-auth-proxy"]).toContain(name);
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("gcp-firebase: the data gateway is the rewrite target, the proxy public", () => {
|
|
561
|
+
const inv = stackIamInvariants("gcp-firebase");
|
|
562
|
+
const on = (service: string) =>
|
|
563
|
+
inv
|
|
564
|
+
.filter((i) => i.target.kind === "run-service" && i.target.service === service)
|
|
565
|
+
.map((i) => `${i.stackResource} ${i.role} ${i.member}`)
|
|
566
|
+
.sort();
|
|
567
|
+
expect(on("gateway")).toEqual([
|
|
568
|
+
"data-gateway-public-invoker roles/run.invoker allUsers",
|
|
569
|
+
"data-gateway-viewer-for-admin-sa roles/run.viewer run-sa",
|
|
570
|
+
]);
|
|
571
|
+
expect(on("mcp-auth-proxy")).toEqual(["gateway-public-invoker roles/run.invoker allUsers"]);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
410
575
|
describe("iamMember", () => {
|
|
411
576
|
const install = { projectId: "acme", projectNumber: "123", env: "production" };
|
|
412
577
|
it("resolves every principal to the name the stack gives it", () => {
|
|
@@ -416,6 +581,9 @@ describe("iamMember", () => {
|
|
|
416
581
|
expect(iamMember("gateway-sa", install)).toBe(
|
|
417
582
|
"serviceAccount:caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
|
|
418
583
|
);
|
|
584
|
+
expect(iamMember("mcp-auth-proxy-sa", install)).toBe(
|
|
585
|
+
"serviceAccount:caelo-production-mcp-proxy-sa@acme.iam.gserviceaccount.com",
|
|
586
|
+
);
|
|
419
587
|
expect(iamMember("static-publisher-sa", install)).toBe(
|
|
420
588
|
"serviceAccount:caelo-prod-publisher@acme.iam.gserviceaccount.com",
|
|
421
589
|
);
|