@caelo-cms/provisioning 0.11.10 → 0.11.12

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 (228) hide show
  1. package/dist/adapter.d.ts +6 -0
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/adopt-existing.d.ts +6 -2
  4. package/dist/adopt-existing.d.ts.map +1 -1
  5. package/dist/adopt-existing.js +57 -0
  6. package/dist/adopt-existing.js.map +1 -1
  7. package/dist/agent/command.d.ts +5 -2
  8. package/dist/agent/command.d.ts.map +1 -1
  9. package/dist/agent/command.js +1 -1
  10. package/dist/agent/command.js.map +1 -1
  11. package/dist/agent-db.js +196 -23
  12. package/dist/cli-routing.js +1 -1
  13. package/dist/cli-routing.js.map +1 -1
  14. package/dist/cli.js +59 -7
  15. package/dist/cli.js.map +1 -1
  16. package/dist/cloudsql-tls.d.ts +30 -0
  17. package/dist/cloudsql-tls.d.ts.map +1 -1
  18. package/dist/cloudsql-tls.js +7 -1
  19. package/dist/cloudsql-tls.js.map +1 -1
  20. package/dist/compose.d.ts +14 -0
  21. package/dist/compose.d.ts.map +1 -1
  22. package/dist/compose.js +54 -1
  23. package/dist/compose.js.map +1 -1
  24. package/dist/converge-db-trust.d.ts +44 -0
  25. package/dist/converge-db-trust.d.ts.map +1 -0
  26. package/dist/converge-db-trust.js +158 -0
  27. package/dist/converge-db-trust.js.map +1 -0
  28. package/dist/converge-job-gcp.d.ts +12 -2
  29. package/dist/converge-job-gcp.d.ts.map +1 -1
  30. package/dist/converge-job-gcp.js +42 -3
  31. package/dist/converge-job-gcp.js.map +1 -1
  32. package/dist/converge-job-preflight.d.ts +34 -2
  33. package/dist/converge-job-preflight.d.ts.map +1 -1
  34. package/dist/converge-job-preflight.js +76 -1
  35. package/dist/converge-job-preflight.js.map +1 -1
  36. package/dist/converge-job-setup.d.ts.map +1 -1
  37. package/dist/converge-job-setup.js +4 -1
  38. package/dist/converge-job-setup.js.map +1 -1
  39. package/dist/converge-job.d.ts +44 -3
  40. package/dist/converge-job.d.ts.map +1 -1
  41. package/dist/converge-job.js +105 -11
  42. package/dist/converge-job.js.map +1 -1
  43. package/dist/gcp-names.d.ts +13 -0
  44. package/dist/gcp-names.d.ts.map +1 -1
  45. package/dist/gcp-names.js +21 -0
  46. package/dist/gcp-names.js.map +1 -1
  47. package/dist/index.d.ts +2 -1
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/infra-contract.d.ts +1 -1
  52. package/dist/infra-contract.d.ts.map +1 -1
  53. package/dist/infra-contract.js +22 -1
  54. package/dist/infra-contract.js.map +1 -1
  55. package/dist/install-job.d.ts +10 -2
  56. package/dist/install-job.d.ts.map +1 -1
  57. package/dist/install-job.js +10 -1
  58. package/dist/install-job.js.map +1 -1
  59. package/dist/install-state.d.ts +7 -0
  60. package/dist/install-state.d.ts.map +1 -1
  61. package/dist/install-state.js.map +1 -1
  62. package/dist/lifecycle.d.ts.map +1 -1
  63. package/dist/lifecycle.js +3 -0
  64. package/dist/lifecycle.js.map +1 -1
  65. package/dist/mcp-auth-proxy-setup.d.ts +3 -1
  66. package/dist/mcp-auth-proxy-setup.d.ts.map +1 -1
  67. package/dist/mcp-auth-proxy-setup.js +9 -4
  68. package/dist/mcp-auth-proxy-setup.js.map +1 -1
  69. package/dist/migration-runner.d.ts.map +1 -1
  70. package/dist/migration-runner.js +8 -3
  71. package/dist/migration-runner.js.map +1 -1
  72. package/dist/migration-targets.d.ts +11 -0
  73. package/dist/migration-targets.d.ts.map +1 -0
  74. package/dist/migration-targets.js +12 -0
  75. package/dist/migration-targets.js.map +1 -0
  76. package/dist/operator-access.d.ts +10 -0
  77. package/dist/operator-access.d.ts.map +1 -1
  78. package/dist/operator-access.js +5 -0
  79. package/dist/operator-access.js.map +1 -1
  80. package/dist/pool-budget-stack.d.ts +13 -1
  81. package/dist/pool-budget-stack.d.ts.map +1 -1
  82. package/dist/pool-budget-stack.js +11 -1
  83. package/dist/pool-budget-stack.js.map +1 -1
  84. package/dist/public-role-isolation.d.ts +7 -3
  85. package/dist/public-role-isolation.d.ts.map +1 -1
  86. package/dist/public-role-isolation.js +25 -13
  87. package/dist/public-role-isolation.js.map +1 -1
  88. package/dist/release-contract.d.ts.map +1 -1
  89. package/dist/release-contract.js +3 -0
  90. package/dist/release-contract.js.map +1 -1
  91. package/dist/runtime-secrets.d.ts +2 -0
  92. package/dist/runtime-secrets.d.ts.map +1 -1
  93. package/dist/runtime-secrets.js +19 -15
  94. package/dist/runtime-secrets.js.map +1 -1
  95. package/dist/scheduler-setup.d.ts +75 -0
  96. package/dist/scheduler-setup.d.ts.map +1 -0
  97. package/dist/scheduler-setup.js +331 -0
  98. package/dist/scheduler-setup.js.map +1 -0
  99. package/dist/self-hosted-roles.d.ts +16 -9
  100. package/dist/self-hosted-roles.d.ts.map +1 -1
  101. package/dist/self-hosted-roles.js +49 -15
  102. package/dist/self-hosted-roles.js.map +1 -1
  103. package/dist/self-hosted-scheduler-interval.d.ts +21 -0
  104. package/dist/self-hosted-scheduler-interval.d.ts.map +1 -0
  105. package/dist/self-hosted-scheduler-interval.js +68 -0
  106. package/dist/self-hosted-scheduler-interval.js.map +1 -0
  107. package/dist/self-hosted-upgrade.d.ts +7 -4
  108. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  109. package/dist/self-hosted-upgrade.js +9 -5
  110. package/dist/self-hosted-upgrade.js.map +1 -1
  111. package/dist/stack-contract.d.ts +67 -6
  112. package/dist/stack-contract.d.ts.map +1 -1
  113. package/dist/stack-contract.js +87 -6
  114. package/dist/stack-contract.js.map +1 -1
  115. package/dist/staging-database.d.ts +22 -0
  116. package/dist/staging-database.d.ts.map +1 -0
  117. package/dist/staging-database.js +117 -0
  118. package/dist/staging-database.js.map +1 -0
  119. package/dist/upgrade/converge.d.ts +5 -0
  120. package/dist/upgrade/converge.d.ts.map +1 -1
  121. package/dist/upgrade/converge.js +110 -10
  122. package/dist/upgrade/converge.js.map +1 -1
  123. package/dist/upgrade/upgrade.d.ts +6 -0
  124. package/dist/upgrade/upgrade.d.ts.map +1 -1
  125. package/dist/upgrade/upgrade.js +3 -0
  126. package/dist/upgrade/upgrade.js.map +1 -1
  127. package/dist/upgrader-gcp.d.ts +5 -4
  128. package/dist/upgrader-gcp.d.ts.map +1 -1
  129. package/dist/upgrader-gcp.js +5 -4
  130. package/dist/upgrader-gcp.js.map +1 -1
  131. package/dist/upgrader-iam-preflight.d.ts +8 -6
  132. package/dist/upgrader-iam-preflight.d.ts.map +1 -1
  133. package/dist/upgrader-iam-preflight.js +5 -1
  134. package/dist/upgrader-iam-preflight.js.map +1 -1
  135. package/dist/wizard.d.ts +7 -0
  136. package/dist/wizard.d.ts.map +1 -1
  137. package/dist/wizard.js +11 -0
  138. package/dist/wizard.js.map +1 -1
  139. package/dist/wizards/gcp-cost.d.ts +31 -0
  140. package/dist/wizards/gcp-cost.d.ts.map +1 -1
  141. package/dist/wizards/gcp-cost.js +82 -12
  142. package/dist/wizards/gcp-cost.js.map +1 -1
  143. package/dist/wizards/gcp.d.ts.map +1 -1
  144. package/dist/wizards/gcp.js +87 -11
  145. package/dist/wizards/gcp.js.map +1 -1
  146. package/package.json +2 -2
  147. package/src/adapter.ts +6 -0
  148. package/src/admin-memory.test.ts +1 -1
  149. package/src/adopt-existing.test.ts +46 -0
  150. package/src/adopt-existing.ts +58 -2
  151. package/src/agent/command.ts +6 -3
  152. package/src/caddy-redirects.test.ts +1 -1
  153. package/src/cli-routing.test.ts +1 -1
  154. package/src/cli-routing.ts +1 -1
  155. package/src/cli.ts +63 -8
  156. package/src/cloudsql-tls.test.ts +30 -0
  157. package/src/cloudsql-tls.ts +10 -3
  158. package/src/compose.ts +68 -0
  159. package/src/converge-db-trust.test.ts +237 -0
  160. package/src/converge-db-trust.ts +212 -0
  161. package/src/converge-job-gcp.ts +42 -2
  162. package/src/converge-job-preflight.test.ts +106 -0
  163. package/src/converge-job-preflight.ts +97 -3
  164. package/src/converge-job-setup.ts +4 -1
  165. package/src/converge-job.test.ts +213 -0
  166. package/src/converge-job.ts +175 -19
  167. package/src/database-tls-builders.test.ts +3 -2
  168. package/src/deployed-release.test.ts +3 -0
  169. package/src/gateway-env-isolation.test.ts +40 -7
  170. package/src/gcp-names.ts +25 -0
  171. package/src/index.test.ts +3 -3
  172. package/src/index.ts +6 -0
  173. package/src/infra-contract.test.ts +5 -3
  174. package/src/infra-contract.ts +22 -1
  175. package/src/install-job.ts +10 -1
  176. package/src/install-state.ts +7 -0
  177. package/src/lifecycle.ts +3 -0
  178. package/src/mcp-auth-proxy-setup.test.ts +33 -1
  179. package/src/mcp-auth-proxy-setup.ts +11 -4
  180. package/src/migration-runner.ts +14 -9
  181. package/src/migration-targets.ts +12 -0
  182. package/src/operator-access.test.ts +15 -0
  183. package/src/operator-access.ts +14 -0
  184. package/src/pool-budget-stack.ts +15 -0
  185. package/src/pool-budget-stacks.test.ts +38 -13
  186. package/src/public-role-isolation.test.ts +18 -8
  187. package/src/public-role-isolation.ts +31 -17
  188. package/src/release-contract.ts +3 -0
  189. package/src/runtime-secrets.test.ts +18 -8
  190. package/src/runtime-secrets.ts +37 -18
  191. package/src/scheduler-setup.test.ts +201 -0
  192. package/src/scheduler-setup.ts +375 -0
  193. package/src/self-hosted-backup.test.ts +1 -0
  194. package/src/self-hosted-compose.test.ts +72 -1
  195. package/src/self-hosted-roles.integration.test.ts +2 -0
  196. package/src/self-hosted-roles.test.ts +49 -19
  197. package/src/self-hosted-roles.ts +60 -20
  198. package/src/self-hosted-scheduler-interval.test.ts +66 -0
  199. package/src/self-hosted-scheduler-interval.ts +79 -0
  200. package/src/self-hosted-start.test.ts +1 -0
  201. package/src/self-hosted-upgrade.test.ts +2 -1
  202. package/src/self-hosted-upgrade.ts +14 -8
  203. package/src/stack-contract.test.ts +31 -3
  204. package/src/stack-contract.ts +113 -6
  205. package/src/stack-converge-secrets.test.ts +2 -0
  206. package/src/stack-converge.test.ts +6 -4
  207. package/src/staging-database.test.ts +114 -0
  208. package/src/staging-database.ts +148 -0
  209. package/src/upgrade/converge.ts +130 -9
  210. package/src/upgrade/upgrade.test.ts +85 -2
  211. package/src/upgrade/upgrade.ts +9 -0
  212. package/src/upgrader-gcp.ts +5 -4
  213. package/src/upgrader-iam-preflight.ts +15 -10
  214. package/src/wizard.ts +19 -0
  215. package/src/wizards/gcp-cost.test.ts +54 -1
  216. package/src/wizards/gcp-cost.ts +102 -12
  217. package/src/wizards/gcp.ts +109 -8
  218. package/stacks/aws/Pulumi.yaml +1 -1
  219. package/stacks/aws/index.ts +41 -1
  220. package/stacks/azure/index.ts +51 -3
  221. package/stacks/gcp/Pulumi.yaml +1 -1
  222. package/stacks/gcp/README.md +1 -0
  223. package/stacks/gcp/index.ts +50 -1
  224. package/stacks/gcp-firebase/Pulumi.yaml +1 -1
  225. package/stacks/gcp-firebase/index.ts +50 -1
  226. package/stacks/self-hosted/Pulumi.yaml +3 -0
  227. package/stacks/self-hosted/README.md +4 -0
  228. package/stacks/self-hosted/index.ts +29 -4
