@caelo-cms/provisioning 0.10.38 → 0.10.40
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/dist/admin-domain.d.ts +21 -3
- package/dist/admin-domain.d.ts.map +1 -1
- package/dist/admin-domain.js +88 -16
- package/dist/admin-domain.js.map +1 -1
- package/dist/cert-wait.d.ts +84 -0
- package/dist/cert-wait.d.ts.map +1 -0
- package/dist/cert-wait.js +95 -0
- package/dist/cert-wait.js.map +1 -0
- package/dist/checkpoint-secrets.d.ts +88 -0
- package/dist/checkpoint-secrets.d.ts.map +1 -0
- package/dist/checkpoint-secrets.js +245 -0
- package/dist/checkpoint-secrets.js.map +1 -0
- package/dist/cli.js +160 -28
- package/dist/cli.js.map +1 -1
- package/dist/dns/groups.d.ts +36 -0
- package/dist/dns/groups.d.ts.map +1 -0
- package/dist/dns/groups.js +46 -0
- package/dist/dns/groups.js.map +1 -0
- package/dist/gcloud-components.d.ts +42 -0
- package/dist/gcloud-components.d.ts.map +1 -0
- package/dist/gcloud-components.js +53 -0
- package/dist/gcloud-components.js.map +1 -0
- package/dist/gcloud.d.ts +14 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +18 -5
- package/dist/gcloud.js.map +1 -1
- package/dist/install-state.d.ts +19 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +37 -3
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +24 -3
- package/dist/lifecycle.js.map +1 -1
- package/dist/redirects-emit.d.ts +0 -13
- package/dist/redirects-emit.d.ts.map +1 -1
- package/dist/redirects-emit.js +19 -0
- package/dist/redirects-emit.js.map +1 -1
- package/dist/regions.d.ts.map +1 -1
- package/dist/regions.js +4 -1
- package/dist/regions.js.map +1 -1
- package/dist/runtime-secrets.d.ts +13 -1
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +40 -4
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +2 -0
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/stack-contract.d.ts +16 -2
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +24 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/wizard.d.ts +5 -19
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +53 -4
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-pulumi.d.ts +21 -0
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +25 -5
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +160 -116
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +3 -3
- package/stacks/gcp/index.ts +20 -3
- package/stacks/gcp-firebase/index.ts +20 -5
- package/stacks/self-hosted/index.ts +3 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Keeps secret material out of the files the CLI writes under
|
|
4
|
+
* `~/.caelo-<install-id>/` (#656).
|
|
5
|
+
*
|
|
6
|
+
* Only `secrets/` may hold secrets (CLAUDE.md §7, §11.C); `progress.json`
|
|
7
|
+
* and `install.json` are plain bookkeeping that tooling, backups, sync
|
|
8
|
+
* clients and agents read. Installs from before #576 checkpointed the full
|
|
9
|
+
* Pulumi outputs, whose database URLs carried admin_role's and
|
|
10
|
+
* public_role's passwords, and every install so far checkpointed the Owner
|
|
11
|
+
* setup link with its token. This module
|
|
12
|
+
*
|
|
13
|
+
* - finds secret material in a JSON value ({@link findSecrets}), which
|
|
14
|
+
* `markStepDone` / `writeMetadata` use to refuse writing it;
|
|
15
|
+
* - removes it from an existing checkpoint ({@link scrubProgressFile}),
|
|
16
|
+
* which `upgrade` and wizard re-runs call, and names the secrets to
|
|
17
|
+
* rotate ({@link rotationHints});
|
|
18
|
+
* - masks credentials in text the CLI prints ({@link redactCredentials}).
|
|
19
|
+
*
|
|
20
|
+
* Detection is by shape (a URL with a password, a URL with a token-like
|
|
21
|
+
* query parameter) and by key name (password, secret, token, ...), so a
|
|
22
|
+
* future secret output is caught without being listed here.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { DATABASE_ROLE_SECRET } from "./stack-contract.js";
|
|
26
|
+
/** `scheme://user:password@` — the user-info of a URL that carries a password. */
|
|
27
|
+
const CREDENTIAL_URL = /[a-z][a-z0-9+.-]{0,31}:\/\/([^\s:@/]*):([^\s@/]+)@/gi;
|
|
28
|
+
/** A URL query parameter that carries a credential (the Owner setup link's `?token=`). */
|
|
29
|
+
const TOKEN_PARAM = /[?&](?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|sig|signature)=[^&\s#]+/i;
|
|
30
|
+
/**
|
|
31
|
+
* Key names whose value is a secret, after lower-casing, dropping non
|
|
32
|
+
* alphanumerics and a Pulumi `Out` suffix: `postgresPassword`,
|
|
33
|
+
* `caeloSecretKek`, `bootstrap_token`, `anthropicApiKey`.
|
|
34
|
+
*/
|
|
35
|
+
const SECRET_KEY = /(password|passwd|passphrase|secret|secretkek|kek|token|apikey|privatekey)$/;
|
|
36
|
+
function isSecretKey(key) {
|
|
37
|
+
const k = key
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9]/g, "")
|
|
40
|
+
.replace(/out$/, "");
|
|
41
|
+
return SECRET_KEY.test(k);
|
|
42
|
+
}
|
|
43
|
+
function urlUser(raw) {
|
|
44
|
+
try {
|
|
45
|
+
return decodeURIComponent(raw);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return raw;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function stringFindings(value, path) {
|
|
52
|
+
const found = [];
|
|
53
|
+
for (const m of value.matchAll(CREDENTIAL_URL)) {
|
|
54
|
+
found.push({ path, kind: "credential-url", user: urlUser(m[1] ?? "") });
|
|
55
|
+
}
|
|
56
|
+
if (found.length === 0 && /:\/\//.test(value) && TOKEN_PARAM.test(value)) {
|
|
57
|
+
found.push({ path, kind: "token-url" });
|
|
58
|
+
}
|
|
59
|
+
return found;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Every place `value` holds secret material.
|
|
63
|
+
*
|
|
64
|
+
* @param value any JSON-shaped value (a checkpoint payload, install.json)
|
|
65
|
+
* @param path the path prefix to report findings under
|
|
66
|
+
*/
|
|
67
|
+
export function findSecrets(value, path = "") {
|
|
68
|
+
if (typeof value === "string")
|
|
69
|
+
return stringFindings(value, path || "(value)");
|
|
70
|
+
const join = (k) => (path ? `${path}.${k}` : String(k));
|
|
71
|
+
if (Array.isArray(value))
|
|
72
|
+
return value.flatMap((v, i) => findSecrets(v, join(i)));
|
|
73
|
+
if (value !== null && typeof value === "object") {
|
|
74
|
+
return Object.entries(value).flatMap(([k, v]) => {
|
|
75
|
+
// A secret-named key is only a finding when it holds something; a
|
|
76
|
+
// null / false / 0 marker is not secret material.
|
|
77
|
+
if (isSecretKey(k) && v !== null && v !== undefined && v !== "" && typeof v !== "boolean") {
|
|
78
|
+
return [{ path: join(k), kind: "secret-key" }];
|
|
79
|
+
}
|
|
80
|
+
return findSecrets(v, join(k));
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Throw when `value` holds secret material. The message names the paths,
|
|
87
|
+
* never the values, so the error itself cannot leak them.
|
|
88
|
+
*
|
|
89
|
+
* @param what what was about to be written, for the message
|
|
90
|
+
*/
|
|
91
|
+
export function assertNoSecrets(value, what) {
|
|
92
|
+
refuseFindings(findSecrets(value), what);
|
|
93
|
+
}
|
|
94
|
+
/** {@link assertNoSecrets} for a whole checkpoint (see {@link findCheckpointSecrets}). */
|
|
95
|
+
export function assertNoCheckpointSecrets(checkpoint, what) {
|
|
96
|
+
refuseFindings(findCheckpointSecrets(checkpoint), what);
|
|
97
|
+
}
|
|
98
|
+
function refuseFindings(found, what) {
|
|
99
|
+
if (found.length === 0)
|
|
100
|
+
return;
|
|
101
|
+
throw new Error(`Refusing to write secret material to ${what} (${found.map((f) => `${f.path}: ${f.kind}`).join(", ")}). ` +
|
|
102
|
+
"Secrets belong in Secret Manager or the install's secrets/ directory; read them at the moment of use instead (#656).");
|
|
103
|
+
}
|
|
104
|
+
function isRecord(v) {
|
|
105
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* {@link findSecrets} for a progress.json checkpoint. Its step names are
|
|
109
|
+
* derived from operator input (`project-<project id>`), so they are
|
|
110
|
+
* identifiers, not field names: a project called `acme-token` must not read
|
|
111
|
+
* as a secret. Only the step payloads (and the other top-level fields) are
|
|
112
|
+
* inspected.
|
|
113
|
+
*/
|
|
114
|
+
export function findCheckpointSecrets(checkpoint) {
|
|
115
|
+
if (!isRecord(checkpoint))
|
|
116
|
+
return findSecrets(checkpoint);
|
|
117
|
+
const { steps, ...rest } = checkpoint;
|
|
118
|
+
const inSteps = isRecord(steps)
|
|
119
|
+
? Object.entries(steps).flatMap(([name, payload]) => findSecrets(payload, `steps.${name}`))
|
|
120
|
+
: findSecrets(steps, "steps");
|
|
121
|
+
return [...findSecrets(rest), ...inSteps];
|
|
122
|
+
}
|
|
123
|
+
/** {@link withoutSecrets} for a checkpoint: every step is kept, its payload cleaned. */
|
|
124
|
+
function checkpointWithoutSecrets(checkpoint) {
|
|
125
|
+
if (!isRecord(checkpoint))
|
|
126
|
+
return withoutSecrets(checkpoint);
|
|
127
|
+
const { steps, ...rest } = checkpoint;
|
|
128
|
+
return {
|
|
129
|
+
...withoutSecrets(rest),
|
|
130
|
+
steps: isRecord(steps)
|
|
131
|
+
? Object.fromEntries(Object.entries(steps).map(([n, p]) => [n, withoutSecrets(p)]))
|
|
132
|
+
: withoutSecrets(steps),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* `value` without its secret material: secret-named keys and strings that
|
|
137
|
+
* hold a credential or token URL are dropped. Everything else is kept.
|
|
138
|
+
*/
|
|
139
|
+
export function withoutSecrets(value) {
|
|
140
|
+
if (typeof value === "string")
|
|
141
|
+
return value;
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
return value
|
|
144
|
+
.filter((v) => typeof v !== "string" || findSecrets(v).length === 0)
|
|
145
|
+
.map(withoutSecrets);
|
|
146
|
+
}
|
|
147
|
+
if (value !== null && typeof value === "object") {
|
|
148
|
+
const out = {};
|
|
149
|
+
for (const [k, v] of Object.entries(value)) {
|
|
150
|
+
if (findSecrets({ [k]: v }).length > 0 && (isSecretKey(k) || typeof v === "string"))
|
|
151
|
+
continue;
|
|
152
|
+
out[k] = withoutSecrets(v);
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The Pulumi outputs worth checkpointing: everything except outputs Pulumi
|
|
160
|
+
* marks secret and anything else that holds secret material (the Owner
|
|
161
|
+
* setup link). Nothing reads secrets back from the checkpoint; code that
|
|
162
|
+
* needs one reads the stack (`readStackOutput`) when it needs it.
|
|
163
|
+
*
|
|
164
|
+
* @param outputs the stack outputs as the Automation API returns them
|
|
165
|
+
*/
|
|
166
|
+
export function checkpointableOutputs(outputs) {
|
|
167
|
+
const kept = {};
|
|
168
|
+
for (const [k, o] of Object.entries(outputs)) {
|
|
169
|
+
if (o.secret)
|
|
170
|
+
continue;
|
|
171
|
+
if (findSecrets({ [k]: o.value }).length > 0)
|
|
172
|
+
continue;
|
|
173
|
+
kept[k] = o.value;
|
|
174
|
+
}
|
|
175
|
+
return kept;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Remove secret material from a `progress.json` in place. The rewrite is
|
|
179
|
+
* atomic (temp file in the same directory, then rename) and the result is
|
|
180
|
+
* mode 0600, so an interrupted scrub never leaves a truncated checkpoint
|
|
181
|
+
* and wizard re-runs still find every completed step.
|
|
182
|
+
*
|
|
183
|
+
* @param path the checkpoint file; a missing file is a no-op
|
|
184
|
+
*/
|
|
185
|
+
export function scrubProgressFile(path) {
|
|
186
|
+
if (!existsSync(path))
|
|
187
|
+
return { removed: [] };
|
|
188
|
+
const checkpoint = JSON.parse(readFileSync(path, "utf8"));
|
|
189
|
+
const removed = findCheckpointSecrets(checkpoint);
|
|
190
|
+
if (removed.length === 0)
|
|
191
|
+
return { removed };
|
|
192
|
+
writeJsonAtomic(path, checkpointWithoutSecrets(checkpoint));
|
|
193
|
+
return { removed };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Write `value` as JSON to `path` atomically with mode 0600: a temp file in
|
|
197
|
+
* the same directory (so the rename stays on one filesystem), then rename.
|
|
198
|
+
*/
|
|
199
|
+
export function writeJsonAtomic(path, value) {
|
|
200
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
201
|
+
try {
|
|
202
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
203
|
+
renameSync(tmp, path);
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
rmSync(tmp, { force: true });
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const ROLE_BY_USER = DATABASE_ROLE_SECRET;
|
|
211
|
+
/**
|
|
212
|
+
* What the operator should do about secrets a scrub removed: one line per
|
|
213
|
+
* affected secret, with the exact command where one exists.
|
|
214
|
+
*
|
|
215
|
+
* @param removed the scrub's findings
|
|
216
|
+
* @param installId the install, named in each command so it acts on the right one
|
|
217
|
+
*/
|
|
218
|
+
export function rotationHints(removed, installId) {
|
|
219
|
+
const rotate = (secret) => `bunx @caelo-cms/provisioning rotate-secret ${secret} --install ${installId}`;
|
|
220
|
+
const hints = new Set();
|
|
221
|
+
for (const f of removed) {
|
|
222
|
+
if (f.kind === "credential-url") {
|
|
223
|
+
const secret = f.user ? ROLE_BY_USER[f.user] : undefined;
|
|
224
|
+
hints.add(secret
|
|
225
|
+
? `${f.user}'s password was stored in progress.json. Rotate it: ${rotate(secret)}`
|
|
226
|
+
: `A password for database user "${f.user ?? "?"}" was stored in progress.json (${f.path}); change it.`);
|
|
227
|
+
}
|
|
228
|
+
else if (f.kind === "token-url") {
|
|
229
|
+
hints.add("The Owner setup link (with its one-time token) was stored in progress.json. It expires 24 hours after it was issued and stops working once the Owner account exists; nothing to rotate.");
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
hints.add(`A secret value was stored in progress.json (${f.path}). Rotate whatever it belongs to.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return [...hints];
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* `text` with every URL password masked (`postgresql://admin_role:***@`),
|
|
239
|
+
* for anything the CLI prints that could carry one (errors from Pulumi or
|
|
240
|
+
* gcloud, for instance). Same masking as the API gateway's error log.
|
|
241
|
+
*/
|
|
242
|
+
export function redactCredentials(text) {
|
|
243
|
+
return text.replace(/([a-z][a-z0-9+.-]{0,31}:\/\/[^\s:@/]*):[^\s@/]+@/gi, "$1:***@");
|
|
244
|
+
}
|
|
245
|
+
//# sourceMappingURL=checkpoint-secrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkpoint-secrets.js","sourceRoot":"","sources":["../src/checkpoint-secrets.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
24
24
|
import { resolve } from "node:path";
|
|
25
|
+
import { defaultStagingHost, normalizeStagingHost, stagingHostProblem } from "@caelo-cms/shared";
|
|
25
26
|
import { generateBootstrapToken } from "./bootstrap-token.js";
|
|
26
27
|
import { generateCaddyfile } from "./caddy.js";
|
|
28
|
+
import { redactCredentials } from "./checkpoint-secrets.js";
|
|
27
29
|
import { initDelegatesToWizard, resolveCliRoute } from "./cli-routing.js";
|
|
28
30
|
import { generateDockerCompose } from "./compose.js";
|
|
29
31
|
import { convergeSelfHostedRoles, decideUp } from "./self-hosted-roles.js";
|
|
@@ -104,6 +106,10 @@ function emitConfig(cfg, extraDomains = []) {
|
|
|
104
106
|
cfg.caeloSecretKek = randomSecret(32);
|
|
105
107
|
saveConfig(cfg);
|
|
106
108
|
}
|
|
109
|
+
if (!cfg.stagingDomain) {
|
|
110
|
+
cfg.stagingDomain = defaultStagingHost(cfg.domain);
|
|
111
|
+
saveConfig(cfg);
|
|
112
|
+
}
|
|
107
113
|
if (!cfg.rolePasswords) {
|
|
108
114
|
// `init` sets them and `up` back-fills + applies them before calling here.
|
|
109
115
|
throw new Error("config has no rolePasswords — run `cms-provision up`");
|
|
@@ -127,7 +133,7 @@ function emitConfig(cfg, extraDomains = []) {
|
|
|
127
133
|
const baseDomains = [
|
|
128
134
|
{ hostname: cfg.domain, kind: "admin", env: "production" },
|
|
129
135
|
{ hostname: cfg.domain, kind: "public", env: "production" },
|
|
130
|
-
{ hostname:
|
|
136
|
+
{ hostname: cfg.stagingDomain, kind: "public", env: "staging" },
|
|
131
137
|
];
|
|
132
138
|
const caddy = generateCaddyfile({
|
|
133
139
|
ownerEmail: cfg.ownerEmail,
|
|
@@ -172,7 +178,7 @@ async function init() {
|
|
|
172
178
|
return;
|
|
173
179
|
}
|
|
174
180
|
if (!domain || !ownerEmail) {
|
|
175
|
-
console.error("Usage: cms-provision init [--provider self-hosted] --domain example.com --owner-email me@example.com\n" +
|
|
181
|
+
console.error("Usage: cms-provision init [--provider self-hosted] --domain example.com [--staging-domain staging.example.com] --owner-email me@example.com\n" +
|
|
176
182
|
"Cloud providers (gcp | gcp-firebase | aws | azure) run the wizard: cms-provision --provider <name>");
|
|
177
183
|
process.exit(2);
|
|
178
184
|
}
|
|
@@ -180,9 +186,21 @@ async function init() {
|
|
|
180
186
|
console.error(`config already exists at ${CONFIG_PATH}; run \`cms-provision up\` to re-deploy`);
|
|
181
187
|
process.exit(2);
|
|
182
188
|
}
|
|
189
|
+
const stagingArg = arg("staging-domain");
|
|
190
|
+
const stagingProblem = stagingArg
|
|
191
|
+
? stagingHostProblem(stagingArg, {
|
|
192
|
+
liveHosts: [domain, `www.${domain}`],
|
|
193
|
+
adminHosts: [`admin.${domain}`],
|
|
194
|
+
})
|
|
195
|
+
: null;
|
|
196
|
+
if (stagingProblem) {
|
|
197
|
+
console.error(`--staging-domain: ${stagingProblem}`);
|
|
198
|
+
process.exit(2);
|
|
199
|
+
}
|
|
183
200
|
saveProvider(providerArg);
|
|
184
201
|
const cfg = {
|
|
185
202
|
domain,
|
|
203
|
+
stagingDomain: stagingArg ? normalizeStagingHost(stagingArg) : defaultStagingHost(domain),
|
|
186
204
|
ownerEmail,
|
|
187
205
|
postgresPassword: randomSecret(32),
|
|
188
206
|
rolePasswords: newRolePasswords(),
|
|
@@ -667,6 +685,7 @@ const handlers = {
|
|
|
667
685
|
destroy: lifecycleDestroy,
|
|
668
686
|
truncate: lifecycleTruncate,
|
|
669
687
|
"admin-domain": adminDomain,
|
|
688
|
+
"staging-domain": stagingDomain,
|
|
670
689
|
// Misc
|
|
671
690
|
"pulumi-output-sync": pulumiOutputSync,
|
|
672
691
|
version,
|
|
@@ -725,20 +744,51 @@ async function lifecycleTruncate() {
|
|
|
725
744
|
* (see admin-domain.ts for why this runs as the operator, not Pulumi).
|
|
726
745
|
*/
|
|
727
746
|
async function adminDomain() {
|
|
747
|
+
await domainMappingCommand("admin");
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* #650 `staging-domain enable [--staging-domain <host>]` — bind the install's
|
|
751
|
+
* staging host (its setting, default staging.<domain>) to the gcp-firebase
|
|
752
|
+
* gateway, which serves the protected staging site. `--staging-domain`
|
|
753
|
+
* records a new host for the install first (after it was changed in the
|
|
754
|
+
* admin under Security → Domains, or to choose one).
|
|
755
|
+
*/
|
|
756
|
+
async function stagingDomain() {
|
|
757
|
+
await domainMappingCommand("staging");
|
|
758
|
+
}
|
|
759
|
+
async function domainMappingCommand(kind) {
|
|
760
|
+
const command = `${kind}-domain`;
|
|
728
761
|
const sub = process.argv[3];
|
|
729
762
|
if (sub !== "enable") {
|
|
730
|
-
console.log(
|
|
763
|
+
console.log(`Usage: cms-provision ${command} enable [--install <install-id>]${kind === "staging" ? " [--staging-domain <host>]" : ""}`);
|
|
731
764
|
process.exit(2);
|
|
732
765
|
}
|
|
733
|
-
const { installFlag, listInstalls, selectInstall } = await import("./install-state.js");
|
|
734
|
-
const { enableAdminDomain } = await import("./admin-domain.js");
|
|
766
|
+
const { installFlag, installStagingHost, listInstalls, selectInstall, writeMetadata } = await import("./install-state.js");
|
|
767
|
+
const { enableAdminDomain, enableStagingDomain } = await import("./admin-domain.js");
|
|
735
768
|
// Guessing among several installs could map the domain in the wrong project.
|
|
736
769
|
const selected = selectInstall(listInstalls().filter((m) => m.projectId), installFlag(process.argv));
|
|
737
770
|
if (!selected.ok) {
|
|
738
771
|
console.error(selected.message);
|
|
739
772
|
process.exit(1);
|
|
740
773
|
}
|
|
741
|
-
|
|
774
|
+
let meta = selected.meta;
|
|
775
|
+
if (kind === "staging") {
|
|
776
|
+
// The install's staging host setting; `--staging-domain` changes it.
|
|
777
|
+
// Validated either way: the recorded/default host may predate a check
|
|
778
|
+
// (e.g. a long default staging.<domain> a Cloud Run mapping refuses).
|
|
779
|
+
const requested = arg("staging-domain");
|
|
780
|
+
const { normalizeStagingHost, stagingHostProblem } = await import("@caelo-cms/shared");
|
|
781
|
+
const host = requested ?? installStagingHost(meta);
|
|
782
|
+
const problem = stagingHostProblem(host, { liveHosts: [meta.domain, `www.${meta.domain}`], adminHosts: [`admin.${meta.domain}`] }, { provider: meta.provider });
|
|
783
|
+
if (problem) {
|
|
784
|
+
console.error(requested
|
|
785
|
+
? `--staging-domain: ${problem}`
|
|
786
|
+
: `the install's staging host: ${problem}. Pass --staging-domain <host>.`);
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
meta = { ...meta, stagingDomain: normalizeStagingHost(host) };
|
|
790
|
+
writeMetadata(meta.installId, meta);
|
|
791
|
+
}
|
|
742
792
|
if (!meta.projectId)
|
|
743
793
|
process.exit(1);
|
|
744
794
|
if (!meta.region) {
|
|
@@ -746,32 +796,105 @@ async function adminDomain() {
|
|
|
746
796
|
process.exit(1);
|
|
747
797
|
}
|
|
748
798
|
if (meta.provider !== "gcp-firebase") {
|
|
749
|
-
console.log(
|
|
750
|
-
?
|
|
751
|
-
|
|
799
|
+
console.log(kind === "admin"
|
|
800
|
+
? meta.provider === "gcp"
|
|
801
|
+
? `Nothing to do: on gcp the load balancer already serves admin.${meta.domain}.`
|
|
802
|
+
: `admin-domain is for gcp-firebase installs; this one is ${meta.provider}.`
|
|
803
|
+
: meta.provider === "self-hosted"
|
|
804
|
+
? `Recorded staging host ${installStagingHost(meta)}; Caddy serves it after \`cms-provision regenerate-caddy\`.`
|
|
805
|
+
: `staging-domain is for gcp-firebase installs; on ${meta.provider} the protected staging domain is not available yet (issue #650 follow-up).`);
|
|
752
806
|
return;
|
|
753
807
|
}
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
});
|
|
808
|
+
const target = { projectId: meta.projectId, region: meta.region, domain: meta.domain };
|
|
809
|
+
const r = kind === "admin"
|
|
810
|
+
? await enableAdminDomain(target)
|
|
811
|
+
: await enableStagingDomain({ ...target, stagingHost: installStagingHost(meta) });
|
|
759
812
|
if (r.status === "failed") {
|
|
760
|
-
console.error(
|
|
813
|
+
console.error(`${command}: ${r.error}`);
|
|
761
814
|
process.exit(1);
|
|
762
815
|
}
|
|
763
816
|
if (r.status === "needs-verification") {
|
|
764
817
|
console.log(`Google only maps ${r.hostname} for a verified owner of ${r.verifyDomain}.\n` +
|
|
765
818
|
"Search Console was opened for you: add the TXT record it shows at your registrar,\n" +
|
|
766
|
-
|
|
819
|
+
`click Verify, then run \`cms-provision ${command} enable\` again.`);
|
|
767
820
|
process.exit(3);
|
|
768
821
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
822
|
+
const mappedTo = kind === "admin" ? "the admin" : "the gateway (protected staging site)";
|
|
823
|
+
console.log(`${r.hostname} ${r.created ? "is now mapped" : "was already mapped"} to ${mappedTo}.`);
|
|
824
|
+
if (kind === "staging") {
|
|
825
|
+
console.log("Staging access (who may open it, crawler credentials): admin → Security → Staging access.\n" +
|
|
826
|
+
"If you changed the host here, set the same host in the admin (Security → Domains) so Stage renders it.");
|
|
827
|
+
}
|
|
828
|
+
if (r.records.length === 0) {
|
|
829
|
+
console.log(`Google has not published the DNS records yet; run \`cms-provision ${command} enable\` again in a minute.`);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const waited = await waitForDomainMappingCert({
|
|
833
|
+
command,
|
|
834
|
+
hostname: r.hostname,
|
|
835
|
+
records: r.records,
|
|
836
|
+
projectId: meta.projectId,
|
|
837
|
+
region: meta.region,
|
|
838
|
+
});
|
|
839
|
+
if (!waited)
|
|
840
|
+
process.exit(4);
|
|
841
|
+
console.log(kind === "admin"
|
|
842
|
+
? `Admin login: https://${r.hostname}/login`
|
|
843
|
+
: `Protected staging site: https://${r.hostname}/`);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* After `<kind>-domain enable` mapped the host: print the DNS records, then
|
|
847
|
+
* poll the mapping until Google has issued its certificate (cert-wait.ts),
|
|
848
|
+
* with a spinner. Returns false on timeout or a gcloud failure, after saying
|
|
849
|
+
* how to check the status. Ctrl-C is safe: the command is idempotent, and
|
|
850
|
+
* re-running it finds the mapping and resumes the wait.
|
|
851
|
+
*/
|
|
852
|
+
async function waitForDomainMappingCert(target) {
|
|
853
|
+
const { spinner } = await import("@clack/prompts");
|
|
854
|
+
const { CERT_WAIT_TIMEOUT_MS, domainMappingProbe, waitForCert } = await import("./cert-wait.js");
|
|
855
|
+
const { gcloud } = await import("./gcloud.js");
|
|
856
|
+
const width = Math.max(...target.records.map((d) => d.name.length));
|
|
857
|
+
console.log(`\nSet this DNS record at your registrar (if you have not already):\n${target.records
|
|
858
|
+
.map((d) => ` ${d.type.padEnd(6)} ${d.name.padEnd(width)} → ${d.value}`)
|
|
859
|
+
.join("\n")}\n` +
|
|
860
|
+
"Google issues the TLS certificate once the record resolves — usually 15-60 minutes.\n" +
|
|
861
|
+
`Safe to stop with Ctrl-C: \`cms-provision ${target.command} enable\` again resumes waiting.\n`);
|
|
862
|
+
// clack's spinner swallows SIGINT; exit explicitly so Ctrl-C stops the wait.
|
|
863
|
+
const s = spinner({
|
|
864
|
+
cancelMessage: `Stopped. Run \`cms-provision ${target.command} enable\` again to resume waiting.`,
|
|
865
|
+
onCancel: () => process.exit(130),
|
|
866
|
+
});
|
|
867
|
+
s.start(`Waiting for the certificate of ${target.hostname} (DNS set? this takes 15-60 min)`);
|
|
868
|
+
const r = await waitForCert({
|
|
869
|
+
probe: () => domainMappingProbe(gcloud, {
|
|
870
|
+
hostname: target.hostname,
|
|
871
|
+
region: target.region,
|
|
872
|
+
projectId: target.projectId,
|
|
873
|
+
}),
|
|
874
|
+
timeoutMs: CERT_WAIT_TIMEOUT_MS,
|
|
875
|
+
intervalMs: 30 * 1000,
|
|
876
|
+
onUpdate: (detail) => s.message(`${target.hostname}: ${detail}`),
|
|
877
|
+
});
|
|
878
|
+
if (r.ok) {
|
|
879
|
+
s.stop(`Certificate for ${target.hostname} is ready.`);
|
|
880
|
+
return true;
|
|
881
|
+
}
|
|
882
|
+
const check = `gcloud beta run domain-mappings describe --domain=${target.hostname} --region=${target.region} --project=${target.projectId}`;
|
|
883
|
+
if (r.reason === "error") {
|
|
884
|
+
s.error(`Could not read the domain mapping: ${r.error}`);
|
|
885
|
+
console.error(`Check it by hand with:\n ${check}`);
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
s.error(`No certificate for ${target.hostname} after 60 minutes.`);
|
|
889
|
+
console.error([
|
|
890
|
+
r.lastDetail ? `Google reports: ${r.lastDetail}` : "",
|
|
891
|
+
`Check that the record resolves: dig +short ${target.hostname} ${target.records[0]?.type ?? "CNAME"}`,
|
|
892
|
+
`Check the mapping's status: ${check}`,
|
|
893
|
+
`Once DNS resolves, run \`cms-provision ${target.command} enable\` again to resume waiting.`,
|
|
894
|
+
]
|
|
895
|
+
.filter(Boolean)
|
|
896
|
+
.join("\n"));
|
|
897
|
+
return false;
|
|
775
898
|
}
|
|
776
899
|
/**
|
|
777
900
|
* §11.C — interactive wizard. Loaded lazily so the bare-CLI startup
|
|
@@ -785,6 +908,7 @@ async function wizardCommand() {
|
|
|
785
908
|
nonInteractive: process.argv.includes("--non-interactive"),
|
|
786
909
|
provider: arg("provider"),
|
|
787
910
|
domain: arg("domain"),
|
|
911
|
+
stagingDomain: arg("staging-domain"),
|
|
788
912
|
ownerEmail: arg("owner-email"),
|
|
789
913
|
projectId: arg("project-id"),
|
|
790
914
|
region: arg("region"),
|
|
@@ -794,14 +918,22 @@ async function wizardCommand() {
|
|
|
794
918
|
// documented one-command form) and a bare `cms-provision` drop into the
|
|
795
919
|
// wizard. `--no-wizard` / `--help` keep the legacy "print usage" shape.
|
|
796
920
|
const route = resolveCliRoute(process.argv, Object.keys(handlers));
|
|
797
|
-
if (route.kind === "handler") {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
921
|
+
if (route.kind === "handler" || route.kind === "wizard") {
|
|
922
|
+
try {
|
|
923
|
+
if (route.kind === "handler")
|
|
924
|
+
await handlers[route.name]?.();
|
|
925
|
+
else
|
|
926
|
+
await wizardCommand();
|
|
927
|
+
}
|
|
928
|
+
catch (e) {
|
|
929
|
+
// #656 — an error from Pulumi, gcloud or a database client can quote a
|
|
930
|
+
// connection URL; print it with any password masked.
|
|
931
|
+
console.error(redactCredentials(e instanceof Error ? (e.stack ?? e.message) : String(e)));
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
802
934
|
}
|
|
803
935
|
else {
|
|
804
|
-
console.log("Usage: cms-provision [wizard] [--provider <name> --domain <d> --owner-email <e> --region <r>] | <init|up|status|upgrade|backup|restore|rotate-secret|truncate|destroy|regenerate-caddy|admin-domain|pulumi-output-sync|version> [options]\n" +
|
|
936
|
+
console.log("Usage: cms-provision [wizard] [--provider <name> --domain <d> [--staging-domain <host>] --owner-email <e> --region <r>] | <init|up|status|upgrade|backup|restore|rotate-secret|truncate|destroy|regenerate-caddy|admin-domain|staging-domain|pulumi-output-sync|version> [options]\n" +
|
|
805
937
|
"With several installs on this machine, pick one with --install <install-id or domain> (status, upgrade and backup also take --install all); in a terminal you are asked otherwise.\n" +
|
|
806
938
|
"Pass --no-wizard with no sub-command to print this usage instead of the wizard.");
|
|
807
939
|
process.exit(cmd && !cmd.startsWith("-") ? 2 : 0);
|