@caelo-cms/provisioning 0.10.29 → 0.10.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/admin-domain.d.ts +54 -0
  2. package/dist/admin-domain.d.ts.map +1 -0
  3. package/dist/admin-domain.js +122 -0
  4. package/dist/admin-domain.js.map +1 -0
  5. package/dist/cli.js +71 -7
  6. package/dist/cli.js.map +1 -1
  7. package/dist/compose.d.ts +7 -9
  8. package/dist/compose.d.ts.map +1 -1
  9. package/dist/compose.js +17 -1
  10. package/dist/compose.js.map +1 -1
  11. package/dist/deployed-release.d.ts +84 -0
  12. package/dist/deployed-release.d.ts.map +1 -0
  13. package/dist/deployed-release.js +182 -0
  14. package/dist/deployed-release.js.map +1 -0
  15. package/dist/gcloud-retry.d.ts +16 -0
  16. package/dist/gcloud-retry.d.ts.map +1 -0
  17. package/dist/gcloud-retry.js +17 -0
  18. package/dist/gcloud-retry.js.map +1 -0
  19. package/dist/gcloud.d.ts.map +1 -1
  20. package/dist/gcloud.js +2 -0
  21. package/dist/gcloud.js.map +1 -1
  22. package/dist/gcp-names.d.ts +58 -0
  23. package/dist/gcp-names.d.ts.map +1 -0
  24. package/dist/gcp-names.js +88 -0
  25. package/dist/gcp-names.js.map +1 -0
  26. package/dist/install-state.d.ts +31 -0
  27. package/dist/install-state.d.ts.map +1 -1
  28. package/dist/install-state.js +38 -0
  29. package/dist/install-state.js.map +1 -1
  30. package/dist/lifecycle.d.ts +5 -0
  31. package/dist/lifecycle.d.ts.map +1 -1
  32. package/dist/lifecycle.js +340 -50
  33. package/dist/lifecycle.js.map +1 -1
  34. package/dist/mcp-iap.d.ts +5 -26
  35. package/dist/mcp-iap.d.ts.map +1 -1
  36. package/dist/mcp-iap.js +6 -18
  37. package/dist/mcp-iap.js.map +1 -1
  38. package/dist/migration-runner.d.ts +25 -0
  39. package/dist/migration-runner.d.ts.map +1 -1
  40. package/dist/migration-runner.js +60 -19
  41. package/dist/migration-runner.js.map +1 -1
  42. package/dist/operator-access.d.ts +83 -0
  43. package/dist/operator-access.d.ts.map +1 -0
  44. package/dist/operator-access.js +562 -0
  45. package/dist/operator-access.js.map +1 -0
  46. package/dist/region-data.d.ts +30 -0
  47. package/dist/region-data.d.ts.map +1 -0
  48. package/dist/region-data.js +406 -0
  49. package/dist/region-data.js.map +1 -0
  50. package/dist/regions.d.ts +130 -0
  51. package/dist/regions.d.ts.map +1 -0
  52. package/dist/regions.js +247 -0
  53. package/dist/regions.js.map +1 -0
  54. package/dist/runtime-secrets.d.ts +128 -0
  55. package/dist/runtime-secrets.d.ts.map +1 -0
  56. package/dist/runtime-secrets.js +444 -0
  57. package/dist/runtime-secrets.js.map +1 -0
  58. package/dist/stack-contract.d.ts +231 -0
  59. package/dist/stack-contract.d.ts.map +1 -0
  60. package/dist/stack-contract.js +455 -0
  61. package/dist/stack-contract.js.map +1 -0
  62. package/dist/stack-converge.d.ts +183 -0
  63. package/dist/stack-converge.d.ts.map +1 -0
  64. package/dist/stack-converge.js +513 -0
  65. package/dist/stack-converge.js.map +1 -0
  66. package/dist/wizard.d.ts +7 -0
  67. package/dist/wizard.d.ts.map +1 -1
  68. package/dist/wizard.js +11 -1
  69. package/dist/wizard.js.map +1 -1
  70. package/dist/wizards/gcp-cost.d.ts +10 -10
  71. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  72. package/dist/wizards/gcp-cost.js +40 -3
  73. package/dist/wizards/gcp-cost.js.map +1 -1
  74. package/dist/wizards/gcp-firebase.d.ts +2 -0
  75. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  76. package/dist/wizards/gcp-firebase.js +1 -0
  77. package/dist/wizards/gcp-firebase.js.map +1 -1
  78. package/dist/wizards/gcp-pulumi.d.ts +51 -1
  79. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  80. package/dist/wizards/gcp-pulumi.js +89 -6
  81. package/dist/wizards/gcp-pulumi.js.map +1 -1
  82. package/dist/wizards/gcp.d.ts +2 -0
  83. package/dist/wizards/gcp.d.ts.map +1 -1
  84. package/dist/wizards/gcp.js +171 -23
  85. package/dist/wizards/gcp.js.map +1 -1
  86. package/package.json +3 -3
  87. package/stacks/aws/Pulumi.yaml +5 -2
  88. package/stacks/aws/README.md +3 -1
  89. package/stacks/aws/index.ts +21 -1
  90. package/stacks/azure/Pulumi.yaml +6 -3
  91. package/stacks/azure/README.md +1 -1
  92. package/stacks/azure/index.ts +22 -3
  93. package/stacks/gcp/Pulumi.yaml +5 -5
  94. package/stacks/gcp/README.md +15 -3
  95. package/stacks/gcp/index.ts +204 -141
  96. package/stacks/gcp-firebase/Pulumi.yaml +5 -2
  97. package/stacks/gcp-firebase/index.ts +169 -76
  98. package/stacks/self-hosted/Pulumi.yaml +4 -0
  99. package/stacks/self-hosted/index.ts +3 -0
