@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
package/dist/stack-contract.js
CHANGED
|
@@ -32,23 +32,125 @@
|
|
|
32
32
|
*
|
|
33
33
|
* Pure module — the stacks import it from `dist/`.
|
|
34
34
|
*/
|
|
35
|
+
import { CLOUD_SQL_CA_BUNDLE, isIpLiteral, verifyFullQuery } from "@caelo-cms/shared";
|
|
35
36
|
import { z } from "zod";
|
|
36
|
-
import { auditJobResource, captureJobResource, convergeJobResource, gatewayServiceAccountEmail, gcpBucketName, gcpNamePrefix, gcpSecretId, mcpIapServiceAccountEmail, operatorAccessJobResource, runServiceAccountEmail, staticPublisherServiceAccountId, upgraderJobResource, } from "./gcp-names.js";
|
|
37
|
+
import { auditJobResource, captureJobResource, convergeJobResource, gatewayServiceAccountEmail, gcpBucketName, gcpNamePrefix, gcpSecretId, mcpAuthProxyServiceAccountEmail, mcpIapServiceAccountEmail, operatorAccessJobResource, runServiceAccountEmail, schedulerServiceAccountEmail, staticPublisherServiceAccountId, upgraderJobResource, } from "./gcp-names.js";
|
|
38
|
+
/** Every {@link CloudRunSlug}, in roll order: the admin first, the proxy last. */
|
|
39
|
+
export const CLOUD_RUN_SLUGS = ["admin", "gateway", "mcp-auth-proxy"];
|
|
40
|
+
/**
|
|
41
|
+
* The name prefix of the Cloud Run service that plays `slug` on an install
|
|
42
|
+
* (Pulumi suffixes the names it generates; the services named explicitly,
|
|
43
|
+
* {@link fixedCloudRunServiceName}, match their prefix exactly).
|
|
44
|
+
*
|
|
45
|
+
* One asymmetry, decided as E13 (a) of the gateway hardening plan (#763):
|
|
46
|
+
* on `gcp-firebase` the MCP-Auth-Proxy is the service that was the gateway
|
|
47
|
+
* before #1328. Its `run.app` URL is the remote-MCP address every connector
|
|
48
|
+
* in claude.ai / ChatGPT holds, and the Google sign-in client's registered
|
|
49
|
+
* redirect URI points at it; neither can move without the Owner
|
|
50
|
+
* re-connecting every client and editing the OAuth client by hand. So that
|
|
51
|
+
* service keeps its name and becomes the proxy, and the visitor gateway is
|
|
52
|
+
* the new `caelo-<env>-data-gateway` (the Firebase `/api/**` rewrite the
|
|
53
|
+
* publisher writes follows the admin's `CAELO_GATEWAY_SERVICE`). On `gcp`
|
|
54
|
+
* the MCP address is the site's own host behind the load balancer, so no
|
|
55
|
+
* service name is pinned there and the proxy is the new service.
|
|
56
|
+
*
|
|
57
|
+
* No prefix is a prefix of another (`…-gateway` does not prefix
|
|
58
|
+
* `…-data-gateway`), so a `metadata.name~^<prefix>` lookup finds one role.
|
|
59
|
+
*/
|
|
60
|
+
export function cloudRunServicePrefix(provider, env, slug) {
|
|
61
|
+
const prefix = gcpNamePrefix(env);
|
|
62
|
+
if (slug === "admin")
|
|
63
|
+
return `${prefix}-admin`;
|
|
64
|
+
if (provider === "gcp-firebase") {
|
|
65
|
+
return slug === "gateway" ? `${prefix}-data-gateway` : `${prefix}-gateway`;
|
|
66
|
+
}
|
|
67
|
+
return slug === "gateway" ? `${prefix}-gateway` : `${prefix}-mcp-auth-proxy`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The exact name of a service that is created under a fixed name (#1328):
|
|
71
|
+
* the `gcp-firebase` data gateway and the `gcp` MCP-Auth-Proxy. Fixed, not
|
|
72
|
+
* Pulumi-suffixed, because the converge creates them on existing installs
|
|
73
|
+
* with gcloud, and the stack's next `pulumi up` must then find the same
|
|
74
|
+
* service instead of creating a second one. Null for the services Pulumi
|
|
75
|
+
* has always auto-named.
|
|
76
|
+
*/
|
|
77
|
+
export function fixedCloudRunServiceName(provider, env, slug) {
|
|
78
|
+
const fixed = (provider === "gcp-firebase" && slug === "gateway") ||
|
|
79
|
+
(provider === "gcp" && slug === "mcp-auth-proxy");
|
|
80
|
+
return fixed ? cloudRunServicePrefix(provider, env, slug) : null;
|
|
81
|
+
}
|
|
37
82
|
/** Port Cloud SQL Postgres listens on (private IP). */
|
|
38
83
|
const DATABASE_PORT = 5432;
|
|
39
84
|
/**
|
|
40
85
|
* Password-less connection URLs for a Cloud SQL instance at `host`. The
|
|
41
86
|
* password reaches the apps separately, as a Secret Manager reference
|
|
42
87
|
* ({@link SERVICE_SECRET_ENV}).
|
|
88
|
+
*
|
|
89
|
+
* TLS is `verify-full` against Google's Cloud SQL shared-CA bundle, which
|
|
90
|
+
* every image carries (#1338, shared database-tls.ts). The shared CA's
|
|
91
|
+
* server certificate names the instance's DNS name and no IP, so `host`
|
|
92
|
+
* must be that name (cloudsql-tls.ts resolves it in the VPC); an IP is
|
|
93
|
+
* refused here rather than producing URLs no connection can verify.
|
|
94
|
+
*
|
|
95
|
+
* @throws Error when `host` is an IP literal
|
|
43
96
|
*/
|
|
44
97
|
export function databaseUrls(host) {
|
|
45
|
-
|
|
98
|
+
if (isIpLiteral(host)) {
|
|
99
|
+
throw new Error(`the Cloud SQL host is the IP address ${host}, but verify-full connections must use the instance's DNS name (its certificate names no IP, #1338). The converge moves the services onto it: run \`cms-provision upgrade\`.`);
|
|
100
|
+
}
|
|
101
|
+
const url = (role, db) => `postgresql://${role}@${host}:${DATABASE_PORT}/${db}?${verifyFullQuery(CLOUD_SQL_CA_BUNDLE)}`;
|
|
46
102
|
return {
|
|
47
103
|
admin: url("admin_role", "cms_admin"),
|
|
48
104
|
publicAdmin: url("admin_role", "cms_public"),
|
|
49
105
|
public: url("public_role", "cms_public"),
|
|
106
|
+
publicStagingAdmin: url("admin_role", "cms_public_staging"),
|
|
107
|
+
publicStaging: url("public_staging_role", "cms_public_staging"),
|
|
50
108
|
};
|
|
51
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* #1338 — the Cloud SQL server CA mode every install runs: Google's shared
|
|
112
|
+
* CA. Its server certificate carries the instance's DNS name, so a client
|
|
113
|
+
* can verify the host (`sslmode=verify-full`). The default per-instance CA
|
|
114
|
+
* names no host a private-IP client can dial ("The SAN field contains the
|
|
115
|
+
* hostname (DNS name of the instance) only for instances enabled with
|
|
116
|
+
* Private Service Connect" — docs.cloud.google.com/sql/docs/postgres/authorize-ssl).
|
|
117
|
+
* An instance moves from per-instance to shared, never back.
|
|
118
|
+
*/
|
|
119
|
+
export const CLOUD_SQL_SERVER_CA_MODE = "GOOGLE_MANAGED_CAS_CA";
|
|
120
|
+
/** #1338 — Cloud SQL refuses unencrypted connections: every client uses verify-full. */
|
|
121
|
+
export const CLOUD_SQL_SSL_MODE = "ENCRYPTED_ONLY";
|
|
122
|
+
/** The `dnsNames[].connectionType` of the name a private-services-access client dials. */
|
|
123
|
+
export const CLOUD_SQL_PSA_CONNECTION_TYPE = "PRIVATE_SERVICES_ACCESS";
|
|
124
|
+
/**
|
|
125
|
+
* The private Cloud DNS zone that resolves the instance's DNS name to its
|
|
126
|
+
* private IP inside the install's VPC. Cloud SQL assigns the name but does
|
|
127
|
+
* not resolve it: "create the DNS record in a private DNS zone in the
|
|
128
|
+
* corresponding Virtual Private Cloud (VPC) network"
|
|
129
|
+
* (docs.cloud.google.com/sql/docs/postgres/configure-ssl-instance). The zone
|
|
130
|
+
* is the name itself, holding its one A record.
|
|
131
|
+
*/
|
|
132
|
+
export function cloudSqlDnsZoneName(env) {
|
|
133
|
+
return `${gcpNamePrefix(env)}-pg-dns`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The DNS name a private-IP client dials, from an instance's `dnsNames`,
|
|
137
|
+
* lower-case and without the trailing dot. Null when the instance has none
|
|
138
|
+
* (a per-instance-CA instance before its move to the shared CA).
|
|
139
|
+
*/
|
|
140
|
+
export function cloudSqlPrivateDnsName(dnsNames) {
|
|
141
|
+
const entry = (dnsNames ?? []).find((d) => d.connectionType === CLOUD_SQL_PSA_CONNECTION_TYPE);
|
|
142
|
+
const name = entry?.name?.replace(/\.$/, "").toLowerCase();
|
|
143
|
+
return name ? name : null;
|
|
144
|
+
}
|
|
145
|
+
/** The database URL vars each service carries (the env contract below sets them). */
|
|
146
|
+
export const DATABASE_URL_VARS = {
|
|
147
|
+
admin: ["ADMIN_DATABASE_URL", "PUBLIC_ADMIN_DATABASE_URL", "PUBLIC_STAGING_ADMIN_DATABASE_URL"],
|
|
148
|
+
gateway: ["PUBLIC_DATABASE_URL"],
|
|
149
|
+
// #1328 — no database login at all. On gcp-firebase the service still
|
|
150
|
+
// carries the gateway's URL until its roll removes it with the rest of
|
|
151
|
+
// the gateway env (RETIRED_SERVICE_ENV), so the TLS step leaves it alone.
|
|
152
|
+
"mcp-auth-proxy": [],
|
|
153
|
+
};
|
|
52
154
|
/** The public site's base URL, as the admin seeds `site_base_url` from it (#551). */
|
|
53
155
|
export function publicSiteUrl(domain) {
|
|
54
156
|
return `https://${domain}`;
|
|
@@ -102,6 +204,46 @@ export const AUDIT_RESULTS_BUCKET_ENV_VAR = "CAELO_AUDIT_RESULTS_BUCKET";
|
|
|
102
204
|
export const CAPTURE_JOB_ENV_VAR = "CAELO_CAPTURE_JOB";
|
|
103
205
|
export const CAPTURE_BUCKET_ENV_VAR = "CAELO_CAPTURE_BUCKET";
|
|
104
206
|
export const MEDIA_BUCKET_ENV_VAR = "CAELO_MEDIA_BUCKET";
|
|
207
|
+
/**
|
|
208
|
+
* #1140 — the admin env var naming the service account of the scheduler
|
|
209
|
+
* tick (scheduler-setup.ts): the one IAP identity the admin's
|
|
210
|
+
* `/api/internal/scheduler/tick` accepts. `upgrade` leaves it untouched when
|
|
211
|
+
* it could not set the tick up.
|
|
212
|
+
*/
|
|
213
|
+
export const SCHEDULER_SERVICE_ACCOUNT_ENV_VAR = "CAELO_SCHEDULER_SERVICE_ACCOUNT";
|
|
214
|
+
/**
|
|
215
|
+
* #1140 — how often the one scheduler job calls the admin's tick, in
|
|
216
|
+
* minutes (the `schedulerIntervalMinutes` install knob). Each task's own
|
|
217
|
+
* cadence lives in the admin's due-table; this only bounds how soon a due
|
|
218
|
+
* task runs. 1 is Cloud Scheduler's floor; above 15 Cloud Run has stopped
|
|
219
|
+
* the idle admin, so every tick would pay a cold start without saving
|
|
220
|
+
* anything (maintainer decision on #1140, 2026-10-26).
|
|
221
|
+
*/
|
|
222
|
+
export const SCHEDULER_INTERVAL_MINUTES = { default: 5, min: 1, max: 15 };
|
|
223
|
+
/**
|
|
224
|
+
* Parse the `schedulerIntervalMinutes` knob. Undefined (not configured) is
|
|
225
|
+
* the default; anything else must be an integer in the bounds — an
|
|
226
|
+
* out-of-range or non-numeric value throws (CLAUDE.md §2: no silent
|
|
227
|
+
* substitute).
|
|
228
|
+
*
|
|
229
|
+
* @param raw - the configured value (CLI flag, Pulumi config, install.json).
|
|
230
|
+
* @param source - names where the value came from, for the error.
|
|
231
|
+
*/
|
|
232
|
+
export function parseSchedulerIntervalMinutes(raw, source = "schedulerIntervalMinutes") {
|
|
233
|
+
if (raw === undefined || raw === "")
|
|
234
|
+
return SCHEDULER_INTERVAL_MINUTES.default;
|
|
235
|
+
const n = typeof raw === "number" ? raw : /^\s*\d+\s*$/.test(raw) ? Number(raw) : Number.NaN;
|
|
236
|
+
const { min, max } = SCHEDULER_INTERVAL_MINUTES;
|
|
237
|
+
if (!Number.isInteger(n) || n < min || n > max) {
|
|
238
|
+
throw new Error(`${source} ${JSON.stringify(raw)} is not a whole number of minutes between ${min} and ${max} (default ${SCHEDULER_INTERVAL_MINUTES.default})`);
|
|
239
|
+
}
|
|
240
|
+
return n;
|
|
241
|
+
}
|
|
242
|
+
/** The Cloud Scheduler / cron expression of a tick every `minutes` minutes (UTC). */
|
|
243
|
+
export function schedulerCron(minutes) {
|
|
244
|
+
const m = parseSchedulerIntervalMinutes(minutes);
|
|
245
|
+
return m === 1 ? "* * * * *" : `*/${m} * * * *`;
|
|
246
|
+
}
|
|
105
247
|
/** Where the admin image ships the static-generator CLI. */
|
|
106
248
|
const GENERATOR_CLI = "/app/apps/static-generator/src/cli.ts";
|
|
107
249
|
// ===========================================================================
|
|
@@ -146,6 +288,8 @@ export function adminMediaVolumeTemplate(volume) {
|
|
|
146
288
|
export const DATABASE_ROLE_SECRET = {
|
|
147
289
|
admin_role: "postgres-password",
|
|
148
290
|
public_role: "public-role-password",
|
|
291
|
+
// #1329 — its own, so neither gateway can ever log in as the other.
|
|
292
|
+
public_staging_role: "public-staging-role-password",
|
|
149
293
|
};
|
|
150
294
|
/**
|
|
151
295
|
* Secrets the CLI generates and stores (runtime-secrets.ts
|
|
@@ -160,6 +304,9 @@ export const CLI_GENERATED_SECRETS = [
|
|
|
160
304
|
// #613 — the gateway's own role password. Generated (never seeded from
|
|
161
305
|
// `postgres-password`), so the gateway never learns admin_role's.
|
|
162
306
|
"public-role-password",
|
|
307
|
+
// #1329 — the staging gateway's role password, never shared with the
|
|
308
|
+
// live gateway's (a test asserts the two public logins share no secret).
|
|
309
|
+
"public-staging-role-password",
|
|
163
310
|
// #650 — the staging access gate's key: signs "Open staging" links and
|
|
164
311
|
// session cookies, derives the crawler credentials. Read by the admin
|
|
165
312
|
// (issues) and the gateway (verifies).
|
|
@@ -171,6 +318,15 @@ export const CLI_GENERATED_SECRETS = [
|
|
|
171
318
|
"operator-oauth-client",
|
|
172
319
|
"mcp-oauth-seal-key",
|
|
173
320
|
];
|
|
321
|
+
/**
|
|
322
|
+
* CLI-generated secrets no running service reads yet, each with what reads
|
|
323
|
+
* it until then. Everything else in {@link CLI_GENERATED_SECRETS} must be
|
|
324
|
+
* read by a service (stack-contract.test.ts), so an orphaned secret cannot
|
|
325
|
+
* ship by accident.
|
|
326
|
+
*/
|
|
327
|
+
export const SECRETS_SET_UP_AHEAD_OF_THEIR_READER = {
|
|
328
|
+
"public-staging-role-password": "#1329 — public_staging_role's password. The stacks (gcp.sql.User) and the converge (staging-database.ts) set the role's password from it; the staging gateway service (#1330) is its first runtime reader.",
|
|
329
|
+
};
|
|
174
330
|
/**
|
|
175
331
|
* How each CLI-generated secret's first version is made. Every secret not
|
|
176
332
|
* listed is 32 random bytes as hex — the shape the stacks always generated.
|
|
@@ -179,6 +335,7 @@ export const GENERATED_SECRET_FORMAT = {
|
|
|
179
335
|
"internal-secret": { kind: "hex32" },
|
|
180
336
|
"tool-approval-secret": { kind: "hex32" },
|
|
181
337
|
"public-role-password": { kind: "hex32" },
|
|
338
|
+
"public-staging-role-password": { kind: "hex32" },
|
|
182
339
|
"staging-gate-secret": { kind: "hex32" },
|
|
183
340
|
// The gateway's seal key contract (#806): base64 of exactly 32 bytes.
|
|
184
341
|
"mcp-oauth-seal-key": { kind: "base64-32" },
|
|
@@ -212,6 +369,8 @@ export const SERVICE_SECRET_ENV = {
|
|
|
212
369
|
admin: {
|
|
213
370
|
ADMIN_DATABASE_PASSWORD: "postgres-password",
|
|
214
371
|
PUBLIC_ADMIN_DATABASE_PASSWORD: "postgres-password",
|
|
372
|
+
// #1329 — admin_role on the staging database.
|
|
373
|
+
PUBLIC_STAGING_ADMIN_DATABASE_PASSWORD: "postgres-password",
|
|
215
374
|
// P18 — encrypts AI provider keys and other at-rest secrets in cms_admin.
|
|
216
375
|
CAELO_SECRET_KEK: "secret-kek",
|
|
217
376
|
// P15.1 — HMAC key of the /api/internal/* bearer tokens (internal-jwt.ts).
|
|
@@ -229,6 +388,10 @@ export const SERVICE_SECRET_ENV = {
|
|
|
229
388
|
PUBLIC_DATABASE_PASSWORD: "public-role-password",
|
|
230
389
|
// #650 — verifies staging-host requests (api-gateway staging-proxy.ts).
|
|
231
390
|
CAELO_STAGING_GATE_SECRET: "staging-gate-secret",
|
|
391
|
+
},
|
|
392
|
+
// #1328 — with the Google sign-in client file below, the proxy's whole
|
|
393
|
+
// Secret Manager reach: no database password, no other key.
|
|
394
|
+
"mcp-auth-proxy": {
|
|
232
395
|
// #806 — seals the remote-MCP broker's state, codes and refresh tokens.
|
|
233
396
|
CAELO_MCP_OAUTH_SEAL_KEY: "mcp-oauth-seal-key",
|
|
234
397
|
},
|
|
@@ -238,7 +401,7 @@ export const SERVICE_SECRET_ENV = {
|
|
|
238
401
|
// ===========================================================================
|
|
239
402
|
/** Env var naming the file that holds the install's Google sign-in client (#914). */
|
|
240
403
|
export const OPERATOR_OAUTH_CLIENT_FILE_ENV = "CAELO_OPERATOR_OAUTH_CLIENT_FILE";
|
|
241
|
-
/** The
|
|
404
|
+
/** The Google sign-in client file the remote-MCP broker reads (#806, #914; in the MCP-Auth-Proxy since #1328). */
|
|
242
405
|
export const OPERATOR_OAUTH_CLIENT_FILE = {
|
|
243
406
|
secret: "operator-oauth-client",
|
|
244
407
|
volumeName: "operator-oauth-client",
|
|
@@ -250,10 +413,11 @@ export const OPERATOR_OAUTH_CLIENT_FILE = {
|
|
|
250
413
|
export function secretFilePath(file) {
|
|
251
414
|
return `${file.mountDir}/${file.fileName}`;
|
|
252
415
|
}
|
|
253
|
-
/** Each service's secret files.
|
|
416
|
+
/** Each service's secret files. Only the MCP-Auth-Proxy has one. */
|
|
254
417
|
export const SERVICE_SECRET_FILES = {
|
|
255
418
|
admin: [],
|
|
256
|
-
gateway: [
|
|
419
|
+
gateway: [],
|
|
420
|
+
"mcp-auth-proxy": [OPERATOR_OAUTH_CLIENT_FILE],
|
|
257
421
|
};
|
|
258
422
|
/**
|
|
259
423
|
* Secret files older contracts mounted that nothing reads any more, per
|
|
@@ -270,6 +434,22 @@ export const RETIRED_SECRET_FILES = {
|
|
|
270
434
|
fileName: "client.json",
|
|
271
435
|
why: "the Google sign-in client is mounted from operator-oauth-client now (#914)",
|
|
272
436
|
},
|
|
437
|
+
{
|
|
438
|
+
secret: "operator-oauth-client",
|
|
439
|
+
mountDir: "/secrets/operator-oauth-client",
|
|
440
|
+
fileName: "client.json",
|
|
441
|
+
why: "the remote-MCP broker moved to the MCP-Auth-Proxy (#1328); the visitor gateway must not hold the Google sign-in client",
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
"mcp-auth-proxy": [
|
|
445
|
+
{
|
|
446
|
+
// gcp-firebase: the proxy is the service that was the gateway, which
|
|
447
|
+
// mounted this file until #914.
|
|
448
|
+
secret: "mcp-oauth-client",
|
|
449
|
+
mountDir: "/secrets/mcp-oauth-client",
|
|
450
|
+
fileName: "client.json",
|
|
451
|
+
why: "the Google sign-in client is mounted from operator-oauth-client now (#914)",
|
|
452
|
+
},
|
|
273
453
|
],
|
|
274
454
|
};
|
|
275
455
|
/**
|
|
@@ -317,6 +497,30 @@ export const RETIRED_SERVICE_ENV = {
|
|
|
317
497
|
// #635 — its gateway_role login on cms_admin is gone too.
|
|
318
498
|
"GATEWAY_DATABASE_URL",
|
|
319
499
|
"GATEWAY_DATABASE_PASSWORD",
|
|
500
|
+
// #1328 — remote MCP moved to the MCP-Auth-Proxy.
|
|
501
|
+
"CAELO_OPERATOR_OAUTH_CLIENT_FILE",
|
|
502
|
+
"CAELO_MCP_OAUTH_SEAL_KEY",
|
|
503
|
+
"CAELO_MCP_OAUTH_SEAL_KEY_PREVIOUS",
|
|
504
|
+
"CAELO_MCP_PUBLIC_URL",
|
|
505
|
+
"CAELO_ADMIN_URL",
|
|
506
|
+
],
|
|
507
|
+
// #1328 — on gcp-firebase the proxy is the service that was the gateway:
|
|
508
|
+
// everything that service carried for visitor traffic goes, above all
|
|
509
|
+
// every database login and the staging gate key. The proxy's SA cannot
|
|
510
|
+
// read their secrets, so a revision that kept one would not even deploy.
|
|
511
|
+
"mcp-auth-proxy": [
|
|
512
|
+
"PUBLIC_DATABASE_URL",
|
|
513
|
+
"PUBLIC_DATABASE_PASSWORD",
|
|
514
|
+
"CAELO_STAGING_GATE_SECRET",
|
|
515
|
+
"CAELO_ENV",
|
|
516
|
+
"MEDIA_STORAGE_URL",
|
|
517
|
+
"CAELO_DB_PUBLIC_POOL_MAX",
|
|
518
|
+
"CAELO_MCP_OAUTH_CLIENT_FILE",
|
|
519
|
+
"CAELO_SECRET_KEK",
|
|
520
|
+
"ADMIN_DATABASE_URL",
|
|
521
|
+
"ADMIN_DATABASE_PASSWORD",
|
|
522
|
+
"GATEWAY_DATABASE_URL",
|
|
523
|
+
"GATEWAY_DATABASE_PASSWORD",
|
|
320
524
|
],
|
|
321
525
|
};
|
|
322
526
|
function secretEnv(service, env) {
|
|
@@ -345,6 +549,14 @@ function commonEnv(inputs) {
|
|
|
345
549
|
* in front of it append, counting the client's own.
|
|
346
550
|
*/
|
|
347
551
|
export const TRUSTED_PROXY_HOPS_ENV = "CAELO_TRUSTED_PROXY_HOPS";
|
|
552
|
+
/**
|
|
553
|
+
* #1329 — the env var naming which environment a gateway serves, `live` or
|
|
554
|
+
* `staging` (apps/api-gateway database-env.ts): its database and login
|
|
555
|
+
* follow from it, and it refuses anything of the other environment's. The
|
|
556
|
+
* gateway refuses to boot without it. Every adapter sets `live` on the
|
|
557
|
+
* gateway it runs today; the staging gateway (P3b-2) gets `staging`.
|
|
558
|
+
*/
|
|
559
|
+
export const GATEWAY_ENVIRONMENT_ENV = "CAELO_GATEWAY_ENVIRONMENT";
|
|
348
560
|
/**
|
|
349
561
|
* {@link TRUSTED_PROXY_HOPS_ENV} per provider — a property of each
|
|
350
562
|
* adapter's topology (CLAUDE.md §11.B), so it is declared here once and every
|
|
@@ -358,10 +570,12 @@ export const GATEWAY_TRUSTED_PROXY_HOPS = {
|
|
|
358
570
|
// ingress is internal-and-LB only, so nothing reaches it around the LB.
|
|
359
571
|
gcp: 2,
|
|
360
572
|
// Cloud Run's front end appends its peer. Behind the Firebase Hosting
|
|
361
|
-
// rewrite that peer is
|
|
362
|
-
// before it cannot be told apart
|
|
363
|
-
// invokable on *.run.app too) — so the
|
|
364
|
-
//
|
|
573
|
+
// rewrite that peer is a Google front-end proxy that changes per request,
|
|
574
|
+
// and the visitor entry Hosting writes before it cannot be told apart
|
|
575
|
+
// from a forged one (the service is invokable on *.run.app too) — so the
|
|
576
|
+
// proxy address is the most that can be trusted
|
|
577
|
+
// (docs/dev/firebase-hosting-behaviour.md). Staging (Cloud Run domain
|
|
578
|
+
// mapping) gets the visitor.
|
|
365
579
|
"gcp-firebase": 1,
|
|
366
580
|
// Caddy without `trusted_proxies` replaces the header with its peer; the
|
|
367
581
|
// gateway is reachable only on the Compose network.
|
|
@@ -380,26 +594,104 @@ export const GATEWAY_TRUSTED_PROXY_HOPS = {
|
|
|
380
594
|
export function gatewayEnvContract(inputs) {
|
|
381
595
|
return [
|
|
382
596
|
...commonEnv(inputs),
|
|
597
|
+
// #1329 — this service is the live site's gateway: it may reach
|
|
598
|
+
// cms_public as public_role and refuses anything of the staging data.
|
|
599
|
+
{ name: GATEWAY_ENVIRONMENT_ENV, value: "live" },
|
|
383
600
|
{ name: TRUSTED_PROXY_HOPS_ENV, value: String(GATEWAY_TRUSTED_PROXY_HOPS[inputs.provider]) },
|
|
384
601
|
{ name: "PUBLIC_DATABASE_URL", value: inputs.databaseUrls.public },
|
|
385
|
-
// #806 — where the remote-MCP broker reads its Google OAuth client
|
|
386
|
-
// (the secret volume of SERVICE_SECRET_FILES).
|
|
387
|
-
{ name: OPERATOR_OAUTH_CLIENT_FILE_ENV, value: secretFilePath(OPERATOR_OAUTH_CLIENT_FILE) },
|
|
388
602
|
...secretEnv("gateway", inputs.env),
|
|
389
603
|
];
|
|
390
604
|
}
|
|
391
605
|
/**
|
|
392
|
-
* #
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
|
|
399
|
-
|
|
606
|
+
* #1328 — whether remote MCP can exist on a provider, as the MCP-Auth-Proxy
|
|
607
|
+
* reads it. It needs the admin behind Google IAP, which only the Google
|
|
608
|
+
* Cloud installs have. `unavailable` makes the proxy answer one fixed 404
|
|
609
|
+
* to everything but its health check: the service exists on every provider
|
|
610
|
+
* at zero minimum instances (E14 a of #763), its attack surface one branch.
|
|
611
|
+
* The proxy refuses to start without the var — no default (CLAUDE.md §2).
|
|
612
|
+
*/
|
|
613
|
+
export const REMOTE_MCP_MODE_ENV = "CAELO_REMOTE_MCP";
|
|
614
|
+
/** {@link REMOTE_MCP_MODE_ENV} per provider. */
|
|
615
|
+
export const REMOTE_MCP_MODE = {
|
|
616
|
+
gcp: "available",
|
|
617
|
+
"gcp-firebase": "available",
|
|
618
|
+
"self-hosted": "unavailable",
|
|
619
|
+
aws: "unavailable",
|
|
620
|
+
azure: "unavailable",
|
|
621
|
+
};
|
|
622
|
+
/**
|
|
623
|
+
* {@link TRUSTED_PROXY_HOPS_ENV} of the MCP-Auth-Proxy per provider: its
|
|
624
|
+
* rate limits key on the client address by the same rule as the gateway's
|
|
625
|
+
* (@caelo-cms/shared client-address.ts), under its own topology.
|
|
626
|
+
*/
|
|
627
|
+
export const MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS = {
|
|
628
|
+
// Behind the external Application LB (`<client>,<LB IP>`); its ingress
|
|
629
|
+
// is internal-and-LB only.
|
|
630
|
+
gcp: 2,
|
|
631
|
+
// Its own run.app URL with no rewrite in front: Cloud Run's front end
|
|
632
|
+
// appends the client.
|
|
633
|
+
"gcp-firebase": 1,
|
|
634
|
+
// Caddy (its own vhost) replaces the header with its peer.
|
|
635
|
+
"self-hosted": 1,
|
|
636
|
+
// Not deployed yet (#749); the gateway's topology once it is.
|
|
637
|
+
aws: 2,
|
|
638
|
+
// Container Apps ingress appends its peer.
|
|
639
|
+
azure: 1,
|
|
640
|
+
};
|
|
641
|
+
/**
|
|
642
|
+
* #1328 — points the `gcp-firebase` MCP-Auth-Proxy at the visitor data
|
|
643
|
+
* gateway for the traffic that still reaches the service that used to be
|
|
644
|
+
* the gateway: the Firebase `/api/**` rewrite of the release that is live
|
|
645
|
+
* until the next Publish live, and the staging host's Cloud Run domain
|
|
646
|
+
* mapping (#650), which only the domain's verified owner could move. The
|
|
647
|
+
* proxy streams that traffic to the data gateway unchanged instead of
|
|
648
|
+
* answering it (apps/mcp-auth-proxy interim-forward.ts). gcp-firebase only.
|
|
649
|
+
*/
|
|
650
|
+
// FIXME(issue #1331): goes with the staging cutover, once neither a mapping
|
|
651
|
+
// nor a published rewrite can point at the proxy any more.
|
|
652
|
+
export const INTERIM_FORWARD_ORIGIN_ENV = "CAELO_INTERIM_FORWARD_ORIGIN";
|
|
653
|
+
/**
|
|
654
|
+
* The env of the MCP-Auth-Proxy. No database URL, no pool size, no site
|
|
655
|
+
* settings: the broker reads env plus one secret file and nothing else
|
|
656
|
+
* (apps/mcp-auth-proxy, whose import lint keeps every database client out).
|
|
657
|
+
* The remote-MCP identity env — its public origin, the admin's URL — comes
|
|
658
|
+
* on top from {@link remoteMcpIdentityEnv}, the gcp-firebase forward origin
|
|
659
|
+
* from the stack or the converge ({@link INTERIM_FORWARD_ORIGIN_ENV}).
|
|
660
|
+
*/
|
|
661
|
+
export function mcpAuthProxyEnvContract(inputs) {
|
|
662
|
+
return [
|
|
663
|
+
{ name: "CAELO_PROVIDER", value: inputs.provider },
|
|
664
|
+
{ name: REMOTE_MCP_MODE_ENV, value: REMOTE_MCP_MODE[inputs.provider] },
|
|
665
|
+
{
|
|
666
|
+
name: TRUSTED_PROXY_HOPS_ENV,
|
|
667
|
+
value: String(MCP_AUTH_PROXY_TRUSTED_PROXY_HOPS[inputs.provider]),
|
|
668
|
+
},
|
|
669
|
+
// #806 — where the broker reads the Google sign-in client (the secret
|
|
670
|
+
// volume of SERVICE_SECRET_FILES).
|
|
671
|
+
{ name: OPERATOR_OAUTH_CLIENT_FILE_ENV, value: secretFilePath(OPERATOR_OAUTH_CLIENT_FILE) },
|
|
672
|
+
...secretEnv("mcp-auth-proxy", inputs.env),
|
|
673
|
+
];
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* #806 — the MCP-Auth-Proxy's request timeout, in seconds: Cloud Run's
|
|
677
|
+
* maximum (the gateway's until #1328 moved the broker out). The proxy
|
|
678
|
+
* streams remote-MCP requests to the admin, and a `caelo_chat` turn streams
|
|
679
|
+
* for as long as the admin's own chat stream may (the admin runs at 3600 s
|
|
680
|
+
* for the same reason). A timeout is a ceiling, not a reservation: Cloud
|
|
681
|
+
* Run bills request time, not the allowed maximum. On `gcp` the load
|
|
682
|
+
* balancer in front adds no shorter limit — the backend-service timeout
|
|
683
|
+
* does not apply to serverless NEGs, which get a fixed 60 minutes
|
|
400
684
|
* (cloud.google.com/load-balancing/docs/negs/serverless-neg-concepts).
|
|
401
685
|
*/
|
|
402
|
-
export const
|
|
686
|
+
export const MCP_AUTH_PROXY_TIMEOUT_SECONDS = 3600;
|
|
687
|
+
/**
|
|
688
|
+
* #1328 — the MCP-Auth-Proxy's maximum instances. It holds no database
|
|
689
|
+
* pool, so the connection budget does not bound it; ten instances of
|
|
690
|
+
* streaming proxies carry far more agent sessions than an install has
|
|
691
|
+
* operators, and the ceiling caps what a flood of sign-in requests can cost.
|
|
692
|
+
* Always provisioned at minimum 0 (E14 a of #763).
|
|
693
|
+
*/
|
|
694
|
+
export const DEFAULT_MCP_AUTH_PROXY_MAX_INSTANCES = 10;
|
|
403
695
|
/**
|
|
404
696
|
* Whether each Cloud Run service gets CPU only while it handles a request
|
|
405
697
|
* (Cloud Run's request-based billing, `cpuIdle: true`) or for its whole
|
|
@@ -408,15 +700,29 @@ export const GATEWAY_TIMEOUT_SECONDS = 3600;
|
|
|
408
700
|
* `resources`, which is how both services ran with always-on CPU without
|
|
409
701
|
* anyone choosing it — billed for every idle minute an instance stayed up.
|
|
410
702
|
*
|
|
411
|
-
* The gateway only
|
|
412
|
-
*
|
|
413
|
-
*
|
|
703
|
+
* The gateway and the MCP-Auth-Proxy only do work inside requests. The
|
|
704
|
+
* admin's periodic work and plugin workers run on the scheduler tick
|
|
705
|
+
* (#1140), but the quality audit and the site-import crawl still run on
|
|
706
|
+
* in-process timers that need CPU between requests (#1359, #1360), and the
|
|
707
|
+
* plugin runtime heartbeat would read an idling instance as down.
|
|
414
708
|
*/
|
|
415
709
|
export const SERVICE_CPU_IDLE = {
|
|
416
|
-
// FIXME(issue #
|
|
417
|
-
//
|
|
710
|
+
// FIXME(issue #1361): true once nothing in the admin depends on CPU
|
|
711
|
+
// between requests (the category-A jobs, the heartbeat at request start).
|
|
418
712
|
admin: false,
|
|
419
713
|
gateway: true,
|
|
714
|
+
"mcp-auth-proxy": true,
|
|
715
|
+
};
|
|
716
|
+
/**
|
|
717
|
+
* Whether each service egresses into the install's VPC: the admin and the
|
|
718
|
+
* gateway reach Cloud SQL's private IP; the MCP-Auth-Proxy reaches nothing
|
|
719
|
+
* private (#1328) — it calls Google's token endpoint and the admin's public,
|
|
720
|
+
* IAP-gated URL, so it gets no route to the database's network at all.
|
|
721
|
+
*/
|
|
722
|
+
export const SERVICE_VPC_EGRESS = {
|
|
723
|
+
admin: true,
|
|
724
|
+
gateway: true,
|
|
725
|
+
"mcp-auth-proxy": false,
|
|
420
726
|
};
|
|
421
727
|
/** Admin env var naming the Secret Manager secret the Owner writes the Google sign-in client to. */
|
|
422
728
|
export const OPERATOR_OAUTH_CLIENT_STORE_ENV = "CAELO_OPERATOR_OAUTH_CLIENT_STORE";
|
|
@@ -435,11 +741,12 @@ export const REMOTE_MCP_ENV = {
|
|
|
435
741
|
* The install's remote-MCP identity.
|
|
436
742
|
*
|
|
437
743
|
* - `gcp`: the broker answers on the site's own host through the load
|
|
438
|
-
* balancer (
|
|
439
|
-
* on `admin.<domain>` behind the LB's IAP; the IAP
|
|
440
|
-
* `/projects/<number>/global/backendServices/<id>`.
|
|
441
|
-
* - `gcp-firebase`: the broker answers on the
|
|
442
|
-
*
|
|
744
|
+
* balancer (its MCP path rules route to the MCP-Auth-Proxy's backend,
|
|
745
|
+
* #1328), the admin on `admin.<domain>` behind the LB's IAP; the IAP
|
|
746
|
+
* audience is `/projects/<number>/global/backendServices/<id>`.
|
|
747
|
+
* - `gcp-firebase`: the broker answers on the MCP-Auth-Proxy's own Cloud Run
|
|
748
|
+
* URL — the URL the gateway had before #1328 (E13 a) — never through
|
|
749
|
+
* Firebase Hosting (rewrites cut requests at 60 s and buffer streams), the
|
|
443
750
|
* admin on its Cloud Run URL behind native IAP — whatever custom domain
|
|
444
751
|
* is mapped to it, IAP and its audience are the service's; the audience
|
|
445
752
|
* is `/projects/<number>/locations/<region>/services/<name>`.
|
|
@@ -457,7 +764,7 @@ export function remoteMcpIdentity(i) {
|
|
|
457
764
|
}
|
|
458
765
|
return {
|
|
459
766
|
adminUrl: origin(i.adminServiceUrl),
|
|
460
|
-
mcpPublicUrl: origin(i.
|
|
767
|
+
mcpPublicUrl: origin(i.mcpAuthProxyServiceUrl),
|
|
461
768
|
iapAudience: `/projects/${i.projectNumber}/locations/${i.region}/services/${i.adminServiceName}`,
|
|
462
769
|
};
|
|
463
770
|
}
|
|
@@ -476,7 +783,10 @@ export function remoteMcpIdentityEnv(id) {
|
|
|
476
783
|
? [{ name: REMOTE_MCP_ENV.googleOrgDomain, value: id.googleOrgDomain }]
|
|
477
784
|
: []),
|
|
478
785
|
],
|
|
479
|
-
gateway
|
|
786
|
+
// #1328 — the broker lives in the MCP-Auth-Proxy; the visitor gateway
|
|
787
|
+
// gets none of it.
|
|
788
|
+
gateway: [],
|
|
789
|
+
"mcp-auth-proxy": [
|
|
480
790
|
{ name: REMOTE_MCP_ENV.adminUrl, value: id.adminUrl },
|
|
481
791
|
{ name: REMOTE_MCP_ENV.mcpPublicUrl, value: id.mcpPublicUrl },
|
|
482
792
|
],
|
|
@@ -534,7 +844,7 @@ export function parseRemoteMcpIdentityConfig(raw) {
|
|
|
534
844
|
*/
|
|
535
845
|
export function remoteMcpIdentityStackEnv(raw) {
|
|
536
846
|
const id = parseRemoteMcpIdentityConfig(raw);
|
|
537
|
-
return id ? remoteMcpIdentityEnv(id) : { admin: [], gateway: [] };
|
|
847
|
+
return id ? remoteMcpIdentityEnv(id) : { admin: [], gateway: [], "mcp-auth-proxy": [] };
|
|
538
848
|
}
|
|
539
849
|
/** Whether two identities would give the services the same env. */
|
|
540
850
|
export function sameRemoteMcpIdentity(a, b) {
|
|
@@ -577,6 +887,9 @@ export function adminEnvContract(inputs) {
|
|
|
577
887
|
// admin_role on cms_public: cross-DB reads, plugin DDL, migrations.
|
|
578
888
|
// Not public_role (write-limited).
|
|
579
889
|
{ name: "PUBLIC_ADMIN_DATABASE_URL", value: inputs.databaseUrls.publicAdmin },
|
|
890
|
+
// #1329 — admin_role on cms_public_staging: the staging database's
|
|
891
|
+
// plugin schemas and its half of the projection.
|
|
892
|
+
{ name: "PUBLIC_STAGING_ADMIN_DATABASE_URL", value: inputs.databaseUrls.publicStagingAdmin },
|
|
580
893
|
// #551 — the admin seeds site_defaults.site_base_url from it (canonical,
|
|
581
894
|
// og:url, sitemap) when it is not configured yet.
|
|
582
895
|
{ name: "CAELO_SITE_URL", value: publicSiteUrl(domain) },
|
|
@@ -608,6 +921,12 @@ export function adminEnvContract(inputs) {
|
|
|
608
921
|
{ name: CAPTURE_JOB_ENV_VAR, value: captureJobResource(projectId, inputs.region, env) },
|
|
609
922
|
{ name: CAPTURE_BUCKET_ENV_VAR, value: gcpBucketName(projectId, env, "capture") },
|
|
610
923
|
{ name: MEDIA_BUCKET_ENV_VAR, value: gcpBucketName(projectId, env, "media") },
|
|
924
|
+
// #1140 — the only IAP identity the admin's scheduler tick endpoint
|
|
925
|
+
// accepts (scheduler-setup.ts).
|
|
926
|
+
{
|
|
927
|
+
name: SCHEDULER_SERVICE_ACCOUNT_ENV_VAR,
|
|
928
|
+
value: schedulerServiceAccountEmail(projectId, env),
|
|
929
|
+
},
|
|
611
930
|
// #914 — where Settings → Google sign-in writes the install's Google
|
|
612
931
|
// sign-in client (add-version only, see the run SA's secretVersionAdder
|
|
613
932
|
// invariant).
|
|
@@ -658,18 +977,24 @@ const SECRET_VERSION_ADDER = "roles/secretmanager.secretVersionAdder";
|
|
|
658
977
|
* on exactly the secrets its env reads ({@link SERVICE_SECRET_ENV}).
|
|
659
978
|
*/
|
|
660
979
|
export function runtimeSecretBindings() {
|
|
661
|
-
return
|
|
980
|
+
return CLOUD_RUN_SLUGS.flatMap((service) => serviceSecrets(service).map((secret) => ({
|
|
662
981
|
service,
|
|
663
982
|
secret,
|
|
664
983
|
// The admin's keep the names they had when one SA served both.
|
|
665
|
-
stackResource: service === "admin" ? `${secret}-binding` :
|
|
984
|
+
stackResource: service === "admin" ? `${secret}-binding` : `${service}-${secret}-binding`,
|
|
666
985
|
})));
|
|
667
986
|
}
|
|
987
|
+
/** The runtime SA each Cloud Run service runs as. */
|
|
988
|
+
export const SERVICE_PRINCIPAL = {
|
|
989
|
+
admin: "run-sa",
|
|
990
|
+
gateway: "gateway-sa",
|
|
991
|
+
"mcp-auth-proxy": "mcp-auth-proxy-sa",
|
|
992
|
+
};
|
|
668
993
|
function secretAccessors() {
|
|
669
994
|
return runtimeSecretBindings().map((b) => ({
|
|
670
995
|
stackResource: b.stackResource,
|
|
671
996
|
role: SECRET_ACCESSOR,
|
|
672
|
-
member: b.service
|
|
997
|
+
member: SERVICE_PRINCIPAL[b.service],
|
|
673
998
|
target: { kind: "secret", name: b.secret },
|
|
674
999
|
onFailure: "abort",
|
|
675
1000
|
why: `the ${b.service} reads ${b.secret} from Secret Manager at boot`,
|
|
@@ -795,7 +1120,7 @@ const GCP_FIREBASE_ONLY = [
|
|
|
795
1120
|
why: "the admin publishes the site to Firebase Hosting",
|
|
796
1121
|
},
|
|
797
1122
|
{
|
|
798
|
-
stackResource: "gateway-public-invoker",
|
|
1123
|
+
stackResource: "data-gateway-public-invoker",
|
|
799
1124
|
role: "roles/run.invoker",
|
|
800
1125
|
member: "allUsers",
|
|
801
1126
|
target: { kind: "run-service", service: "gateway" },
|
|
@@ -803,13 +1128,24 @@ const GCP_FIREBASE_ONLY = [
|
|
|
803
1128
|
why: "Firebase Hosting rewrites /api/** to the public gateway",
|
|
804
1129
|
},
|
|
805
1130
|
{
|
|
806
|
-
stackResource: "gateway-viewer-for-admin-sa",
|
|
1131
|
+
stackResource: "data-gateway-viewer-for-admin-sa",
|
|
807
1132
|
role: "roles/run.viewer",
|
|
808
1133
|
member: "run-sa",
|
|
809
1134
|
target: { kind: "run-service", service: "gateway" },
|
|
810
1135
|
onFailure: "abort",
|
|
811
1136
|
why: "Firebase Hosting checks run.services.get on the rewrite target at deploy time (v0.6.5; without it every publish 403s)",
|
|
812
1137
|
},
|
|
1138
|
+
{
|
|
1139
|
+
// #1328 — the service that was the gateway (E13 a); MCP clients call
|
|
1140
|
+
// its run.app URL without a Google identity of their own. The stack
|
|
1141
|
+
// keeps the binding's old logical name, like the service's.
|
|
1142
|
+
stackResource: "gateway-public-invoker",
|
|
1143
|
+
role: "roles/run.invoker",
|
|
1144
|
+
member: "allUsers",
|
|
1145
|
+
target: { kind: "run-service", service: "mcp-auth-proxy" },
|
|
1146
|
+
onFailure: "abort",
|
|
1147
|
+
why: "MCP clients sign in and call remote MCP at the MCP-Auth-Proxy's run.app URL",
|
|
1148
|
+
},
|
|
813
1149
|
{
|
|
814
1150
|
stackResource: "admin-iap-sa-invoker",
|
|
815
1151
|
role: "roles/run.invoker",
|
|
@@ -837,6 +1173,26 @@ export const RETIRED_IAM_BINDINGS = [
|
|
|
837
1173
|
target: { kind: "secret", name: "gateway-role-password" },
|
|
838
1174
|
why: "the gateway has no cms_admin login at all and must not be able to read gateway_role's password (CLAUDE.md §2, #635)",
|
|
839
1175
|
},
|
|
1176
|
+
{
|
|
1177
|
+
role: SECRET_ACCESSOR,
|
|
1178
|
+
member: "gateway-sa",
|
|
1179
|
+
target: { kind: "secret", name: "mcp-oauth-seal-key" },
|
|
1180
|
+
why: "the remote-MCP broker moved to the MCP-Auth-Proxy (#1328); a hole in a visitor handler must not open or mint the broker's sealed tokens",
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
role: SECRET_ACCESSOR,
|
|
1184
|
+
member: "gateway-sa",
|
|
1185
|
+
target: { kind: "secret", name: "operator-oauth-client" },
|
|
1186
|
+
why: "the remote-MCP broker moved to the MCP-Auth-Proxy (#1328); the visitor gateway must not read the Google sign-in client's secret",
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
// gcp-firebase: the Firebase rewrite targets the data gateway since
|
|
1190
|
+
// #1328, so the publisher no longer needs to read the proxy.
|
|
1191
|
+
role: "roles/run.viewer",
|
|
1192
|
+
member: "run-sa",
|
|
1193
|
+
target: { kind: "run-service", service: "mcp-auth-proxy" },
|
|
1194
|
+
why: "the Firebase /api/** rewrite targets the data gateway since #1328; the admin needs no read access to the MCP-Auth-Proxy",
|
|
1195
|
+
},
|
|
840
1196
|
];
|
|
841
1197
|
/** Every IAM binding upgrade ensures on an install of `provider`. */
|
|
842
1198
|
export function stackIamInvariants(provider) {
|
|
@@ -873,6 +1229,8 @@ export function iamMember(principal, install) {
|
|
|
873
1229
|
return `serviceAccount:${runServiceAccountEmail(install.projectId, install.env)}`;
|
|
874
1230
|
case "gateway-sa":
|
|
875
1231
|
return `serviceAccount:${gatewayServiceAccountEmail(install.projectId, install.env)}`;
|
|
1232
|
+
case "mcp-auth-proxy-sa":
|
|
1233
|
+
return `serviceAccount:${mcpAuthProxyServiceAccountEmail(install.projectId, install.env)}`;
|
|
876
1234
|
case "static-publisher-sa":
|
|
877
1235
|
return `serviceAccount:${staticPublisherServiceAccountId(install.env)}@${install.projectId}.iam.gserviceaccount.com`;
|
|
878
1236
|
case "iap-service-agent":
|