@clearance/cli 0.1.4
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/LICENSE +29 -0
- package/LICENSE.md +20 -0
- package/dist/api-client.d.ts +18 -0
- package/dist/api-client.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2494 -0
- package/dist/operator-auth.d.ts +28 -0
- package/dist/operator-auth.js +310 -0
- package/dist/ops/deploy/compose/docker-compose.production.yml +129 -0
- package/dist/ops/deploy/upgrades/README.md +24 -0
- package/dist/ops/deploy/upgrades/steps/0.2.0/apply.sh +62 -0
- package/dist/ops/scripts/backup-create.sh +197 -0
- package/dist/ops/scripts/backup-restore-verify.sh +129 -0
- package/dist/ops/scripts/backup-verify.sh +121 -0
- package/dist/ops/scripts/lib/ops-common.sh +375 -0
- package/dist/ops/scripts/scim-legacy-preflight.sh +103 -0
- package/dist/ops/scripts/upgrade-apply.sh +168 -0
- package/dist/ops/scripts/upgrade-plan.sh +129 -0
- package/dist/ops/scripts/upgrade-preflight.sh +136 -0
- package/dist/ops/scripts/upgrade-rollback.sh +394 -0
- package/dist/ops/scripts/upgrade-verify.sh +98 -0
- package/dist/ops/scripts/validate-production-env.sh +232 -0
- package/dist/output.d.ts +19 -0
- package/dist/output.js +52 -0
- package/dist/remote-dispatch.d.ts +10 -0
- package/dist/remote-dispatch.js +232 -0
- package/dist/store.d.ts +9 -0
- package/dist/store.js +43 -0
- package/dist/upgrade.d.ts +114 -0
- package/dist/upgrade.js +262 -0
- package/package.json +38 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Post-apply upgrade verification: plan state, backup reference, schema health, optional HTTP.
|
|
3
|
+
set -Eeuo pipefail
|
|
4
|
+
|
|
5
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
6
|
+
# shellcheck source=lib/ops-common.sh
|
|
7
|
+
source "$ROOT/scripts/lib/ops-common.sh"
|
|
8
|
+
|
|
9
|
+
usage() {
|
|
10
|
+
cat <<'EOF'
|
|
11
|
+
Usage: upgrade-verify.sh --plan PLAN_ID_OR_PATH [--dir DIR] [--health-url URL]
|
|
12
|
+
EOF
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
PLAN_DIR="${CLEARANCE_UPGRADE_DIR:-$ROOT/.clearance/upgrades}"
|
|
16
|
+
PLAN_REF=""
|
|
17
|
+
HEALTH_URL="${CLEARANCE_HEALTH_URL:-}"
|
|
18
|
+
|
|
19
|
+
while [[ $# -gt 0 ]]; do
|
|
20
|
+
case "$1" in
|
|
21
|
+
--plan) PLAN_REF="$2"; shift 2 ;;
|
|
22
|
+
--dir) PLAN_DIR="$2"; shift 2 ;;
|
|
23
|
+
--health-url) HEALTH_URL="$2"; shift 2 ;;
|
|
24
|
+
-h|--help) usage; exit 0 ;;
|
|
25
|
+
*) die "unknown argument: $1" ;;
|
|
26
|
+
esac
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
[[ -n "$PLAN_REF" ]] || die "--plan is required"
|
|
30
|
+
if [[ -f "$PLAN_REF" ]]; then
|
|
31
|
+
PLAN_PATH="$PLAN_REF"
|
|
32
|
+
else
|
|
33
|
+
PLAN_PATH="$PLAN_DIR/${PLAN_REF}.plan.json"
|
|
34
|
+
fi
|
|
35
|
+
[[ -f "$PLAN_PATH" ]] || die "plan not found"
|
|
36
|
+
require_cmd node
|
|
37
|
+
|
|
38
|
+
PLAN_ID="$(node -e 'const j=require(process.argv[1]); process.stdout.write(j.planId)' "$PLAN_PATH")"
|
|
39
|
+
STATE_PATH="$PLAN_DIR/${PLAN_ID}.state.json"
|
|
40
|
+
[[ -f "$STATE_PATH" ]] || die "state missing"
|
|
41
|
+
|
|
42
|
+
STATUS="$(node -e 'const j=require(process.argv[1]); process.stdout.write(j.status||"")' "$STATE_PATH")"
|
|
43
|
+
[[ "$STATUS" == "applied" || "$STATUS" == "verified" ]] || die "plan state is '$STATUS' (expected applied|verified)"
|
|
44
|
+
|
|
45
|
+
BACKUP_ID="$(node -e 'const j=require(process.argv[1]); process.stdout.write(j.backupId||"")' "$STATE_PATH")"
|
|
46
|
+
[[ -n "$BACKUP_ID" ]] || die "missing backupId rollback reference in state"
|
|
47
|
+
BACKUP_META="$(node -e 'const j=require(process.argv[1]); process.stdout.write(j.backupMeta||"")' "$STATE_PATH")"
|
|
48
|
+
[[ -f "$BACKUP_META" ]] || die "backup meta missing: $BACKUP_META"
|
|
49
|
+
bash "$ROOT/scripts/backup-verify.sh" --dump "$(node -e 'const j=require(process.argv[1]); process.stdout.write(j.backupDump)' "$STATE_PATH")" --meta "$BACKUP_META" >/dev/null
|
|
50
|
+
|
|
51
|
+
# Schema still queryable
|
|
52
|
+
if [[ -n "${DATABASE_URL:-}" ]]; then
|
|
53
|
+
require_pg_client
|
|
54
|
+
URL="$(resolve_database_url)"
|
|
55
|
+
psql_q "$URL" -c 'SELECT 1' >/dev/null || die "post-apply database health query failed"
|
|
56
|
+
TARGET="$(node -e 'const j=require(process.argv[1]); process.stdout.write(j.targetVersion)' "$PLAN_PATH")"
|
|
57
|
+
require_application_release "$URL" "$TARGET"
|
|
58
|
+
assert_safe_version "$TARGET" "target version"
|
|
59
|
+
LEDGER_PLAN="$(psql_q "$URL" -At -c "SELECT plan_id FROM clearance_schema_migrations WHERE version = '${TARGET}'")"
|
|
60
|
+
[[ "$LEDGER_PLAN" == "$PLAN_ID" ]] || die "migration ledger missing plan/version transition"
|
|
61
|
+
# Fingerprint may change after real migrations; ensure DB still has public schema access
|
|
62
|
+
EV="$(mktemp "${TMPDIR:-/tmp}/clearance-post-ev.XXXXXX")"
|
|
63
|
+
collect_db_evidence "$URL" "$EV"
|
|
64
|
+
[[ -s "$EV" ]] || die "failed to collect post-apply schema evidence"
|
|
65
|
+
rm -f "$EV"
|
|
66
|
+
printf 'upgrade-verify: database evidence collected\n' >&2
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Optional HTTP health
|
|
70
|
+
if [[ -n "$HEALTH_URL" ]]; then
|
|
71
|
+
require_cmd curl
|
|
72
|
+
code="$(curl -sS -o /dev/null -w '%{http_code}' "$HEALTH_URL" || true)"
|
|
73
|
+
[[ "$code" == "200" ]] || die "health URL $HEALTH_URL returned HTTP $code"
|
|
74
|
+
printf 'upgrade-verify: health URL OK\n' >&2
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Applied marker exists
|
|
78
|
+
MARKER="$PLAN_DIR/markers/${PLAN_ID}.applied"
|
|
79
|
+
[[ -f "$MARKER" ]] || die "apply marker missing"
|
|
80
|
+
|
|
81
|
+
TARGET="$(node -e 'const j=require(process.argv[1]); process.stdout.write(j.targetVersion)' "$PLAN_PATH")"
|
|
82
|
+
MARKER_TO="$(node -e 'const fs=require("fs"); const j=JSON.parse(fs.readFileSync(process.argv[1],"utf8")); process.stdout.write(j.toVersion||"")' "$MARKER")"
|
|
83
|
+
[[ "$MARKER_TO" == "$TARGET" ]] || die "marker version mismatch"
|
|
84
|
+
|
|
85
|
+
node -e '
|
|
86
|
+
const fs=require("fs");
|
|
87
|
+
const p=process.argv[1];
|
|
88
|
+
const j=JSON.parse(fs.readFileSync(p,"utf8"));
|
|
89
|
+
const at=new Date().toISOString().replace(/\.\d{3}Z$/,"Z");
|
|
90
|
+
j.status="verified";
|
|
91
|
+
j.updatedAt=at;
|
|
92
|
+
j.applyJournal=j.applyJournal||[];
|
|
93
|
+
j.applyJournal.push({at, event:"verified"});
|
|
94
|
+
fs.writeFileSync(p, JSON.stringify(j,null,2)+"\n");
|
|
95
|
+
' "$STATE_PATH"
|
|
96
|
+
|
|
97
|
+
printf 'upgrade-verify: OK plan=%s target=%s\n' "$PLAN_ID" "$TARGET"
|
|
98
|
+
printf 'VERIFY_OK=1\n'
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Fail-closed production environment validation for Compose/Helm/TF operators.
|
|
3
|
+
# Does not print secret values. Exits non-zero on any missing/weak/default secret.
|
|
4
|
+
set -Eeuo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
7
|
+
# shellcheck source=lib/ops-common.sh
|
|
8
|
+
source "$ROOT/scripts/lib/ops-common.sh"
|
|
9
|
+
require_cmd node
|
|
10
|
+
|
|
11
|
+
usage() {
|
|
12
|
+
cat <<'EOF'
|
|
13
|
+
Usage: validate-production-env.sh
|
|
14
|
+
|
|
15
|
+
Validates operator-supplied environment for production Compose overlay:
|
|
16
|
+
deploy/compose/docker-compose.production.yml
|
|
17
|
+
|
|
18
|
+
Required (strong secrets, no defaults):
|
|
19
|
+
CLEARANCE_OPERATOR_TOKEN
|
|
20
|
+
CLEARANCE_SECRET
|
|
21
|
+
CLEARANCE_CREDENTIAL_KEY
|
|
22
|
+
CLEARANCE_CREDENTIAL_KEY_ID
|
|
23
|
+
CLEARANCE_CONSOLE_ADMIN_USER
|
|
24
|
+
CLEARANCE_CONSOLE_ADMIN_PASSWORD
|
|
25
|
+
CLEARANCE_CONSOLE_SESSION_SECRET
|
|
26
|
+
CLEARANCE_DB_USER
|
|
27
|
+
CLEARANCE_DB_PASSWORD
|
|
28
|
+
CLEARANCE_DB_NAME
|
|
29
|
+
DATABASE_URL # full postgres URL; no compose password interpolation
|
|
30
|
+
CLEARANCE_BASE_URL
|
|
31
|
+
CLEARANCE_CONSOLE_URL
|
|
32
|
+
CLEARANCE_CORS_ORIGINS
|
|
33
|
+
CLEARANCE_API_PORT
|
|
34
|
+
CLEARANCE_CONSOLE_PORT
|
|
35
|
+
CLEARANCE_SAMPLE_PORT
|
|
36
|
+
CLEARANCE_PG_VOLUME
|
|
37
|
+
CLEARANCE_BACKUP_VOLUME
|
|
38
|
+
CLEARANCE_IMAGE_REPOSITORY
|
|
39
|
+
CLEARANCE_IMAGE_DIGEST # sha256:... from the signed release
|
|
40
|
+
CLEARANCE_BACKUP_IMAGE_REPOSITORY
|
|
41
|
+
CLEARANCE_BACKUP_IMAGE_DIGEST # sha256:... from the signed release
|
|
42
|
+
|
|
43
|
+
Optional:
|
|
44
|
+
CLEARANCE_POSTGRES_PORT # only if intentionally publishing Postgres to the host
|
|
45
|
+
CLEARANCE_GITHUB_CLIENT_ID + CLEARANCE_GITHUB_CLIENT_SECRET
|
|
46
|
+
CLEARANCE_GOOGLE_CLIENT_ID + CLEARANCE_GOOGLE_CLIENT_SECRET
|
|
47
|
+
|
|
48
|
+
Fails closed on missing, empty, short, or known-weak values.
|
|
49
|
+
EOF
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
53
|
+
usage
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
errors=0
|
|
58
|
+
note() { printf 'ok: %s\n' "$*"; }
|
|
59
|
+
fail() { printf 'fail: %s\n' "$*" >&2; errors=$((errors + 1)); }
|
|
60
|
+
|
|
61
|
+
check_secret() {
|
|
62
|
+
local label="$1"
|
|
63
|
+
local value="${2-}"
|
|
64
|
+
if is_forbidden_secret "$value"; then
|
|
65
|
+
fail "$label is missing, empty, short (<16), or a known weak default"
|
|
66
|
+
else
|
|
67
|
+
note "$label present and not a known weak default (len=${#value})"
|
|
68
|
+
fi
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
check_present() {
|
|
72
|
+
local label="$1"
|
|
73
|
+
local value="${2-}"
|
|
74
|
+
if [[ -z "$value" ]]; then
|
|
75
|
+
fail "$label is required and must be non-empty"
|
|
76
|
+
else
|
|
77
|
+
note "$label is set"
|
|
78
|
+
fi
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
check_port() {
|
|
82
|
+
local label="$1"
|
|
83
|
+
local value="$2"
|
|
84
|
+
if [[ ! "$value" =~ ^[0-9]+$ ]] || (( 10#$value < 1 || 10#$value > 65535 )); then
|
|
85
|
+
fail "$label must be an integer from 1 through 65535"
|
|
86
|
+
else
|
|
87
|
+
note "$label is a valid TCP port"
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
check_https_url() {
|
|
92
|
+
local label="$1"
|
|
93
|
+
local value="$2"
|
|
94
|
+
if [[ "$value" =~ ^https://[^[:space:]]+$ ]]; then
|
|
95
|
+
note "$label uses HTTPS"
|
|
96
|
+
elif [[ "${CLEARANCE_ALLOW_LOCALHOST_PRODUCTION:-}" == "1" \
|
|
97
|
+
&& "$value" =~ ^http://(localhost|127\.0\.0\.1)(:[0-9]+)?(/.*)?$ ]]; then
|
|
98
|
+
note "$label uses explicitly allowed local HTTP"
|
|
99
|
+
else
|
|
100
|
+
fail "$label must be an absolute HTTPS URL (local HTTP requires CLEARANCE_ALLOW_LOCALHOST_PRODUCTION=1)"
|
|
101
|
+
fi
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Secrets / credentials
|
|
105
|
+
check_secret CLEARANCE_OPERATOR_TOKEN "${CLEARANCE_OPERATOR_TOKEN-}"
|
|
106
|
+
check_secret CLEARANCE_SECRET "${CLEARANCE_SECRET-}"
|
|
107
|
+
check_secret CLEARANCE_CREDENTIAL_KEY "${CLEARANCE_CREDENTIAL_KEY-}"
|
|
108
|
+
check_present CLEARANCE_CREDENTIAL_KEY_ID "${CLEARANCE_CREDENTIAL_KEY_ID-}"
|
|
109
|
+
check_present CLEARANCE_CONSOLE_ADMIN_USER "${CLEARANCE_CONSOLE_ADMIN_USER-}"
|
|
110
|
+
check_secret CLEARANCE_CONSOLE_ADMIN_PASSWORD "${CLEARANCE_CONSOLE_ADMIN_PASSWORD-}"
|
|
111
|
+
check_secret CLEARANCE_CONSOLE_SESSION_SECRET "${CLEARANCE_CONSOLE_SESSION_SECRET-}"
|
|
112
|
+
check_secret CLEARANCE_DB_PASSWORD "${CLEARANCE_DB_PASSWORD-}"
|
|
113
|
+
|
|
114
|
+
# Non-secret required production knobs (no compose defaults in overlay)
|
|
115
|
+
check_present CLEARANCE_DB_USER "${CLEARANCE_DB_USER-}"
|
|
116
|
+
check_present CLEARANCE_DB_NAME "${CLEARANCE_DB_NAME-}"
|
|
117
|
+
check_present CLEARANCE_BASE_URL "${CLEARANCE_BASE_URL-}"
|
|
118
|
+
check_present CLEARANCE_CONSOLE_URL "${CLEARANCE_CONSOLE_URL-}"
|
|
119
|
+
check_present CLEARANCE_CORS_ORIGINS "${CLEARANCE_CORS_ORIGINS-}"
|
|
120
|
+
check_port CLEARANCE_API_PORT "${CLEARANCE_API_PORT-}"
|
|
121
|
+
check_port CLEARANCE_CONSOLE_PORT "${CLEARANCE_CONSOLE_PORT-}"
|
|
122
|
+
check_port CLEARANCE_SAMPLE_PORT "${CLEARANCE_SAMPLE_PORT-}"
|
|
123
|
+
check_present CLEARANCE_PG_VOLUME "${CLEARANCE_PG_VOLUME-}"
|
|
124
|
+
check_present CLEARANCE_BACKUP_VOLUME "${CLEARANCE_BACKUP_VOLUME-}"
|
|
125
|
+
check_present CLEARANCE_IMAGE_REPOSITORY "${CLEARANCE_IMAGE_REPOSITORY-}"
|
|
126
|
+
check_present CLEARANCE_BACKUP_IMAGE_REPOSITORY "${CLEARANCE_BACKUP_IMAGE_REPOSITORY-}"
|
|
127
|
+
if [[ "${CLEARANCE_IMAGE_DIGEST-}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
|
|
128
|
+
note "CLEARANCE_IMAGE_DIGEST is an immutable sha256 digest"
|
|
129
|
+
else
|
|
130
|
+
fail "CLEARANCE_IMAGE_DIGEST must be sha256 followed by 64 lowercase hex characters"
|
|
131
|
+
fi
|
|
132
|
+
if [[ "${CLEARANCE_BACKUP_IMAGE_DIGEST-}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
|
|
133
|
+
note "CLEARANCE_BACKUP_IMAGE_DIGEST is an immutable sha256 digest"
|
|
134
|
+
else
|
|
135
|
+
fail "CLEARANCE_BACKUP_IMAGE_DIGEST must be sha256 followed by 64 lowercase hex characters"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
check_optional_pair() {
|
|
139
|
+
local provider="$1"
|
|
140
|
+
local client_id="$2"
|
|
141
|
+
local client_secret="$3"
|
|
142
|
+
if [[ -n "$client_id" && -n "$client_secret" ]]; then
|
|
143
|
+
check_secret "${provider} client secret" "$client_secret"
|
|
144
|
+
note "${provider} social credentials are configured as a complete pair"
|
|
145
|
+
elif [[ -n "$client_id" || -n "$client_secret" ]]; then
|
|
146
|
+
fail "${provider} social credentials must set both client id and client secret"
|
|
147
|
+
else
|
|
148
|
+
note "${provider} social provider is disabled"
|
|
149
|
+
fi
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
check_optional_pair GitHub "${CLEARANCE_GITHUB_CLIENT_ID-}" "${CLEARANCE_GITHUB_CLIENT_SECRET-}"
|
|
153
|
+
check_optional_pair Google "${CLEARANCE_GOOGLE_CLIENT_ID-}" "${CLEARANCE_GOOGLE_CLIENT_SECRET-}"
|
|
154
|
+
|
|
155
|
+
# DATABASE_URL: full string required — refuse weak defaults and incomplete forms.
|
|
156
|
+
if is_weak_database_url "${DATABASE_URL-}"; then
|
|
157
|
+
fail "DATABASE_URL missing, not a postgres URL, or uses weak/default credentials (redacted=$(redact_url "${DATABASE_URL-}"))"
|
|
158
|
+
else
|
|
159
|
+
note "DATABASE_URL looks like a non-default postgres URL (redacted=$(redact_url "$DATABASE_URL"))"
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# The database container credentials and application URL must describe the same
|
|
163
|
+
# user, password, and database. Compare via process environment; print no values.
|
|
164
|
+
if [[ -n "${DATABASE_URL-}" ]]; then
|
|
165
|
+
if DATABASE_URL_CHECK="$DATABASE_URL" \
|
|
166
|
+
EXPECT_DB_USER="${CLEARANCE_DB_USER-}" \
|
|
167
|
+
EXPECT_DB_PASSWORD="${CLEARANCE_DB_PASSWORD-}" \
|
|
168
|
+
EXPECT_DB_NAME="${CLEARANCE_DB_NAME-}" \
|
|
169
|
+
node -e '
|
|
170
|
+
const u=new URL(process.env.DATABASE_URL_CHECK);
|
|
171
|
+
if(!/^postgres(ql)?:$/.test(u.protocol)) process.exit(1);
|
|
172
|
+
if(decodeURIComponent(u.username)!==process.env.EXPECT_DB_USER) process.exit(2);
|
|
173
|
+
if(decodeURIComponent(u.password)!==process.env.EXPECT_DB_PASSWORD) process.exit(3);
|
|
174
|
+
if(decodeURIComponent(u.pathname.replace(/^\//,""))!==process.env.EXPECT_DB_NAME) process.exit(4);
|
|
175
|
+
' 2>/dev/null; then
|
|
176
|
+
note "DATABASE_URL credentials and database match Compose Postgres settings"
|
|
177
|
+
else
|
|
178
|
+
fail "DATABASE_URL user/password/database must match CLEARANCE_DB_USER/CLEARANCE_DB_PASSWORD/CLEARANCE_DB_NAME"
|
|
179
|
+
fi
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
check_https_url CLEARANCE_BASE_URL "${CLEARANCE_BASE_URL-}"
|
|
183
|
+
check_https_url CLEARANCE_CONSOLE_URL "${CLEARANCE_CONSOLE_URL-}"
|
|
184
|
+
IFS=',' read -r -a cors_origins <<<"${CLEARANCE_CORS_ORIGINS-}"
|
|
185
|
+
for origin in "${cors_origins[@]}"; do
|
|
186
|
+
check_https_url "CLEARANCE_CORS_ORIGINS entry" "${origin//[[:space:]]/}"
|
|
187
|
+
done
|
|
188
|
+
|
|
189
|
+
# Refuse localhost-only defaults that are fine for dev but not production profiles
|
|
190
|
+
if [[ "${CLEARANCE_BASE_URL-}" == *"localhost"* || "${CLEARANCE_BASE_URL-}" == *"127.0.0.1"* ]]; then
|
|
191
|
+
if [[ "${CLEARANCE_ALLOW_LOCALHOST_PRODUCTION:-}" != "1" ]]; then
|
|
192
|
+
fail "CLEARANCE_BASE_URL points at localhost; set CLEARANCE_ALLOW_LOCALHOST_PRODUCTION=1 only for intentional local prod-profile tests"
|
|
193
|
+
else
|
|
194
|
+
note "CLEARANCE_BASE_URL is localhost (explicitly allowed for local prod-profile tests)"
|
|
195
|
+
fi
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# NODE_ENV must not be development when operators claim production
|
|
199
|
+
if [[ "${NODE_ENV-}" == "development" || "${CLEARANCE_NODE_ENV-}" == "development" ]]; then
|
|
200
|
+
fail "NODE_ENV/CLEARANCE_NODE_ENV must not be 'development' for production validation"
|
|
201
|
+
else
|
|
202
|
+
note "NODE_ENV is not development"
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
# Compose production overlay must exist
|
|
206
|
+
overlay="$ROOT/deploy/compose/docker-compose.production.yml"
|
|
207
|
+
if [[ ! -f "$overlay" ]]; then
|
|
208
|
+
fail "missing production overlay: deploy/compose/docker-compose.production.yml"
|
|
209
|
+
else
|
|
210
|
+
# Static fail-closed markers in overlay
|
|
211
|
+
if grep -qE 'CLEARANCE_SECRET:-\$\{|:-dev|:-secret|:-change-me|:-clearance\}' "$overlay"; then
|
|
212
|
+
fail "production overlay appears to embed weak defaults"
|
|
213
|
+
fi
|
|
214
|
+
if grep -qE 'DATABASE_URL:-\$\{' "$overlay"; then
|
|
215
|
+
fail "production overlay must not construct DATABASE_URL from password parts"
|
|
216
|
+
fi
|
|
217
|
+
if ! grep -q 'DATABASE_URL: \${DATABASE_URL:?' "$overlay"; then
|
|
218
|
+
fail "production overlay must require DATABASE_URL with fail-closed \${DATABASE_URL:?...}"
|
|
219
|
+
fi
|
|
220
|
+
if ! grep -q 'NODE_ENV: production' "$overlay"; then
|
|
221
|
+
fail "production overlay must force NODE_ENV: production"
|
|
222
|
+
fi
|
|
223
|
+
note "production overlay present with fail-closed DATABASE_URL and NODE_ENV"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
if [[ "$errors" -ne 0 ]]; then
|
|
227
|
+
printf '\nvalidate-production-env: FAILED (%s checks)\n' "$errors" >&2
|
|
228
|
+
exit 1
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
printf '\nvalidate-production-env: OK\n'
|
|
232
|
+
exit 0
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ClearanceError } from "@clearance/management";
|
|
2
|
+
export interface GlobalOpts {
|
|
3
|
+
json?: boolean;
|
|
4
|
+
noInput?: boolean;
|
|
5
|
+
yes?: boolean;
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
dataPath?: string;
|
|
8
|
+
localDirect?: boolean;
|
|
9
|
+
profile?: string;
|
|
10
|
+
apiUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare class CliExitError extends Error {
|
|
13
|
+
readonly exitCode: number;
|
|
14
|
+
constructor(exitCode?: number);
|
|
15
|
+
}
|
|
16
|
+
export declare function printResult(opts: GlobalOpts, data: unknown, human?: string): void;
|
|
17
|
+
export declare function fail(err: unknown, opts: GlobalOpts): never;
|
|
18
|
+
export declare function exitCodeFromDoctor(ok: boolean): number;
|
|
19
|
+
export declare function asClearanceError(err: unknown): ClearanceError | null;
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { isClearanceError } from "@clearance/management";
|
|
2
|
+
export class CliExitError extends Error {
|
|
3
|
+
exitCode;
|
|
4
|
+
constructor(exitCode = 1) {
|
|
5
|
+
super("CLI command failed");
|
|
6
|
+
this.name = "CliExitError";
|
|
7
|
+
this.exitCode = exitCode;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function printResult(opts, data, human) {
|
|
11
|
+
if (opts.json) {
|
|
12
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (human) {
|
|
16
|
+
process.stdout.write(`${human}\n`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
20
|
+
}
|
|
21
|
+
export function fail(err, opts) {
|
|
22
|
+
// A CliExitError means fail() already emitted a structured document and is
|
|
23
|
+
// unwinding. Re-throw untouched so a catch block that funnels back into
|
|
24
|
+
// fail() can never emit a second JSON document on stdout (--json contract:
|
|
25
|
+
// exactly one document per invocation).
|
|
26
|
+
if (err instanceof CliExitError) {
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
if (isClearanceError(err)) {
|
|
30
|
+
if (opts.json) {
|
|
31
|
+
process.stdout.write(`${JSON.stringify(err.toJSON(), null, 2)}\n`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
process.stderr.write(`Error [${err.code}] stage=${err.stage}: ${err.message}\nRemediation: ${err.remediation}\n`);
|
|
35
|
+
}
|
|
36
|
+
throw new CliExitError(1);
|
|
37
|
+
}
|
|
38
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
+
if (opts.json) {
|
|
40
|
+
process.stdout.write(`${JSON.stringify({ error: { code: "INTERNAL", message, stage: "unknown", retryable: false } }, null, 2)}\n`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
44
|
+
}
|
|
45
|
+
throw new CliExitError(1);
|
|
46
|
+
}
|
|
47
|
+
export function exitCodeFromDoctor(ok) {
|
|
48
|
+
return ok ? 0 : 2;
|
|
49
|
+
}
|
|
50
|
+
export function asClearanceError(err) {
|
|
51
|
+
return isClearanceError(err) ? err : null;
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { type ApiSession } from "./api-client.js";
|
|
3
|
+
import type { GlobalOpts } from "./output.js";
|
|
4
|
+
export declare const HOST_LOCAL_COMMANDS: Map<string, string>;
|
|
5
|
+
export declare const REMOTE_COMMANDS: Set<string>;
|
|
6
|
+
export type CommandExecution = "authentication" | "remote-api" | "host-local" | "unavailable";
|
|
7
|
+
export declare function classifyCommandPath(path: string): CommandExecution;
|
|
8
|
+
export declare function commandPath(command: Command): string;
|
|
9
|
+
export declare function isHostLocalCommand(path: string): boolean;
|
|
10
|
+
export declare function dispatchRemoteCommand(session: ApiSession, path: string, args: unknown[], opts: Record<string, unknown>, global: GlobalOpts): Promise<unknown>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { ClearanceError } from "@clearance/management";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { requestManagementApi } from "./api-client.js";
|
|
5
|
+
export const HOST_LOCAL_COMMANDS = new Map([
|
|
6
|
+
["dev", "prints and launches host development paths"],
|
|
7
|
+
["users export", "writes a bounded export to a caller-selected host path"],
|
|
8
|
+
["orgs members import", "reads a caller-selected host file"],
|
|
9
|
+
["events export", "writes a bounded export to a caller-selected host path"],
|
|
10
|
+
["events tail", "owns a long-running terminal stream"],
|
|
11
|
+
["import legacy", "reads a migration fixture from the host"],
|
|
12
|
+
["migration plan", "creates host migration artifacts"],
|
|
13
|
+
["migration run", "executes a host migration artifact"],
|
|
14
|
+
["migration verify", "verifies a host migration fixture"],
|
|
15
|
+
["migration rollback", "rolls back a host migration fixture"],
|
|
16
|
+
["migration status", "inspects host migration artifacts"],
|
|
17
|
+
["backup create", "runs database tooling and writes a host backup"],
|
|
18
|
+
["backup verify", "verifies a host backup artifact"],
|
|
19
|
+
["backup restore", "runs database tooling against the selected host"],
|
|
20
|
+
["upgrade check", "inspects host deployment and database state"],
|
|
21
|
+
["upgrade plan", "creates signed host upgrade artifacts"],
|
|
22
|
+
["upgrade apply", "runs host deployment and database upgrade tooling"],
|
|
23
|
+
["upgrade verify", "verifies host upgrade artifacts and database state"],
|
|
24
|
+
["upgrade rollback", "restores or verifies a host database backup"],
|
|
25
|
+
["schema status", "inspects the directly selected runtime database"],
|
|
26
|
+
["schema generate", "writes SQL to a caller-selected host path"],
|
|
27
|
+
["schema migrate", "runs migrations against the directly selected runtime database"],
|
|
28
|
+
]);
|
|
29
|
+
export const REMOTE_COMMANDS = new Set([
|
|
30
|
+
"init", "doctor", "overview",
|
|
31
|
+
"project list", "project inspect", "project create",
|
|
32
|
+
"env list", "env inspect", "env create", "env promote",
|
|
33
|
+
"users list", "users inspect", "users create", "users update", "users disable", "users delete",
|
|
34
|
+
"orgs list", "orgs inspect", "orgs create", "orgs update", "orgs archive",
|
|
35
|
+
"orgs members list", "orgs members add", "orgs members update", "orgs members remove",
|
|
36
|
+
"events list", "events inspect", "events replay",
|
|
37
|
+
"keys list", "keys create", "keys rotate", "keys revoke",
|
|
38
|
+
"sessions list", "sessions revoke",
|
|
39
|
+
"roles list", "roles validate", "roles create", "roles update",
|
|
40
|
+
"sso create", "sso configure", "sso test", "sso list", "sso setup-link", "sso rotate", "sso disable",
|
|
41
|
+
"scim create", "scim test", "scim list", "scim setup-link", "scim rotate", "scim disable", "scim replay",
|
|
42
|
+
"readiness check", "readiness report",
|
|
43
|
+
"config get", "config set", "config validate", "config diff",
|
|
44
|
+
]);
|
|
45
|
+
export function classifyCommandPath(path) {
|
|
46
|
+
if (path === "login" || path === "logout" || path === "whoami")
|
|
47
|
+
return "authentication";
|
|
48
|
+
if (HOST_LOCAL_COMMANDS.has(path))
|
|
49
|
+
return "host-local";
|
|
50
|
+
if (REMOTE_COMMANDS.has(path))
|
|
51
|
+
return "remote-api";
|
|
52
|
+
return "unavailable";
|
|
53
|
+
}
|
|
54
|
+
function error(code, message, remediation) {
|
|
55
|
+
return new ClearanceError({ code, message, stage: "cli.dispatch", remediation });
|
|
56
|
+
}
|
|
57
|
+
function query(path, values) {
|
|
58
|
+
const params = new URLSearchParams();
|
|
59
|
+
for (const [key, value] of Object.entries(values)) {
|
|
60
|
+
if (value !== undefined && value !== false && value !== "")
|
|
61
|
+
params.set(key, String(value));
|
|
62
|
+
}
|
|
63
|
+
return `${path}${params.size ? `?${params}` : ""}`;
|
|
64
|
+
}
|
|
65
|
+
function body(values) {
|
|
66
|
+
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
|
|
67
|
+
}
|
|
68
|
+
export function commandPath(command) {
|
|
69
|
+
const names = [];
|
|
70
|
+
let current = command;
|
|
71
|
+
while (current?.parent) {
|
|
72
|
+
names.unshift(current.name());
|
|
73
|
+
current = current.parent;
|
|
74
|
+
}
|
|
75
|
+
return names.join(" ");
|
|
76
|
+
}
|
|
77
|
+
export function isHostLocalCommand(path) {
|
|
78
|
+
return HOST_LOCAL_COMMANDS.has(path);
|
|
79
|
+
}
|
|
80
|
+
function requireRemoteMutation(global, path) {
|
|
81
|
+
if (global.dryRun) {
|
|
82
|
+
throw error("CLI_REMOTE_DRY_RUN_UNSUPPORTED", `${path} does not yet expose a server-side dry-run contract.`, "Use the command without --dry-run, or choose --local-direct for a local development preview.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function requireConfirmation(global, code, label) {
|
|
86
|
+
if (!global.yes && !global.dryRun) {
|
|
87
|
+
throw error(code, `${label} requires --yes.`, "Review the target, then pass --yes to confirm.");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function requireLiveTestMode(global, code, label) {
|
|
91
|
+
if (global.dryRun) {
|
|
92
|
+
throw error(code, `${label} cannot combine --live with --dry-run.`, "Remove --dry-run, review the live target, then pass --yes to confirm.");
|
|
93
|
+
}
|
|
94
|
+
requireConfirmation(global, code, label);
|
|
95
|
+
}
|
|
96
|
+
export async function dispatchRemoteCommand(session, path, args, opts, global) {
|
|
97
|
+
const id = typeof args[0] === "string" ? encodeURIComponent(args[0]) : "";
|
|
98
|
+
switch (path) {
|
|
99
|
+
case "init":
|
|
100
|
+
requireRemoteMutation(global, path);
|
|
101
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/init", body: body({ name: opts.name, environment: opts.environment }) });
|
|
102
|
+
case "doctor": return requestManagementApi(session, { path: "/v1/doctor" });
|
|
103
|
+
case "overview": return requestManagementApi(session, { path: "/v1/overview" });
|
|
104
|
+
case "project list": return requestManagementApi(session, { path: "/v1/projects" });
|
|
105
|
+
case "project inspect": return requestManagementApi(session, { path: id ? `/v1/projects/${id}` : "/v1/projects/current" });
|
|
106
|
+
case "project create":
|
|
107
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/projects", body: { name: opts.name, dryRun: global.dryRun } });
|
|
108
|
+
case "env list": return requestManagementApi(session, { path: "/v1/environments" });
|
|
109
|
+
case "env inspect": return requestManagementApi(session, { path: id ? `/v1/environments/${id}` : "/v1/environments/current" });
|
|
110
|
+
case "env create":
|
|
111
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/environments", body: body({ name: opts.name, projectId: opts.projectId, kind: opts.kind, dryRun: global.dryRun }) });
|
|
112
|
+
case "env promote":
|
|
113
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/environments/promote", body: body({ to: opts.to, from: opts.from, dryRun: global.dryRun || !global.yes, confirm: global.yes && !global.dryRun }) });
|
|
114
|
+
case "users list": return requestManagementApi(session, { path: query("/v1/users", { limit: opts.limit, cursor: opts.cursor }) });
|
|
115
|
+
case "users inspect": return requestManagementApi(session, { path: `/v1/users/${id}` });
|
|
116
|
+
case "users create":
|
|
117
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/users", body: body({ email: opts.email, name: opts.name, password: opts.password, dryRun: global.dryRun }) });
|
|
118
|
+
case "users update":
|
|
119
|
+
return requestManagementApi(session, { method: "PATCH", path: `/v1/users/${id}`, body: body({ email: opts.email, name: opts.name, status: opts.status, dryRun: global.dryRun }) });
|
|
120
|
+
case "users disable":
|
|
121
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/users/${id}/disable`, body: { dryRun: global.dryRun } });
|
|
122
|
+
case "users delete":
|
|
123
|
+
requireConfirmation(global, "USER_DELETE_CONFIRM_REQUIRED", "User deletion");
|
|
124
|
+
requireRemoteMutation(global, path);
|
|
125
|
+
return requestManagementApi(session, { method: "DELETE", path: `/v1/users/${id}` });
|
|
126
|
+
case "orgs list": return requestManagementApi(session, { path: query("/v1/organizations", { limit: opts.limit, cursor: opts.cursor }) });
|
|
127
|
+
case "orgs inspect": return requestManagementApi(session, { path: `/v1/organizations/${id}` });
|
|
128
|
+
case "orgs create":
|
|
129
|
+
requireRemoteMutation(global, path);
|
|
130
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/organizations", body: body({ name: opts.name, slug: opts.slug, ownerUserId: opts.ownerUser }) });
|
|
131
|
+
case "orgs update":
|
|
132
|
+
return requestManagementApi(session, { method: "PATCH", path: `/v1/organizations/${id}`, body: body({ name: opts.name, slug: opts.slug, dryRun: global.dryRun }) });
|
|
133
|
+
case "orgs archive":
|
|
134
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/organizations/${id}/archive`, body: { dryRun: global.dryRun || !global.yes, confirm: global.yes && !global.dryRun } });
|
|
135
|
+
case "orgs members list": return requestManagementApi(session, { path: `/v1/organizations/${encodeURIComponent(String(opts.org))}/members` });
|
|
136
|
+
case "orgs members add":
|
|
137
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/organizations/${encodeURIComponent(String(opts.org))}/members`, body: body({ principalId: opts.user, role: opts.role, dryRun: global.dryRun }) });
|
|
138
|
+
case "orgs members update": {
|
|
139
|
+
if (!opts.member)
|
|
140
|
+
throw error("CLI_REMOTE_MEMBER_ID_REQUIRED", "Remote member update requires --member.", "List organization members and pass the membership id with --member.");
|
|
141
|
+
return requestManagementApi(session, { method: "PATCH", path: `/v1/organizations/${encodeURIComponent(String(opts.org))}/members/${encodeURIComponent(String(opts.member))}`, body: { role: opts.role, dryRun: global.dryRun } });
|
|
142
|
+
}
|
|
143
|
+
case "orgs members remove": {
|
|
144
|
+
requireConfirmation(global, "MEMBER_REMOVE_CONFIRM_REQUIRED", "Membership removal");
|
|
145
|
+
if (!opts.member)
|
|
146
|
+
throw error("CLI_REMOTE_MEMBER_ID_REQUIRED", "Remote member removal requires --member.", "List organization members and pass the membership id with --member.");
|
|
147
|
+
return requestManagementApi(session, { method: "DELETE", path: `/v1/organizations/${encodeURIComponent(String(opts.org))}/members/${encodeURIComponent(String(opts.member))}`, body: { dryRun: global.dryRun } });
|
|
148
|
+
}
|
|
149
|
+
case "events list": return requestManagementApi(session, { path: query("/v1/events", { limit: opts.limit, cursor: opts.cursor, action: opts.action, actor: opts.actor, outcome: opts.outcome }) });
|
|
150
|
+
case "events inspect": return requestManagementApi(session, { path: `/v1/events/${id}` });
|
|
151
|
+
case "events replay": return requestManagementApi(session, { method: "POST", path: "/v1/events/replay", body: { id: args[0], dryRun: global.dryRun || !global.yes, confirm: global.yes && !global.dryRun } });
|
|
152
|
+
case "keys list": return requestManagementApi(session, { path: query("/v1/keys", { includeRevoked: opts.includeRevoked }) });
|
|
153
|
+
case "keys create":
|
|
154
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/keys", body: { name: opts.name, scopes: opts.scope, dryRun: global.dryRun } });
|
|
155
|
+
case "keys rotate":
|
|
156
|
+
requireConfirmation(global, "API_KEY_CONFIRMATION_REQUIRED", "API key rotation");
|
|
157
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/keys/${id}/rotate`, body: { dryRun: global.dryRun } });
|
|
158
|
+
case "keys revoke":
|
|
159
|
+
requireConfirmation(global, "API_KEY_CONFIRMATION_REQUIRED", "API key revocation");
|
|
160
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/keys/${id}/revoke`, body: { dryRun: global.dryRun } });
|
|
161
|
+
case "sessions list": return requestManagementApi(session, { path: query("/v1/sessions", { limit: opts.limit, cursor: opts.cursor, userId: opts.user, status: opts.status }) });
|
|
162
|
+
case "sessions revoke":
|
|
163
|
+
requireConfirmation(global, "SESSION_REVOKE_CONFIRM_REQUIRED", "Session revocation");
|
|
164
|
+
requireRemoteMutation(global, path);
|
|
165
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/sessions/${id}/revoke` });
|
|
166
|
+
case "roles list": return requestManagementApi(session, { path: "/v1/roles" });
|
|
167
|
+
case "roles validate": return requestManagementApi(session, { method: "POST", path: "/v1/roles/validate", body: body({ name: opts.name, slug: opts.slug, permissions: opts.permission }) });
|
|
168
|
+
case "roles create":
|
|
169
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/roles", body: body({ name: opts.name, slug: opts.slug, description: opts.description, permissions: opts.permission, dryRun: global.dryRun }) });
|
|
170
|
+
case "roles update":
|
|
171
|
+
return requestManagementApi(session, { method: "PATCH", path: `/v1/roles/${id}`, body: body({ name: opts.name, description: opts.description, permissions: opts.permission, dryRun: global.dryRun }) });
|
|
172
|
+
case "sso create":
|
|
173
|
+
requireRemoteMutation(global, path);
|
|
174
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/sso", body: body({ organizationId: opts.org, provider: opts.provider, protocol: opts.protocol, issuer: opts.issuer, audience: opts.audience, domain: opts.domain, samlEntryPoint: opts.entryPoint, samlCertificate: opts.certificate ? readFileSync(resolve(String(opts.certificate)), "utf8") : undefined }) });
|
|
175
|
+
case "sso configure":
|
|
176
|
+
requireRemoteMutation(global, path);
|
|
177
|
+
return requestManagementApi(session, { method: "PATCH", path: `/v1/sso/${id}`, body: body({ issuer: opts.issuer, audience: opts.audience, domain: opts.domain }) });
|
|
178
|
+
case "sso test":
|
|
179
|
+
if (opts.live && opts.fixture)
|
|
180
|
+
throw error("SSO_TEST_MODE_CONFLICT", "--live and --fixture are mutually exclusive.", "Use one SSO test mode.");
|
|
181
|
+
if (opts.live)
|
|
182
|
+
requireLiveTestMode(global, "SSO_LIVE_CONFIRM_REQUIRED", "Live SSO conformance");
|
|
183
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/sso/${id}/test`, body: body({ fixture: opts.fixture, live: opts.live }) });
|
|
184
|
+
case "sso list": return requestManagementApi(session, { path: query("/v1/sso", { organizationId: opts.org }) });
|
|
185
|
+
case "sso setup-link":
|
|
186
|
+
requireRemoteMutation(global, path);
|
|
187
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/sso/setup-links", body: { organizationId: opts.org } });
|
|
188
|
+
case "sso rotate":
|
|
189
|
+
requireConfirmation(global, "SSO_CONFIRM_REQUIRED", "SSO credential rotation");
|
|
190
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/sso/${id}/rotate`, body: { dryRun: global.dryRun } });
|
|
191
|
+
case "sso disable":
|
|
192
|
+
requireConfirmation(global, "SSO_DISABLE_CONFIRM_REQUIRED", "SSO disable");
|
|
193
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/sso/${id}/disable`, body: { dryRun: global.dryRun } });
|
|
194
|
+
case "scim create":
|
|
195
|
+
requireRemoteMutation(global, path);
|
|
196
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/scim", body: body({ organizationId: opts.org, provider: opts.provider, endpoint: opts.endpoint }) });
|
|
197
|
+
case "scim test":
|
|
198
|
+
if (opts.live && opts.fixture)
|
|
199
|
+
throw error("SCIM_TEST_MODE_CONFLICT", "--live and --fixture are mutually exclusive.", "Use one SCIM test mode.");
|
|
200
|
+
if (opts.live)
|
|
201
|
+
requireLiveTestMode(global, "SCIM_LIVE_CONFIRM_REQUIRED", "Live SCIM conformance");
|
|
202
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/scim/${id}/test`, body: body({ fixture: opts.fixture, live: opts.live, dryRun: !opts.apply }) });
|
|
203
|
+
case "scim list": return requestManagementApi(session, { path: query("/v1/scim", { organizationId: opts.org }) });
|
|
204
|
+
case "scim setup-link":
|
|
205
|
+
requireRemoteMutation(global, path);
|
|
206
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/scim/setup-links", body: { organizationId: opts.org } });
|
|
207
|
+
case "scim rotate":
|
|
208
|
+
requireConfirmation(global, "SCIM_CONFIRM_REQUIRED", "SCIM credential rotation");
|
|
209
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/scim/${id}/rotate`, body: { dryRun: global.dryRun } });
|
|
210
|
+
case "scim disable":
|
|
211
|
+
requireConfirmation(global, "SCIM_DISABLE_CONFIRM_REQUIRED", "SCIM disable");
|
|
212
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/scim/${id}/disable`, body: { dryRun: global.dryRun } });
|
|
213
|
+
case "scim replay":
|
|
214
|
+
return requestManagementApi(session, { method: "POST", path: `/v1/scim/traces/${encodeURIComponent(String(args[0]))}/replay`, body: { dryRun: global.dryRun || !global.yes, confirm: global.yes && !global.dryRun } });
|
|
215
|
+
case "readiness check":
|
|
216
|
+
requireRemoteMutation(global, path);
|
|
217
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/readiness/check", body: { organizationId: opts.org } });
|
|
218
|
+
case "readiness report": return requestManagementApi(session, { path: `/v1/readiness/${encodeURIComponent(String(opts.org))}` });
|
|
219
|
+
case "config get": return requestManagementApi(session, { path: query("/v1/config", { key: args[0] }) });
|
|
220
|
+
case "config set": return requestManagementApi(session, { method: "PATCH", path: `/v1/config/${encodeURIComponent(String(args[0]))}`, body: { value: args[1], dryRun: global.dryRun } });
|
|
221
|
+
case "config validate": {
|
|
222
|
+
const config = opts.file ? JSON.parse(readFileSync(resolve(String(opts.file)), "utf8")) : undefined;
|
|
223
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/config/validate", body: body({ config }) });
|
|
224
|
+
}
|
|
225
|
+
case "config diff": {
|
|
226
|
+
const config = JSON.parse(readFileSync(resolve(String(opts.file)), "utf8"));
|
|
227
|
+
return requestManagementApi(session, { method: "POST", path: "/v1/config/diff", body: { config } });
|
|
228
|
+
}
|
|
229
|
+
default:
|
|
230
|
+
throw error("CLI_REMOTE_COMMAND_UNAVAILABLE", `${path} has no versioned management API contract in this release.`, "Upgrade the Clearance API, or use --local-direct only for a development or host-local workflow.");
|
|
231
|
+
}
|
|
232
|
+
}
|