@@ -30,12 +30,32 @@ import * as pulumi from "@pulumi/pulumi";
30
30
  import * as random from "@pulumi/random";
31
31
  import type { CloudAdapterOutputs, DnsRecord } from "../../dist/adapter.js";
32
32
  import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
33
+ import {
34
+ gatewayServiceAccountId,
35
+ gcpBucketName,
36
+ gcpSecretId,
37
+ MCP_IAP_SERVICE_ACCOUNT_ID,
38
+ runServiceAccountId,
39
+ } from "../../dist/gcp-names.js";
40
+ import {
41
+ ADMIN_MEMORY_DEFAULT,
42
+ adminEnvContract,
43
+ type CloudRunEnvVar,
44
+ type CloudRunSlug,
45
+ databaseUrls,
46
+ gatewayEnvContract,
47
+ type RuntimeSecret,
48
+ runtimeSecretBindings,
49
+ } from "../../dist/stack-contract.js";
33
50
 
34
51
  const cfg = new pulumi.Config();
35
52
  const domain = cfg.require("domain");
36
53
  const ownerEmail = cfg.require("ownerEmail");
37
54
  const project = cfg.require("project");
38
- const region = cfg.get("region") ?? "us-central1";
55
+ // #607 — chosen by the operator in the wizard and fixed after install; no
56
+ // default, so a stack run without it fails instead of landing in a region
57
+ // nobody picked.
58
+ const region = cfg.require("region");
39
59
 
40
60
  // Where Secret Manager keeps the secret payloads. Fixed at create time —
41
61
  // changing it replaces every secret — so the wizard pins it per stack:
@@ -57,6 +77,9 @@ const cloudSqlHa = cfg.getBoolean("cloudSqlHa") ?? false;
57
77
  const cloudSqlEdition = cfg.get("cloudSqlEdition") ?? "ENTERPRISE";
58
78
  const deletionProtection = cfg.getBoolean("deletionProtection") ?? false;
59
79
  const adminMinInstances = Number.parseInt(cfg.get("adminMinInstances") ?? "0", 10);
80
+ // #553 — the admin also runs the Lighthouse quality audit; same knob and
81
+ // default on every adapter (stack-contract.ts ADMIN_MEMORY_DEFAULT).
82
+ const adminMemory = cfg.get("adminMemory") ?? ADMIN_MEMORY_DEFAULT;
60
83
  const adminMaxInstances = Number.parseInt(cfg.get("adminMaxInstances") ?? "5", 10);
61
84
  const gatewayMinInstances = Number.parseInt(cfg.get("gatewayMinInstances") ?? "0", 10);
62
85
  const maxConnections = Number.parseInt(cfg.get("maxConnections") ?? "100", 10);
