@caelo-cms/provisioning 0.10.49 → 0.10.50

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 (137) hide show
  1. package/caddy/Caddyfile.production +4 -0
  2. package/caddy/Caddyfile.staging +4 -0
  3. package/dist/agent-db.js +4 -0
  4. package/dist/caddy-redirects.d.ts +15 -0
  5. package/dist/caddy-redirects.d.ts.map +1 -0
  6. package/dist/caddy-redirects.js +65 -0
  7. package/dist/caddy-redirects.js.map +1 -0
  8. package/dist/caddy.d.ts +22 -2
  9. package/dist/caddy.d.ts.map +1 -1
  10. package/dist/caddy.js +66 -17
  11. package/dist/caddy.js.map +1 -1
  12. package/dist/cli-routing.d.ts.map +1 -1
  13. package/dist/cli-routing.js +7 -0
  14. package/dist/cli-routing.js.map +1 -1
  15. package/dist/cli.js +248 -254
  16. package/dist/cli.js.map +1 -1
  17. package/dist/compose.d.ts +54 -2
  18. package/dist/compose.d.ts.map +1 -1
  19. package/dist/compose.js +86 -32
  20. package/dist/compose.js.map +1 -1
  21. package/dist/gcp-names.d.ts +12 -0
  22. package/dist/gcp-names.d.ts.map +1 -1
  23. package/dist/gcp-names.js +21 -0
  24. package/dist/gcp-names.js.map +1 -1
  25. package/dist/gcp-url-map.d.ts +27 -0
  26. package/dist/gcp-url-map.d.ts.map +1 -0
  27. package/dist/gcp-url-map.js +62 -0
  28. package/dist/gcp-url-map.js.map +1 -0
  29. package/dist/index.d.ts +0 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +0 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/infra-contract.d.ts +20 -1
  34. package/dist/infra-contract.d.ts.map +1 -1
  35. package/dist/infra-contract.js +39 -1
  36. package/dist/infra-contract.js.map +1 -1
  37. package/dist/public-role-isolation.d.ts +19 -0
  38. package/dist/public-role-isolation.d.ts.map +1 -0
  39. package/dist/public-role-isolation.js +69 -0
  40. package/dist/public-role-isolation.js.map +1 -0
  41. package/dist/release-contract.d.ts.map +1 -1
  42. package/dist/release-contract.js +3 -0
  43. package/dist/release-contract.js.map +1 -1
  44. package/dist/release-manifest.d.ts.map +1 -1
  45. package/dist/release-manifest.js +2 -1
  46. package/dist/release-manifest.js.map +1 -1
  47. package/dist/self-hosted-backup.d.ts +86 -0
  48. package/dist/self-hosted-backup.d.ts.map +1 -0
  49. package/dist/self-hosted-backup.js +350 -0
  50. package/dist/self-hosted-backup.js.map +1 -0
  51. package/dist/self-hosted-release.d.ts +12 -3
  52. package/dist/self-hosted-release.d.ts.map +1 -1
  53. package/dist/self-hosted-release.js +6 -4
  54. package/dist/self-hosted-release.js.map +1 -1
  55. package/dist/self-hosted-start.d.ts +50 -0
  56. package/dist/self-hosted-start.d.ts.map +1 -0
  57. package/dist/self-hosted-start.js +59 -0
  58. package/dist/self-hosted-start.js.map +1 -0
  59. package/dist/self-hosted-upgrade.d.ts +30 -0
  60. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  61. package/dist/self-hosted-upgrade.js +29 -7
  62. package/dist/self-hosted-upgrade.js.map +1 -1
  63. package/dist/tar-entries.d.ts +14 -0
  64. package/dist/tar-entries.d.ts.map +1 -0
  65. package/dist/tar-entries.js +104 -0
  66. package/dist/tar-entries.js.map +1 -0
  67. package/dist/upgrade/converge.d.ts +2 -2
  68. package/dist/upgrade/converge.d.ts.map +1 -1
  69. package/dist/upgrade/converge.js +44 -2
  70. package/dist/upgrade/converge.js.map +1 -1
  71. package/dist/upgrader-assess.d.ts.map +1 -1
  72. package/dist/upgrader-assess.js +2 -1
  73. package/dist/upgrader-assess.js.map +1 -1
  74. package/dist/url-map-redirects-access.d.ts +15 -0
  75. package/dist/url-map-redirects-access.d.ts.map +1 -0
  76. package/dist/url-map-redirects-access.js +81 -0
  77. package/dist/url-map-redirects-access.js.map +1 -0
  78. package/dist/wizard.d.ts.map +1 -1
  79. package/dist/wizard.js +3 -1
  80. package/dist/wizard.js.map +1 -1
  81. package/dist/wizards/gcp.d.ts.map +1 -1
  82. package/dist/wizards/gcp.js +45 -0
  83. package/dist/wizards/gcp.js.map +1 -1
  84. package/package.json +2 -2
  85. package/src/admin-memory.test.ts +2 -2
  86. package/src/caddy-redirects.docker.test.ts +255 -0
  87. package/src/caddy-redirects.test.ts +115 -0
  88. package/src/caddy-redirects.ts +68 -0
  89. package/src/caddy-validate.test.ts +119 -0
  90. package/src/caddy.test.ts +108 -0
  91. package/src/caddy.ts +89 -20
  92. package/src/cli-routing.test.ts +12 -0
  93. package/src/cli-routing.ts +7 -0
  94. package/src/cli.ts +299 -287
  95. package/src/compose.ts +106 -34
  96. package/src/gcp-names.ts +23 -0
  97. package/src/gcp-url-map.test.ts +150 -0
  98. package/src/gcp-url-map.ts +81 -0
  99. package/src/index.test.ts +18 -146
  100. package/src/index.ts +0 -9
  101. package/src/infra-contract.test.ts +39 -0
  102. package/src/infra-contract.ts +42 -1
  103. package/src/public-role-isolation.test.ts +89 -0
  104. package/src/public-role-isolation.ts +83 -0
  105. package/src/release-contract.ts +3 -0
  106. package/src/release-manifest.test.ts +12 -0
  107. package/src/release-manifest.ts +2 -1
  108. package/src/self-hosted-backup.test.ts +344 -0
  109. package/src/self-hosted-backup.ts +480 -0
  110. package/src/self-hosted-compose.test.ts +88 -4
  111. package/src/self-hosted-release.ts +22 -7
  112. package/src/self-hosted-start.test.ts +68 -0
  113. package/src/self-hosted-start.ts +123 -0
  114. package/src/self-hosted-upgrade.ts +35 -8
  115. package/src/stack-contract.test.ts +2 -2
  116. package/src/tar-entries.test.ts +52 -0
  117. package/src/tar-entries.ts +116 -0
  118. package/src/upgrade/converge.ts +51 -4
  119. package/src/upgrade/upgrade.test.ts +78 -1
  120. package/src/upgrader-assess.test.ts +22 -0
  121. package/src/upgrader-assess.ts +2 -1
  122. package/src/url-map-redirects-access.test.ts +88 -0
  123. package/src/url-map-redirects-access.ts +89 -0
  124. package/src/wizard.ts +3 -1
  125. package/src/wizards/gcp.ts +48 -0
  126. package/stacks/aws/index.ts +34 -2
  127. package/stacks/azure/index.ts +13 -0
  128. package/stacks/gcp/index.ts +24 -17
  129. package/stacks/gcp-firebase/index.ts +7 -0
  130. package/stacks/self-hosted/Pulumi.yaml +2 -2
  131. package/stacks/self-hosted/README.md +3 -3
  132. package/stacks/self-hosted/index.ts +45 -22
  133. package/dist/redirects-emit.d.ts +0 -52
  134. package/dist/redirects-emit.d.ts.map +0 -1
  135. package/dist/redirects-emit.js +0 -111
  136. package/dist/redirects-emit.js.map +0 -1
  137. package/src/redirects-emit.ts +0 -175