@@ -33,8 +33,10 @@ import {
33
33
  } from "../../dist/gcp-names.js";
34
34
  import { createSiteUrlMap } from "../../dist/gcp-url-map.js";
35
35
  import {
36
+ DEFAULT_ADMIN_MAX_INSTANCES,
36
37
  DEFAULT_GATEWAY_MAX_INSTANCES,
37
38
  DEFAULT_MAX_CONNECTIONS,
39
+ DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
38
40
  stackPoolBudget,
39
41
  } from "../../dist/pool-budget-stack.js";
40
42
  import {
@@ -119,7 +121,10 @@ const adminMemory = cfg.get("adminMemory") ?? ADMIN_MEMORY_DEFAULT;
119
121
  // against the original 25-slot Postgres default — connection exhaustion
120
122
  // took the admin offline. v0.2.49 bakes the post-incident shape so fresh
121
123
  // installs don't trip the same wall.
122
- const adminMaxInstances = Number.parseInt(cfg.get("adminMaxInstances") ?? "5", 10);
124
+ const adminMaxInstances = Number.parseInt(
125
+ cfg.get("adminMaxInstances") ?? String(DEFAULT_ADMIN_MAX_INSTANCES),
126
+ 10,
127
+ );
123
128
  const gatewayMinInstances = Number.parseInt(cfg.get("gatewayMinInstances") ?? "0", 10);
124
129
  // #1254 — the gateway's scale-out is part of the connection budget: each
125
130
  // instance holds a cms_public pool.
@@ -137,6 +142,8 @@ const poolBudget = stackPoolBudget("gcp", {
137
142
  maxConnections,
138
143
  adminMaxInstances,
139
144
  gatewayMaxInstances,
145
+ // #1329 — the staging gateway (P3b-2) is budgeted from this release on.
146
+ stagingGatewayMaxInstances: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
140
147
  });
141
148
  const wafAdaptiveProtection = cfg.getBoolean("wafAdaptiveProtection") ?? false;
142
149
  const backupRetentionDays = Number.parseInt(cfg.get("backupRetentionDays") ?? "7", 10);
@@ -281,6 +288,8 @@ const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
281
288
  "tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
282
289
  "staging-gate-secret": gcpSecretId(env, "staging-gate-secret"),
283
290
  "public-role-password": gcpSecretId(env, "public-role-password"),
291
+ // #1329 — the staging gateway's login, never the live one's.
292
+ "public-staging-role-password": gcpSecretId(env, "public-staging-role-password"),
284
293
  // #914 / #806 — the Owner-set Google sign-in client (a placeholder until
285
294
  // then; IAP login, IAP programmatic access and the remote-MCP broker) and
286
295
  // the broker's seal key, both CLI-generated.
@@ -297,6 +306,14 @@ const publicRolePassword = pulumi.secret(
297
306
  { provider: gcpProvider },
298
307
  ).secretData,
299
308
  );