@@ -139,15 +162,9 @@ const sqlPeering = new gcp.servicenetworking.Connection(
139
162
  // =========================================================================
140
163
 
141
164
  const pgPasswordBytes = new random.RandomBytes(`${namePrefix}-pg-password-bytes`, { length: 32 });
142
- const csrfSecretBytes = new random.RandomBytes(`${namePrefix}-csrf-secret-bytes`, { length: 32 });
143
- const cookieSecretBytes = new random.RandomBytes(`${namePrefix}-cookie-secret-bytes`, {
144
- length: 32,
145
- });
146
165
  const kekBytes = new random.RandomBytes(`${namePrefix}-secret-kek-bytes`, { length: 32 });
147
166
 
148
167
  const postgresPassword = pulumi.secret(pgPasswordBytes.hex);
149
- const csrfSecret = pulumi.secret(csrfSecretBytes.hex);
150
- const cookieSecret = pulumi.secret(cookieSecretBytes.hex);
151
168
  const caeloSecretKek = pulumi.secret(kekBytes.hex);
152
169
  // v0.3.2 — anthropic-api-key Secret + Pulumi config dropped. The
153
170
  // runtime AI provider configuration lives at /security/ai (key
@@ -162,19 +179,26 @@ const caeloSecretKek = pulumi.secret(kekBytes.hex);
162
179
  // in v0.3.0 but never wired to runtime (transport.ts reads from
163
180
  // cfg.config.apiKey, never from Secret Manager). Drop to reduce
164
181
  // surface area + monthly cost.
182
+ // csrf-secret + cookie-secret were dropped for the same reason: CSRF
183
+ // secrets are per session (in cms_admin) and the gateway's cookie secret
184
+ // lives in site_settings — nothing read the Secret Manager copies.
185
+ // `pulumi up` deletes them; `cms-provision upgrade` leaves them in place.
186
+ //
187
+ // internal-secret (CAELO_INTERNAL_SECRET) and tool-approval-secret
188
+ // (CAELO_TOOL_APPROVAL_SECRET) are generated by the CLI, not here (see the
189
+ // same note in stacks/gcp/index.ts); this program references them by id.
165
190
 
166
191
  interface MadeSecret {
167
192
  resource: gcp.secretmanager.Secret;
168
- version: gcp.secretmanager.SecretVersion | null;
193
+ version: gcp.secretmanager.SecretVersion;
169
194
  }
170
195
 
171
- function makeSecret(name: string, value: pulumi.Output<string> | null): MadeSecret {
196
+ function makeSecret(name: string, value: pulumi.Output<string>): MadeSecret {
172
197
  const resource = new gcp.secretmanager.Secret(
173
198
  `${namePrefix}-${name}`,
174
- { secretId: `${namePrefix}-${name}`, replication: secretReplication },
199
+ { secretId: gcpSecretId(env, name), replication: secretReplication },
175
200
  opts,
176
201
  );
177
- if (value === null) return { resource, version: null };
178
202
  const version = new gcp.secretmanager.SecretVersion(
179
203
  `${namePrefix}-${name}-v1`,
180
204
  { secret: resource.name, secretData: value },
@@ -184,10 +208,17 @@ function makeSecret(name: string, value: pulumi.Output<string> | null): MadeSecr
184
208
  }
185
209
 
186
210
  const pgSecret = makeSecret("postgres-password", postgresPassword);
187
- const csrfSecretRes = makeSecret("csrf-secret", csrfSecret);
188
- const cookieSecretRes = makeSecret("cookie-secret", cookieSecret);
189
211
  const kekSecret = makeSecret("secret-kek", caeloSecretKek);
190
212
 
213
+ // The id each runtime secret is bound by: the Pulumi resource where this
214
+ // program owns the secret, the CLI-generated id otherwise.
215
+ const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
216
+ "postgres-password": pgSecret.resource.secretId,
217
+ "secret-kek": kekSecret.resource.secretId,
218
+ "internal-secret": gcpSecretId(env, "internal-secret"),
219
+ "tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
220
+ };
221
+
191
222
  // =========================================================================
192
223
  // Tier 4 — Cloud SQL Postgres (private IP only; HA configurable)
193
224
  // =========================================================================
@@ -216,7 +247,12 @@ const sqlInstance = new gcp.sql.DatabaseInstance(
216
247
  privateNetwork: network.id,
217
248
  enablePrivatePathForGoogleCloudServices: true,
218
249
  },
219
- databaseFlags: [{ name: "max_connections", value: maxConnections.toString() }],
250
+ databaseFlags: [
251
+ { name: "max_connections", value: maxConnections.toString() },
252
+ // The operator-access sync job logs in as its service account (Cloud
253
+ // SQL IAM database authentication, no password). No restart needed.
254
+ { name: "cloudsql.iam_authentication", value: "on" },
255
+ ],
220
256
  },
221
257
  deletionProtection,
222
258
  },