@@ -9,7 +9,8 @@
9
9
  * written to `<caeloDir>/pending-token.json`. Captured as a Pulumi
10
10
  * output so operators can read `pulumi stack output bootstrapUrl`
11
11
  * after `pulumi up`.
12
- * 3. `caeloDockerCompose` — runs `docker compose up -d` and tears down
12
+ * 3. `caeloDockerCompose` — starts Postgres, runs the migrations, runs
13
+ * `docker compose up -d` and tears down
13
14
  * with `docker compose down -v` on destroy.
14
15
  *
15
16
  * Why thin: Caelo's self-hosted stack is fundamentally a single Compose
@@ -24,8 +25,15 @@ import { resolve } from "node:path";
24
25
  import { local } from "@pulumi/command";
25
26
  import * as pulumi from "@pulumi/pulumi";
26
27
  import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
27
- import { generateCaddyfile } from "../../dist/caddy.js";
28
- import { generateDockerCompose } from "../../dist/compose.js";
28
+ import { generateCaddyfile, selfHostedAdminHost, selfHostedSeedDomains } from "../../dist/caddy.js";
29
+ import {
30
+ COMPOSE_ADMIN_UPSTREAM,
31
+ COMPOSE_GATEWAY_UPSTREAM,
32
+ composeProjectName,
33
+ generateDockerCompose,
34
+ SITE_OUTPUT_ROOT,
35
+ } from "../../dist/compose.js";
36
+ import { selfHostedBootstrapScript } from "../../dist/self-hosted-roles.js";
29
37
 
