@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.
- package/caddy/Caddyfile.production +4 -0
- package/caddy/Caddyfile.staging +4 -0
- package/dist/agent-db.js +4 -0
- package/dist/caddy-redirects.d.ts +15 -0
- package/dist/caddy-redirects.d.ts.map +1 -0
- package/dist/caddy-redirects.js +65 -0
- package/dist/caddy-redirects.js.map +1 -0
- package/dist/caddy.d.ts +22 -2
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +66 -17
- package/dist/caddy.js.map +1 -1
- package/dist/cli-routing.d.ts.map +1 -1
- package/dist/cli-routing.js +7 -0
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +248 -254
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +54 -2
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +86 -32
- package/dist/compose.js.map +1 -1
- package/dist/gcp-names.d.ts +12 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +21 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/gcp-url-map.d.ts +27 -0
- package/dist/gcp-url-map.d.ts.map +1 -0
- package/dist/gcp-url-map.js +62 -0
- package/dist/gcp-url-map.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +20 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +39 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/public-role-isolation.d.ts +19 -0
- package/dist/public-role-isolation.d.ts.map +1 -0
- package/dist/public-role-isolation.js +69 -0
- package/dist/public-role-isolation.js.map +1 -0
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +3 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/release-manifest.d.ts.map +1 -1
- package/dist/release-manifest.js +2 -1
- package/dist/release-manifest.js.map +1 -1
- package/dist/self-hosted-backup.d.ts +86 -0
- package/dist/self-hosted-backup.d.ts.map +1 -0
- package/dist/self-hosted-backup.js +350 -0
- package/dist/self-hosted-backup.js.map +1 -0
- package/dist/self-hosted-release.d.ts +12 -3
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +6 -4
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-start.d.ts +50 -0
- package/dist/self-hosted-start.d.ts.map +1 -0
- package/dist/self-hosted-start.js +59 -0
- package/dist/self-hosted-start.js.map +1 -0
- package/dist/self-hosted-upgrade.d.ts +30 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +29 -7
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/tar-entries.d.ts +14 -0
- package/dist/tar-entries.d.ts.map +1 -0
- package/dist/tar-entries.js +104 -0
- package/dist/tar-entries.js.map +1 -0
- package/dist/upgrade/converge.d.ts +2 -2
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +44 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrader-assess.d.ts.map +1 -1
- package/dist/upgrader-assess.js +2 -1
- package/dist/upgrader-assess.js.map +1 -1
- package/dist/url-map-redirects-access.d.ts +15 -0
- package/dist/url-map-redirects-access.d.ts.map +1 -0
- package/dist/url-map-redirects-access.js +81 -0
- package/dist/url-map-redirects-access.js.map +1 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +3 -1
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +45 -0
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-memory.test.ts +2 -2
- package/src/caddy-redirects.docker.test.ts +255 -0
- package/src/caddy-redirects.test.ts +115 -0
- package/src/caddy-redirects.ts +68 -0
- package/src/caddy-validate.test.ts +119 -0
- package/src/caddy.test.ts +108 -0
- package/src/caddy.ts +89 -20
- package/src/cli-routing.test.ts +12 -0
- package/src/cli-routing.ts +7 -0
- package/src/cli.ts +299 -287
- package/src/compose.ts +106 -34
- package/src/gcp-names.ts +23 -0
- package/src/gcp-url-map.test.ts +150 -0
- package/src/gcp-url-map.ts +81 -0
- package/src/index.test.ts +18 -146
- package/src/index.ts +0 -9
- package/src/infra-contract.test.ts +39 -0
- package/src/infra-contract.ts +42 -1
- package/src/public-role-isolation.test.ts +89 -0
- package/src/public-role-isolation.ts +83 -0
- package/src/release-contract.ts +3 -0
- package/src/release-manifest.test.ts +12 -0
- package/src/release-manifest.ts +2 -1
- package/src/self-hosted-backup.test.ts +344 -0
- package/src/self-hosted-backup.ts +480 -0
- package/src/self-hosted-compose.test.ts +88 -4
- package/src/self-hosted-release.ts +22 -7
- package/src/self-hosted-start.test.ts +68 -0
- package/src/self-hosted-start.ts +123 -0
- package/src/self-hosted-upgrade.ts +35 -8
- package/src/stack-contract.test.ts +2 -2
- package/src/tar-entries.test.ts +52 -0
- package/src/tar-entries.ts +116 -0
- package/src/upgrade/converge.ts +51 -4
- package/src/upgrade/upgrade.test.ts +78 -1
- package/src/upgrader-assess.test.ts +22 -0
- package/src/upgrader-assess.ts +2 -1
- package/src/url-map-redirects-access.test.ts +88 -0
- package/src/url-map-redirects-access.ts +89 -0
- package/src/wizard.ts +3 -1
- package/src/wizards/gcp.ts +48 -0
- package/stacks/aws/index.ts +34 -2
- package/stacks/azure/index.ts +13 -0
- package/stacks/gcp/index.ts +24 -17
- package/stacks/gcp-firebase/index.ts +7 -0
- package/stacks/self-hosted/Pulumi.yaml +2 -2
- package/stacks/self-hosted/README.md +3 -3
- package/stacks/self-hosted/index.ts +45 -22
- package/dist/redirects-emit.d.ts +0 -52
- package/dist/redirects-emit.d.ts.map +0 -1
- package/dist/redirects-emit.js +0 -111
- package/dist/redirects-emit.js.map +0 -1
- 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` —
|
|
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 {
|
|
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
|
-
|
|
60
|
-
const
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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 {
|
|
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;
|
package/dist/redirects-emit.d.ts
DELETED
|
@@ -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"}
|
package/dist/redirects-emit.js
DELETED
|
@@ -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":"AAAA,mCAAmC;AAEnC;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAUxD;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,KAA4B,EAAE,IAAY;IACjE,+EAA+E;IAC/E,mFAAmF;IACnF,mFAAmF;IACnF,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CACb,mBAAmB,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,wDAAwD,CACzG,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,IAAgC;IACvD,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,eAAe,CAAC,UAAU,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;QACxC,eAAe,CAAC,QAAQ,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;QACpC,iEAAiE;QACjE,sDAAsD;QACtD,MAAM,WAAW,GAAG,mBAAmB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAClD,IAAI,WAAW;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,WAAW,EAAE,CAAC,CAAC;IACrE,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAgC;IACtE,eAAe,CAAC,IAAI,CAAC,CAAC;IACtB,MAAM,MAAM,GAAG,iEAAiE,CAAC;IACjF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;IAC3E,OAAO,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACnE,CAAC;AAeD,MAAM,UAAU,uBAAuB,CACrC,IAAgC;IAEhC,eAAe,CAAC,IAAI,CAAC,CAAC;IACtB,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAC/B,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAC1F,IAAI,EACJ,CAAC,CACF,CAAC;IACF,oEAAoE;IACpE,oEAAoE;IACpE,MAAM,YAAY,GAAG;;;;;;;;;;;;;GAapB,CAAC;IACF,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,CAAC;AACtC,CAAC;AA2BD,MAAM,UAAU,2BAA2B,CACzC,IAAgC;IAEhC,eAAe,CAAC,IAAI,CAAC,CAAC;IACtB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACzB,IAAI,EAAE,YAAY,CAAC,GAAG,CAAC,EAAE;QACzB,KAAK,EAAE,CAAC,GAAG,CAAC;QACZ,UAAU,EAAE;YACV;gBACE,IAAI,EAAE,YAAqB;gBAC3B,UAAU,EAAE,EAAE,QAAQ,EAAE,OAAgB,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE;aACtE;SACF;QACD,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,aAAsB;gBAC5B,UAAU,EAAE;oBACV,YAAY,EAAE,iBAAiB,CAAC,CAAC,CAAC,UAAU,CAAC;oBAC7C,eAAe,EAAE,CAAC,CAAC,MAAM;iBAC1B;aACF;SACF;KACF,CAAC,CAAC,CAAC;AACN,CAAC;AAED,SAAS,iBAAiB,CACxB,MAA0B;IAE1B,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,GAAG;YACN,OAAO,OAAO,CAAC;QACjB,KAAK,GAAG;YACN,OAAO,OAAO,CAAC;QACjB,KAAK,GAAG;YACN,OAAO,mBAAmB,CAAC;QAC7B,KAAK,GAAG;YACN,OAAO,mBAAmB,CAAC;IAC/B,CAAC;AACH,CAAC"}
|
package/src/redirects-emit.ts
DELETED
|
@@ -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
|
-
}
|