@caelo-cms/provisioning 0.10.46 → 0.10.48

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 (342) hide show
  1. package/dist/adc-impersonation.d.ts.map +1 -1
  2. package/dist/adc-impersonation.js +23 -1
  3. package/dist/adc-impersonation.js.map +1 -1
  4. package/dist/agent/apply.d.ts +45 -0
  5. package/dist/agent/apply.d.ts.map +1 -0
  6. package/dist/agent/apply.js +83 -0
  7. package/dist/agent/apply.js.map +1 -0
  8. package/dist/agent/cli-provenance.d.ts +42 -0
  9. package/dist/agent/cli-provenance.d.ts.map +1 -0
  10. package/dist/agent/cli-provenance.js +138 -0
  11. package/dist/agent/cli-provenance.js.map +1 -0
  12. package/dist/agent/command.d.ts +79 -0
  13. package/dist/agent/command.d.ts.map +1 -0
  14. package/dist/agent/command.js +454 -0
  15. package/dist/agent/command.js.map +1 -0
  16. package/dist/agent/config.d.ts +30 -0
  17. package/dist/agent/config.d.ts.map +1 -0
  18. package/dist/agent/config.js +60 -0
  19. package/dist/agent/config.js.map +1 -0
  20. package/dist/agent/lock.d.ts +13 -0
  21. package/dist/agent/lock.d.ts.map +1 -0
  22. package/dist/agent/lock.js +100 -0
  23. package/dist/agent/lock.js.map +1 -0
  24. package/dist/agent/queue.d.ts +35 -0
  25. package/dist/agent/queue.d.ts.map +1 -0
  26. package/dist/agent/queue.js +50 -0
  27. package/dist/agent/queue.js.map +1 -0
  28. package/dist/agent/run.d.ts +97 -0
  29. package/dist/agent/run.d.ts.map +1 -0
  30. package/dist/agent/run.js +198 -0
  31. package/dist/agent/run.js.map +1 -0
  32. package/dist/agent/systemd.d.ts +55 -0
  33. package/dist/agent/systemd.d.ts.map +1 -0
  34. package/dist/agent/systemd.js +171 -0
  35. package/dist/agent/systemd.js.map +1 -0
  36. package/dist/agent/units.d.ts +44 -0
  37. package/dist/agent/units.d.ts.map +1 -0
  38. package/dist/agent/units.js +127 -0
  39. package/dist/agent/units.js.map +1 -0
  40. package/dist/agent-db.js +18362 -0
  41. package/dist/cli.js +122 -16
  42. package/dist/cli.js.map +1 -1
  43. package/dist/cloudsql-iam-user.d.ts +39 -0
  44. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  45. package/dist/cloudsql-iam-user.js +119 -0
  46. package/dist/cloudsql-iam-user.js.map +1 -0
  47. package/dist/compose.d.ts +16 -0
  48. package/dist/compose.d.ts.map +1 -1
  49. package/dist/compose.js +26 -1
  50. package/dist/compose.js.map +1 -1
  51. package/dist/dns/index.d.ts +2 -0
  52. package/dist/dns/index.d.ts.map +1 -1
  53. package/dist/dns/index.js +3 -2
  54. package/dist/dns/index.js.map +1 -1
  55. package/dist/dns/manual.d.ts +5 -0
  56. package/dist/dns/manual.d.ts.map +1 -1
  57. package/dist/dns/manual.js +28 -12
  58. package/dist/dns/manual.js.map +1 -1
  59. package/dist/gcloud.d.ts +2 -0
  60. package/dist/gcloud.d.ts.map +1 -1
  61. package/dist/gcloud.js +1 -1
  62. package/dist/gcloud.js.map +1 -1
  63. package/dist/gcp-names.d.ts +15 -0
  64. package/dist/gcp-names.d.ts.map +1 -1
  65. package/dist/gcp-names.js +23 -0
  66. package/dist/gcp-names.js.map +1 -1
  67. package/dist/index.d.ts +6 -0
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +4 -0
  70. package/dist/index.js.map +1 -1
  71. package/dist/infra-contract.d.ts +1 -1
  72. package/dist/infra-contract.js +7 -1
  73. package/dist/infra-contract.js.map +1 -1
  74. package/dist/install-job-runner.d.ts +13 -0
  75. package/dist/install-job-runner.d.ts.map +1 -0
  76. package/dist/install-job-runner.js +155 -0
  77. package/dist/install-job-runner.js.map +1 -0
  78. package/dist/install-job.d.ts +210 -0
  79. package/dist/install-job.d.ts.map +1 -0
  80. package/dist/install-job.js +449 -0
  81. package/dist/install-job.js.map +1 -0
  82. package/dist/install-state.d.ts +19 -1
  83. package/dist/install-state.d.ts.map +1 -1
  84. package/dist/install-state.js +31 -3
  85. package/dist/install-state.js.map +1 -1
  86. package/dist/lifecycle.d.ts.map +1 -1
  87. package/dist/lifecycle.js +24 -5
  88. package/dist/lifecycle.js.map +1 -1
  89. package/dist/migration-runner.d.ts +5 -0
  90. package/dist/migration-runner.d.ts.map +1 -1
  91. package/dist/migration-runner.js +1 -1
  92. package/dist/migration-runner.js.map +1 -1
  93. package/dist/operator-access.d.ts +14 -8
  94. package/dist/operator-access.d.ts.map +1 -1
  95. package/dist/operator-access.js +16 -102
  96. package/dist/operator-access.js.map +1 -1
  97. package/dist/run-mode.d.ts +27 -0
  98. package/dist/run-mode.d.ts.map +1 -0
  99. package/dist/run-mode.js +22 -0
  100. package/dist/run-mode.js.map +1 -0
  101. package/dist/runtime-secrets.d.ts +19 -0
  102. package/dist/runtime-secrets.d.ts.map +1 -1
  103. package/dist/runtime-secrets.js +44 -0
  104. package/dist/runtime-secrets.js.map +1 -1
  105. package/dist/self-hosted-roles.d.ts +22 -1
  106. package/dist/self-hosted-roles.d.ts.map +1 -1
  107. package/dist/self-hosted-roles.js +81 -12
  108. package/dist/self-hosted-roles.js.map +1 -1
  109. package/dist/self-hosted-upgrade.d.ts +17 -0
  110. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  111. package/dist/self-hosted-upgrade.js +16 -1
  112. package/dist/self-hosted-upgrade.js.map +1 -1
  113. package/dist/stack-contract.d.ts +6 -0
  114. package/dist/stack-contract.d.ts.map +1 -1
  115. package/dist/stack-contract.js +10 -1
  116. package/dist/stack-contract.js.map +1 -1
  117. package/dist/state-attach.d.ts +23 -0
  118. package/dist/state-attach.d.ts.map +1 -0
  119. package/dist/state-attach.js +112 -0
  120. package/dist/state-attach.js.map +1 -0
  121. package/dist/state-backend.d.ts +11 -0
  122. package/dist/state-backend.d.ts.map +1 -1
  123. package/dist/state-backend.js +20 -0
  124. package/dist/state-backend.js.map +1 -1
  125. package/dist/state-command.d.ts +20 -0
  126. package/dist/state-command.d.ts.map +1 -0
  127. package/dist/state-command.js +237 -0
  128. package/dist/state-command.js.map +1 -0
  129. package/dist/state-migrate.d.ts +121 -0
  130. package/dist/state-migrate.d.ts.map +1 -0
  131. package/dist/state-migrate.js +290 -0
  132. package/dist/state-migrate.js.map +1 -0
  133. package/dist/state-pulumi.d.ts +17 -0
  134. package/dist/state-pulumi.d.ts.map +1 -0
  135. package/dist/state-pulumi.js +218 -0
  136. package/dist/state-pulumi.js.map +1 -0
  137. package/dist/state-store.d.ts +27 -0
  138. package/dist/state-store.d.ts.map +1 -1
  139. package/dist/state-store.js +65 -0
  140. package/dist/state-store.js.map +1 -1
  141. package/dist/upgrade/converge.d.ts +6 -0
  142. package/dist/upgrade/converge.d.ts.map +1 -1
  143. package/dist/upgrade/converge.js +59 -2
  144. package/dist/upgrade/converge.js.map +1 -1
  145. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  146. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  147. package/dist/upgrade/gcp-lookup.js +25 -0
  148. package/dist/upgrade/gcp-lookup.js.map +1 -1
  149. package/dist/upgrade/release-images.d.ts +12 -0
  150. package/dist/upgrade/release-images.d.ts.map +1 -1
  151. package/dist/upgrade/release-images.js +31 -0
  152. package/dist/upgrade/release-images.js.map +1 -1
  153. package/dist/upgrade/upgrade.d.ts +5 -0
  154. package/dist/upgrade/upgrade.d.ts.map +1 -1
  155. package/dist/upgrade/upgrade.js +28 -1
  156. package/dist/upgrade/upgrade.js.map +1 -1
  157. package/dist/upgrader-assess.d.ts +82 -0
  158. package/dist/upgrader-assess.d.ts.map +1 -0
  159. package/dist/upgrader-assess.js +116 -0
  160. package/dist/upgrader-assess.js.map +1 -0
  161. package/dist/upgrader-gcp.d.ts +17 -0
  162. package/dist/upgrader-gcp.d.ts.map +1 -0
  163. package/dist/upgrader-gcp.js +87 -0
  164. package/dist/upgrader-gcp.js.map +1 -0
  165. package/dist/upgrader-job.d.ts +131 -0
  166. package/dist/upgrader-job.d.ts.map +1 -0
  167. package/dist/upgrader-job.js +259 -0
  168. package/dist/upgrader-job.js.map +1 -0
  169. package/dist/upgrader-reporter.d.ts +35 -0
  170. package/dist/upgrader-reporter.d.ts.map +1 -0
  171. package/dist/upgrader-reporter.js +79 -0
  172. package/dist/upgrader-reporter.js.map +1 -0
  173. package/dist/upgrader-setup.d.ts +59 -0
  174. package/dist/upgrader-setup.d.ts.map +1 -0
  175. package/dist/upgrader-setup.js +308 -0
  176. package/dist/upgrader-setup.js.map +1 -0
  177. package/dist/wizard.d.ts +6 -0
  178. package/dist/wizard.d.ts.map +1 -1
  179. package/dist/wizard.js +32 -4
  180. package/dist/wizard.js.map +1 -1
  181. package/dist/wizards/gcp-firebase.d.ts +3 -0
  182. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  183. package/dist/wizards/gcp-firebase.js +1 -0
  184. package/dist/wizards/gcp-firebase.js.map +1 -1
  185. package/dist/wizards/gcp-install-job.d.ts +74 -0
  186. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  187. package/dist/wizards/gcp-install-job.js +398 -0
  188. package/dist/wizards/gcp-install-job.js.map +1 -0
  189. package/dist/wizards/gcp-prompts.d.ts +3 -0
  190. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  191. package/dist/wizards/gcp-prompts.js +25 -0
  192. package/dist/wizards/gcp-prompts.js.map +1 -0
  193. package/dist/wizards/gcp-pulumi.d.ts +9 -2
  194. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  195. package/dist/wizards/gcp-pulumi.js +8 -6
  196. package/dist/wizards/gcp-pulumi.js.map +1 -1
  197. package/dist/wizards/gcp.d.ts +40 -0
  198. package/dist/wizards/gcp.d.ts.map +1 -1
  199. package/dist/wizards/gcp.js +291 -83
  200. package/dist/wizards/gcp.js.map +1 -1
  201. package/package.json +6 -4
  202. package/src/adapter.ts +97 -0
  203. package/src/adc-impersonation.ts +176 -0
  204. package/src/admin-domain.test.ts +254 -0
  205. package/src/admin-domain.ts +245 -0
  206. package/src/admin-memory.test.ts +151 -0
  207. package/src/agent/apply.test.ts +185 -0
  208. package/src/agent/apply.ts +124 -0
  209. package/src/agent/cli-provenance.test.ts +159 -0
  210. package/src/agent/cli-provenance.ts +191 -0
  211. package/src/agent/command.test.ts +77 -0
  212. package/src/agent/command.ts +564 -0
  213. package/src/agent/config.ts +86 -0
  214. package/src/agent/lock-config.test.ts +97 -0
  215. package/src/agent/lock.ts +110 -0
  216. package/src/agent/queue.test.ts +10 -0
  217. package/src/agent/queue.ts +91 -0
  218. package/src/agent/run.test.ts +382 -0
  219. package/src/agent/run.ts +295 -0
  220. package/src/agent/systemd.ts +209 -0
  221. package/src/agent/units.test.ts +223 -0
  222. package/src/agent/units.ts +163 -0
  223. package/src/azure-front-door.test.ts +119 -0
  224. package/src/azure-front-door.ts +56 -0
  225. package/src/bootstrap-token.ts +20 -0
  226. package/src/caddy.ts +112 -0
  227. package/src/cdn-copy.ts +91 -0
  228. package/src/cert-wait.test.ts +194 -0
  229. package/src/cert-wait.ts +163 -0
  230. package/src/checkpoint-secrets.test.ts +380 -0
  231. package/src/checkpoint-secrets.ts +283 -0
  232. package/src/cli-routing.test.ts +45 -0
  233. package/src/cli-routing.ts +43 -0
  234. package/src/cli.ts +1491 -0
  235. package/src/cloudsql-iam-user.ts +147 -0
  236. package/src/compose.ts +334 -0
  237. package/src/default-firewall.test.ts +411 -0
  238. package/src/default-firewall.ts +342 -0
  239. package/src/deployed-release.test.ts +231 -0
  240. package/src/deployed-release.ts +220 -0
  241. package/src/dns/cloudflare.ts +182 -0
  242. package/src/dns/groups.test.ts +76 -0
  243. package/src/dns/groups.ts +86 -0
  244. package/src/dns/index.ts +56 -0
  245. package/src/dns/manual.ts +130 -0
  246. package/src/dns/types.ts +27 -0
  247. package/src/gateway-credentials.test.ts +180 -0
  248. package/src/gateway-credentials.ts +144 -0
  249. package/src/gcloud-components.test.ts +76 -0
  250. package/src/gcloud-components.ts +85 -0
  251. package/src/gcloud-retry.ts +41 -0
  252. package/src/gcloud-runner.test.ts +61 -0
  253. package/src/gcloud.test.ts +99 -0
  254. package/src/gcloud.ts +322 -0
  255. package/src/gcp-names.ts +135 -0
  256. package/src/index.test.ts +332 -0
  257. package/src/index.ts +114 -0
  258. package/src/infra-contract.ts +32 -0
  259. package/src/install-job-runner.ts +166 -0
  260. package/src/install-job.test.ts +566 -0
  261. package/src/install-job.ts +542 -0
  262. package/src/install-state.test.ts +239 -0
  263. package/src/install-state.ts +583 -0
  264. package/src/internal-token.ts +35 -0
  265. package/src/lifecycle.ts +900 -0
  266. package/src/mcp-iap-stacks.test.ts +58 -0
  267. package/src/mcp-iap.test.ts +149 -0
  268. package/src/mcp-iap.ts +131 -0
  269. package/src/media-volume.test.ts +296 -0
  270. package/src/migration-runner.ts +365 -0
  271. package/src/operator-access-stacks.test.ts +55 -0
  272. package/src/operator-access.test.ts +332 -0
  273. package/src/operator-access.ts +554 -0
  274. package/src/ops-backend.test.ts +320 -0
  275. package/src/ops-backend.ts +414 -0
  276. package/src/provisioner-identity.test.ts +482 -0
  277. package/src/provisioner-identity.ts +407 -0
  278. package/src/redirects-emit.ts +175 -0
  279. package/src/region-data.ts +428 -0
  280. package/src/regions.test.ts +277 -0
  281. package/src/regions.ts +370 -0
  282. package/src/release-contract.ts +57 -0
  283. package/src/release-manifest.test.ts +293 -0
  284. package/src/release-manifest.ts +430 -0
  285. package/src/run-mode.ts +40 -0
  286. package/src/runtime-secrets.test.ts +627 -0
  287. package/src/runtime-secrets.ts +754 -0
  288. package/src/self-command.test.ts +14 -0
  289. package/src/self-command.ts +21 -0
  290. package/src/self-hosted-compose.test.ts +177 -0
  291. package/src/self-hosted-release.ts +240 -0
  292. package/src/self-hosted-roles.integration.test.ts +161 -0
  293. package/src/self-hosted-roles.test.ts +124 -0
  294. package/src/self-hosted-roles.ts +203 -0
  295. package/src/self-hosted-upgrade.test.ts +394 -0
  296. package/src/self-hosted-upgrade.ts +548 -0
  297. package/src/stack-contract-parity.test.ts +199 -0
  298. package/src/stack-contract.test.ts +391 -0
  299. package/src/stack-contract.ts +888 -0
  300. package/src/stack-converge-secrets.test.ts +421 -0
  301. package/src/stack-converge.test.ts +833 -0
  302. package/src/stack-converge.ts +909 -0
  303. package/src/staging-admin.test.ts +246 -0
  304. package/src/staging-admin.ts +324 -0
  305. package/src/state-attach.test.ts +211 -0
  306. package/src/state-attach.ts +132 -0
  307. package/src/state-backend.test.ts +145 -0
  308. package/src/state-backend.ts +262 -0
  309. package/src/state-command.ts +293 -0
  310. package/src/state-migrate.test.ts +479 -0
  311. package/src/state-migrate.ts +434 -0
  312. package/src/state-pulumi.ts +289 -0
  313. package/src/state-store.test.ts +212 -0
  314. package/src/state-store.ts +243 -0
  315. package/src/upgrade/app-roll.ts +486 -0
  316. package/src/upgrade/backup.ts +109 -0
  317. package/src/upgrade/converge.ts +437 -0
  318. package/src/upgrade/gcp-lookup.ts +167 -0
  319. package/src/upgrade/health.test.ts +279 -0
  320. package/src/upgrade/health.ts +305 -0
  321. package/src/upgrade/release-images.test.ts +119 -0
  322. package/src/upgrade/release-images.ts +391 -0
  323. package/src/upgrade/reporter.ts +46 -0
  324. package/src/upgrade/upgrade.test.ts +626 -0
  325. package/src/upgrade/upgrade.ts +249 -0
  326. package/src/upgrader-assess.test.ts +265 -0
  327. package/src/upgrader-assess.ts +233 -0
  328. package/src/upgrader-gcp.ts +123 -0
  329. package/src/upgrader-job.test.ts +408 -0
  330. package/src/upgrader-job.ts +499 -0
  331. package/src/upgrader-reporter.test.ts +46 -0
  332. package/src/upgrader-reporter.ts +105 -0
  333. package/src/upgrader-setup.test.ts +169 -0
  334. package/src/upgrader-setup.ts +355 -0
  335. package/src/wizard.ts +425 -0
  336. package/src/wizards/gcp-cost.ts +188 -0
  337. package/src/wizards/gcp-firebase.ts +72 -0
  338. package/src/wizards/gcp-install-job.ts +506 -0
  339. package/src/wizards/gcp-prompts.ts +26 -0
  340. package/src/wizards/gcp-pulumi.test.ts +246 -0
  341. package/src/wizards/gcp-pulumi.ts +516 -0
  342. package/src/wizards/gcp.ts +1935 -0
