@caelo-cms/provisioning 0.10.50 → 0.10.52
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/caddy/Caddyfile.production +1 -1
- package/caddy/Caddyfile.staging +1 -1
- package/dist/adapter.d.ts +4 -6
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin-internal.d.ts +41 -0
- package/dist/admin-internal.d.ts.map +1 -0
- package/dist/admin-internal.js +169 -0
- package/dist/admin-internal.js.map +1 -0
- package/dist/agent/units.js +1 -1
- package/dist/agent/units.js.map +1 -1
- package/dist/agent-db.js +169 -65
- package/dist/aws-task-roles.d.ts +27 -0
- package/dist/aws-task-roles.d.ts.map +1 -0
- package/dist/aws-task-roles.js +35 -0
- package/dist/aws-task-roles.js.map +1 -0
- package/dist/cli.js +34 -1
- package/dist/cli.js.map +1 -1
- package/dist/compose.js +2 -2
- package/dist/compose.js.map +1 -1
- package/dist/deployed-release.d.ts +3 -2
- package/dist/deployed-release.d.ts.map +1 -1
- package/dist/deployed-release.js +12 -5
- package/dist/deployed-release.js.map +1 -1
- package/dist/gateway-credentials.d.ts.map +1 -1
- package/dist/gateway-credentials.js +6 -3
- package/dist/gateway-credentials.js.map +1 -1
- package/dist/gcloud.d.ts +2 -2
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +3 -3
- package/dist/gcloud.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 +12 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/ops-backend.js +1 -1
- package/dist/owner-reset.d.ts +37 -0
- package/dist/owner-reset.d.ts.map +1 -0
- package/dist/owner-reset.js +80 -0
- package/dist/owner-reset.js.map +1 -0
- 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/stack-contract.d.ts +9 -10
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +21 -10
- package/dist/stack-contract.js.map +1 -1
- package/dist/staging-admin.d.ts +12 -20
- package/dist/staging-admin.d.ts.map +1 -1
- package/dist/staging-admin.js +7 -159
- package/dist/staging-admin.js.map +1 -1
- package/dist/state-store.d.ts +8 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +10 -2
- package/dist/state-store.js.map +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +7 -1
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-setup.d.ts +5 -0
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +40 -6
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizards/gcp-cost.js +1 -1
- package/dist/wizards/gcp.js +1 -1
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +7 -5
- package/src/adapter.ts +4 -6
- package/src/admin-internal.ts +212 -0
- package/src/agent/units.ts +1 -1
- package/src/aws-task-roles.ts +58 -0
- package/src/caddy-redirects.docker.test.ts +8 -1
- package/src/checkpoint-secrets.test.ts +0 -1
- package/src/cli.ts +41 -1
- package/src/compose.ts +2 -2
- package/src/deployed-release.test.ts +22 -4
- package/src/deployed-release.ts +14 -7
- package/src/error-pages-stacks.test.ts +38 -0
- package/src/gateway-credentials.ts +6 -3
- package/src/gateway-env-isolation.test.ts +212 -0
- package/src/gcloud.ts +5 -3
- package/src/index.test.ts +6 -6
- package/src/infra-contract.test.ts +5 -3
- package/src/infra-contract.ts +12 -1
- package/src/ops-backend.ts +1 -1
- package/src/owner-reset.test.ts +165 -0
- package/src/owner-reset.ts +106 -0
- package/src/release-contract.ts +3 -0
- package/src/release-manifest.test.ts +1 -0
- package/src/runtime-secrets.test.ts +25 -8
- package/src/stack-contract.test.ts +9 -18
- package/src/stack-contract.ts +25 -16
- package/src/stack-converge-secrets.test.ts +45 -10
- package/src/stack-converge.test.ts +19 -10
- package/src/staging-admin.ts +18 -181
- package/src/state-store.ts +11 -2
- package/src/upgrader-job.test.ts +17 -1
- package/src/upgrader-job.ts +13 -3
- package/src/upgrader-setup.test.ts +12 -1
- package/src/upgrader-setup.ts +41 -5
- package/src/welcome-page.test.ts +40 -0
- package/src/wizards/gcp-cost.ts +1 -1
- package/src/wizards/gcp.ts +1 -1
- package/stacks/aws/README.md +3 -1
- package/stacks/aws/index.ts +62 -43
- package/stacks/azure/README.md +3 -1
- package/stacks/azure/index.ts +8 -14
- package/stacks/gcp/README.md +1 -1
- package/stacks/gcp/index.ts +2 -5
- package/stacks/gcp-firebase/index.ts +0 -3
- package/static/welcome.html +93 -111
package/stacks/aws/index.ts
CHANGED
|
@@ -32,6 +32,12 @@ import * as aws from "@pulumi/aws";
|
|
|
32
32
|
import * as awsx from "@pulumi/awsx";
|
|
33
33
|
import * as pulumi from "@pulumi/pulumi";
|
|
34
34
|
import type { CloudAdapterOutputs, DnsRecord } from "../../dist/adapter.js";
|
|
35
|
+
import {
|
|
36
|
+
AWS_SERVICE_SECRETS,
|
|
37
|
+
type AwsSecret,
|
|
38
|
+
type AwsTaskService,
|
|
39
|
+
secretsReadPolicy,
|
|
40
|
+
} from "../../dist/aws-task-roles.js";
|
|
35
41
|
import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
|
|
36
42
|
import { ADMIN_MEMORY_DEFAULT, memoryQuantityMiB } from "../../dist/stack-contract.js";
|
|
37
43
|
|
|
@@ -89,9 +95,11 @@ function randomHex(bytes: number): string {
|
|
|
89
95
|
}
|
|
90
96
|
|
|
91
97
|
const postgresPassword = pulumi.secret(randomHex(32));
|
|
92
|
-
// #613 — one password per database role, so the gateway (public_role
|
|
93
|
-
//
|
|
94
|
-
//
|
|
98
|
+
// #613 — one password per database role, so the gateway (public_role only,
|
|
99
|
+
// #635) never holds admin_role's. bootstrap.sh creates the roles with these
|
|
100
|
+
// (PUBLIC_ROLE_PASSWORD / GATEWAY_ROLE_PASSWORD). gateway_role is no longer
|
|
101
|
+
// any service's login; its password stays until the contract release that
|
|
102
|
+
// drops the role (#635).
|
|
95
103
|
const publicRolePassword = pulumi.secret(randomHex(32));
|
|
96
104
|
const gatewayRolePassword = pulumi.secret(randomHex(32));
|
|
97
105
|
const csrfSecret = pulumi.secret(randomHex(32));
|
|
@@ -163,13 +171,6 @@ const publicDatabaseUrl = pulumi
|
|
|
163
171
|
([host, port, pw]) =>
|
|
164
172
|
`postgresql://public_role:${pw}@${host}:${port}/cms_public?sslmode=require`,
|
|
165
173
|
);
|
|
166
|
-
// The gateway's cms_admin login (#613): grants from migration 0248 only.
|
|
167
|
-
const gatewayDatabaseUrl = pulumi
|
|
168
|
-
.all([db.address, db.port, gatewayRolePassword])
|
|
169
|
-
.apply(
|
|
170
|
-
([host, port, pw]) =>
|
|
171
|
-
`postgresql://gateway_role:${pw}@${host}:${port}/cms_admin?sslmode=require`,
|
|
172
|
-
);
|
|
173
174
|
|
|
174
175
|
// === 4. S3 buckets ===
|
|
175
176
|
const mediaBucket = new aws.s3.BucketV2(`${namePrefix}-media`, {
|
|
@@ -296,7 +297,14 @@ const distribution = new aws.cloudfront.Distribution(`${namePrefix}-cdn`, {
|
|
|
296
297
|
minimumProtocolVersion: "TLSv1.2_2021",
|
|
297
298
|
},
|
|
298
299
|
restrictions: { geoRestriction: { restrictionType: "none" } },
|
|
300
|
+
// #802 — unknown URLs get the site's own 404 page (the generator writes
|
|
301
|
+
// `/404.html` from the designated page) with status 404. The S3 origins
|
|
302
|
+
// are OAI-only with `s3:GetObject` and no `s3:ListBucket`, so S3 answers
|
|
303
|
+
// a missing key with 403, not 404; without the 403 entry visitors saw
|
|
304
|
+
// S3's AccessDenied XML. Both origins on this distribution are private
|
|
305
|
+
// buckets, so a 403 here always means "no such object".
|
|
299
306
|
customErrorResponses: [
|
|
307
|
+
{ errorCode: 403, responseCode: 404, responsePagePath: "/404.html", errorCachingMinTtl: 60 },
|
|
300
308
|
{ errorCode: 404, responseCode: 404, responsePagePath: "/404.html", errorCachingMinTtl: 60 },
|
|
301
309
|
],
|
|
302
310
|
});
|
|
@@ -349,38 +357,48 @@ const cluster = new aws.ecs.Cluster(`${namePrefix}-ecs`, {
|
|
|
349
357
|
settings: [{ name: "containerInsights", value: "enabled" }],
|
|
350
358
|
});
|
|
351
359
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
360
|
+
// One task role per service (#635): the gateway's may read only the
|
|
361
|
+
// public_role password, never a secret that unlocks a cms_admin login
|
|
362
|
+
// (src/aws-task-roles.ts AWS_SERVICE_SECRETS, asserted by
|
|
363
|
+
// gateway-env-isolation.test.ts). The admin keeps the logical names the
|
|
364
|
+
// former shared role had, so upgrading replaces nothing on its side.
|
|
365
|
+
const secretArns: Record<AwsSecret, pulumi.Output<string>> = {
|
|
366
|
+
"postgres-password": pgSecret.arn,
|
|
367
|
+
"public-role-password": publicRoleSecret.arn,
|
|
368
|
+
"gateway-role-password": gatewayRoleSecret.arn,
|
|
369
|
+
"csrf-secret": csrfSecretRes.arn,
|
|
370
|
+
"cookie-secret": cookieSecretRes.arn,
|
|
371
|
+
"anthropic-api-key": anthropicSecretRes.arn,
|
|
372
|
+
"resend-api-key": resendSecretRes.arn,
|
|
373
|
+
};
|
|
364
374
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
375
|
+
function taskRole(service: AwsTaskService): aws.iam.Role {
|
|
376
|
+
const prefix = service === "admin" ? namePrefix : `${namePrefix}-${service}`;
|
|
377
|
+
const role = new aws.iam.Role(`${prefix}-task-role`, {
|
|
378
|
+
assumeRolePolicy: JSON.stringify({
|
|
379
|
+
Version: "2012-10-17",
|
|
380
|
+
Statement: [
|
|
381
|
+
{
|
|
382
|
+
Effect: "Allow",
|
|
383
|
+
Principal: { Service: "ecs-tasks.amazonaws.com" },
|
|
384
|
+
Action: "sts:AssumeRole",
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
}),
|
|
388
|
+
});
|
|
389
|
+
new aws.iam.RolePolicy(`${prefix}-task-secrets`, {
|
|
390
|
+
role: role.id,
|
|
391
|
+
policy: pulumi
|
|
392
|
+
.all(AWS_SERVICE_SECRETS[service].map((name) => secretArns[name]))
|
|
393
|
+
.apply(secretsReadPolicy),
|
|
394
|
+
});
|
|
395
|
+
return role;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const taskRoles: Record<AwsTaskService, aws.iam.Role> = {
|
|
399
|
+
admin: taskRole("admin"),
|
|
400
|
+
gateway: taskRole("gateway"),
|
|
401
|
+
};
|
|
384
402
|
|
|
385
403
|
const execRole = new aws.iam.Role(`${namePrefix}-exec-role`, {
|
|
386
404
|
assumeRolePolicy: JSON.stringify({
|
|
@@ -421,7 +439,6 @@ const dnsRecordsRequired: DnsRecord[] = [
|
|
|
421
439
|
const out: CloudAdapterOutputs = {
|
|
422
440
|
adminDatabaseUrl: adminDatabaseUrl as unknown as string,
|
|
423
441
|
publicDatabaseUrl: publicDatabaseUrl as unknown as string,
|
|
424
|
-
gatewayDatabaseUrl: gatewayDatabaseUrl as unknown as string,
|
|
425
442
|
mediaStorageUrl: pulumi.interpolate`s3://${mediaBucket.bucket}` as unknown as string,
|
|
426
443
|
mediaCdnBaseUrl:
|
|
427
444
|
pulumi.interpolate`https://${distribution.domainName}/media` as unknown as string,
|
|
@@ -436,7 +453,6 @@ const out: CloudAdapterOutputs = {
|
|
|
436
453
|
|
|
437
454
|
export const adminDatabaseUrlOut = out.adminDatabaseUrl;
|
|
438
455
|
export const publicDatabaseUrlOut = out.publicDatabaseUrl;
|
|
439
|
-
export const gatewayDatabaseUrlOut = out.gatewayDatabaseUrl;
|
|
440
456
|
export const mediaStorageUrlOut = out.mediaStorageUrl;
|
|
441
457
|
export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
|
|
442
458
|
export const bootstrapUrlOut = out.bootstrapUrl;
|
|
@@ -448,6 +464,9 @@ export const cloudfrontDomainOut = distribution.domainName;
|
|
|
448
464
|
export const redirectKvsArnOut = redirectKvs.arn;
|
|
449
465
|
export const ecsClusterArnOut = cluster.arn;
|
|
450
466
|
export const rdsAddressOut = db.address;
|
|
467
|
+
// The task roles the admin-side and gateway task definitions run as.
|
|
468
|
+
export const adminTaskRoleArnOut = taskRoles.admin.arn;
|
|
469
|
+
export const gatewayTaskRoleArnOut = taskRoles.gateway.arn;
|
|
451
470
|
|
|
452
471
|
// Note: the bootstrap token (and other secrets like postgres-password,
|
|
453
472
|
// CSRF/cookie secrets, Anthropic key) are stored in Pulumi's encrypted
|
package/stacks/azure/README.md
CHANGED
|
@@ -10,13 +10,15 @@ Pulumi stack provisioning Caelo on Azure managed services. Implements the shared
|
|
|
10
10
|
| Blob storage | Storage Account with a private `media` container and static-website hosting (`$web`) |
|
|
11
11
|
| CDN / Edge | Azure Front Door Standard: the primary domain serves the static website as files, with a Front Door managed certificate; no edge compute on the request path |
|
|
12
12
|
| Container runtime | Four Container Apps (admin / gateway / orchestrator / runner) in one managed environment |
|
|
13
|
-
| Secret store | Key Vault (postgres-password, public-role-password, gateway-role-password, csrf-secret, cookie-secret, anthropic-api-key, resend-api-key). The gateway container gets `
|
|
13
|
+
| Secret store | Key Vault (postgres-password, public-role-password, gateway-role-password, csrf-secret, cookie-secret, anthropic-api-key, resend-api-key). The gateway container gets `PUBLIC_DATABASE_URL` only, never a cms_admin credential of any role (#613, #635). |
|
|
14
14
|
| Edge-log sink | Log Analytics workspace receives Container Apps logs (queryable by P12A analytics plugin via Azure Monitor) |
|
|
15
15
|
|
|
16
16
|
The Caelo runtime never knows it's on Azure — every Container App consumes plain `DATABASE_URL` / `MEDIA_STORAGE_URL` / `SECRETS_PROVIDER` env vars.
|
|
17
17
|
|
|
18
18
|
## First-time install
|
|
19
19
|
|
|
20
|
+
Run this stack from a checkout of the Caelo repository after `bun install` at its root. `@pulumi/azure-native` is a devDependency of `@caelo-cms/provisioning`: the CLI wizard does not provision Azure yet, so the published package does not make every GCP and self-hosted install download it (the one-click install from Cloud Shell runs in a 5 GB home directory). It moves back to `dependencies` when the Azure wizard ships.
|
|
21
|
+
|
|
20
22
|
```bash
|
|
21
23
|
cd packages/provisioning/stacks/azure
|
|
22
24
|
pulumi stack init prod
|
package/stacks/azure/index.ts
CHANGED
|
@@ -81,9 +81,11 @@ function randomHex(bytes: number): string {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const postgresPassword = pulumi.secret(randomHex(32));
|
|
84
|
-
// #613 — one password per database role, so the gateway (public_role
|
|
85
|
-
//
|
|
86
|
-
//
|
|
84
|
+
// #613 — one password per database role, so the gateway (public_role only,
|
|
85
|
+
// #635) never holds admin_role's. bootstrap.sh creates the roles with these
|
|
86
|
+
// (PUBLIC_ROLE_PASSWORD / GATEWAY_ROLE_PASSWORD). gateway_role is no longer
|
|
87
|
+
// any service's login; its password stays until the contract release that
|
|
88
|
+
// drops the role (#635).
|
|
87
89
|
const publicRolePassword = pulumi.secret(randomHex(32));
|
|
88
90
|
const gatewayRolePassword = pulumi.secret(randomHex(32));
|
|
89
91
|
const csrfSecret = pulumi.secret(randomHex(32));
|
|
@@ -162,14 +164,11 @@ const adminDatabaseUrl = pulumi
|
|
|
162
164
|
const publicAdminDatabaseUrl = pulumi
|
|
163
165
|
.all([pgServer.fullyQualifiedDomainName, postgresPassword])
|
|
164
166
|
.apply(([host, pw]) => `postgresql://caelo_admin:${pw}@${host}:5432/cms_public?sslmode=require`);
|
|
165
|
-
// The gateway's
|
|
166
|
-
//
|
|
167
|
+
// The gateway's only login (#613, #635): public_role on cms_public. It has
|
|
168
|
+
// no cms_admin login at all (CLAUDE.md §2).
|
|
167
169
|
const publicDatabaseUrl = pulumi
|
|
168
170
|
.all([pgServer.fullyQualifiedDomainName, publicRolePassword])
|
|
169
171
|
.apply(([host, pw]) => `postgresql://public_role:${pw}@${host}:5432/cms_public?sslmode=require`);
|
|
170
|
-
const gatewayDatabaseUrl = pulumi
|
|
171
|
-
.all([pgServer.fullyQualifiedDomainName, gatewayRolePassword])
|
|
172
|
-
.apply(([host, pw]) => `postgresql://gateway_role:${pw}@${host}:5432/cms_admin?sslmode=require`);
|
|
173
172
|
|
|
174
173
|
// === 4. Storage account + containers ===
|
|
175
174
|
const storage = new azure.storage.StorageAccount(`${namePrefix}-st`, {
|
|
@@ -419,10 +418,7 @@ const adminApp = containerApp({
|
|
|
419
418
|
});
|
|
420
419
|
const _gatewayApp = containerApp({
|
|
421
420
|
serviceName: "gateway",
|
|
422
|
-
databaseEnv: [
|
|
423
|
-
{ name: "GATEWAY_DATABASE_URL", value: gatewayDatabaseUrl },
|
|
424
|
-
{ name: "PUBLIC_DATABASE_URL", value: publicDatabaseUrl },
|
|
425
|
-
],
|
|
421
|
+
databaseEnv: [{ name: "PUBLIC_DATABASE_URL", value: publicDatabaseUrl }],
|
|
426
422
|
});
|
|
427
423
|
const _orchestratorApp = containerApp({
|
|
428
424
|
serviceName: "orchestrator",
|
|
@@ -446,7 +442,6 @@ const dnsRecordsRequired = pulumi
|
|
|
446
442
|
const out: CloudAdapterOutputs = {
|
|
447
443
|
adminDatabaseUrl: adminDatabaseUrl as unknown as string,
|
|
448
444
|
publicDatabaseUrl: publicDatabaseUrl as unknown as string,
|
|
449
|
-
gatewayDatabaseUrl: gatewayDatabaseUrl as unknown as string,
|
|
450
445
|
mediaStorageUrl:
|
|
451
446
|
pulumi.interpolate`https://${storage.name}.blob.core.windows.net/media` as unknown as string,
|
|
452
447
|
mediaCdnBaseUrl: pulumi.interpolate`https://${domain}/media` as unknown as string,
|
|
@@ -460,7 +455,6 @@ const out: CloudAdapterOutputs = {
|
|
|
460
455
|
|
|
461
456
|
export const adminDatabaseUrlOut = out.adminDatabaseUrl;
|
|
462
457
|
export const publicDatabaseUrlOut = out.publicDatabaseUrl;
|
|
463
|
-
export const gatewayDatabaseUrlOut = out.gatewayDatabaseUrl;
|
|
464
458
|
export const mediaStorageUrlOut = out.mediaStorageUrl;
|
|
465
459
|
export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
|
|
466
460
|
export const bootstrapUrlOut = out.bootstrapUrl;
|
package/stacks/gcp/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Pulumi stack provisioning Caelo on GCP managed services. Implements the shared `
|
|
|
10
10
|
| Blob storage | Two GCS buckets (`<project>-caelo-<env>-media`, `<project>-caelo-<env>-static`) with uniform bucket-level access |
|
|
11
11
|
| CDN | Global HTTPS load balancer + Cloud CDN backend bucket: `<domain>` serves the static bucket as files, `/api/*` goes to the gateway, `admin.<domain>` to the IAP-gated admin |
|
|
12
12
|
| Container runtime | Four Cloud Run services (admin / gateway / orchestrator / runner) |
|
|
13
|
-
| Secret store | Secret Manager: postgres-password (admin_role) + secret-kek (Pulumi); internal-secret, tool-approval-secret, public-role-password and gateway-role-password (created by the CLI before `pulumi up`, see `src/runtime-secrets.ts`). Cloud Run reads each as a `secretKeyRef`; database URLs carry no password. The gateway's service account reads only the
|
|
13
|
+
| Secret store | Secret Manager: postgres-password (admin_role) + secret-kek (Pulumi); internal-secret, tool-approval-secret, public-role-password and gateway-role-password (created by the CLI before `pulumi up`, see `src/runtime-secrets.ts`). Cloud Run reads each as a `secretKeyRef`; database URLs carry no password. The gateway's service account reads only `public-role-password` (#613, #635: the gateway has no cms_admin login); `gateway_role`, created by migration 0248, is no service's login any more. |
|
|
14
14
|
| Edge-log sink | Cloud Logging project sink → BigQuery dataset `<env>_edge_logs` (queryable by P12A analytics plugin) |
|
|
15
15
|
|
|
16
16
|
The Caelo runtime never knows it's on GCP — every Cloud Run service consumes plain `DATABASE_URL` / `MEDIA_STORAGE_URL` / `SECRETS_PROVIDER` env vars.
|
package/stacks/gcp/index.ts
CHANGED
|
@@ -662,7 +662,6 @@ const envContractInputs = {
|
|
|
662
662
|
admin: dbUrls.admin,
|
|
663
663
|
publicAdmin: dbUrls.publicAdmin,
|
|
664
664
|
public: dbUrls.public,
|
|
665
|
-
gateway: dbUrls.gateway,
|
|
666
665
|
},
|
|
667
666
|
} as const;
|
|
668
667
|
|
|
@@ -753,8 +752,8 @@ const gatewaySvc = cloudRunService({
|
|
|
753
752
|
minInstances: gatewayMinInstances,
|
|
754
753
|
maxInstances: 100,
|
|
755
754
|
memory: "512Mi",
|
|
756
|
-
// public_role on cms_public
|
|
757
|
-
//
|
|
755
|
+
// public_role on cms_public only: no cms_admin login of any role
|
|
756
|
+
// (#613, #635, CLAUDE.md §2).
|
|
758
757
|
contractEnv: gatewayEnvContract(envContractInputs),
|
|
759
758
|
});
|
|
760
759
|
|
|
@@ -1158,7 +1157,6 @@ const out: CloudAdapterOutputs = {
|
|
|
1158
1157
|
// Password-less — the password lives only in Secret Manager.
|
|
1159
1158
|
adminDatabaseUrl: dbUrls.admin as unknown as string,
|
|
1160
1159
|
publicDatabaseUrl: dbUrls.public as unknown as string,
|
|
1161
|
-
gatewayDatabaseUrl: dbUrls.gateway as unknown as string,
|
|
1162
1160
|
mediaStorageUrl: pulumi.interpolate`gs://${mediaBucket.name}` as unknown as string,
|
|
1163
1161
|
mediaCdnBaseUrl: pulumi.interpolate`https://${domain}/media` as unknown as string,
|
|
1164
1162
|
bootstrapUrl:
|
|
@@ -1172,7 +1170,6 @@ const out: CloudAdapterOutputs = {
|
|
|
1172
1170
|
|
|
1173
1171
|
export const adminDatabaseUrlOut = out.adminDatabaseUrl;
|
|
1174
1172
|
export const publicDatabaseUrlOut = out.publicDatabaseUrl;
|
|
1175
|
-
export const gatewayDatabaseUrlOut = out.gatewayDatabaseUrl;
|
|
1176
1173
|
export const mediaStorageUrlOut = out.mediaStorageUrl;
|
|
1177
1174
|
export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
|
|
1178
1175
|
export const bootstrapUrlOut = out.bootstrapUrl;
|
|
@@ -606,7 +606,6 @@ const envContractInputs = {
|
|
|
606
606
|
admin: dbUrls.admin,
|
|
607
607
|
publicAdmin: dbUrls.publicAdmin,
|
|
608
608
|
public: dbUrls.public,
|
|
609
|
-
gateway: dbUrls.gateway,
|
|
610
609
|
},
|
|
611
610
|
} as const;
|
|
612
611
|
|
|
@@ -988,7 +987,6 @@ const out: CloudAdapterOutputs = {
|
|
|
988
987
|
// Password-less — the password lives only in Secret Manager.
|
|
989
988
|
adminDatabaseUrl: dbUrls.admin as unknown as string,
|
|
990
989
|
publicDatabaseUrl: dbUrls.public as unknown as string,
|
|
991
|
-
gatewayDatabaseUrl: dbUrls.gateway as unknown as string,
|
|
992
990
|
mediaStorageUrl: pulumi.interpolate`gs://${mediaBucket.name}` as unknown as string,
|
|
993
991
|
// Firebase Hosting serves media via its own CDN; we proxy through
|
|
994
992
|
// the Firebase site's apex domain for canonical URLs.
|
|
@@ -1005,7 +1003,6 @@ const out: CloudAdapterOutputs = {
|
|
|
1005
1003
|
|
|
1006
1004
|
export const adminDatabaseUrlOut = out.adminDatabaseUrl;
|
|
1007
1005
|
export const publicDatabaseUrlOut = out.publicDatabaseUrl;
|
|
1008
|
-
export const gatewayDatabaseUrlOut = out.gatewayDatabaseUrl;
|
|
1009
1006
|
export const mediaStorageUrlOut = out.mediaStorageUrl;
|
|
1010
1007
|
export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
|
|
1011
1008
|
export const bootstrapUrlOut = out.bootstrapUrl;
|
package/static/welcome.html
CHANGED
|
@@ -5,151 +5,133 @@
|
|
|
5
5
|
Uploaded by the provisioning wizard the first time it brings up the
|
|
6
6
|
static bucket. Replaced the moment the operator publishes their
|
|
7
7
|
first deploy via the admin app's "Publish" flow.
|
|
8
|
+
|
|
9
|
+
#781 — the first arrival screen: the Caelatum sky with the message on
|
|
10
|
+
a relief plate, matching the admin's sign-in screens. Fully
|
|
11
|
+
self-contained on purpose: inline CSS, inline SVG mark, no web fonts
|
|
12
|
+
and no other external requests (the page is served from a bucket that
|
|
13
|
+
has nothing else in it yet). Schibsted Grotesk is used when installed
|
|
14
|
+
locally; otherwise the fallback stack renders.
|
|
8
15
|
-->
|
|
9
16
|
<html lang="en">
|
|
10
17
|
<head>
|
|
11
18
|
<meta charset="utf-8">
|
|
12
19
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
13
20
|
<meta name="robots" content="noindex">
|
|
21
|
+
<meta name="color-scheme" content="light dark">
|
|
14
22
|
<title>Caelo CMS — your install is ready</title>
|
|
15
23
|
<style>
|
|
16
24
|
*,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
17
25
|
:root {
|
|
18
|
-
--
|
|
19
|
-
--muted: #
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
26
|
+
--ink: #0e1a2e;
|
|
27
|
+
--muted: #4a576b;
|
|
28
|
+
--card: #ffffff;
|
|
29
|
+
--line: #e0e5ee;
|
|
30
|
+
--code-bg: #f2f5fa;
|
|
31
|
+
--sky-label: #1f5299;
|
|
32
|
+
--sky: linear-gradient(180deg, #c3dbf2 0%, #dce9f6 40%, #efe7ef 72%, #f8e6d8 100%);
|
|
33
|
+
--plate-ring: rgba(255, 255, 255, 0.55);
|
|
34
|
+
}
|
|
35
|
+
/* The sky is light-mode only; dark mode is the admin's night blue. */
|
|
36
|
+
@media (prefers-color-scheme: dark) {
|
|
37
|
+
:root {
|
|
38
|
+
--ink: #e6ecf5;
|
|
39
|
+
--muted: #9fb0c8;
|
|
40
|
+
--card: #111c2e;
|
|
41
|
+
--line: #22314a;
|
|
42
|
+
--code-bg: #16233a;
|
|
43
|
+
--sky-label: #7fb4ff;
|
|
44
|
+
--sky: none;
|
|
45
|
+
--plate-ring: rgba(255, 255, 255, 0.08);
|
|
46
|
+
}
|
|
25
47
|
}
|
|
26
|
-
html, body { height: 100%; }
|
|
48
|
+
html, body { min-height: 100%; }
|
|
27
49
|
body {
|
|
28
|
-
font: 16px/1.55
|
|
29
|
-
color: var(--
|
|
50
|
+
font: 16px/1.55 "Schibsted Grotesk", Arial, Helvetica, sans-serif;
|
|
51
|
+
color: var(--ink);
|
|
52
|
+
background: #f8f9fc var(--sky);
|
|
53
|
+
min-height: 100vh;
|
|
30
54
|
display: grid;
|
|
31
55
|
place-items: center;
|
|
32
|
-
padding:
|
|
33
|
-
background: #f6f6fb;
|
|
34
|
-
overflow: hidden;
|
|
35
|
-
position: relative;
|
|
36
|
-
}
|
|
37
|
-
/* Animated mesh-gradient background. Three soft blobs orbiting at
|
|
38
|
-
different speeds; backdrop-filter on the card softens them. */
|
|
39
|
-
.blob {
|
|
40
|
-
position: fixed;
|
|
41
|
-
border-radius: 50%;
|
|
42
|
-
filter: blur(80px);
|
|
43
|
-
opacity: 0.55;
|
|
44
|
-
pointer-events: none;
|
|
45
|
-
animation: drift 22s ease-in-out infinite;
|
|
46
|
-
will-change: transform;
|
|
56
|
+
padding: 3rem 1rem;
|
|
47
57
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.blob.b3 { width: 380px; height: 380px; background: #67e8f9; top: 40%; left: 55%; animation-delay: -14s; animation-duration: 27s; }
|
|
51
|
-
@keyframes drift {
|
|
52
|
-
0%, 100% { transform: translate(0, 0) scale(1); }
|
|
53
|
-
33% { transform: translate(40px, -30px) scale(1.07); }
|
|
54
|
-
66% { transform: translate(-30px, 35px) scale(0.96); }
|
|
58
|
+
@media (prefers-color-scheme: dark) {
|
|
59
|
+
body { background: #0b1422; }
|
|
55
60
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
main {
|
|
61
|
-
position: relative;
|
|
62
|
-
z-index: 1;
|
|
63
|
-
max-width: 36rem;
|
|
64
|
-
width: 100%;
|
|
65
|
-
background: var(--card-bg);
|
|
66
|
-
border: 1px solid var(--card-border);
|
|
67
|
-
border-radius: 22px;
|
|
68
|
-
padding: 2.5rem;
|
|
69
|
-
box-shadow: var(--shadow);
|
|
70
|
-
backdrop-filter: blur(14px) saturate(140%);
|
|
71
|
-
-webkit-backdrop-filter: blur(14px) saturate(140%);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.brand {
|
|
75
|
-
display: flex;
|
|
76
|
-
align-items: center;
|
|
77
|
-
gap: 0.65rem;
|
|
61
|
+
.frame { width: 100%; max-width: 36rem; }
|
|
62
|
+
.kicker {
|
|
63
|
+
text-align: center;
|
|
64
|
+
font-size: 0.75rem;
|
|
78
65
|
font-weight: 600;
|
|
79
|
-
letter-spacing:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
margin-bottom:
|
|
66
|
+
letter-spacing: 0.14em;
|
|
67
|
+
text-transform: uppercase;
|
|
68
|
+
color: var(--sky-label);
|
|
69
|
+
margin-bottom: 1rem;
|
|
83
70
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@keyframes pulse {
|
|
91
|
-
0% { box-shadow: 0 0 0 0 rgba(79,70,229,0.55); }
|
|
92
|
-
70% { box-shadow: 0 0 0 12px rgba(79,70,229,0); }
|
|
93
|
-
100% { box-shadow: 0 0 0 0 rgba(79,70,229,0); }
|
|
71
|
+
main {
|
|
72
|
+
background: var(--card);
|
|
73
|
+
border: 1px solid var(--line);
|
|
74
|
+
border-radius: 14px;
|
|
75
|
+
padding: 2.25rem 2.5rem;
|
|
76
|
+
box-shadow: 0 0 0 5px var(--plate-ring), 0 18px 40px -26px rgba(42, 64, 110, 0.45);
|
|
94
77
|
}
|
|
95
|
-
@media (
|
|
96
|
-
|
|
78
|
+
@media (max-width: 480px) {
|
|
79
|
+
main { padding: 1.5rem 1.25rem; }
|
|
97
80
|
}
|
|
98
|
-
|
|
99
81
|
h1 {
|
|
100
|
-
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: 0.75rem;
|
|
85
|
+
font-size: clamp(1.5rem, 1.2rem + 1.2vw, 2rem);
|
|
101
86
|
letter-spacing: -0.02em;
|
|
102
87
|
line-height: 1.15;
|
|
103
|
-
margin-bottom: 0.
|
|
88
|
+
margin-bottom: 0.85rem;
|
|
104
89
|
}
|
|
105
|
-
|
|
106
|
-
p
|
|
107
|
-
|
|
108
|
-
.cta {
|
|
109
|
-
display: inline-flex;
|
|
110
|
-
align-items: center;
|
|
111
|
-
gap: 0.5rem;
|
|
112
|
-
margin-top: 1.5rem;
|
|
113
|
-
padding: 0.75rem 1.1rem;
|
|
114
|
-
background: var(--fg);
|
|
115
|
-
color: #fff;
|
|
116
|
-
text-decoration: none;
|
|
117
|
-
border-radius: 12px;
|
|
118
|
-
font-weight: 500;
|
|
119
|
-
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
120
|
-
}
|
|
121
|
-
.cta:hover { transform: translateY(-1px); box-shadow: 0 12px 24px -8px rgba(15,17,40,0.25); }
|
|
122
|
-
.cta svg { width: 14px; height: 14px; }
|
|
123
|
-
|
|
90
|
+
h1 svg { flex-shrink: 0; width: 36px; height: 36px; }
|
|
91
|
+
p { color: var(--muted); }
|
|
124
92
|
.hint {
|
|
125
|
-
margin-top:
|
|
126
|
-
padding-top: 1.
|
|
127
|
-
border-top: 1px solid
|
|
93
|
+
margin-top: 1.75rem;
|
|
94
|
+
padding-top: 1.25rem;
|
|
95
|
+
border-top: 1px solid var(--line);
|
|
128
96
|
font-size: 0.875rem;
|
|
129
97
|
color: var(--muted);
|
|
130
98
|
}
|
|
131
99
|
.hint code {
|
|
132
|
-
font: 0.85em
|
|
133
|
-
|
|
100
|
+
font: 0.85em Menlo, Consolas, monospace;
|
|
101
|
+
color: var(--ink);
|
|
102
|
+
background: var(--code-bg);
|
|
134
103
|
padding: 0.15em 0.45em;
|
|
135
104
|
border-radius: 6px;
|
|
105
|
+
overflow-wrap: anywhere;
|
|
106
|
+
}
|
|
107
|
+
/* One entrance settle, only for people who have not asked for
|
|
108
|
+
reduced motion; no looping animation on this page. */
|
|
109
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
110
|
+
main { animation: settle 0.5s ease-out both; }
|
|
111
|
+
@keyframes settle {
|
|
112
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
113
|
+
to { opacity: 1; transform: none; }
|
|
114
|
+
}
|
|
136
115
|
}
|
|
137
|
-
.hint a { color: var(--accent); text-decoration: none; }
|
|
138
|
-
.hint a:hover { text-decoration: underline; }
|
|
139
116
|
</style>
|
|
140
117
|
</head>
|
|
141
118
|
<body>
|
|
142
|
-
<div class="
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
119
|
+
<div class="frame">
|
|
120
|
+
<p class="kicker">Caelo CMS</p>
|
|
121
|
+
<main>
|
|
122
|
+
<h1>
|
|
123
|
+
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false">
|
|
124
|
+
<path d="M23.07 7.57A11 11 0 1 0 23.07 24.43" fill="none" stroke="currentColor" stroke-width="3.4" stroke-linecap="round"/>
|
|
125
|
+
<path d="M20.5 10.64A7 7 0 1 0 20.5 21.36" fill="none" stroke="#c9952f" stroke-width="1.1" stroke-linecap="round"/>
|
|
126
|
+
<circle cx="25.2" cy="16" r="2.6" fill="#e8684a"/>
|
|
127
|
+
</svg>
|
|
128
|
+
Your install is up. One more step.
|
|
129
|
+
</h1>
|
|
130
|
+
<p>This is the placeholder Caelo serves on a fresh deploy. The operator who provisioned this install has a one-time bootstrap link in their terminal output — opening it walks through Owner-account setup. After publish, this page disappears.</p>
|
|
131
|
+
<div class="hint">
|
|
132
|
+
Bootstrap link expires in 24h and rotates per <code>bunx @caelo-cms/provisioning</code> run. After Owner setup, the admin lives at <code>{{ADMIN_URL}}</code> behind <code>Sign in with Google</code> (IAP allowlist).
|
|
133
|
+
</div>
|
|
134
|
+
</main>
|
|
135
|
+
</div>
|
|
154
136
|
</body>
|
|
155
137
|
</html>
|