@caelo-cms/provisioning 0.10.47 → 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.
- package/dist/agent/apply.d.ts +45 -0
- package/dist/agent/apply.d.ts.map +1 -0
- package/dist/agent/apply.js +83 -0
- package/dist/agent/apply.js.map +1 -0
- package/dist/agent/cli-provenance.d.ts +42 -0
- package/dist/agent/cli-provenance.d.ts.map +1 -0
- package/dist/agent/cli-provenance.js +138 -0
- package/dist/agent/cli-provenance.js.map +1 -0
- package/dist/agent/command.d.ts +79 -0
- package/dist/agent/command.d.ts.map +1 -0
- package/dist/agent/command.js +454 -0
- package/dist/agent/command.js.map +1 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +60 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/lock.d.ts +13 -0
- package/dist/agent/lock.d.ts.map +1 -0
- package/dist/agent/lock.js +100 -0
- package/dist/agent/lock.js.map +1 -0
- package/dist/agent/queue.d.ts +35 -0
- package/dist/agent/queue.d.ts.map +1 -0
- package/dist/agent/queue.js +50 -0
- package/dist/agent/queue.js.map +1 -0
- package/dist/agent/run.d.ts +97 -0
- package/dist/agent/run.d.ts.map +1 -0
- package/dist/agent/run.js +198 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/systemd.d.ts +55 -0
- package/dist/agent/systemd.d.ts.map +1 -0
- package/dist/agent/systemd.js +171 -0
- package/dist/agent/systemd.js.map +1 -0
- package/dist/agent/units.d.ts +44 -0
- package/dist/agent/units.d.ts.map +1 -0
- package/dist/agent/units.js +127 -0
- package/dist/agent/units.js.map +1 -0
- package/dist/agent-db.js +18362 -0
- package/dist/cli.js +119 -16
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-iam-user.d.ts +39 -0
- package/dist/cloudsql-iam-user.d.ts.map +1 -0
- package/dist/cloudsql-iam-user.js +119 -0
- package/dist/cloudsql-iam-user.js.map +1 -0
- package/dist/compose.d.ts +16 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +26 -1
- package/dist/compose.js.map +1 -1
- package/dist/dns/index.d.ts +2 -0
- package/dist/dns/index.d.ts.map +1 -1
- package/dist/dns/index.js +3 -2
- package/dist/dns/index.js.map +1 -1
- package/dist/dns/manual.d.ts +5 -0
- package/dist/dns/manual.d.ts.map +1 -1
- package/dist/dns/manual.js +28 -12
- package/dist/dns/manual.js.map +1 -1
- package/dist/gcloud.d.ts +2 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +1 -1
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +15 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +23 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.js +7 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job-runner.d.ts +13 -0
- package/dist/install-job-runner.d.ts.map +1 -0
- package/dist/install-job-runner.js +155 -0
- package/dist/install-job-runner.js.map +1 -0
- package/dist/install-job.d.ts +210 -0
- package/dist/install-job.d.ts.map +1 -0
- package/dist/install-job.js +449 -0
- package/dist/install-job.js.map +1 -0
- package/dist/install-state.d.ts +10 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +10 -2
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +19 -4
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts +5 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +1 -1
- package/dist/migration-runner.js.map +1 -1
- package/dist/operator-access.d.ts +14 -8
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +16 -102
- package/dist/operator-access.js.map +1 -1
- package/dist/run-mode.d.ts +27 -0
- package/dist/run-mode.d.ts.map +1 -0
- package/dist/run-mode.js +22 -0
- package/dist/run-mode.js.map +1 -0
- package/dist/runtime-secrets.d.ts +19 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +44 -0
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +22 -1
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +81 -12
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +17 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +16 -1
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +6 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +10 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/upgrade/converge.d.ts +6 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +59 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +6 -0
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +25 -0
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +31 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +5 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +28 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +82 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +116 -0
- package/dist/upgrader-assess.js.map +1 -0
- package/dist/upgrader-gcp.d.ts +17 -0
- package/dist/upgrader-gcp.d.ts.map +1 -0
- package/dist/upgrader-gcp.js +87 -0
- package/dist/upgrader-gcp.js.map +1 -0
- package/dist/upgrader-job.d.ts +131 -0
- package/dist/upgrader-job.d.ts.map +1 -0
- package/dist/upgrader-job.js +259 -0
- package/dist/upgrader-job.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +35 -0
- package/dist/upgrader-reporter.d.ts.map +1 -0
- package/dist/upgrader-reporter.js +79 -0
- package/dist/upgrader-reporter.js.map +1 -0
- package/dist/upgrader-setup.d.ts +59 -0
- package/dist/upgrader-setup.d.ts.map +1 -0
- package/dist/upgrader-setup.js +308 -0
- package/dist/upgrader-setup.js.map +1 -0
- package/dist/wizard.d.ts +6 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +32 -4
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-firebase.d.ts +3 -0
- package/dist/wizards/gcp-firebase.d.ts.map +1 -1
- package/dist/wizards/gcp-firebase.js +1 -0
- package/dist/wizards/gcp-firebase.js.map +1 -1
- package/dist/wizards/gcp-install-job.d.ts +74 -0
- package/dist/wizards/gcp-install-job.d.ts.map +1 -0
- package/dist/wizards/gcp-install-job.js +398 -0
- package/dist/wizards/gcp-install-job.js.map +1 -0
- package/dist/wizards/gcp-prompts.d.ts +3 -0
- package/dist/wizards/gcp-prompts.d.ts.map +1 -0
- package/dist/wizards/gcp-prompts.js +25 -0
- package/dist/wizards/gcp-prompts.js.map +1 -0
- package/dist/wizards/gcp-pulumi.d.ts +4 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +1 -1
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts +40 -0
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +223 -59
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +6 -4
- package/src/adapter.ts +97 -0
- package/src/adc-impersonation.ts +176 -0
- package/src/admin-domain.test.ts +254 -0
- package/src/admin-domain.ts +245 -0
- package/src/admin-memory.test.ts +151 -0
- package/src/agent/apply.test.ts +185 -0
- package/src/agent/apply.ts +124 -0
- package/src/agent/cli-provenance.test.ts +159 -0
- package/src/agent/cli-provenance.ts +191 -0
- package/src/agent/command.test.ts +77 -0
- package/src/agent/command.ts +564 -0
- package/src/agent/config.ts +86 -0
- package/src/agent/lock-config.test.ts +97 -0
- package/src/agent/lock.ts +110 -0
- package/src/agent/queue.test.ts +10 -0
- package/src/agent/queue.ts +91 -0
- package/src/agent/run.test.ts +382 -0
- package/src/agent/run.ts +295 -0
- package/src/agent/systemd.ts +209 -0
- package/src/agent/units.test.ts +223 -0
- package/src/agent/units.ts +163 -0
- package/src/azure-front-door.test.ts +119 -0
- package/src/azure-front-door.ts +56 -0
- package/src/bootstrap-token.ts +20 -0
- package/src/caddy.ts +112 -0
- package/src/cdn-copy.ts +91 -0
- package/src/cert-wait.test.ts +194 -0
- package/src/cert-wait.ts +163 -0
- package/src/checkpoint-secrets.test.ts +380 -0
- package/src/checkpoint-secrets.ts +283 -0
- package/src/cli-routing.test.ts +45 -0
- package/src/cli-routing.ts +43 -0
- package/src/cli.ts +1491 -0
- package/src/cloudsql-iam-user.ts +147 -0
- package/src/compose.ts +334 -0
- package/src/default-firewall.test.ts +411 -0
- package/src/default-firewall.ts +342 -0
- package/src/deployed-release.test.ts +231 -0
- package/src/deployed-release.ts +220 -0
- package/src/dns/cloudflare.ts +182 -0
- package/src/dns/groups.test.ts +76 -0
- package/src/dns/groups.ts +86 -0
- package/src/dns/index.ts +56 -0
- package/src/dns/manual.ts +130 -0
- package/src/dns/types.ts +27 -0
- package/src/gateway-credentials.test.ts +180 -0
- package/src/gateway-credentials.ts +144 -0
- package/src/gcloud-components.test.ts +76 -0
- package/src/gcloud-components.ts +85 -0
- package/src/gcloud-retry.ts +41 -0
- package/src/gcloud-runner.test.ts +61 -0
- package/src/gcloud.test.ts +99 -0
- package/src/gcloud.ts +322 -0
- package/src/gcp-names.ts +135 -0
- package/src/index.test.ts +332 -0
- package/src/index.ts +114 -0
- package/src/infra-contract.ts +32 -0
- package/src/install-job-runner.ts +166 -0
- package/src/install-job.test.ts +566 -0
- package/src/install-job.ts +542 -0
- package/src/install-state.test.ts +239 -0
- package/src/install-state.ts +583 -0
- package/src/internal-token.ts +35 -0
- package/src/lifecycle.ts +900 -0
- package/src/mcp-iap-stacks.test.ts +58 -0
- package/src/mcp-iap.test.ts +149 -0
- package/src/mcp-iap.ts +131 -0
- package/src/media-volume.test.ts +296 -0
- package/src/migration-runner.ts +365 -0
- package/src/operator-access-stacks.test.ts +55 -0
- package/src/operator-access.test.ts +332 -0
- package/src/operator-access.ts +554 -0
- package/src/ops-backend.test.ts +320 -0
- package/src/ops-backend.ts +414 -0
- package/src/provisioner-identity.test.ts +482 -0
- package/src/provisioner-identity.ts +407 -0
- package/src/redirects-emit.ts +175 -0
- package/src/region-data.ts +428 -0
- package/src/regions.test.ts +277 -0
- package/src/regions.ts +370 -0
- package/src/release-contract.ts +57 -0
- package/src/release-manifest.test.ts +293 -0
- package/src/release-manifest.ts +430 -0
- package/src/run-mode.ts +40 -0
- package/src/runtime-secrets.test.ts +627 -0
- package/src/runtime-secrets.ts +754 -0
- package/src/self-command.test.ts +14 -0
- package/src/self-command.ts +21 -0
- package/src/self-hosted-compose.test.ts +177 -0
- package/src/self-hosted-release.ts +240 -0
- package/src/self-hosted-roles.integration.test.ts +161 -0
- package/src/self-hosted-roles.test.ts +124 -0
- package/src/self-hosted-roles.ts +203 -0
- package/src/self-hosted-upgrade.test.ts +394 -0
- package/src/self-hosted-upgrade.ts +548 -0
- package/src/stack-contract-parity.test.ts +199 -0
- package/src/stack-contract.test.ts +391 -0
- package/src/stack-contract.ts +888 -0
- package/src/stack-converge-secrets.test.ts +421 -0
- package/src/stack-converge.test.ts +833 -0
- package/src/stack-converge.ts +909 -0
- package/src/staging-admin.test.ts +246 -0
- package/src/staging-admin.ts +324 -0
- package/src/state-attach.test.ts +211 -0
- package/src/state-attach.ts +132 -0
- package/src/state-backend.test.ts +145 -0
- package/src/state-backend.ts +262 -0
- package/src/state-command.ts +293 -0
- package/src/state-migrate.test.ts +479 -0
- package/src/state-migrate.ts +434 -0
- package/src/state-pulumi.ts +289 -0
- package/src/state-store.test.ts +212 -0
- package/src/state-store.ts +243 -0
- package/src/upgrade/app-roll.ts +486 -0
- package/src/upgrade/backup.ts +109 -0
- package/src/upgrade/converge.ts +437 -0
- package/src/upgrade/gcp-lookup.ts +167 -0
- package/src/upgrade/health.test.ts +279 -0
- package/src/upgrade/health.ts +305 -0
- package/src/upgrade/release-images.test.ts +119 -0
- package/src/upgrade/release-images.ts +391 -0
- package/src/upgrade/reporter.ts +46 -0
- package/src/upgrade/upgrade.test.ts +626 -0
- package/src/upgrade/upgrade.ts +249 -0
- package/src/upgrader-assess.test.ts +265 -0
- package/src/upgrader-assess.ts +233 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +408 -0
- package/src/upgrader-job.ts +499 -0
- package/src/upgrader-reporter.test.ts +46 -0
- package/src/upgrader-reporter.ts +105 -0
- package/src/upgrader-setup.test.ts +169 -0
- package/src/upgrader-setup.ts +355 -0
- package/src/wizard.ts +425 -0
- package/src/wizards/gcp-cost.ts +188 -0
- package/src/wizards/gcp-firebase.ts +72 -0
- package/src/wizards/gcp-install-job.ts +506 -0
- package/src/wizards/gcp-prompts.ts +26 -0
- package/src/wizards/gcp-pulumi.test.ts +246 -0
- package/src/wizards/gcp-pulumi.ts +516 -0
- package/src/wizards/gcp.ts +1935 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keeps secret material out of the files the CLI writes under
|
|
5
|
+
* `~/.caelo-<install-id>/` (#656).
|
|
6
|
+
*
|
|
7
|
+
* Only `secrets/` may hold secrets (CLAUDE.md §7, §11.C); `progress.json`
|
|
8
|
+
* and `install.json` are plain bookkeeping that tooling, backups, sync
|
|
9
|
+
* clients and agents read. Installs from before #576 checkpointed the full
|
|
10
|
+
* Pulumi outputs, whose database URLs carried admin_role's and
|
|
11
|
+
* public_role's passwords, and every install so far checkpointed the Owner
|
|
12
|
+
* setup link with its token. This module
|
|
13
|
+
*
|
|
14
|
+
* - finds secret material in a JSON value ({@link findSecrets}), which
|
|
15
|
+
* `markStepDone` / `writeMetadata` use to refuse writing it;
|
|
16
|
+
* - removes it from an existing checkpoint ({@link scrubProgressFile}),
|
|
17
|
+
* which `upgrade` and wizard re-runs call, and names the secrets to
|
|
18
|
+
* rotate ({@link rotationHints});
|
|
19
|
+
* - masks credentials in text the CLI prints ({@link redactCredentials}).
|
|
20
|
+
*
|
|
21
|
+
* Detection is by shape (a URL with a password, a URL with a token-like
|
|
22
|
+
* query parameter) and by key name (password, secret, token, ...), so a
|
|
23
|
+
* future secret output is caught without being listed here.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { selfCommand } from "./self-command.js";
|
|
28
|
+
import { DATABASE_ROLE_SECRET, type DatabaseRole } from "./stack-contract.js";
|
|
29
|
+
|
|
30
|
+
/** One place a JSON value holds secret material. Never carries the secret itself. */
|
|
31
|
+
export interface SecretFinding {
|
|
32
|
+
/** Dotted path to the offending value, e.g. `steps.pulumi-up-acme.outputs.adminDatabaseUrlOut`. */
|
|
33
|
+
readonly path: string;
|
|
34
|
+
/**
|
|
35
|
+
* `credential-url`: a URL whose user-info carries a password.
|
|
36
|
+
* `token-url`: a URL with a token / secret / password query parameter.
|
|
37
|
+
* `secret-key`: a key whose name marks its value as secret.
|
|
38
|
+
*/
|
|
39
|
+
readonly kind: "credential-url" | "token-url" | "secret-key";
|
|
40
|
+
/** For a credential URL: the user whose password it carries. */
|
|
41
|
+
readonly user?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `scheme://user:password@` — the user-info of a URL that carries a password. */
|
|
45
|
+
const CREDENTIAL_URL = /[a-z][a-z0-9+.-]{0,31}:\/\/([^\s:@/]*):([^\s@/]+)@/gi;
|
|
46
|
+
|
|
47
|
+
/** A URL query parameter that carries a credential (the Owner setup link's `?token=`). */
|
|
48
|
+
const TOKEN_PARAM =
|
|
49
|
+
/[?&](?:token|secret|password|passwd|pwd|api[_-]?key|access[_-]?key|sig|signature)=[^&\s#]+/i;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Key names whose value is a secret, after lower-casing, dropping non
|
|
53
|
+
* alphanumerics and a Pulumi `Out` suffix: `postgresPassword`,
|
|
54
|
+
* `caeloSecretKek`, `bootstrap_token`, `anthropicApiKey`.
|
|
55
|
+
*/
|
|
56
|
+
const SECRET_KEY = /(password|passwd|passphrase|secret|secretkek|kek|token|apikey|privatekey)$/;
|
|
57
|
+
|
|
58
|
+
function isSecretKey(key: string): boolean {
|
|
59
|
+
const k = key
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[^a-z0-9]/g, "")
|
|
62
|
+
.replace(/out$/, "");
|
|
63
|
+
return SECRET_KEY.test(k);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function urlUser(raw: string): string {
|
|
67
|
+
try {
|
|
68
|
+
return decodeURIComponent(raw);
|
|
69
|
+
} catch {
|
|
70
|
+
return raw;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stringFindings(value: string, path: string): SecretFinding[] {
|
|
75
|
+
const found: SecretFinding[] = [];
|
|
76
|
+
for (const m of value.matchAll(CREDENTIAL_URL)) {
|
|
77
|
+
found.push({ path, kind: "credential-url", user: urlUser(m[1] ?? "") });
|
|
78
|
+
}
|
|
79
|
+
if (found.length === 0 && /:\/\//.test(value) && TOKEN_PARAM.test(value)) {
|
|
80
|
+
found.push({ path, kind: "token-url" });
|
|
81
|
+
}
|
|
82
|
+
return found;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Every place `value` holds secret material.
|
|
87
|
+
*
|
|
88
|
+
* @param value any JSON-shaped value (a checkpoint payload, install.json)
|
|
89
|
+
* @param path the path prefix to report findings under
|
|
90
|
+
*/
|
|
91
|
+
export function findSecrets(value: unknown, path = ""): SecretFinding[] {
|
|
92
|
+
if (typeof value === "string") return stringFindings(value, path || "(value)");
|
|
93
|
+
const join = (k: string | number) => (path ? `${path}.${k}` : String(k));
|
|
94
|
+
if (Array.isArray(value)) return value.flatMap((v, i) => findSecrets(v, join(i)));
|
|
95
|
+
if (value !== null && typeof value === "object") {
|
|
96
|
+
return Object.entries(value).flatMap(([k, v]) => {
|
|
97
|
+
// A secret-named key is only a finding when it holds something; a
|
|
98
|
+
// null / false / 0 marker is not secret material.
|
|
99
|
+
if (isSecretKey(k) && v !== null && v !== undefined && v !== "" && typeof v !== "boolean") {
|
|
100
|
+
return [{ path: join(k), kind: "secret-key" as const }];
|
|
101
|
+
}
|
|
102
|
+
return findSecrets(v, join(k));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Throw when `value` holds secret material. The message names the paths,
|
|
110
|
+
* never the values, so the error itself cannot leak them.
|
|
111
|
+
*
|
|
112
|
+
* @param what what was about to be written, for the message
|
|
113
|
+
*/
|
|
114
|
+
export function assertNoSecrets(value: unknown, what: string): void {
|
|
115
|
+
refuseFindings(findSecrets(value), what);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** {@link assertNoSecrets} for a whole checkpoint (see {@link findCheckpointSecrets}). */
|
|
119
|
+
export function assertNoCheckpointSecrets(checkpoint: unknown, what: string): void {
|
|
120
|
+
refuseFindings(findCheckpointSecrets(checkpoint), what);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function refuseFindings(found: readonly SecretFinding[], what: string): void {
|
|
124
|
+
if (found.length === 0) return;
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Refusing to write secret material to ${what} (${found.map((f) => `${f.path}: ${f.kind}`).join(", ")}). ` +
|
|
127
|
+
"Secrets belong in Secret Manager or the install's secrets/ directory; read them at the moment of use instead (#656).",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
132
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* {@link findSecrets} for a progress.json checkpoint. Its step names are
|
|
137
|
+
* derived from operator input (`project-<project id>`), so they are
|
|
138
|
+
* identifiers, not field names: a project called `acme-token` must not read
|
|
139
|
+
* as a secret. Only the step payloads (and the other top-level fields) are
|
|
140
|
+
* inspected.
|
|
141
|
+
*/
|
|
142
|
+
export function findCheckpointSecrets(checkpoint: unknown): SecretFinding[] {
|
|
143
|
+
if (!isRecord(checkpoint)) return findSecrets(checkpoint);
|
|
144
|
+
const { steps, ...rest } = checkpoint;
|
|
145
|
+
const inSteps = isRecord(steps)
|
|
146
|
+
? Object.entries(steps).flatMap(([name, payload]) => findSecrets(payload, `steps.${name}`))
|
|
147
|
+
: findSecrets(steps, "steps");
|
|
148
|
+
return [...findSecrets(rest), ...inSteps];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** {@link withoutSecrets} for a checkpoint: every step is kept, its payload cleaned. */
|
|
152
|
+
function checkpointWithoutSecrets(checkpoint: unknown): unknown {
|
|
153
|
+
if (!isRecord(checkpoint)) return withoutSecrets(checkpoint);
|
|
154
|
+
const { steps, ...rest } = checkpoint;
|
|
155
|
+
return {
|
|
156
|
+
...(withoutSecrets(rest) as Record<string, unknown>),
|
|
157
|
+
steps: isRecord(steps)
|
|
158
|
+
? Object.fromEntries(Object.entries(steps).map(([n, p]) => [n, withoutSecrets(p)]))
|
|
159
|
+
: withoutSecrets(steps),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* `value` without its secret material: secret-named keys and strings that
|
|
165
|
+
* hold a credential or token URL are dropped. Everything else is kept.
|
|
166
|
+
*/
|
|
167
|
+
export function withoutSecrets(value: unknown): unknown {
|
|
168
|
+
if (typeof value === "string") return value;
|
|
169
|
+
if (Array.isArray(value)) {
|
|
170
|
+
return value
|
|
171
|
+
.filter((v) => typeof v !== "string" || findSecrets(v).length === 0)
|
|
172
|
+
.map(withoutSecrets);
|
|
173
|
+
}
|
|
174
|
+
if (value !== null && typeof value === "object") {
|
|
175
|
+
const out: Record<string, unknown> = {};
|
|
176
|
+
for (const [k, v] of Object.entries(value)) {
|
|
177
|
+
if (findSecrets({ [k]: v }).length > 0 && (isSecretKey(k) || typeof v === "string")) continue;
|
|
178
|
+
out[k] = withoutSecrets(v);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The Pulumi outputs worth checkpointing: everything except outputs Pulumi
|
|
187
|
+
* marks secret and anything else that holds secret material (the Owner
|
|
188
|
+
* setup link). Nothing reads secrets back from the checkpoint; code that
|
|
189
|
+
* needs one reads the stack (`readStackOutput`) when it needs it.
|
|
190
|
+
*
|
|
191
|
+
* @param outputs the stack outputs as the Automation API returns them
|
|
192
|
+
*/
|
|
193
|
+
export function checkpointableOutputs(
|
|
194
|
+
outputs: Readonly<Record<string, { value: unknown; secret?: boolean }>>,
|
|
195
|
+
): Record<string, unknown> {
|
|
196
|
+
const kept: Record<string, unknown> = {};
|
|
197
|
+
for (const [k, o] of Object.entries(outputs)) {
|
|
198
|
+
if (o.secret) continue;
|
|
199
|
+
if (findSecrets({ [k]: o.value }).length > 0) continue;
|
|
200
|
+
kept[k] = o.value;
|
|
201
|
+
}
|
|
202
|
+
return kept;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Outcome of {@link scrubProgressFile}. */
|
|
206
|
+
export interface ScrubReport {
|
|
207
|
+
/** What was removed (paths + kinds, never values). Empty when the file was clean or absent. */
|
|
208
|
+
readonly removed: readonly SecretFinding[];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Remove secret material from a `progress.json` in place. The rewrite is
|
|
213
|
+
* atomic (temp file in the same directory, then rename) and the result is
|
|
214
|
+
* mode 0600, so an interrupted scrub never leaves a truncated checkpoint
|
|
215
|
+
* and wizard re-runs still find every completed step.
|
|
216
|
+
*
|
|
217
|
+
* @param path the checkpoint file; a missing file is a no-op
|
|
218
|
+
*/
|
|
219
|
+
export function scrubProgressFile(path: string): ScrubReport {
|
|
220
|
+
if (!existsSync(path)) return { removed: [] };
|
|
221
|
+
const checkpoint: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
222
|
+
const removed = findCheckpointSecrets(checkpoint);
|
|
223
|
+
if (removed.length === 0) return { removed };
|
|
224
|
+
writeJsonAtomic(path, checkpointWithoutSecrets(checkpoint));
|
|
225
|
+
return { removed };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Write `value` as JSON to `path` atomically with mode 0600: a temp file in
|
|
230
|
+
* the same directory (so the rename stays on one filesystem), then rename.
|
|
231
|
+
*/
|
|
232
|
+
export function writeJsonAtomic(path: string, value: unknown): void {
|
|
233
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
234
|
+
try {
|
|
235
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
236
|
+
renameSync(tmp, path);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
rmSync(tmp, { force: true });
|
|
239
|
+
throw e;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const ROLE_BY_USER = DATABASE_ROLE_SECRET as Readonly<Record<string, string | undefined>>;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* What the operator should do about secrets a scrub removed: one line per
|
|
247
|
+
* affected secret, with the exact command where one exists.
|
|
248
|
+
*
|
|
249
|
+
* @param removed the scrub's findings
|
|
250
|
+
* @param installId the install, named in each command so it acts on the right one
|
|
251
|
+
*/
|
|
252
|
+
export function rotationHints(removed: readonly SecretFinding[], installId: string): string[] {
|
|
253
|
+
const rotate = (secret: string) => selfCommand("rotate-secret", secret, "--install", installId);
|
|
254
|
+
const hints = new Set<string>();
|
|
255
|
+
for (const f of removed) {
|
|
256
|
+
if (f.kind === "credential-url") {
|
|
257
|
+
const secret = f.user ? ROLE_BY_USER[f.user as DatabaseRole] : undefined;
|
|
258
|
+
hints.add(
|
|
259
|
+
secret
|
|
260
|
+
? `${f.user}'s password was stored in progress.json. Rotate it: ${rotate(secret)}`
|
|
261
|
+
: `A password for database user "${f.user ?? "?"}" was stored in progress.json (${f.path}); change it.`,
|
|
262
|
+
);
|
|
263
|
+
} else if (f.kind === "token-url") {
|
|
264
|
+
hints.add(
|
|
265
|
+
"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.",
|
|
266
|
+
);
|
|
267
|
+
} else {
|
|
268
|
+
hints.add(
|
|
269
|
+
`A secret value was stored in progress.json (${f.path}). Rotate whatever it belongs to.`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return [...hints];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* `text` with every URL password masked (`postgresql://admin_role:***@`),
|
|
278
|
+
* for anything the CLI prints that could carry one (errors from Pulumi or
|
|
279
|
+
* gcloud, for instance). Same masking as the API gateway's error log.
|
|
280
|
+
*/
|
|
281
|
+
export function redactCredentials(text: string): string {
|
|
282
|
+
return text.replace(/([a-z][a-z0-9+.-]{0,31}:\/\/[^\s:@/]*):[^\s@/]+@/gi, "$1:***@");
|
|
283
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import { initDelegatesToWizard, resolveCliRoute } from "./cli-routing.js";
|
|
5
|
+
|
|
6
|
+
const HANDLERS = ["init", "upgrade", "wizard", "version", "--version", "-v"];
|
|
7
|
+
const argv = (...rest: string[]) => ["bun", "cli.js", ...rest];
|
|
8
|
+
|
|
9
|
+
describe("resolveCliRoute", () => {
|
|
10
|
+
it("routes the documented flags-only invocation to the wizard", () => {
|
|
11
|
+
expect(
|
|
12
|
+
resolveCliRoute(argv("--provider", "gcp-firebase", "--domain", "example.com"), HANDLERS),
|
|
13
|
+
).toEqual({ kind: "wizard" });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("routes a bare invocation to the wizard", () => {
|
|
17
|
+
expect(resolveCliRoute(argv(), HANDLERS)).toEqual({ kind: "wizard" });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("dispatches known sub-commands, including flag-shaped ones", () => {
|
|
21
|
+
expect(resolveCliRoute(argv("upgrade", "--channel", "rc"), HANDLERS)).toEqual({
|
|
22
|
+
kind: "handler",
|
|
23
|
+
name: "upgrade",
|
|
24
|
+
});
|
|
25
|
+
expect(resolveCliRoute(argv("--version"), HANDLERS)).toEqual({
|
|
26
|
+
kind: "handler",
|
|
27
|
+
name: "--version",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("prints usage for unknown sub-commands, --no-wizard and --help", () => {
|
|
32
|
+
expect(resolveCliRoute(argv("deploy"), HANDLERS)).toEqual({ kind: "usage" });
|
|
33
|
+
expect(resolveCliRoute(argv("--no-wizard"), HANDLERS)).toEqual({ kind: "usage" });
|
|
34
|
+
expect(resolveCliRoute(argv("--help"), HANDLERS)).toEqual({ kind: "usage" });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("initDelegatesToWizard", () => {
|
|
39
|
+
it("hands every cloud provider to the wizard and keeps self-hosted on init", () => {
|
|
40
|
+
for (const p of ["gcp", "gcp-firebase", "aws", "azure"]) {
|
|
41
|
+
expect(initDelegatesToWizard(p)).toBe(true);
|
|
42
|
+
}
|
|
43
|
+
expect(initDelegatesToWizard("self-hosted")).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Argv → command routing for `cms-provision`, kept free of side effects so
|
|
5
|
+
* it can be unit-tested (cli.ts dispatches at module load).
|
|
6
|
+
*
|
|
7
|
+
* The documented one-command entry point (CLAUDE.md §11.C) is
|
|
8
|
+
* `bunx @caelo-cms/provisioning --provider gcp --domain …` — flags only,
|
|
9
|
+
* no sub-command. That shape must reach the wizard; treating the first
|
|
10
|
+
* flag as an unknown sub-command silently prints usage instead.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** What the CLI should do for a given argv. */
|
|
14
|
+
export type CliRoute = { kind: "handler"; name: string } | { kind: "wizard" } | { kind: "usage" };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the route for `argv` (a full `process.argv`, so the sub-command
|
|
18
|
+
* sits at index 2).
|
|
19
|
+
*
|
|
20
|
+
* @param handlerNames - registered sub-commands; flag-shaped names such as
|
|
21
|
+
* `--version` win over the wizard fallback.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveCliRoute(argv: readonly string[], handlerNames: Iterable<string>): CliRoute {
|
|
24
|
+
const first = argv[2];
|
|
25
|
+
if (first !== undefined && new Set(handlerNames).has(first)) {
|
|
26
|
+
return { kind: "handler", name: first };
|
|
27
|
+
}
|
|
28
|
+
const flagsOnly = first === undefined || first.startsWith("-");
|
|
29
|
+
if (!flagsOnly) return { kind: "usage" };
|
|
30
|
+
if (argv.includes("--no-wizard") || argv.includes("--help") || argv.includes("-h")) {
|
|
31
|
+
return { kind: "usage" };
|
|
32
|
+
}
|
|
33
|
+
return { kind: "wizard" };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether `init --provider <p>` belongs to the wizard. Only self-hosted
|
|
38
|
+
* keeps the compose-file `init` flow; every cloud provider is provisioned
|
|
39
|
+
* end-to-end by the wizard rather than by printed manual Pulumi steps.
|
|
40
|
+
*/
|
|
41
|
+
export function initDelegatesToWizard(provider: string): boolean {
|
|
42
|
+
return provider !== "self-hosted";
|
|
43
|
+
}
|