@@ -245,9 +281,9 @@ new gcp.sql.User(
245
281
  opts,
246
282
  );
247
283
 
248
- const adminDatabaseUrl = pulumi.interpolate`postgres://admin_role:${postgresPassword}@${sqlInstance.privateIpAddress}:5432/cms_admin?sslmode=require`;
249
- const publicAdminDatabaseUrl = pulumi.interpolate`postgres://admin_role:${postgresPassword}@${sqlInstance.privateIpAddress}:5432/cms_public?sslmode=require`;
250
- const publicDatabaseUrl = pulumi.interpolate`postgres://public_role:${postgresPassword}@${sqlInstance.privateIpAddress}:5432/cms_public?sslmode=require`;
284
+ // Password-less connection URLs (stack-contract.ts databaseUrls); the
285
+ // services get the password as a Secret Manager reference.
286
+ const dbUrls = sqlInstance.privateIpAddress.apply(databaseUrls);
251
287
 
252
288
  // =========================================================================
253
289
  // Media bucket — private, signed-URL access only (same as `gcp` stack)
@@ -256,7 +292,7 @@ const publicDatabaseUrl = pulumi.interpolate`postgres://public_role:${postgresPa
256
292
  const mediaBucket = new gcp.storage.Bucket(
257
293
  `${namePrefix}-media`,
258
294
  {
259
- name: `${project}-${namePrefix}-media`,
295
+ name: gcpBucketName(project, env, "media"),
260
296
  location: region.toUpperCase(),
261
297
  uniformBucketLevelAccess: true,
262
298
  forceDestroy: env !== "production",
@@ -272,26 +308,47 @@ const mediaBucket = new gcp.storage.Bucket(
272
308
  const runSa = new gcp.serviceaccount.Account(
273
309
  `${namePrefix}-run-sa`,
274
310
  {
275
- accountId: `${namePrefix}-run-sa`,
311
+ accountId: runServiceAccountId(env),
276
312
  displayName: `Caelo ${env} Cloud Run service account`,
277
313
  },
278
314
  opts,
279
315
  );
280
316
 
281
- for (const made of [
282
- { name: "postgres-password", made: pgSecret },
283
- { name: "csrf-secret", made: csrfSecretRes },
284
- { name: "cookie-secret", made: cookieSecretRes },
285
- { name: "secret-kek", made: kekSecret },
286
- ]) {
287
- new gcp.secretmanager.SecretIamMember(
288
- `${namePrefix}-${made.name}-binding`,
289
- {
290
- secretId: made.made.resource.secretId,
291
- role: "roles/secretmanager.secretAccessor",
292
- member: pulumi.interpolate`serviceAccount:${runSa.email}`,
293
- },
294
- { ...opts, dependsOn: [made.made.resource] },
317
+ // The gateway runs as its own SA so it can read only the secrets it needs
318
+ // (stack-contract.ts SERVICE_SECRET_ENV). `cms-provision upgrade` creates it
319
+ // on older installs, hence createIgnoreAlreadyExists.
320
+ const gatewaySa = new gcp.serviceaccount.Account(
321
+ `${namePrefix}-gateway-sa`,
322
+ {
323
+ accountId: gatewayServiceAccountId(env),
324
+ displayName: `Caelo ${env} gateway`,
325
+ createIgnoreAlreadyExists: true,
326
+ },
327
+ opts,
328
+ );
329
+ const serviceSa: Record<CloudRunSlug, gcp.serviceaccount.Account> = {
330
+ admin: runSa,
331
+ gateway: gatewaySa,
332
+ };
333
+
334
+ // secretAccessor for each service's SA on exactly the secrets its env reads.
335
+ // Cloud Run refuses to deploy a revision whose SA can't read a referenced
336
+ // secret, so each service waits for its bindings.
337
+ const secretBindings: Record<CloudRunSlug, gcp.secretmanager.SecretIamMember[]> = {
338
+ admin: [],
339
+ gateway: [],
340
+ };
341
+ for (const binding of runtimeSecretBindings()) {
342
+ secretBindings[binding.service].push(
343
+ new gcp.secretmanager.SecretIamMember(
344
+ `${namePrefix}-${binding.stackResource}`,
345
+ {
346
+ secretId: runtimeSecretIds[binding.secret],
347
+ role: "roles/secretmanager.secretAccessor",
348
+ member: pulumi.interpolate`serviceAccount:${serviceSa[binding.service].email}`,
349
+ },
350
+ opts,
351
+ ),
295
352
  );
296
353
  }
297
354
 
@@ -346,6 +403,24 @@ new gcp.projects.IAMMember(
346
403
  },
347
404
  opts,
348
405
  );
406
+ new gcp.projects.IAMMember(
407
+ `${namePrefix}-gateway-log-writer`,
408
+ {
409
+ project,
410
+ role: "roles/logging.logWriter",
411
+ member: pulumi.interpolate`serviceAccount:${gatewaySa.email}`,
412
+ },
413
+ opts,
414
+ );
415
+ new gcp.projects.IAMMember(
416
+ `${namePrefix}-gateway-metric-writer`,
417
+ {
418
+ project,
419
+ role: "roles/monitoring.metricWriter",
420
+ member: pulumi.interpolate`serviceAccount:${gatewaySa.email}`,
421
+ },
422
+ opts,
423
+ );
349
424
 
350
425
  // =========================================================================
351
426
  // Tier 1 — Firebase Hosting site
@@ -394,12 +469,16 @@ const firebaseCustomDomain = new gcp.firebase.HostingCustomDomain(
394
469
  // =========================================================================
395
470
 
396
471
  interface CloudRunArgs {
397
- readonly serviceName: string;
472
+ readonly serviceName: "admin" | "gateway";
398
473
  readonly minInstances: number;
399
474
  readonly maxInstances: number;
400
475
  readonly memory: string;
401
476
  readonly timeout: string;
402
- readonly extraEnv?: ReadonlyArray<{ name: string; value: pulumi.Input<string> }>;
477
+ /**
478
+ * The service's complete env — plain values and Secret Manager
479
+ * references: its env contract (src/stack-contract.ts).
480
+ */
481
+ readonly contractEnv: ReadonlyArray<CloudRunEnvVar<pulumi.Input<string>>>;
403
482
  /** v0.3.1 — admin: ALL (so IAP gates internet traffic). gateway:
404
483
  * INTERNAL_LOAD_BALANCER (locked down; only Firebase Hosting
405
484
  * rewrites + run.invoker-authorised callers can reach it). */
@@ -434,7 +513,7 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
434
513
  iapEnabled: args.iapEnabled ?? false,
435
514
  deletionProtection,
436
515
  template: {
437
- serviceAccount: runSa.email,
516
+ serviceAccount: serviceSa[args.serviceName].email,
438
517
  scaling: {
439
518
  minInstanceCount: args.minInstances,
440
519
  maxInstanceCount: args.maxInstances,
@@ -443,19 +522,7 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
443
522
  containers: [
444
523
  {
445
524
  image: imageRef(args.serviceName),
446
- envs: [
447
- { name: "CAELO_PROVIDER", value: "gcp-firebase" },
448
- { name: "CAELO_ENV", value: env },
449
- { name: "ADMIN_DATABASE_URL", value: adminDatabaseUrl },
450
- { name: "MEDIA_STORAGE_URL", value: pulumi.interpolate`gs://${mediaBucket.name}` },
451
- {
452
- name: "CAELO_SECRET_KEK",
453
- valueSource: {
454
- secretKeyRef: { secret: kekSecret.resource.secretId, version: "latest" },
455
- },
456
- },
457
- ...(args.extraEnv ?? []),
458
- ],
525
+ envs: [...args.contractEnv],
459
526
  resources: { limits: { cpu: "1", memory: args.memory } },