30
38
  const cfg = new pulumi.Config();
31
39
  const domain = cfg.require("domain");
@@ -44,6 +52,8 @@ const gatewayImage = cfg.require("gatewayImage");
44
52
 
45
53
  const composePath = resolve(caeloDir, "docker-compose.yml");
46
54
  const caddyPath = resolve(caeloDir, "Caddyfile");
55
+ // What Postgres runs on its first start: the roles + databases (compose.ts mounts it).
56
+ const bootstrapPath = resolve(caeloDir, "bootstrap.sh");
47
57
  // The admin container mounts only `<caeloDir>/bootstrap/` (compose.ts).
48
58
  const tokenPath = resolve(caeloDir, "bootstrap", "pending-token.json");
49
59
 
@@ -56,44 +66,45 @@ const postgresPassword = pulumi.secret(randomHex(32));
56
66
  const adminRolePassword = pulumi.secret(randomHex(32));
57
67
  const publicRolePassword = pulumi.secret(randomHex(32));
58
68
  const gatewayRolePassword = pulumi.secret(randomHex(32));
59
- const minioRootUser = "caelo";
60
- const minioRootPassword = pulumi.secret(randomHex(32));
69
+ // P18 — the KEK that encrypts stored provider keys.
70
+ const caeloSecretKek = pulumi.secret(randomHex(32));
61
71
 
62
72
  const composeYaml = pulumi
63
73
  .all([
64
74
  postgresPassword,
65
- minioRootPassword,
66
75
  adminRolePassword,
67
76
  publicRolePassword,
68
77
  gatewayRolePassword,
78
+ caeloSecretKek,
69
79
  ])