309
+ // #1329 — public_staging_role's own password (CLI-generated like the one
310
+ // above): the staging gateway's only login, on cms_public_staging only.
311
+ const publicStagingRolePassword = pulumi.secret(
312
+ gcp.secretmanager.getSecretVersionOutput(
313
+ { project, secret: gcpSecretId(env, "public-staging-role-password") },
314
+ { provider: gcpProvider },
315
+ ).secretData,
316
+ );
300
317
 
301
318
  // =========================================================================
302
319
  // Tier 4 — Cloud SQL Postgres (private IP only; HA + retention configurable)
@@ -371,6 +388,14 @@ const pgPublicUser = new gcp.sql.User(
371
388
  { instance: sqlInstance.name, name: "public_role", password: publicRolePassword },
372
389
  opts,
373
390
  );
391
+ // #1329 — the staging gateway's login: an Admin API user like public_role,
392
+ // so the same membership revoke (public-role-isolation.ts) and the same
393
+ // audit apply; migration 0530 gives it CONNECT on cms_public_staging only.
394
+ new gcp.sql.User(
395
+ `${namePrefix}-pg-public-staging`,
396
+ { instance: sqlInstance.name, name: "public_staging_role", password: publicStagingRolePassword },
397
+ opts,
398
+ );
374
399
 