460
527
  },
461
528
  ],
@@ -471,13 +538,31 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
471
538
  pgAdminUser,
472
539
  cmsAdminDb,
473
540
  cmsPublicDb,
474
- ...(pgSecret.version ? [pgSecret.version] : []),
475
- ...(kekSecret.version ? [kekSecret.version] : []),
541
+ // The env contract names the media bucket as a string; keep it
542
+ // ordered before the services that use it.
543
+ mediaBucket,
544
+ // Referenced secrets must have a version, and the service's SA
545
+ // must be able to read them, before Cloud Run deploys a revision.
546
+ pgSecret.version,
547
+ kekSecret.version,
548
+ ...secretBindings[args.serviceName],
476
549
  ],
477
550
  },
478
551
  );
479
552
  }
480
553
 
554
+ // Inputs of the admin + gateway env contracts. The contracts are shared with
555
+ // `cms-provision upgrade`, which applies them to installs it rolls — add new
556
+ // env vars and secrets there, not here.
557
+ const envContractInputs = {
558
+ provider: "gcp-firebase",
559
+ projectId: project,
560
+ env,
561
+ domain,
562
+ region,
563
+ databaseUrls: { admin: dbUrls.admin, publicAdmin: dbUrls.publicAdmin, public: dbUrls.public },
564
+ } as const;
565
+
481
566
  // Gateway provisioned first so we can pass its name/region into the