@@ -0,0 +1,119 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * The public site on every provider is object storage behind the provider's
5
+ * CDN, with no edge compute on the request path (CLAUDE.md §11.B Tier 1).
6
+ * Epic #676 removed the per-provider A/B edge routers; these checks keep them
7
+ * from coming back and pin the Azure primary domain to its Front Door
8
+ * endpoint. Stack checks are pure-string, like the other stack tests.
9
+ */
10
+
11
+ import { describe, expect, it } from "bun:test";
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { resolve } from "node:path";
14
+ import { azureFrontDoorDnsRecords } from "./azure-front-door.js";
15
+
16
+ const stackDir = (provider: string) => resolve(import.meta.dir, `../stacks/${provider}`);
17
+ const stack = (provider: string) => readFileSync(`${stackDir(provider)}/index.ts`, "utf8");
18
+
19
+ describe("azureFrontDoorDnsRecords", () => {
20
+ const records = azureFrontDoorDnsRecords({
21
+ domain: "example.com",
22
+ endpointHostName: "caeloproduction-site-abc123.z01.azurefd.net",
23
+ validationToken: "tok-123",
24
+ });
25
+
26
+ it("points the primary domain at the Front Door endpoint", () => {
27
+ expect(records).toContainEqual(
28
+ expect.objectContaining({
29
+ hostname: "example.com",
30
+ type: "CNAME",
31
+ value: "caeloproduction-site-abc123.z01.azurefd.net",
32
+ }),
33
+ );
34
+ });
35
+
36
+ it("publishes the managed-certificate validation token", () => {
37
+ expect(records).toContainEqual(
38
+ expect.objectContaining({ hostname: "_dnsauth.example.com", type: "TXT", value: "tok-123" }),
39
+ );
40
+ });
41
+
42
+ it("refuses an endpoint without a hostname instead of emitting a dangling record", () => {
43
+ expect(() =>
44
+ azureFrontDoorDnsRecords({
45
+ domain: "example.com",
46
+ endpointHostName: "",
47
+ validationToken: "t",
48
+ }),
49
+ ).toThrow(/no hostName/);
50
+ });
51
+
52
+ it("refuses a custom domain without a validation token", () => {
53
+ expect(() =>
54
+ azureFrontDoorDnsRecords({
55
+ domain: "example.com",
56
+ endpointHostName: "x.azurefd.net",
57
+ validationToken: "",
58
+ }),
59
+ ).toThrow(/no validation token/);
60
+ });
61
+ });
62
+
63
+ describe("azure stack — static site behind Front Door", () => {
64
+ const src = stack("azure");
65
+
66
+ it("builds its DNS records from the Front Door endpoint and custom domain", () => {
67
+ expect(src).toContain("new azure.cdn.AFDEndpoint(");
68
+ expect(src).toContain("new azure.cdn.AFDCustomDomain(");
69
+ expect(src).toMatch(
70
+ /pulumi\s*\.all\(\[frontDoorEndpoint\.hostName, siteDomain\.validationProperties\]\)[\s\S]*?azureFrontDoorDnsRecords\(/,
71
+ );
72
+ });
73
+
74
+ it("routes the custom domain to the static-website origin", () => {
75
+ expect(src).toContain("new azure.storage.StorageAccountStaticWebsite(");
76
+ expect(src).toMatch(
77
+ /new azure\.cdn\.Route\([\s\S]*?customDomains: \[\{ id: siteDomain\.id \}\]/,
78
+ );
79
+ });
80
+
81
+ it("keeps owning the published $web container and edge-caches the route", () => {
82
+ expect(src).toMatch(/new azure\.storage\.BlobContainer\([\s\S]*?containerName: "\$web"/);
83
+ expect(src).toMatch(/new azure\.cdn\.Route\([\s\S]*?cacheConfiguration: \{/);
84
+ });
85
+
86
+ it("exposes no Container App to the public internet", () => {
87
+ expect(src).toContain("external: false");
88
+ expect(src).not.toMatch(/external:\s*(true|args\.)/);
89
+ });
90
+ });
91
+
92
+ describe("aws stack — edge-log sink", () => {
93
+ const src = stack("aws");
94
+
95
+ it("delivers CloudFront access logs to the log group it reports as the sink", () => {
96
+ expect(src).toMatch(
97
+ /new aws\.cloudwatch\.LogDeliverySource\([\s\S]*?logType: "ACCESS_LOGS",\s*resourceArn: distribution\.arn/,
98
+ );
99
+ expect(src).toContain("destinationResourceArn: edgeLogGroup.arn");
100
+ expect(src).toMatch(/cloudwatch:\/\/us-east-1\/\$\{edgeLogGroup\.name\}/);
101
+ });
102
+ });
103
+
104
+ describe.each(["aws", "azure", "gcp", "gcp-firebase", "self-hosted"])(
105
+ "%s stack — no edge router",
106
+ (provider) => {
107
+ const src = stack(provider);
108
+
109
+ it("runs no edge compute for the public site", () => {
110
+ expect(src).not.toContain("edge-router");
111
+ expect(src).not.toContain("lambdaFunctionAssociations");
112
+ expect(src).not.toContain("ab-routing.json");
113
+ });
114
+
115
+ it("ships no edge handler source", () => {
116
+ expect(existsSync(`${stackDir(provider)}/edge-handler.ts`)).toBe(false);
117
+ });
118
+ },
119
+ );
@@ -0,0 +1,56 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * DNS records the Azure stack asks the operator's registrar to hold.
5
+ *
6
+ * The public site is served by Azure Front Door (CLAUDE.md §11.B Tier 1:
7
+ * object storage + edge CDN + managed TLS, no compute on the request path),
8
+ * so the primary domain points at the Front Door endpoint, and Front Door's
9
+ * managed certificate needs the `_dnsauth` TXT record to prove domain
10
+ * ownership before it issues.
11
+ */
12
+
13
+ import type { DnsRecord } from "./adapter.js";
14
+
15
+ /** Inputs the Azure stack resolves from its Front Door resources. */
16
+ export interface AzureFrontDoorDnsInputs {
17
+ /** The site's primary domain (`caelo-azure:domain`). */
18
+ readonly domain: string;
19
+ /** `AFDEndpoint.hostName`, e.g. `caelo-prod-abc123.z01.azurefd.net`. */
20
+ readonly endpointHostName: string;
21
+ /** `AFDCustomDomain.validationProperties.validationToken`. */
22
+ readonly validationToken: string;
23
+ }
24
+
25
+ /**
26
+ * The records for the primary domain: a CNAME to the Front Door endpoint and
27
+ * the TXT record that validates the custom domain for its managed certificate.
28
+ *
29
+ * @throws when the endpoint hostname or validation token is empty — a record
30
+ * pointing at nothing would read fine in the DNS guidance page and then fail
31
+ * at the registrar (no fallbacks pre-1.0, CLAUDE.md §2).
32
+ */
33
+ export function azureFrontDoorDnsRecords(input: AzureFrontDoorDnsInputs): DnsRecord[] {
34
+ if (input.endpointHostName.length === 0) {
35
+ throw new Error("azure-front-door: the Front Door endpoint has no hostName yet");
36
+ }
37
+ if (input.validationToken.length === 0) {
38
+ throw new Error(
39
+ `azure-front-door: the custom domain ${input.domain} has no validation token yet`,
40
+ );
41
+ }
42
+ return [
43
+ {
44
+ hostname: input.domain,
45
+ type: "CNAME",
46
+ value: input.endpointHostName,
47
+ purpose: "Primary domain → Azure Front Door endpoint (static site)",
48
+ },
49
+ {
50
+ hostname: `_dnsauth.${input.domain}`,
51
+ type: "TXT",
52
+ value: input.validationToken,
53
+ purpose: "Validates the primary domain for Front Door's managed TLS certificate",
54
+ },
55
+ ];
56
+ }
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * P14 — owner bootstrap token. cms-provision generates one at first
5
+ * `up`; the operator visits /setup?token=<…> to create the first
6
+ * Owner. Single-use, 24h TTL.
7
+ */
8
+
9
+ export interface BootstrapToken {
10
+ readonly token: string;
11
+ readonly expiresAt: string;
12
+ }
13
+
14
+ export function generateBootstrapToken(): BootstrapToken {
15
+ const bytes = new Uint8Array(32);
16
+ crypto.getRandomValues(bytes);
17
+ const token = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
18
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
19
+ return { token, expiresAt };
20
+ }
package/src/caddy.ts ADDED
@@ -0,0 +1,112 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * P14 — Caddyfile generator.
5
+ *
6
+ * Reads the `domains` table + the deploy targets and emits a
7
+ * deterministic Caddyfile string. The CLI's `regenerate-caddy`
8
+ * sub-command writes this to `/etc/caddy/Caddyfile` and runs
9
+ * `caddy reload`.
10
+ *
11
+ * Per-vhost shape:
12
+ * <hostname> {
13
+ * # admin → reverse_proxy localhost:5173
14
+ * # public → root + try_files (static), with /api/* → gateway
15
+ * tls <ownerEmail>
16
+ * }
17
+ *
18
+ * Staging vhosts force `X-Robots-Tag: noindex`.
19
+ *
20
+ * Public vhosts mark content-hashed build outputs (fonts, plugin
21
+ * bundles — `CONTENT_HASHED_PATH_PATTERN` in @caelo-cms/shared)
22
+ * `immutable` for a year. Everything else keeps Caddy's file_server
23
+ * default (no Cache-Control; ETag + Last-Modified for revalidation),
24
+ * so pages and slug-addressed media are never pinned in caches. The
25
+ * matcher also requires the file to exist (`file`), so a 404 for a
26
+ * hashed URL (an old page requesting an asset the new build dropped)
27
+ * is never cached for a year.
28
+ *
29
+ * `.ico` files (theme favicons) are served as `image/x-icon`, the one
30
+ * icon MIME the media library stores and `<link rel="icon" type>`
31
+ * declares. file_server only fills Content-Type when no earlier handler
32
+ * set it, so the `header` line wins over the host's mime.types name.
33
+ */
34
+
35
+ import { CONTENT_HASHED_PATH_PATTERN, IMMUTABLE_CACHE_CONTROL } from "@caelo-cms/shared";
36
+
37
+ export interface CaddyDomainSpec {
38
+ readonly hostname: string;
39
+ readonly kind: "admin" | "public";
40
+ readonly env: "production" | "staging";
41
+ }
42
+
43
+ export interface CaddyfileSpec {
44
+ readonly ownerEmail: string;
45
+ readonly publicSiteRoot: string; // absolute path on disk
46
+ readonly stagingSiteRoot: string;
47
+ readonly adminPort: number;
48
+ readonly gatewayPort: number;
49
+ readonly domains: ReadonlyArray<CaddyDomainSpec>;
50
+ }
51
+
52
+ export function generateCaddyfile(spec: CaddyfileSpec): string {
53
+ const blocks: string[] = [];
54
+
55
+ // Global options.
56
+ blocks.push(`{
57
+ email ${spec.ownerEmail}
58
+ }
59
+ `);
60
+
61
+ for (const d of spec.domains) {
62
+ blocks.push(vhost(d, spec));
63
+ }
64
+
65
+ // Localhost dev fallback when no domains are configured (so a fresh
66
+ // `cms-provision` produces a working Caddyfile even pre-DNS).
67
+ if (spec.domains.length === 0) {
68
+ blocks.push(`# No domains configured yet — cms-provision regenerate-caddy
69
+ # will overwrite this file when you add one at /settings/domains.
70
+ :8081 {
71
+ reverse_proxy localhost:${spec.adminPort}
72
+ }
73
+ `);
74
+ }
75
+
76
+ return blocks.join("\n");
77
+ }
78
+
79
+ function vhost(d: CaddyDomainSpec, spec: CaddyfileSpec): string {
80
+ const noindex = d.env === "staging" ? `\n header X-Robots-Tag "noindex"` : "";
81
+ if (d.kind === "admin") {
82
+ return `${d.hostname} {${noindex}
83
+ reverse_proxy localhost:${spec.adminPort}
84
+ }
85
+ `;
86
+ }
87
+ // public — API routes go to the gateway,
88
+ // the rest serves static files. Locale variants serve from a
89
+ const root = d.env === "staging" ? spec.stagingSiteRoot : spec.publicSiteRoot;
90
+ return `${d.hostname} {${noindex}
91
+ root * ${root}
92
+ @content_hashed {
93
+ path_regexp content_hashed ${CONTENT_HASHED_PATH_PATTERN}
94
+ file
95
+ }
96
+ header @content_hashed Cache-Control "${IMMUTABLE_CACHE_CONTROL}"
97
+ @ico path *.ico
98
+ header @ico Content-Type "image/x-icon"
99
+ handle /api/* {
100
+ reverse_proxy localhost:${spec.gatewayPort}
101
+ }
102
+ handle /admin/* {
103
+ reverse_proxy localhost:${spec.adminPort}
104
+ }
105
+ handle {
106
+ file_server {
107
+ try_files {path} {path}/index.html /index.html
108
+ }
109
+ }
110
+ }
111
+ `;
112
+ }
@@ -0,0 +1,91 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * P15 — per-provider CDN-copy adapter contract.
5
+ *
6
+ * P7 ships the `media_assets.usage_count` threshold + the `media.set_cdn`
7
+ * op that flips a per-asset boolean. P15 wires the actual edge-pin
8
+ * operation per provider:
9
+ * - AWS → CloudFront cache invalidation + warmer request.
10
+ * - GCP → set Cache-Control metadata on the GCS object so Cloud CDN
11
+ * respects the long TTL.
12
+ * - Azure → Front Door purge + warm endpoint.
13
+ *
14
+ * The redeploy-orchestrator's `cdnCopyTick()` (currently a no-op for
15
+ * cloud installs) drains the per-asset CDN-pin queue using the adapter
16
+ * registered for the active provider.
17
+ *
18
+ * Each provider's stack publishes a `CdnCopyAdapter` instance via its
19
+ * Pulumi outputs (the actual implementation is dynamically imported by
20
+ * the orchestrator at runtime so `@caelo-cms/provisioning` doesn't pull in
21
+ * `@aws-sdk/...`/`@google-cloud/...`/`@azure/...` as static deps). The
22
+ * interface defined here is the only thing both sides agree on.
23
+ */
24
+
25
+ export interface CdnCopyAdapter {
26
+ /**
27
+ * Mark the asset at `assetKey` (object key in primary storage) as
28
+ * CDN-cached. Returns the public CDN URL — admin uses this as the
29
+ * canonical URL for the asset thereafter.
30
+ */
31
+ pin(assetKey: string): Promise<string>;
32
+ /** Reverse — drop from CDN edge cache. */
33
+ unpin(assetKey: string): Promise<void>;
34
+ }
35
+
36
+ /**
37
+ * No-op adapter for self-hosted installs (Caddy serves directly from
38
+ * disk; no separate CDN tier). Returns the input asset key as a relative
39
+ * URL so callers don't have to special-case the no-CDN case.
40
+ */
41
+ export const selfHostedCdnCopy: CdnCopyAdapter = {
42
+ async pin(assetKey) {
43
+ return `/media/${assetKey}`;
44
+ },
45
+ async unpin() {
46
+ // no-op
47
+ },
48
+ };
49
+
50
+ /**
51
+ * Resolves the right adapter per `process.env.CAELO_PROVIDER`. Lazy
52
+ * dynamic-imports the per-provider implementation so a self-hosted
53
+ * install never loads the cloud SDKs.
54
+ *
55
+ * Returns `selfHostedCdnCopy` ONLY for `provider==='self-hosted'` or an
56
+ * unset env (treated as self-hosted). For `gcp`/`aws`/`azure`, the
57
+ * matching `cdn-copy-<gcs|aws|azure>.ts` file MUST exist in this
58
+ * directory — those land with the per-provider PRs. Per CLAUDE.md §2
59
+ * "no fallbacks pre-1.0" we deliberately do NOT silently degrade to
60
+ * the self-hosted no-op when the file is missing: a missing module is
61
+ * a deploy bug + the orchestrator tick should crash loudly so the
62
+ * operator notices instead of silently dropping CDN-pin requests.
63
+ */
64
+ export async function loadCdnCopyAdapter(provider?: string): Promise<CdnCopyAdapter> {
65
+ switch (provider) {
66
+ case "gcp":
67
+ // biome-ignore lint/suspicious/noExplicitAny: opt-in dynamic import
68
+ return ((await import("./cdn-copy-gcs.js" as string)) as any).gcsCloudCdnPin;
69
+ case "gcp-firebase":
70
+ // v0.3.0 — Firebase Hosting manages CDN caching natively (each
71
+ // deploy creates an immutable version + the CDN's freshness is
72
+ // dictated by Firebase Hosting headers). There's no per-asset
73
+ // "pin to edge" API; this adapter is effectively a no-op that
74
+ // returns the same shape as self-hosted.
75
+ return selfHostedCdnCopy;
76
+ case "aws":
77
+ // biome-ignore lint/suspicious/noExplicitAny: opt-in dynamic import
78
+ return ((await import("./cdn-copy-aws.js" as string)) as any).s3CloudfrontPrewarm;
79
+ case "azure":
80
+ // biome-ignore lint/suspicious/noExplicitAny: opt-in dynamic import
81
+ return ((await import("./cdn-copy-azure.js" as string)) as any).azureBlobCdnPrefetch;
82
+ case undefined:
83
+ case "":
84
+ case "self-hosted":
85
+ return selfHostedCdnCopy;
86
+ default:
87
+ throw new Error(
88
+ `loadCdnCopyAdapter: unknown CAELO_PROVIDER='${provider}'. Expected one of: self-hosted | gcp | gcp-firebase | aws | azure.`,
89
+ );
90
+ }
91
+ }
@@ -0,0 +1,194 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ import { describe, expect, it } from "bun:test";
4
+ import { type CertProbe, domainMappingProbe, lbCertProbe, waitForCert } from "./cert-wait.js";
5
+ import type { GcloudResult } from "./gcloud.js";
6
+
7
+ const ok = (stdout = ""): GcloudResult => ({ ok: true, stdout, stderr: "", exitCode: 0 });
8
+ const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
9
+
10
+ /** A fake clock the injected sleep advances. */
11
+ function clock() {
12
+ let t = 0;
13
+ return {
14
+ now: () => t,
15
+ sleep: async (ms: number) => {
16
+ t += ms;
17
+ },
18
+ elapsed: () => t,
19
+ };
20
+ }
21
+
22
+ function probes(...seq: CertProbe[]) {
23
+ let i = 0;
24
+ return async () => seq[Math.min(i++, seq.length - 1)] as CertProbe;
25
+ }
26
+
27
+ describe("waitForCert", () => {
28
+ it("returns once the probe reports ready, reporting each new detail once", async () => {
29
+ const c = clock();
30
+ const updates: string[] = [];
31
+ const r = await waitForCert({
32
+ probe: probes(
33
+ { state: "pending", detail: "waiting for DNS" },
34
+ { state: "pending", detail: "waiting for DNS" },
35
+ { state: "pending", detail: "issuing" },
36
+ { state: "ready" },
37
+ ),
38
+ timeoutMs: 60_000,
39
+ intervalMs: 10_000,
40
+ onUpdate: (d) => updates.push(d),
41
+ ...c,
42
+ });
43
+ expect(r).toEqual({ ok: true });
44
+ expect(updates).toEqual(["waiting for DNS", "issuing"]);
45
+ expect(c.elapsed()).toBe(30_000);
46
+ });
47
+
48
+ it("times out with the last detail instead of waiting forever", async () => {
49
+ const c = clock();
50
+ const r = await waitForCert({
51
+ probe: probes({ state: "pending", detail: "CertificateProvisioned: DNS not set" }),
52
+ timeoutMs: 25_000,
53
+ intervalMs: 10_000,
54
+ ...c,
55
+ });
56
+ expect(r).toEqual({
57
+ ok: false,
58
+ reason: "timeout",
59
+ lastDetail: "CertificateProvisioned: DNS not set",
60
+ });
61
+ // Probed at 0, 10s, 20s, 25s (the deadline) — never slept past it.
62
+ expect(c.elapsed()).toBe(25_000);
63
+ });
64
+
65
+ it("stops at once when gcloud fails, since waiting cannot fix that", async () => {
66
+ const c = clock();
67
+ const r = await waitForCert({
68
+ probe: probes({ state: "error", error: "PERMISSION_DENIED" }),
69
+ timeoutMs: 60_000,
70
+ intervalMs: 10_000,
71
+ ...c,
72
+ });
73
+ expect(r).toEqual({ ok: false, reason: "error", error: "PERMISSION_DENIED" });
74
+ expect(c.elapsed()).toBe(0);
75
+ });
76
+ });
77
+
78
+ const target = { hostname: "admin.example.com", region: "europe-west1", projectId: "p" };
79
+
80
+ describe("domainMappingProbe", () => {
81
+ it("is ready when the mapping's Ready condition is True", async () => {
82
+ const calls: string[][] = [];
83
+ const run = async (args: string[]) => {
84
+ calls.push(args);
85
+ return ok(JSON.stringify({ status: { conditions: [{ type: "Ready", status: "True" }] } }));
86
+ };
87
+ expect(await domainMappingProbe(run, target)).toEqual({ state: "ready" });
88
+ expect(calls[0]).toEqual([
89
+ "beta",
90
+ "run",
91
+ "domain-mappings",
92
+ "describe",
93
+ "--domain=admin.example.com",
94
+ "--region=europe-west1",
95
+ "--project=p",
96
+ "--format=json",
97
+ ]);
98
+ });
99
+
100
+ it("reports the certificate condition's message while pending", async () => {
101
+ const run = async () =>
102
+ ok(
103
+ JSON.stringify({
104
+ status: {
105
+ conditions: [
106
+ { type: "Ready", status: "Unknown", message: "not ready" },
107
+ {
108
+ type: "CertificateProvisioned",
109
+ status: "Unknown",
110
+ message: "Certificate issuance pending. Add the DNS record.",
111
+ },
112
+ { type: "DomainRoutable", status: "True" },
113
+ ],
114
+ },
115
+ }),
116
+ );
117
+ expect(await domainMappingProbe(run, target)).toEqual({
118
+ state: "pending",
119
+ detail: "CertificateProvisioned: Certificate issuance pending. Add the DNS record.",
120
+ });
121
+ });
122
+
123
+ it("is pending (not ready) before Google reports any condition", async () => {
124
+ const run = async () => ok(JSON.stringify({ status: {} }));
125
+ expect((await domainMappingProbe(run, target)).state).toBe("pending");
126
+ });
127
+
128
+ it("surfaces a gcloud failure as an error", async () => {
129
+ const run = async () => fail("ERROR: (gcloud) You do not currently have an active account");
130
+ const r = await domainMappingProbe(run, target);
131
+ expect(r.state).toBe("error");
132
+ });
133
+ });
134
+
135
+ describe("lbCertProbe", () => {
136
+ const list = (certs: unknown[]) => async () => ok(JSON.stringify(certs));
137
+ const lb = { projectId: "p", hostname: "admin.example.com" };
138
+
139
+ it("is ready when the admin's own certificate is ACTIVE although the main domain's is not", async () => {
140
+ const r = await lbCertProbe(
141
+ list([
142
+ {
143
+ name: "combined",
144
+ managed: {
145
+ status: "PROVISIONING",
146
+ domainStatus: { "example.com": "FAILED_NOT_VISIBLE", "admin.example.com": "ACTIVE" },
147
+ },
148
+ },
149
+ {
150
+ name: "admin-only",
151
+ managed: { status: "ACTIVE", domainStatus: { "admin.example.com": "ACTIVE" } },
152
+ },
153
+ ]),
154
+ lb,
155
+ );
156
+ expect(r).toEqual({ state: "ready" });
157
+ });
158
+
159
+ it("does not count a domain that is ACTIVE on a certificate that is not served yet", async () => {
160
+ const r = await lbCertProbe(
161
+ list([
162
+ {
163
+ name: "combined",
164
+ managed: {
165
+ status: "PROVISIONING",
166
+ domainStatus: { "example.com": "FAILED_NOT_VISIBLE", "admin.example.com": "ACTIVE" },
167
+ },
168
+ },
169
+ ]),
170
+ lb,
171
+ );
172
+ expect(r).toEqual({ state: "pending", detail: "combined: admin.example.com ACTIVE" });
173
+ });
174
+
175
+ it("is ready for installs that only have the combined certificate, once it is ACTIVE", async () => {
176
+ const r = await lbCertProbe(
177
+ list([
178
+ {
179
+ name: "combined",
180
+ managed: {
181
+ status: "ACTIVE",
182
+ domainStatus: { "example.com": "ACTIVE", "admin.example.com": "ACTIVE" },
183
+ },
184
+ },
185
+ ]),
186
+ lb,
187
+ );
188
+ expect(r).toEqual({ state: "ready" });
189
+ });
190
+
191
+ it("is pending when no certificate lists the host", async () => {
192
+ expect((await lbCertProbe(list([]), lb)).state).toBe("pending");
193
+ });
194
+ });