70
- .apply(([pgPw, minioPw, adminPw, publicPw, gatewayPw]) =>
80
+ .apply(([pgPw, adminPw, publicPw, gatewayPw, kek]) =>
71
81
  generateDockerCompose({
72
82
  domain,
73
83
  postgresPassword: pgPw,
74
84
  rolePasswords: { admin: adminPw, public: publicPw, gateway: gatewayPw },
75
- minioRootUser,
76
- minioRootPassword: minioPw,
85
+ caeloSecretKek: kek,
77
86
  anthropicApiKey: process.env.ANTHROPIC_API_KEY,
78
87
  resendApiKey: process.env.RESEND_API_KEY,
79
88
  diskSize: "20Gi",
80
89
  ...(adminMemory ? { adminMemory } : {}),
81
90
  images: { mode: "release", admin: adminImage, gateway: gatewayImage },
91
+ ports: { http: 80, https: 443 },
92
+ project: composeProjectName(domain),
82
93
  }),
83
94
  );
84
95
 
96
+ // Same generator inputs as `cms-provision init` (#707): the admin on
97
+ // admin.<domain>, Caddy reaching the services by their Compose names.
85
98
  const caddyConf = generateCaddyfile({
86
99
  ownerEmail,
87
- publicSiteRoot: "/srv/caelo/output/production/current",
88
- stagingSiteRoot: "/srv/caelo/output/staging/current",
89
- adminPort: 5173,
90
- gatewayPort: 8090,
91
- domains: [
92
- { hostname: domain, kind: "admin", env: "production" as const },
93
- { hostname: domain, kind: "public", env: "production" as const },
94
- { hostname: stagingHost, kind: "public", env: "staging" as const },
95
- ],
100
+ tls: "acme",
101
+ publicSiteRoot: `${SITE_OUTPUT_ROOT}/output/production/current`,
102
+ stagingSiteRoot: `${SITE_OUTPUT_ROOT}/output/staging/current`,
103
+ adminUpstream: COMPOSE_ADMIN_UPSTREAM,
104
+ gatewayUpstream: COMPOSE_GATEWAY_UPSTREAM,
105
+ domains: selfHostedSeedDomains(domain, stagingHost),
96
106
  });
107
+ const bootstrapScript = selfHostedBootstrapScript({ agent: false });
97
108
 
98
109
  // Mint the owner bootstrap token at preview time. Captured below as a
99
110
  // Pulumi output so the URL surfaces on `pulumi up`.
@@ -111,8 +122,12 @@ EOF
111
122
  cat > ${tokenPath} <<'EOF'
112
123
  ${JSON.stringify(tokenInfo, null, 2)}
113
124
  EOF
125
+ cat > ${bootstrapPath} <<'EOF'
126
+ ${bootstrapScript}
127
+ EOF
128
+ chmod 755 ${bootstrapPath}
114
129
  `,
115
- delete: pulumi.interpolate`rm -f ${composePath} ${caddyPath} ${tokenPath}`,
130
+ delete: pulumi.interpolate`rm -f ${composePath} ${caddyPath} ${tokenPath} ${bootstrapPath}`,
116
131
  triggers: [composeYaml, caddyConf],
117
132
  });
118
133
 
@@ -121,7 +136,15 @@ EOF
121
136
  const composeUp = new local.Command(
122
137
  "caeloDockerCompose",
123
138
  {
124
- create: pulumi.interpolate`docker compose -f ${composePath} up -d`,
139
+ // Same order as `cms-provision init` (self-hosted-start.ts): Postgres
140
+ // (its first start runs bootstrap.sh), the migrations with the
141
+ // install's admin image, then everything else.
142
+ create: pulumi.interpolate`set -e
143
+ docker compose -f ${composePath} up -d --wait postgres
144
+ for target in admin public; do
145
+ docker compose -f ${composePath} run --rm --no-deps -T caelo-admin bun --bun /app/packages/migrations/src/migrate.ts "$target"
146
+ done
147
+ docker compose -f ${composePath} up -d --remove-orphans`,
125
148
  delete: pulumi.interpolate`docker compose -f ${composePath} down -v`,
126
149
  triggers: [composeYaml],
127
150
  },
@@ -130,9 +153,9 @@ const composeUp = new local.Command(
130
153
 
131
154
  export const composeFilePath = composePath;
132
155
  export const caddyFilePath = caddyPath;
133
- export const bootstrapUrl = pulumi.interpolate`https://${domain}/setup?token=${tokenInfo.token}`;
156
+ export const bootstrapUrl = pulumi.interpolate`https://${selfHostedAdminHost(domain)}/setup?token=${tokenInfo.token}`;
134
157
  export const bootstrapTokenExpiresAt = tokenInfo.expiresAt;
135
- export { minioRootPassword, postgresPassword };
158
+ export { postgresPassword };
136
159
 
137
160
  // Re-export so the destroy summary shows operators what they'll lose.
138
161
  export const composeRunOutput = composeUp.stdout;
@@ -1,52 +0,0 @@
1
- export type RedirectStatusCode = 301 | 302 | 307 | 308;
2
- export interface RedirectRow {
3
- readonly fromPath: string;
4
- readonly toPath: string;
5
- readonly statusCode: RedirectStatusCode;
6
- }
7
- /**
8
- * Cloudflare Pages `_redirects` format (also consumed by Netlify, Vercel,
9
- * Render, etc.). One redirect per line: `<from> <to> <status>`. Wildcards
10
- * use `:splat`; we only support exact-path redirects in v1 (matches what
11
- * Caelo's redirects table allows).
12
- */
13
- export declare function emitRedirectsCloudflare(rows: ReadonlyArray<RedirectRow>): string;
14
- /**
15
- * CloudFront via Lambda@Edge. Returns a JSON config the L@E function
16
- * reads at startup + a tiny Lambda source that consumes it. The L@E
17
- * function is deployed to `us-east-1` (CloudFront constraint); the
18
- * config blob is bundled into the Lambda deployment artifact.
19
- *
20
- * Returning both halves lets the AWS stack ship a single asset.
21
- */
22
- export interface CloudFrontRedirectArtifact {
23
- readonly jsonConfig: string;
24
- readonly lambdaSource: string;
25
- }
26
- export declare function emitRedirectsCloudFront(rows: ReadonlyArray<RedirectRow>): CloudFrontRedirectArtifact;
27
- /**
28
- * Azure Front Door rules engine. Returns a Pulumi-compatible
29
- * RuleEngineRule[]-shaped array (untyped here so callers don't need to
30
- * import @pulumi/azure-native). Each rule fires on a path-equals match
31
- * + executes a 301/302/etc. response.
32
- */
33
- export interface FrontDoorRule {
34
- readonly name: string;
35
- readonly order: number;
36
- readonly conditions: ReadonlyArray<{
37
- readonly name: "RequestUri";
38
- readonly parameters: {
39
- readonly operator: "Equal";
40
- readonly matchValues: ReadonlyArray<string>;
41
- };
42
- }>;
43
- readonly actions: ReadonlyArray<{
44
- readonly name: "UrlRedirect";
45
- readonly parameters: {
46
- readonly redirectType: "Moved" | "Found" | "TemporaryRedirect" | "PermanentRedirect";
47
- readonly destinationPath: string;
48
- };
49
- }>;
50
- }
51
- export declare function emitRedirectsAzureFrontDoor(rows: ReadonlyArray<RedirectRow>): ReadonlyArray<FrontDoorRule>;
52
- //# sourceMappingURL=redirects-emit.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"redirects-emit.d.ts","sourceRoot":"","sources":["../src/redirects-emit.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,kBAAkB,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEvD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;CACzC;AAiCD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,MAAM,CAKhF;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,aAAa,CAAC,WAAW,CAAC,GAC/B,0BAA0B,CAwB5B;AAED;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QACjC,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;QAC5B,QAAQ,CAAC,UAAU,EAAE;YACnB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;YAC3B,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;SAC7C,CAAC;KACH,CAAC,CAAC;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;QAC9B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;QAC7B,QAAQ,CAAC,UAAU,EAAE;YACnB,QAAQ,CAAC,YAAY,EAAE,OAAO,GAAG,OAAO,GAAG,mBAAmB,GAAG,mBAAmB,CAAC;YACrF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;SAClC,CAAC;KACH,CAAC,CAAC;CACJ;AAED,wBAAgB,2BAA2B,CACzC,IAAI,EAAE,aAAa,CAAC,WAAW,CAAC,GAC/B,aAAa,CAAC,aAAa,CAAC,CAqB9B"}
@@ -1,111 +0,0 @@
1
- // SPDX-License-Identifier: MPL-2.0
2
- /**
3
- * P15 — per-provider redirect file generators.
4
- *
5
- * P14's self-hosted Caddy consumes a `_redirects.caddy` file. Cloud
6
- * providers want different formats: Cloudflare Pages reads `_redirects`
7
- * (also a fine fit for any platform that consumes that format),
8
- * CloudFront wants a JSON config a Lambda@Edge function reads at
9
- * startup, and Azure Front Door wants a rules-engine RuleEngineRule[].
10
- *
11
- * The deploy step calls the right emitter based on `process.env.CAELO_PROVIDER`
12
- * and uploads the result to the right place. Each emitter is pure +
13
- * idempotent — same input → same byte output.
14
- */
15
- import { redirectTargetIssue } from "@caelo-cms/shared";
16
- /**
17
- * Reject whitespace + control chars in either path before any emitter
18
- * touches them. The Cloudflare `_redirects` format is space-delimited;
19
- * a `fromPath` containing a space ambiguates the line (CF parses
20
- * `/old foo /new 301` as from=`/old`, to=`foo`, status=`/new`, then
21
- * fails on `301`). Caelo's redirects table normally rejects these
22
- * upstream but the emitter shouldn't trust that — pure functions
23
- * defend their own contract.
24
- */
25
- function assertPathClean(label, path) {
26
- // \p{Cc} matches the Unicode control category (U+0000–U+001F + U+007F–U+009F);
27
- // the linter rejects literal \x00-\x1f ranges in regex, but the intent — defending
28
- // emitter contracts against control chars — is the explicit reason for this check.
29
- if (/[\s\p{Cc}]/u.test(path)) {
30
- throw new Error(`redirects-emit: ${label}=${JSON.stringify(path)} contains whitespace or control chars; reject upstream`);
31
- }
32
- }
33
- function assertRowsClean(rows) {
34
- for (const r of rows) {
35
- assertPathClean("fromPath", r.fromPath);
36
- assertPathClean("toPath", r.toPath);
37
- // #657 — a `//host` or `/https://…` target would ship as an open
38
- // redirect or a broken 301 in every CDN format below.
39
- const targetIssue = redirectTargetIssue(r.toPath);
40
- if (targetIssue)
41
- throw new Error(`redirects-emit: ${targetIssue}`);
42
- }
43
- }
44
- /**
45
- * Cloudflare Pages `_redirects` format (also consumed by Netlify, Vercel,
46
- * Render, etc.). One redirect per line: `<from> <to> <status>`. Wildcards
47
- * use `:splat`; we only support exact-path redirects in v1 (matches what
48
- * Caelo's redirects table allows).
49
- */
50
- export function emitRedirectsCloudflare(rows) {
51
- assertRowsClean(rows);
52
- const header = "# Generated by @caelo-cms/provisioning — do not edit by hand.\n";
53
- const lines = rows.map((r) => `${r.fromPath} ${r.toPath} ${r.statusCode}`);
54
- return header + lines.join("\n") + (rows.length > 0 ? "\n" : "");
55
- }
56
- export function emitRedirectsCloudFront(rows) {
57
- assertRowsClean(rows);
58
- const jsonConfig = JSON.stringify({ redirects: rows.map((r) => ({ from: r.fromPath, to: r.toPath, status: r.statusCode })) }, null, 2);
59
- // Tiny Lambda@Edge handler. v1 is exact-path match (matches Caelo's
60
- // redirects table). Wildcard support lands when the table grows it.
61
- const lambdaSource = `// SPDX-License-Identifier: MPL-2.0
62
- // Generated by @caelo-cms/provisioning. Bundled with redirects.json.
63
- const REDIRECTS = require("./redirects.json").redirects;
64
- const TABLE = new Map(REDIRECTS.map((r) => [r.from, r]));
65
- exports.handler = (event, _ctx, callback) => {
66
- const req = event.Records[0].cf.request;
67
- const m = TABLE.get(req.uri);
68
- if (!m) return callback(null, req);
69
- callback(null, {
70
- status: String(m.status),
71
- statusDescription: "Redirect",
72
- headers: { location: [{ key: "Location", value: m.to }] },
73
- });
74
- };`;
75
- return { jsonConfig, lambdaSource };
76
- }
77
- export function emitRedirectsAzureFrontDoor(rows) {
78
- assertRowsClean(rows);
79
- return rows.map((r, i) => ({
80
- name: `redirect-${i + 1}`,
81
- order: i + 1,
82
- conditions: [
83
- {
84
- name: "RequestUri",
85
- parameters: { operator: "Equal", matchValues: [r.fromPath] },
86
- },
87
- ],
88
- actions: [
89
- {
90
- name: "UrlRedirect",
91
- parameters: {
92
- redirectType: statusToFrontDoor(r.statusCode),
93
- destinationPath: r.toPath,
94
- },
95
- },
96
- ],
97
- }));
98
- }
99
- function statusToFrontDoor(status) {
100
- switch (status) {
101
- case 301:
102
- return "Moved";
103
- case 302:
104
- return "Found";
105
- case 307:
106
- return "TemporaryRedirect";
107
- case 308:
108
- return "PermanentRedirect";
109
- }
110
- }
111
- //# sourceMappingURL=redirects-emit.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"redirects-emit.js","sourceRoot":"","sources":["../src/redirects-emit.ts"],"names":[],"mappings":"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"}
@@ -1,175 +0,0 @@
1
- // SPDX-License-Identifier: MPL-2.0
2
-
3
- /**
4
- * P15 — per-provider redirect file generators.
5
- *
6
- * P14's self-hosted Caddy consumes a `_redirects.caddy` file. Cloud
7
- * providers want different formats: Cloudflare Pages reads `_redirects`
8
- * (also a fine fit for any platform that consumes that format),
9
- * CloudFront wants a JSON config a Lambda@Edge function reads at
10
- * startup, and Azure Front Door wants a rules-engine RuleEngineRule[].
11
- *
12
- * The deploy step calls the right emitter based on `process.env.CAELO_PROVIDER`
13
- * and uploads the result to the right place. Each emitter is pure +
14
- * idempotent — same input → same byte output.
15
- */
16
-
17
- import { redirectTargetIssue } from "@caelo-cms/shared";
18
-
19
- export type RedirectStatusCode = 301 | 302 | 307 | 308;
20
-
21
- export interface RedirectRow {
22
- readonly fromPath: string;
23
- readonly toPath: string;
24
- readonly statusCode: RedirectStatusCode;
25
- }
26
-
27
- /**
28
- * Reject whitespace + control chars in either path before any emitter
29
- * touches them. The Cloudflare `_redirects` format is space-delimited;
30
- * a `fromPath` containing a space ambiguates the line (CF parses
31
- * `/old foo /new 301` as from=`/old`, to=`foo`, status=`/new`, then
32
- * fails on `301`). Caelo's redirects table normally rejects these
33
- * upstream but the emitter shouldn't trust that — pure functions
34
- * defend their own contract.
35
- */
36
- function assertPathClean(label: "fromPath" | "toPath", path: string): void {
37
- // \p{Cc} matches the Unicode control category (U+0000–U+001F + U+007F–U+009F);
38
- // the linter rejects literal \x00-\x1f ranges in regex, but the intent — defending
39
- // emitter contracts against control chars — is the explicit reason for this check.
40
- if (/[\s\p{Cc}]/u.test(path)) {
41
- throw new Error(
42
- `redirects-emit: ${label}=${JSON.stringify(path)} contains whitespace or control chars; reject upstream`,
43
- );
44
- }
45
- }
46
-
47
- function assertRowsClean(rows: ReadonlyArray<RedirectRow>): void {
48
- for (const r of rows) {
49
- assertPathClean("fromPath", r.fromPath);
50
- assertPathClean("toPath", r.toPath);
51
- // #657 — a `//host` or `/https://…` target would ship as an open
52
- // redirect or a broken 301 in every CDN format below.
53
- const targetIssue = redirectTargetIssue(r.toPath);
54
- if (targetIssue) throw new Error(`redirects-emit: ${targetIssue}`);
55
- }
56
- }
57
-
58
- /**
59
- * Cloudflare Pages `_redirects` format (also consumed by Netlify, Vercel,
60
- * Render, etc.). One redirect per line: `<from> <to> <status>`. Wildcards
61
- * use `:splat`; we only support exact-path redirects in v1 (matches what
62
- * Caelo's redirects table allows).
63
- */
64
- export function emitRedirectsCloudflare(rows: ReadonlyArray<RedirectRow>): string {
65
- assertRowsClean(rows);
66
- const header = "# Generated by @caelo-cms/provisioning — do not edit by hand.\n";
67
- const lines = rows.map((r) => `${r.fromPath} ${r.toPath} ${r.statusCode}`);
68
- return header + lines.join("\n") + (rows.length > 0 ? "\n" : "");
69
- }
70
-
71
- /**
72
- * CloudFront via Lambda@Edge. Returns a JSON config the L@E function
73
- * reads at startup + a tiny Lambda source that consumes it. The L@E
74
- * function is deployed to `us-east-1` (CloudFront constraint); the
75
- * config blob is bundled into the Lambda deployment artifact.
76
- *
77
- * Returning both halves lets the AWS stack ship a single asset.
78
- */
79
- export interface CloudFrontRedirectArtifact {
80
- readonly jsonConfig: string;
81
- readonly lambdaSource: string;
82
- }
83
-
84
- export function emitRedirectsCloudFront(
85
- rows: ReadonlyArray<RedirectRow>,
86
- ): CloudFrontRedirectArtifact {
87
- assertRowsClean(rows);
88
- const jsonConfig = JSON.stringify(
89
- { redirects: rows.map((r) => ({ from: r.fromPath, to: r.toPath, status: r.statusCode })) },
90
- null,
91
- 2,
92
- );
93
- // Tiny Lambda@Edge handler. v1 is exact-path match (matches Caelo's
94
- // redirects table). Wildcard support lands when the table grows it.
95
- const lambdaSource = `// SPDX-License-Identifier: MPL-2.0
96
- // Generated by @caelo-cms/provisioning. Bundled with redirects.json.
97
- const REDIRECTS = require("./redirects.json").redirects;
98
- const TABLE = new Map(REDIRECTS.map((r) => [r.from, r]));
99
- exports.handler = (event, _ctx, callback) => {
100
- const req = event.Records[0].cf.request;
101
- const m = TABLE.get(req.uri);
102
- if (!m) return callback(null, req);
103
- callback(null, {
104
- status: String(m.status),
105
- statusDescription: "Redirect",
106
- headers: { location: [{ key: "Location", value: m.to }] },
107
- });
108
- };`;
109
- return { jsonConfig, lambdaSource };
110
- }
111
-
112
- /**
113
- * Azure Front Door rules engine. Returns a Pulumi-compatible
114
- * RuleEngineRule[]-shaped array (untyped here so callers don't need to
115
- * import @pulumi/azure-native). Each rule fires on a path-equals match
116
- * + executes a 301/302/etc. response.
117
- */
118
- export interface FrontDoorRule {
119
- readonly name: string;
120
- readonly order: number;
121
- readonly conditions: ReadonlyArray<{
122
- readonly name: "RequestUri";
123
- readonly parameters: {
124
- readonly operator: "Equal";
125
- readonly matchValues: ReadonlyArray<string>;
126
- };
127
- }>;
128
- readonly actions: ReadonlyArray<{
129
- readonly name: "UrlRedirect";
130
- readonly parameters: {
131
- readonly redirectType: "Moved" | "Found" | "TemporaryRedirect" | "PermanentRedirect";
132
- readonly destinationPath: string;
133
- };
134
- }>;
135
- }
136
-
137
- export function emitRedirectsAzureFrontDoor(
138
- rows: ReadonlyArray<RedirectRow>,
139
- ): ReadonlyArray<FrontDoorRule> {
140
- assertRowsClean(rows);
141
- return rows.map((r, i) => ({
142
- name: `redirect-${i + 1}`,
143
- order: i + 1,
144
- conditions: [
145
- {
146
- name: "RequestUri" as const,
147
- parameters: { operator: "Equal" as const, matchValues: [r.fromPath] },
148
- },
149
- ],
150
- actions: [
151
- {
152
- name: "UrlRedirect" as const,
153
- parameters: {
154
- redirectType: statusToFrontDoor(r.statusCode),
155
- destinationPath: r.toPath,
156
- },
157
- },
158
- ],
159
- }));
160
- }
161
-
162
- function statusToFrontDoor(
163
- status: RedirectStatusCode,
164
- ): FrontDoorRule["actions"][0]["parameters"]["redirectType"] {
165
- switch (status) {
166
- case 301:
167
- return "Moved";
168
- case 302:
169
- return "Found";
170
- case 307:
171
- return "TemporaryRedirect";
172
- case 308:
173
- return "PermanentRedirect";
174
- }
175
- }