@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.
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env bash
2
+ # Create a real Postgres backup (pg_dump) with checksum + metadata.
3
+ # Never prints credentials. Never overwrites without a new backup id.
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
+
10
+ # Dumps contain production data. Make every created directory/artifact owner-only.
11
+ umask 077
12
+
13
+ usage() {
14
+ cat <<'EOF'
15
+ Usage: backup-create.sh [--dir DIR]
16
+
17
+ Environment:
18
+ DATABASE_URL Required. Postgres connection string for the active database.
19
+
20
+ Writes:
21
+ DIR/<id>.sql plain-format pg_dump
22
+ DIR/<id>.meta.json checksum, counts, timestamps, source db name
23
+ DIR/<id>.verified.json read-only evidence binding the dump and metadata
24
+ Prints backup id on stdout (last line: BACKUP_ID=<id>)
25
+ EOF
26
+ }
27
+
28
+ BACKUP_DIR="${CLEARANCE_BACKUP_DIR:-$ROOT/.clearance/backups}"
29
+ while [[ $# -gt 0 ]]; do
30
+ case "$1" in
31
+ --dir) BACKUP_DIR="$2"; shift 2 ;;
32
+ -h|--help) usage; exit 0 ;;
33
+ *) die "unknown argument: $1" ;;
34
+ esac
35
+ done
36
+
37
+ require_pg_client
38
+ require_cmd node
39
+ require_cmd openssl
40
+ URL="$(resolve_database_url)"
41
+ require_database_url "$URL"
42
+
43
+ ACTIVE_DB="$(db_name_from_url "$URL")"
44
+ assert_safe_ident "$ACTIVE_DB" "active database name"
45
+
46
+ mkdir -p "$BACKUP_DIR"
47
+ chmod 700 "$BACKUP_DIR"
48
+ # Unique id: bak_<utc>_<random>
49
+ ID="bak_$(date -u +%Y%m%dT%H%M%SZ)_$(openssl rand -hex 4)"
50
+ DUMP="$BACKUP_DIR/${ID}.sql"
51
+ META="$BACKUP_DIR/${ID}.meta.json"
52
+ VERIFIED="$BACKUP_DIR/${ID}.verified.json"
53
+ EVIDENCE_TMP="$(mktemp "${TMPDIR:-/tmp}/clearance-bak-evidence.XXXXXX")"
54
+ SNAPSHOT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/clearance-bak-snapshot.XXXXXX")"
55
+ SNAPSHOT_IN="$SNAPSHOT_DIR/in"
56
+ SNAPSHOT_OUT="$SNAPSHOT_DIR/out"
57
+ SNAPSHOT_OPEN=0
58
+ SNAPSHOT_PID=""
59
+ cleanup_tmp() {
60
+ local ec=$?
61
+ if [[ "$SNAPSHOT_OPEN" -eq 1 ]]; then
62
+ printf 'ROLLBACK;\n\\q\n' >&8 2>/dev/null || true
63
+ exec 8>&-
64
+ exec 9<&-
65
+ wait "$SNAPSHOT_PID" 2>/dev/null || true
66
+ fi
67
+ find "$SNAPSHOT_DIR" -type p -delete 2>/dev/null || true
68
+ rmdir "$SNAPSHOT_DIR" 2>/dev/null || true
69
+ find "$EVIDENCE_TMP" -type f -delete 2>/dev/null || true
70
+ if [[ "$ec" -ne 0 ]]; then
71
+ find "$DUMP" "$META" "$VERIFIED" -type f -delete 2>/dev/null || true
72
+ fi
73
+ }
74
+ trap cleanup_tmp EXIT
75
+
76
+ printf 'backup-create: dumping database=%s (url redacted=%s)\n' "$ACTIVE_DB" "$(redact_url "$URL")" >&2
77
+
78
+ # Hold one exported repeatable-read snapshot across both pg_dump and the
79
+ # resource/version evidence queries. Without this, ordinary writes between the
80
+ # dump and count collection can make a valid backup permanently unverifiable.
81
+ mkfifo "$SNAPSHOT_IN" "$SNAPSHOT_OUT"
82
+ psql --no-psqlrc -qAt -v ON_ERROR_STOP=1 "$URL" <"$SNAPSHOT_IN" >"$SNAPSHOT_OUT" &
83
+ SNAPSHOT_PID=$!
84
+ exec 8>"$SNAPSHOT_IN"
85
+ exec 9<"$SNAPSHOT_OUT"
86
+ SNAPSHOT_OPEN=1
87
+ printf 'BEGIN ISOLATION LEVEL REPEATABLE READ;\nSELECT pg_export_snapshot();\n' >&8
88
+ IFS= read -r SNAPSHOT_ID <&9 || die "could not export backup snapshot"
89
+ [[ "$SNAPSHOT_ID" =~ ^[0-9A-Fa-f-]+$ ]] || die "postgres returned an invalid backup snapshot id"
90
+ if [[ "${CLEARANCE_OPS_TESTING:-0}" == "1" ]]; then
91
+ if [[ -n "${CLEARANCE_BACKUP_TEST_SNAPSHOT_READY_FILE:-}" ]]; then
92
+ : >"$CLEARANCE_BACKUP_TEST_SNAPSHOT_READY_FILE"
93
+ fi
94
+ delay="${CLEARANCE_BACKUP_TEST_DELAY_AFTER_SNAPSHOT_SECONDS:-0}"
95
+ [[ "$delay" =~ ^[0-9]+([.][0-9]+)?$ ]] || die "invalid backup snapshot testing delay"
96
+ if [[ "$delay" != "0" ]]; then sleep "$delay"; fi
97
+ fi
98
+
99
+ # Real pg_dump — plain SQL for inspectability. Fail closed on any dump error.
100
+ # Uses matching major client via Docker when local pg_dump is older/newer than server.
101
+ pg_dump_to_file "$URL" "$DUMP" "$SNAPSHOT_ID"
102
+
103
+ printf '%s\n' \
104
+ 'CREATE TEMP TABLE clearance_backup_counts(table_name text PRIMARY KEY, row_count bigint NOT NULL);' \
105
+ "SELECT format('INSERT INTO clearance_backup_counts SELECT %L, count(*) FROM %I.%I;', table_name, table_schema, table_name) FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE' ORDER BY table_name;" \
106
+ '\gexec' \
107
+ "SELECT json_build_object('capturedAt', to_char(CURRENT_TIMESTAMP AT TIME ZONE 'UTC','YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"'), 'tables', COALESCE((SELECT json_object_agg(table_name,row_count ORDER BY table_name) FROM clearance_backup_counts),'{}'::json))::text;" \
108
+ "SELECT CASE WHEN to_regclass('public.clearance_management_snapshot') IS NULL THEN 'SELECT '''' || ''__CLEARANCE_APP_VERSION__'';' ELSE 'SELECT COALESCE((SELECT data->>''releaseVersion'' FROM clearance_management_snapshot WHERE id=1),'''') || ''__CLEARANCE_APP_VERSION__'';' END;" \
109
+ '\gexec' \
110
+ "SELECT '__CLEARANCE_SNAPSHOT_END__';" >&8
111
+
112
+ SNAPSHOT_EVIDENCE=""
113
+ APP_VERSION=""
114
+ while IFS= read -r line <&9; do
115
+ if [[ "$line" == "__CLEARANCE_SNAPSHOT_END__" ]]; then
116
+ break
117
+ elif [[ "$line" == *"__CLEARANCE_APP_VERSION__" ]]; then
118
+ APP_VERSION="${line%__CLEARANCE_APP_VERSION__}"
119
+ elif [[ "$line" == \{* ]]; then
120
+ SNAPSHOT_EVIDENCE="$line"
121
+ fi
122
+ done
123
+ [[ -n "$SNAPSHOT_EVIDENCE" ]] || die "could not collect evidence from the backup snapshot"
124
+ printf '%s\n' "$SNAPSHOT_EVIDENCE" >"$EVIDENCE_TMP"
125
+ printf 'COMMIT;\n\\q\n' >&8
126
+ exec 8>&-
127
+ exec 9<&-
128
+ wait "$SNAPSHOT_PID"
129
+ SNAPSHOT_OPEN=0
130
+
131
+ [[ -s "$DUMP" ]] || die "pg_dump produced empty file"
132
+ if ! grep -q 'PostgreSQL database dump' "$DUMP"; then
133
+ die "pg_dump output missing expected PostgreSQL dump header"
134
+ fi
135
+ grep -q 'PostgreSQL database dump complete' "$DUMP" \
136
+ || die "pg_dump output missing completion marker"
137
+
138
+ CHECKSUM="$(sha256_file "$DUMP")"
139
+ BYTES="$(wc -c <"$DUMP" | tr -d ' ')"
140
+ PG_VER="$(grep -m1 'Dumped from database version' "$DUMP" || pg_dump --version | head -n1 || true)"
141
+
142
+ # Resource counts as flat map from evidence tables
143
+ COUNTS_JSON="$(node -e '
144
+ const fs=require("fs");
145
+ const j=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));
146
+ if (!j.tables || typeof j.tables !== "object" || Array.isArray(j.tables)) process.exit(2);
147
+ process.stdout.write(JSON.stringify(j.tables));
148
+ ' "$EVIDENCE_TMP")" || die "failed to serialize required backup evidence"
149
+
150
+ CREATED="$(iso_now)"
151
+ cat >"$META" <<EOF
152
+ {
153
+ "id": $(json_escape "$ID"),
154
+ "format": "pg_dump_plain",
155
+ "createdAt": $(json_escape "$CREATED"),
156
+ "dumpFile": $(json_escape "$(basename "$DUMP")"),
157
+ "metaFile": $(json_escape "$(basename "$META")"),
158
+ "checksumSha256": $(json_escape "$CHECKSUM"),
159
+ "bytes": $BYTES,
160
+ "sourceDatabase": $(json_escape "$ACTIVE_DB"),
161
+ "applicationVersion": $(if [[ -n "$APP_VERSION" ]]; then json_escape "$APP_VERSION"; else printf 'null'; fi),
162
+ "pgDumpVersion": $(json_escape "$PG_VER"),
163
+ "resourceCounts": $COUNTS_JSON,
164
+ "tool": "scripts/backup-create.sh"
165
+ }
166
+ EOF
167
+
168
+ # Refuse writing secrets into meta
169
+ if grep -qiE 'password|postgresql://[^"]+:[^"]+@' "$META"; then
170
+ find "$DUMP" "$META" -type f -delete 2>/dev/null || true
171
+ die "metadata unexpectedly contained credential material; aborted"
172
+ fi
173
+
174
+ # Bind both artifacts into immutable local verification evidence. The evidence
175
+ # is owner-readable and non-writable; backup-verify checks every digest.
176
+ META_CHECKSUM="$(sha256_file "$META")"
177
+ RESOURCE_FINGERPRINT="$(schema_fingerprint_from_evidence "$EVIDENCE_TMP")"
178
+ cat >"$VERIFIED" <<EOF
179
+ {
180
+ "id": $(json_escape "$ID"),
181
+ "verifiedAt": $(json_escape "$(iso_now)"),
182
+ "dumpFile": $(json_escape "$(basename "$DUMP")"),
183
+ "metaFile": $(json_escape "$(basename "$META")"),
184
+ "dumpSha256": $(json_escape "$CHECKSUM"),
185
+ "metaSha256": $(json_escape "$META_CHECKSUM"),
186
+ "resourceEvidenceSha256": $(json_escape "$RESOURCE_FINGERPRINT"),
187
+ "checks": ["nonempty", "pg_dump_complete", "metadata_bound", "resource_counts_captured"]
188
+ }
189
+ EOF
190
+ chmod 600 "$DUMP" "$META"
191
+ chmod 400 "$VERIFIED"
192
+
193
+ printf 'backup-create: wrote %s (%s bytes, sha256=%s)\n' "$DUMP" "$BYTES" "$CHECKSUM" >&2
194
+ printf 'BACKUP_ID=%s\n' "$ID"
195
+ printf 'BACKUP_DUMP=%s\n' "$DUMP"
196
+ printf 'BACKUP_META=%s\n' "$META"
197
+ printf 'BACKUP_EVIDENCE=%s\n' "$VERIFIED"
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env bash
2
+ # Restore a verified backup into an ISOLATED temporary database, compare evidence,
3
+ # and clean up. Never drops or overwrites the active database.
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
+
10
+ usage() {
11
+ cat <<'EOF'
12
+ Usage: backup-restore-verify.sh --id ID [--dir DIR]
13
+ backup-restore-verify.sh --dump PATH --meta PATH
14
+
15
+ Restores into a temporary database name (clr_rv_*), queries management/runtime
16
+ tables when present, compares row counts to backup metadata evidence, drops the
17
+ temp database, and exits non-zero on mismatch.
18
+
19
+ Refuses to target the active database name under all circumstances.
20
+ EOF
21
+ }
22
+
23
+ BACKUP_DIR="${CLEARANCE_BACKUP_DIR:-$ROOT/.clearance/backups}"
24
+ ID=""
25
+ DUMP=""
26
+ META=""
27
+ KEEP_TEMP=0
28
+
29
+ while [[ $# -gt 0 ]]; do
30
+ case "$1" in
31
+ --id) ID="$2"; shift 2 ;;
32
+ --dir) BACKUP_DIR="$2"; shift 2 ;;
33
+ --dump) DUMP="$2"; shift 2 ;;
34
+ --meta) META="$2"; shift 2 ;;
35
+ --keep-temp) KEEP_TEMP=1; shift ;;
36
+ -h|--help) usage; exit 0 ;;
37
+ *) die "unknown argument: $1" ;;
38
+ esac
39
+ done
40
+
41
+ if [[ -n "$ID" ]]; then
42
+ DUMP="${DUMP:-$BACKUP_DIR/${ID}.sql}"
43
+ META="${META:-$BACKUP_DIR/${ID}.meta.json}"
44
+ fi
45
+ [[ -n "$DUMP" && -f "$DUMP" ]] || die "dump required"
46
+ [[ -n "$META" && -f "$META" ]] || die "meta required"
47
+
48
+ require_pg_client
49
+ require_cmd node
50
+ require_cmd openssl
51
+ URL="$(resolve_database_url)"
52
+ require_database_url "$URL"
53
+ ACTIVE_DB="$(db_name_from_url "$URL")"
54
+ ADMIN_URL="$(admin_url_from_url "$URL")"
55
+
56
+ # Integrity first
57
+ bash "$ROOT/scripts/backup-verify.sh" --dump "$DUMP" --meta "$META" >/dev/null
58
+
59
+ # Isolated temp DB name
60
+ SUFFIX="$(openssl rand -hex 4)"
61
+ TEMP_DB="clr_rv_${SUFFIX}"
62
+ assert_safe_ident "$TEMP_DB" "temp database"
63
+ assert_not_active_db "$TEMP_DB"
64
+ [[ "$TEMP_DB" != "$ACTIVE_DB" ]] || die "temp db collided with active db"
65
+
66
+ TEMP_URL="$(url_with_db "$URL" "$TEMP_DB")"
67
+ CREATED=0
68
+ RESTORED_EV=""
69
+
70
+ drop_temp_database() {
71
+ assert_not_active_db "$TEMP_DB"
72
+ psql_q "$ADMIN_URL" -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${TEMP_DB}' AND pid <> pg_backend_pid();" >/dev/null 2>&1 || true
73
+ psql_q "$ADMIN_URL" -c "DROP DATABASE IF EXISTS \"${TEMP_DB}\";" >/dev/null 2>&1 || return 1
74
+ local remains
75
+ remains="$(psql_q "$ADMIN_URL" -At -c "SELECT count(*) FROM pg_database WHERE datname = '${TEMP_DB}'")" || return 1
76
+ [[ "$remains" == "0" ]]
77
+ }
78
+
79
+ cleanup() {
80
+ local ec=$?
81
+ [[ -z "$RESTORED_EV" ]] || rm -f "$RESTORED_EV"
82
+ if [[ "$KEEP_TEMP" -eq 1 ]]; then
83
+ printf 'backup-restore-verify: keeping temp database %s (active remains %s)\n' "$TEMP_DB" "$ACTIVE_DB" >&2
84
+ exit "$ec"
85
+ fi
86
+ if [[ "$CREATED" -eq 1 ]]; then
87
+ if ! drop_temp_database; then
88
+ printf 'backup-restore-verify: failed to clean up isolated database %s\n' "$TEMP_DB" >&2
89
+ ec=1
90
+ fi
91
+ fi
92
+ exit "$ec"
93
+ }
94
+ trap cleanup EXIT INT TERM
95
+
96
+ printf 'backup-restore-verify: creating isolated database %s (active=%s remains untouched)\n' \
97
+ "$TEMP_DB" "$ACTIVE_DB" >&2
98
+
99
+ psql_q "$ADMIN_URL" -c "CREATE DATABASE \"${TEMP_DB}\";"
100
+ CREATED=1
101
+
102
+ printf 'backup-restore-verify: restoring dump into isolated database\n' >&2
103
+ psql_restore_file "$TEMP_URL" "$DUMP"
104
+
105
+ # Evidence from restored DB
106
+ RESTORED_EV="$(mktemp "${TMPDIR:-/tmp}/clearance-restore-ev.XXXXXX")"
107
+ collect_db_evidence "$TEMP_URL" "$RESTORED_EV"
108
+
109
+ # Compare table evidence and the application release captured with the backup.
110
+ compare_backup_evidence "$META" "$RESTORED_EV"
111
+ EXPECTED_APP_VERSION="$(node -e 'const j=require(process.argv[1]); process.stdout.write(j.applicationVersion||"")' "$META")"
112
+ if [[ -n "$EXPECTED_APP_VERSION" ]]; then
113
+ require_application_release "$TEMP_URL" "$EXPECTED_APP_VERSION"
114
+ fi
115
+ rm -f "$RESTORED_EV"
116
+ RESTORED_EV=""
117
+
118
+ # Confirm active DB still exists and was not the restore target
119
+ STILL="$(psql_q "$ADMIN_URL" -At -c "SELECT 1 FROM pg_database WHERE datname = '${ACTIVE_DB}'")"
120
+ [[ "$STILL" == "1" ]] || die "active database disappeared — aborting (investigation required)"
121
+ assert_not_active_db "$TEMP_DB"
122
+
123
+ if [[ "$KEEP_TEMP" -eq 0 ]]; then
124
+ drop_temp_database || die "isolated restore succeeded but temporary database cleanup failed"
125
+ CREATED=0
126
+ fi
127
+
128
+ printf 'backup-restore-verify: OK (isolated restore compared; active db %s untouched; cleanup verified)\n' "$ACTIVE_DB"
129
+ printf 'RESTORE_VERIFIED=1\n'
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env bash
2
+ # Verify a Clearance Postgres backup archive: checksum + dump structure + metadata.
3
+ # Does not connect to the live database and never restores.
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
+
10
+ usage() {
11
+ cat <<'EOF'
12
+ Usage: backup-verify.sh --id ID [--dir DIR]
13
+ backup-verify.sh --dump PATH [--meta PATH] [--evidence PATH]
14
+
15
+ Inspects archive integrity only (checksum, pg_dump markers, metadata consistency).
16
+ Does not restore and does not touch the active database.
17
+ EOF
18
+ }
19
+
20
+ BACKUP_DIR="${CLEARANCE_BACKUP_DIR:-$ROOT/.clearance/backups}"
21
+ ID=""
22
+ DUMP=""
23
+ META=""
24
+ EVIDENCE=""
25
+
26
+ while [[ $# -gt 0 ]]; do
27
+ case "$1" in
28
+ --id) ID="$2"; shift 2 ;;
29
+ --dir) BACKUP_DIR="$2"; shift 2 ;;
30
+ --dump) DUMP="$2"; shift 2 ;;
31
+ --meta) META="$2"; shift 2 ;;
32
+ --evidence) EVIDENCE="$2"; shift 2 ;;
33
+ -h|--help) usage; exit 0 ;;
34
+ *) die "unknown argument: $1" ;;
35
+ esac
36
+ done
37
+
38
+ if [[ -n "$ID" ]]; then
39
+ DUMP="${DUMP:-$BACKUP_DIR/${ID}.sql}"
40
+ META="${META:-$BACKUP_DIR/${ID}.meta.json}"
41
+ EVIDENCE="${EVIDENCE:-$BACKUP_DIR/${ID}.verified.json}"
42
+ fi
43
+
44
+ [[ -n "$DUMP" ]] || die "provide --id or --dump"
45
+ require_cmd node
46
+ [[ -f "$DUMP" ]] || die "dump file missing: $DUMP"
47
+ [[ -s "$DUMP" ]] || die "dump file empty: $DUMP"
48
+
49
+ if [[ -z "$META" && -n "$ID" ]]; then
50
+ META="$BACKUP_DIR/${ID}.meta.json"
51
+ fi
52
+ [[ -n "$META" && -f "$META" ]] || die "metadata file missing (expected alongside dump)"
53
+ if [[ -z "$EVIDENCE" ]]; then
54
+ EVIDENCE="${DUMP%.sql}.verified.json"
55
+ fi
56
+ [[ -f "$EVIDENCE" ]] || die "immutable verification evidence missing: $EVIDENCE"
57
+
58
+ # Checksum
59
+ ACTUAL="$(sha256_file "$DUMP")"
60
+ EXPECTED="$(node -e '
61
+ const fs=require("fs");
62
+ const j=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));
63
+ if(!j.checksumSha256) process.exit(2);
64
+ process.stdout.write(j.checksumSha256);
65
+ ' "$META")" || die "metadata missing checksumSha256"
66
+
67
+ [[ "$ACTUAL" == "$EXPECTED" ]] || die "checksum mismatch (archive may be corrupted)"
68
+
69
+ # Archive structure inspection (plain pg_dump)
70
+ grep -q 'PostgreSQL database dump' "$DUMP" || die "archive missing PostgreSQL dump header"
71
+ grep -q 'PostgreSQL database dump complete' "$DUMP" || die "archive missing PostgreSQL dump completion marker"
72
+ # At least one CREATE or COPY / table structure signal
73
+ if ! grep -qE '^(CREATE TABLE|COPY |CREATE SCHEMA)' "$DUMP"; then
74
+ printf 'backup-verify: archive contains no table definitions (valid empty database)\n' >&2
75
+ fi
76
+
77
+ # Metadata consistency
78
+ node -e '
79
+ const fs=require("fs");
80
+ const meta=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));
81
+ const dumpBase=require("path").basename(process.argv[2]);
82
+ if(!meta.id) { console.error("meta missing id"); process.exit(1); }
83
+ if(meta.format !== "pg_dump_plain") { console.error("unsupported format"); process.exit(1); }
84
+ if(meta.dumpFile && meta.dumpFile !== dumpBase) {
85
+ console.error("meta.dumpFile does not match dump basename");
86
+ process.exit(1);
87
+ }
88
+ if(typeof meta.bytes === "number") {
89
+ const st=fs.statSync(process.argv[2]);
90
+ if(st.size !== meta.bytes) {
91
+ console.error("meta.bytes does not match dump size");
92
+ process.exit(1);
93
+ }
94
+ }
95
+ // Fail closed: meta must not embed credentials
96
+ const raw=fs.readFileSync(process.argv[1],"utf8");
97
+ if(/password\s*[:=]/i.test(raw) || /postgres(ql)?:\/\/[^/\s"]+:[^@\s"]+@/i.test(raw)) {
98
+ console.error("metadata appears to contain credentials");
99
+ process.exit(1);
100
+ }
101
+ ' "$META" "$DUMP"
102
+
103
+ # Verification evidence is content-addressed. This catches metadata rewriting
104
+ # as well as dump corruption, even when only one checksum was updated.
105
+ node -e '
106
+ const fs=require("fs");
107
+ const path=require("path");
108
+ const crypto=require("crypto");
109
+ const ev=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));
110
+ const dump=process.argv[2];
111
+ const meta=process.argv[3];
112
+ const sha=(p)=>crypto.createHash("sha256").update(fs.readFileSync(p)).digest("hex");
113
+ if(!ev.id || !ev.verifiedAt || !Array.isArray(ev.checks)) process.exit(2);
114
+ if(ev.dumpFile!==path.basename(dump) || ev.metaFile!==path.basename(meta)) process.exit(3);
115
+ if(ev.dumpSha256!==sha(dump) || ev.metaSha256!==sha(meta)) process.exit(4);
116
+ if((fs.statSync(process.argv[1]).mode & 0o222)!==0) process.exit(5);
117
+ ' "$EVIDENCE" "$DUMP" "$META" || die "immutable backup verification evidence mismatch"
118
+
119
+ printf 'backup-verify: OK id=%s sha256=%s\n' "$(basename "$DUMP" .sql)" "$ACTUAL"
120
+ printf 'BACKUP_VERIFIED=1\n'
121
+ printf 'BACKUP_EVIDENCE=%s\n' "$EVIDENCE"