482
567
  // admin's env (the Firebase publisher needs them to wire rewrites).
483
568
  const gatewaySvc = cloudRunService({
@@ -490,39 +575,27 @@ const gatewaySvc = cloudRunService({
490
575
  // rewrites or any caller holding roles/run.invoker on the
491
576
  // service. Anonymous internet traffic gets a Cloud Run 403.
492
577
  ingress: "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER",
493
- extraEnv: [{ name: "PUBLIC_DATABASE_URL", value: publicDatabaseUrl }],
578
+ contractEnv: gatewayEnvContract(envContractInputs),
494
579
  });
495
580
 
496
- // Issue #37 — deterministic, so the admin env can carry it as a plain string.
497
- const mcpServiceAccountEmail = `caelo-mcp@${project}.iam.gserviceaccount.com`;
498
-
499
581
  const adminSvc = cloudRunService({
500
582
  serviceName: "admin",
501
583
  minInstances: adminMinInstances,
502
584
  maxInstances: adminMaxInstances,
503
- memory: "1Gi",
585
+ memory: adminMemory,
504
586
  timeout: "3600s",
505
587
  // v0.3.1 — admin is internet-reachable but gated by Cloud Run
506
588
  // native IAP. iapEnabled flips the IAP integration on the service.
507
589
  ingress: "INGRESS_TRAFFIC_ALL",
508
590
  iapEnabled: true,
509
- extraEnv: [
510
- { name: "PUBLIC_ADMIN_DATABASE_URL", value: publicAdminDatabaseUrl },
511
- // Use the input string (known at config time) — resource output
512
- // is Output<string | undefined> which Input<string> rejects.
513
- // #551 — the public site URL; the admin seeds site_defaults.site_base_url
514
- // from it (canonical, og:url, sitemap) when it is not configured yet.
515
- { name: "CAELO_SITE_URL", value: `https://${domain}` },
516
- { name: "CAELO_FIREBASE_SITE", value: firebaseSiteId },
517
- { name: "CAELO_GENERATOR_CLI", value: "/app/apps/static-generator/src/cli.ts" },
518
- // v0.3.1 — Firebase publisher needs the gateway service name +
519
- // region to declare the /api/** rewrite when creating each
520
- // version.
521
- { name: "CAELO_GATEWAY_SERVICE", value: gatewaySvc.name },
522
- { name: "CAELO_GATEWAY_REGION", value: region },
523
- // Issue #37 — shown in the /security/mcp `claude mcp add` command.
524
- { name: "CAELO_MCP_IAP_SERVICE_ACCOUNT", value: mcpServiceAccountEmail },
525
- ],
591
+ // Use the input string (known at config time) for the site — the
592
+ // resource output is Output<string | undefined>, which Input<string>
593
+ // rejects.
594
+ contractEnv: adminEnvContract({
595
+ ...envContractInputs,
596
+ firebaseSiteId,
597
+ gatewayService: gatewaySvc.name,
598
+ }),
526
599
  });
527
600
 
528
601
  // =========================================================================
@@ -613,6 +686,11 @@ new gcp.cloudrunv2.ServiceIamMember(
613
686
  // (`https://<admin-svc>-<hash>-<region>.a.run.app`). They can flip
614
687
  // the knob + verify the domain at https://search.google.com/search-console
615
688
  // later and run `pulumi up` to bind admin.<domain> → Cloud Run.
689
+ //
690
+ // The supported path is `cms-provision admin-domain enable`
691
+ // (src/admin-domain.ts): it creates the same mapping as the operator's own
692
+ // gcloud identity — the verified domain owner — which the provisioner SA
693
+ // running this stack never is. Leave this knob false after using it.
616
694
  const provisionAdminDomain = cfg.getBoolean("provisionAdminDomain") ?? false;
617
695
  const adminDomainMapping = provisionAdminDomain
618
696
  ? new gcp.cloudrun.DomainMapping(
@@ -737,7 +815,7 @@ const mcpServiceAccount = new gcp.serviceaccount.Account(
737
815
  `${namePrefix}-mcp-sa`,
738
816
  {
739
817
  project,
740
- accountId: "caelo-mcp",
818
+ accountId: MCP_IAP_SERVICE_ACCOUNT_ID,
741
819
  displayName: "Caelo MCP (IAP ingress)",
742
820
  description:
743
821
  "Signs the IAP credential external MCP clients use to reach the admin (issue #37).",
@@ -774,6 +852,20 @@ new gcp.iap.WebCloudRunServiceIamMember(
774
852
  opts,
775
853
  );
776
854
 
855
+ // =========================================================================
856
+ // Operator access — deliberately NOT here
857
+ // =========================================================================
858
+ //
859
+ // Who passes IAP beyond the allowlist above follows the Caelo user list. A
860
+ // Cloud Run job with its own service account keeps the IAP binding and the
861
+ // caelo-mcp token-creator binding in step (admin-core
862
+ // security/operator-access/sync-job.ts); the admin's run SA holds no IAM
863
+ // policy rights and may only start that job. The CLI owns the job, its
864
+ // account, its grants and its schedule (packages/provisioning/src/
865
+ // operator-access.ts, run by the wizard after migrations and by `upgrade`),
866
+ // because the job's database user can only be assigned its role after the
867
+ // migrations have created that role.
868
+
777
869
  // =========================================================================
778
870
  // Outputs — DNS records the operator wires manually
779
871
  // =========================================================================
@@ -835,8 +927,9 @@ const tokenInfo = generateBootstrapToken();
835
927
  // plain strings in stack outputs at runtime; the cast bridges the
836
928
  // build-time type. Same pattern as packages/provisioning/stacks/gcp/index.ts.
837
929
  const out: CloudAdapterOutputs = {
838
- adminDatabaseUrl: adminDatabaseUrl as unknown as string,
839
- publicDatabaseUrl: publicDatabaseUrl as unknown as string,
930
+ // Password-less — the password lives only in Secret Manager.
931
+ adminDatabaseUrl: dbUrls.admin as unknown as string,
932
+ publicDatabaseUrl: dbUrls.public as unknown as string,
840
933
  mediaStorageUrl: pulumi.interpolate`gs://${mediaBucket.name}` as unknown as string,
841
934
  // Firebase Hosting serves media via its own CDN; we proxy through
842
935
  // the Firebase site's apex domain for canonical URLs.
@@ -25,3 +25,7 @@ config:
25
25
  type: string
26
26
  default: ./.caelo
27
27
  description: Where the generated docker-compose.yml + Caddyfile + secrets live.
28
+ caelo-self-hosted:adminMemory:
29
+ type: string
30
+ default: "2Gi"
31
+ description: Memory reserved for the admin container (it runs the Lighthouse quality audit).
@@ -31,6 +31,8 @@ const cfg = new pulumi.Config();
31
31
  const domain = cfg.require("domain");
32
32
  const ownerEmail = cfg.require("ownerEmail");
33
33
  const caeloDir = cfg.get("caeloDir") ?? "./.caelo";
34
+ // #553 — same knob as every adapter; undefined = stack-contract default.
35
+ const adminMemory = cfg.get("adminMemory");
34
36
 
35
37
  const composePath = resolve(caeloDir, "docker-compose.yml");
36
38
  const caddyPath = resolve(caeloDir, "Caddyfile");
@@ -52,6 +54,7 @@ const composeYaml = pulumi.all([postgresPassword, minioRootPassword]).apply(([pg
52
54
  anthropicApiKey: process.env.ANTHROPIC_API_KEY,
53
55
  resendApiKey: process.env.RESEND_API_KEY,
54
56
  diskSize: "20Gi",
57
+ ...(adminMemory ? { adminMemory } : {}),
55
58
  }),
56
59
  );
57
60