375
400
  const cmsAdminDb = new gcp.sql.Database(
376
401
  `${namePrefix}-cms-admin-db`,
@@ -382,6 +407,13 @@ const cmsPublicDb = new gcp.sql.Database(
382
407
  { instance: sqlInstance.name, name: "cms_public" },
383
408
  opts,
384
409
  );
410
+ // #1329 — staging's own data: the twin of cms_public on the same instance
411
+ // (no extra cost), reached by the staging gateway only.
412
+ const cmsPublicStagingDb = new gcp.sql.Database(
413
+ `${namePrefix}-cms-public-staging-db`,
414
+ { instance: sqlInstance.name, name: "cms_public_staging" },
415
+ opts,
416
+ );
385
417
 
386
418
  // #1338 — the instance's DNS name, resolved to its private IP by a private
387
419
  // zone in the VPC (Cloud SQL assigns the name, it does not resolve it).
@@ -767,6 +799,8 @@ const envContractInputs = {
767
799
  admin: dbUrls.admin,
768
800
  publicAdmin: dbUrls.publicAdmin,
769
801
  public: dbUrls.public,
802
+ publicStagingAdmin: dbUrls.publicStagingAdmin,
803
+ publicStaging: dbUrls.publicStaging,
770
804
  },
771
805
  } as const;
772
806
 
@@ -841,6 +875,7 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
841
875
  pgAdminUser,
842
876
  cmsAdminDb,
843
877
  cmsPublicDb,
878
+ cmsPublicStagingDb,
844
879
  // The env contract names the buckets as strings; keep them ordered
845
880
  // before the services that use them.
846
881
  mediaBucket,
@@ -1155,6 +1190,18 @@ new gcp.iap.WebBackendServiceIamMember(
1155
1190
  // (packages/provisioning/src/capture-job-setup.ts), for the same reason as
1156
1191
  // the audit job above.
1157
1192
 
1193
+ // =========================================================================
1194
+ // Scheduler tick — deliberately NOT here
1195
+ // =========================================================================
1196
+ //
1197
+ // #1140 — one Cloud Scheduler job starts a Cloud Workflows execution every
1198
+ // `schedulerIntervalMinutes` (1–15, default 5); the workflow signs an IAP
1199
+ // credential as the scheduler's own service account and POSTs the admin's
1200
+ // /api/internal/scheduler/tick, where the admin runs its periodic work and
1201
+ // plugin workers from its due-table. The CLI owns the account, its grants,
1202
+ // the workflow and the job (packages/provisioning/src/scheduler-setup.ts),
1203
+ // for the same reason as the audit job above.
1204
+
1158
1205
  // =========================================================================
1159
1206
  // Operator access — deliberately NOT here
1160
1207
  // =========================================================================
@@ -1325,6 +1372,7 @@ const out: CloudAdapterOutputs = {
1325
1372
  // Password-less — the password lives only in Secret Manager.
1326
1373
  adminDatabaseUrl: dbUrls.admin as unknown as string,
1327
1374
  publicDatabaseUrl: dbUrls.public as unknown as string,
1375
+ publicStagingDatabaseUrl: dbUrls.publicStaging as unknown as string,
1328
1376
  mediaStorageUrl: pulumi.interpolate`gs://${mediaBucket.name}` as unknown as string,
1329
1377
  mediaCdnBaseUrl: pulumi.interpolate`https://${domain}/media` as unknown as string,
1330
1378
  bootstrapUrl:
@@ -1338,6 +1386,7 @@ const out: CloudAdapterOutputs = {
1338
1386
 
1339
1387
  export const adminDatabaseUrlOut = out.adminDatabaseUrl;
1340
1388
  export const publicDatabaseUrlOut = out.publicDatabaseUrl;
1389
+ export const publicStagingDatabaseUrlOut = out.publicStagingDatabaseUrl;
1341
1390
  export const mediaStorageUrlOut = out.mediaStorageUrl;
1342
1391
  export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
1343
1392
  export const bootstrapUrlOut = out.bootstrapUrl;
@@ -58,7 +58,7 @@ config:
58
58
  description: Admin Cloud Run memory. 2Gi leaves room for the Lighthouse quality audit next to editor requests; billed only while an instance runs.
59
59
  caelo-gcp-firebase:adminMaxInstances:
60
60
  type: string
61
- default: "5"
61
+ default: "4"
62
62
  description: Admin Cloud Run max instances.
63
63
  caelo-gcp-firebase:maxConnections:
64
64
  type: string
@@ -41,8 +41,10 @@ import {
41
41
  runServiceAccountId,
42
42
  } from "../../dist/gcp-names.js";
43
43
  import {
44
+ DEFAULT_ADMIN_MAX_INSTANCES,
44
45
  DEFAULT_GATEWAY_MAX_INSTANCES,
45
46
  DEFAULT_MAX_CONNECTIONS,
47
+ DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
46
48
  stackPoolBudget,
47
49
  } from "../../dist/pool-budget-stack.js";
48
50
  import {
@@ -109,7 +111,10 @@ const adminMinInstances = Number.parseInt(cfg.get("adminMinInstances") ?? "0", 1
109
111
  // #553 — the admin also runs the Lighthouse quality audit; same knob and
110
112
  // default on every adapter (stack-contract.ts ADMIN_MEMORY_DEFAULT).
111
113
  const adminMemory = cfg.get("adminMemory") ?? ADMIN_MEMORY_DEFAULT;
112
- const adminMaxInstances = Number.parseInt(cfg.get("adminMaxInstances") ?? "5", 10);
114
+ const adminMaxInstances = Number.parseInt(
115
+ cfg.get("adminMaxInstances") ?? String(DEFAULT_ADMIN_MAX_INSTANCES),
116
+ 10,
117
+ );
113
118
  const gatewayMinInstances = Number.parseInt(cfg.get("gatewayMinInstances") ?? "0", 10);
114
119
  // #1254 — part of the connection budget (stacks/gcp has the long comment).
115
120
  const gatewayMaxInstances = Number.parseInt(
@@ -125,6 +130,8 @@ const poolBudget = stackPoolBudget("gcp-firebase", {
125
130
  maxConnections,
126
131
  adminMaxInstances,
127
132
  gatewayMaxInstances,
133
+ // #1329 — the staging gateway (P3b-2) is budgeted from this release on.
134
+ stagingGatewayMaxInstances: DEFAULT_STAGING_GATEWAY_MAX_INSTANCES,
128
135
  });
129
136
  const backupRetentionDays = Number.parseInt(cfg.get("backupRetentionDays") ?? "7", 10);
130
137
  const iapAllowlistRaw = cfg.get("iapAllowlist");
@@ -262,6 +269,8 @@ const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
262
269
  "tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
263
270
  "staging-gate-secret": gcpSecretId(env, "staging-gate-secret"),
264
271
  "public-role-password": gcpSecretId(env, "public-role-password"),
272
+ // #1329 — the staging gateway's login, never the live one's.
273
+ "public-staging-role-password": gcpSecretId(env, "public-staging-role-password"),
265
274
  // #914 / #806 — the Owner-set Google sign-in client (a placeholder until
266
275
  // then; IAP login, IAP programmatic access and the remote-MCP broker) and
267
276
  // the broker's seal key, both CLI-generated.
@@ -278,6 +287,14 @@ const publicRolePassword = pulumi.secret(
278
287
  { provider: gcpProvider },
279
288
  ).secretData,
280
289
  );
290
+ // #1329 — public_staging_role's own password (CLI-generated like the one
291
+ // above): the staging gateway's only login, on cms_public_staging only.
292
+ const publicStagingRolePassword = pulumi.secret(
293
+ gcp.secretmanager.getSecretVersionOutput(
294
+ { project, secret: gcpSecretId(env, "public-staging-role-password") },
295
+ { provider: gcpProvider },
296
+ ).secretData,
297
+ );
281
298
 
282
299
  // =========================================================================
283
300
  // Tier 4 — Cloud SQL Postgres (private IP only; HA configurable)
@@ -334,6 +351,13 @@ const cmsPublicDb = new gcp.sql.Database(
334
351
  { instance: sqlInstance.name, name: "cms_public" },
335
352
  opts,
336
353
  );
354
+ // #1329 — staging's own data: the twin of cms_public on the same instance
355
+ // (no extra cost), reached by the staging gateway only.
356
+ const cmsPublicStagingDb = new gcp.sql.Database(
357
+ `${namePrefix}-cms-public-staging`,
358
+ { instance: sqlInstance.name, name: "cms_public_staging" },
359
+ opts,
360
+ );
337
361
 
338
362
  const pgAdminUser = new gcp.sql.User(
339
363
  `${namePrefix}-admin-user`,
@@ -352,6 +376,14 @@ new gcp.sql.User(
352
376
  { instance: sqlInstance.name, name: "public_role", password: publicRolePassword },
353
377
  opts,
354
378
  );
379
+ // #1329 — the staging gateway's login: an Admin API user like public_role,
380
+ // so the same membership revoke (public-role-isolation.ts) and the same
381
+ // audit apply; migration 0530 gives it CONNECT on cms_public_staging only.
382
+ new gcp.sql.User(
383
+ `${namePrefix}-public-staging-user`,
384
+ { instance: sqlInstance.name, name: "public_staging_role", password: publicStagingRolePassword },
385
+ opts,
386
+ );
355
387
 
356
388
  // #1338 — the instance's DNS name, resolved to its private IP by a private
357
389
  // zone in the VPC (Cloud SQL assigns the name, it does not resolve it).
@@ -729,6 +761,7 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
729
761
  pgAdminUser,
730
762
  cmsAdminDb,
731
763
  cmsPublicDb,
764
+ cmsPublicStagingDb,
732
765
  // The env contract names the media bucket as a string; keep it
733
766
  // ordered before the services that use it.
734
767
  mediaBucket,
@@ -756,6 +789,8 @@ const envContractInputs = {
756
789
  admin: dbUrls.admin,
757
790
  publicAdmin: dbUrls.publicAdmin,
758
791
  public: dbUrls.public,
792
+ publicStagingAdmin: dbUrls.publicStagingAdmin,
793
+ publicStaging: dbUrls.publicStaging,
759
794
  },
760
795
  } as const;
761
796
 
@@ -1129,6 +1164,18 @@ new gcp.iap.WebCloudRunServiceIamMember(
1129
1164
  // (packages/provisioning/src/capture-job-setup.ts), for the same reason as
1130
1165
  // the audit job above.
1131
1166
 
1167
+ // =========================================================================
1168
+ // Scheduler tick — deliberately NOT here
1169
+ // =========================================================================
1170
+ //
1171
+ // #1140 — one Cloud Scheduler job starts a Cloud Workflows execution every
1172
+ // `schedulerIntervalMinutes` (1–15, default 5); the workflow signs an IAP
1173
+ // credential as the scheduler's own service account and POSTs the admin's
1174
+ // /api/internal/scheduler/tick, where the admin runs its periodic work and
1175
+ // plugin workers from its due-table. The CLI owns the account, its grants,
1176
+ // the workflow and the job (packages/provisioning/src/scheduler-setup.ts),
1177
+ // for the same reason as the audit job above.
1178
+
1132
1179
  // =========================================================================
1133
1180
  // Operator access — deliberately NOT here
1134
1181
  // =========================================================================
@@ -1213,6 +1260,7 @@ const out: CloudAdapterOutputs = {
1213
1260
  // Password-less — the password lives only in Secret Manager.
1214
1261
  adminDatabaseUrl: dbUrls.admin as unknown as string,
1215
1262
  publicDatabaseUrl: dbUrls.public as unknown as string,
1263
+ publicStagingDatabaseUrl: dbUrls.publicStaging as unknown as string,
1216
1264
  mediaStorageUrl: pulumi.interpolate`gs://${mediaBucket.name}` as unknown as string,
1217
1265
  // Firebase Hosting serves media via its own CDN; we proxy through
1218
1266
  // the Firebase site's apex domain for canonical URLs.
@@ -1229,6 +1277,7 @@ const out: CloudAdapterOutputs = {
1229
1277
 
1230
1278
  export const adminDatabaseUrlOut = out.adminDatabaseUrl;
1231
1279
  export const publicDatabaseUrlOut = out.publicDatabaseUrl;
1280
+ export const publicStagingDatabaseUrlOut = out.publicStagingDatabaseUrl;
1232
1281
  export const mediaStorageUrlOut = out.mediaStorageUrl;
1233
1282
  export const mediaCdnBaseUrlOut = out.mediaCdnBaseUrl;
1234
1283
  export const bootstrapUrlOut = out.bootstrapUrl;
@@ -29,3 +29,6 @@ config:
29
29
  type: string
30
30
  default: "2Gi"
31
31
  description: Memory reserved for the admin container (it runs the Lighthouse quality audit).
32
+ caelo-self-hosted:schedulerIntervalMinutes:
33
+ type: integer
34
+ description: Minutes between scheduler ticks (1-15, default 5; #1140). On an install `cms-provision init` set up, `.caelo/config.json` records it and this key must match.
@@ -33,6 +33,10 @@ pulumi stack output bootstrapUrl
33
33
 
34
34
  Open that URL once Caddy has issued the cert (~30 s).
35
35
 
36
+ ## Scheduler tick
37
+
38
+ The Compose file runs a `caelo-scheduler` container (the admin image, no database access, only the internal secret it signs with) that POSTs the admin's `/api/internal/scheduler/tick` every `schedulerIntervalMinutes` (#1140; 1–15, default 5). The admin runs its periodic work and plugin workers on that tick. Set the interval with `cms-provision init --scheduler-interval-minutes 2` (recorded in `.caelo/config.json`, which every CLI render reads) or, on an install without a `config.json`, with `pulumi config set caelo-self-hosted:schedulerIntervalMinutes 2`. The stack reads `.caelo/config.json` too, so a `pulumi up` keeps the recorded interval; a Pulumi key that disagrees with it is refused. An install from before the scheduler gets the container on its next `upgrade`, which rewrites the Compose file.
39
+
36
40
  ## Connection budget
37
41
 
38
42
  The generated Compose file starts Postgres with `max_connections=100` and sizes every pool from it (#1254, docs/dev/connection-budget.md): the admin gets a cms_admin pool of 77 and a cms_public pool of 4, the gateway a pool of 2, and static builds 2 per pool.
@@ -34,7 +34,12 @@ import {
34
34
  generateDockerCompose,
35
35
  SITE_OUTPUT_ROOT,
36
36
  } from "../../dist/compose.js";
37
+ import { MIGRATION_TARGETS } from "../../dist/migration-targets.js";
37
38
  import { selfHostedBootstrapScript } from "../../dist/self-hosted-roles.js";
39
+ import {
40
+ recordedSchedulerIntervalMinutes,
41
+ selfHostedSchedulerIntervalMinutes,
42
+ } from "../../dist/self-hosted-scheduler-interval.js";
38
43
 
39
44
  const cfg = new pulumi.Config();
40
45
  const domain = cfg.require("domain");
@@ -44,6 +49,17 @@ const ownerEmail = cfg.require("ownerEmail");
44
49
  const caeloDir = cfg.get("caeloDir") ?? "./.caelo";
45
50
  // #553 — same knob as every adapter; undefined = stack-contract default.
46
51
  const adminMemory = cfg.get("adminMemory");
52
+ // #1140 — how often the scheduler container calls the admin's tick (1–15
53
+ // minutes, default 5). `init --scheduler-interval-minutes` records it in
54
+ // <caeloDir>/config.json, which the CLI renders from; the stack reads the
55
+ // same record so a `pulumi up` does not put it back to the default, and
56
+ // refuses a Pulumi key that disagrees with it.
57
+ const caeloConfigPath = resolve(caeloDir, "config.json");
58
+ const schedulerIntervalMinutes = selfHostedSchedulerIntervalMinutes(
59
+ cfg.getNumber("schedulerIntervalMinutes"),
60
+ recordedSchedulerIntervalMinutes(caeloConfigPath),
61
+ caeloConfigPath,
62
+ );
47
63
  // #696 — the prebuilt, cosign-verified release images, pinned by digest
48
64
  // (`ghcr.io/caelo-cms/<svc>@sha256:…`; the generator refuses anything else).
49
65
  // `cms-provision init` resolves + verifies them and records them in
@@ -69,21 +85,30 @@ const postgresPassword = pulumi.secret(randomHex(32));
69
85
  // public_role one (#635: no cms_admin login).
70
86
  const adminRolePassword = pulumi.secret(randomHex(32));
71
87
  const publicRolePassword = pulumi.secret(randomHex(32));
88
+ // #1329 — the staging gateway's own login on cms_public_staging.
89
+ const publicStagingRolePassword = pulumi.secret(randomHex(32));
72
90
  // P18 — the KEK that encrypts stored provider keys.
73
91
  const caeloSecretKek = pulumi.secret(randomHex(32));
74
92
 
75
93
  const composeYaml = pulumi
76
- .all([postgresPassword, adminRolePassword, publicRolePassword, caeloSecretKek])
77
- .apply(([pgPw, adminPw, publicPw, kek]) =>
94
+ .all([
95
+ postgresPassword,
96
+ adminRolePassword,
97
+ publicRolePassword,
98
+ publicStagingRolePassword,
99
+ caeloSecretKek,
100
+ ])
101
+ .apply(([pgPw, adminPw, publicPw, publicStagingPw, kek]) =>
78
102
  generateDockerCompose({
79
103
  domain,
80
104
  postgresPassword: pgPw,
81
- rolePasswords: { admin: adminPw, public: publicPw },
105
+ rolePasswords: { admin: adminPw, public: publicPw, publicStaging: publicStagingPw },
82
106
  caeloSecretKek: kek,
83
107
  anthropicApiKey: process.env.ANTHROPIC_API_KEY,
84
108
  resendApiKey: process.env.RESEND_API_KEY,
85
109
  diskSize: "20Gi",
86
110
  ...(adminMemory ? { adminMemory } : {}),
111
+ ...(schedulerIntervalMinutes !== undefined ? { schedulerIntervalMinutes } : {}),
87
112
  images: {
88
113
  mode: "release",
89
114
  admin: adminImage,
@@ -144,7 +169,7 @@ const composeUp = new local.Command(
144
169
  // install's admin image, then everything else.
145
170
  create: pulumi.interpolate`set -e
146
171
  docker compose -f ${composePath} up -d --wait postgres
147
- for target in admin public; do
172
+ for target in ${MIGRATION_TARGETS.join(" ")}; do
148
173
  docker compose -f ${composePath} run --rm --no-deps -T caelo-admin bun --bun /app/packages/migrations/src/migrate.ts "$target"
149
174
  done
150
175
  docker compose -f ${composePath} up -d --remove-orphans`,