ts-prorm-orm 1.1.0
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/CHANGELOG.md +1111 -0
- package/LICENSE +21 -0
- package/README.md +573 -0
- package/dist/audit/history-query.js +180 -0
- package/dist/audit/index.js +23 -0
- package/dist/audit/logger.js +236 -0
- package/dist/cache/cache-manager.js +84 -0
- package/dist/cache/index.js +16 -0
- package/dist/cache/redis-cluster-cache.js +557 -0
- package/dist/cli.js +2200 -0
- package/dist/compliance/audit-trail.js +68 -0
- package/dist/compliance/backup-verification.js +685 -0
- package/dist/compliance/breach-detector.js +505 -0
- package/dist/compliance/consent-record.js +227 -0
- package/dist/compliance/consent-versioning.js +331 -0
- package/dist/compliance/cross-border-log.js +530 -0
- package/dist/compliance/data-classifier.js +258 -0
- package/dist/compliance/data-lineage.js +565 -0
- package/dist/compliance/data-masker.js +303 -0
- package/dist/compliance/data-portability.js +265 -0
- package/dist/compliance/data-retention.js +195 -0
- package/dist/compliance/dsar-workflow.js +342 -0
- package/dist/compliance/field-encryption.js +223 -0
- package/dist/compliance/immutable-record.js +148 -0
- package/dist/compliance/index.js +260 -0
- package/dist/compliance/privacy-impact-assessment.js +364 -0
- package/dist/compliance/pseudonymization.js +269 -0
- package/dist/compliance/query-firewall.js +1182 -0
- package/dist/compliance/rate-limiter.js +580 -0
- package/dist/compliance/right-to-erasure.js +117 -0
- package/dist/compliance/row-level-security.js +246 -0
- package/dist/compliance/security-decorator.js +574 -0
- package/dist/compliance/security-monitor.js +525 -0
- package/dist/compliance/sensitive-data-discovery.js +476 -0
- package/dist/compliance/session-isolation.js +472 -0
- package/dist/compliance/tls-enforcer.js +108 -0
- package/dist/compliance/worm-storage.js +712 -0
- package/dist/connection-manager.js +198 -0
- package/dist/connection-pool.js +519 -0
- package/dist/decorators/audit.js +136 -0
- package/dist/decorators/belongs-to-many.js +115 -0
- package/dist/decorators/belongs-to.js +115 -0
- package/dist/decorators/check.js +435 -0
- package/dist/decorators/collate.js +329 -0
- package/dist/decorators/comment.js +205 -0
- package/dist/decorators/database-settings.js +236 -0
- package/dist/decorators/default.js +244 -0
- package/dist/decorators/encryption.js +235 -0
- package/dist/decorators/engine.js +97 -0
- package/dist/decorators/fk-constraints.js +594 -0
- package/dist/decorators/foreign-table.js +136 -0
- package/dist/decorators/generated.js +274 -0
- package/dist/decorators/has-many.js +127 -0
- package/dist/decorators/has-one.js +116 -0
- package/dist/decorators/hstore.js +129 -0
- package/dist/decorators/index.js +355 -0
- package/dist/decorators/json-column.js +83 -0
- package/dist/decorators/jsonb.js +101 -0
- package/dist/decorators/orm-decorators.js +425 -0
- package/dist/decorators/permissions.js +294 -0
- package/dist/decorators/procedure.js +210 -0
- package/dist/decorators/query-options.js +556 -0
- package/dist/decorators/range.js +167 -0
- package/dist/decorators/set-column.js +83 -0
- package/dist/decorators/spatial.js +114 -0
- package/dist/decorators/storage.js +580 -0
- package/dist/decorators/timezone.js +512 -0
- package/dist/decorators/trigger.js +90 -0
- package/dist/decorators/uuid.js +135 -0
- package/dist/decorators/view.js +258 -0
- package/dist/diagrams/chen-diagram.js +354 -0
- package/dist/diagrams/class-diagram.js +384 -0
- package/dist/diagrams/dependency-diagram.js +432 -0
- package/dist/diagrams/er-diagram.js +605 -0
- package/dist/diagrams/flow-diagram.js +394 -0
- package/dist/diagrams/gantt-diagram.js +411 -0
- package/dist/diagrams/index-diagram.js +353 -0
- package/dist/diagrams/index.js +184 -0
- package/dist/diagrams/migration-diagram.js +316 -0
- package/dist/diagrams/model-diagram.js +616 -0
- package/dist/diagrams/package-diagram.js +376 -0
- package/dist/diagrams/palette.js +85 -0
- package/dist/diagrams/relational-diagram.js +455 -0
- package/dist/diagrams/schemadoc-diagram.js +309 -0
- package/dist/diagrams/sequence-diagram.js +307 -0
- package/dist/diagrams/state-diagram.js +344 -0
- package/dist/diagrams/svg-dom.js +111 -0
- package/dist/diagrams/tree-diagram.js +250 -0
- package/dist/dialects/clickhouse/index.js +1742 -0
- package/dist/dialects/cockroachdb/index.js +4677 -0
- package/dist/dialects/cratedb/index.js +441 -0
- package/dist/dialects/databricks/index.js +517 -0
- package/dist/dialects/db2/index.js +2225 -0
- package/dist/dialects/dialect.js +720 -0
- package/dist/dialects/duckdb/index.js +2014 -0
- package/dist/dialects/exasol/index.js +357 -0
- package/dist/dialects/firebird/index.js +495 -0
- package/dist/dialects/greenplum/index.js +457 -0
- package/dist/dialects/hana/index.js +1741 -0
- package/dist/dialects/mariadb/index.js +3590 -0
- package/dist/dialects/mssql/index.js +2296 -0
- package/dist/dialects/mysql/index.js +4058 -0
- package/dist/dialects/oracle/index.js +2999 -0
- package/dist/dialects/postgres/index.js +5319 -0
- package/dist/dialects/query-stream-helper.js +152 -0
- package/dist/dialects/questdb/index.js +433 -0
- package/dist/dialects/redshift/index.js +2277 -0
- package/dist/dialects/singlestore/index.js +354 -0
- package/dist/dialects/snowflake/index.js +2082 -0
- package/dist/dialects/spanner/index.js +1768 -0
- package/dist/dialects/sqlite/index.js +3382 -0
- package/dist/dialects/tidb/index.js +377 -0
- package/dist/dialects/timescaledb/index.js +164 -0
- package/dist/dialects/trino/index.js +420 -0
- package/dist/dialects/turso/index.js +227 -0
- package/dist/dialects/vertica/index.js +317 -0
- package/dist/dialects/yugabytedb/index.js +424 -0
- package/dist/errors/index.js +472 -0
- package/dist/errors/utils.js +340 -0
- package/dist/errors.js +21 -0
- package/dist/extensions/catalog/cloud-warehouse-features.js +207 -0
- package/dist/extensions/catalog/mssql-features.js +147 -0
- package/dist/extensions/catalog/mysql-mariadb-plugins.js +229 -0
- package/dist/extensions/catalog/oracle-db2-features.js +196 -0
- package/dist/extensions/catalog/postgres-extensions.js +516 -0
- package/dist/extensions/index.js +71 -0
- package/dist/extensions/types.js +13 -0
- package/dist/foreign-data.js +173 -0
- package/dist/hooks/hooks-manager.js +350 -0
- package/dist/hooks/index.js +37 -0
- package/dist/index.js +463 -0
- package/dist/logging.js +335 -0
- package/dist/migrations/index.js +40 -0
- package/dist/migrations/migration.js +196 -0
- package/dist/migrations/migrator.js +411 -0
- package/dist/migrations/prormmigration.js +275 -0
- package/dist/migrations/query-interface.js +435 -0
- package/dist/migrations/seeder.js +353 -0
- package/dist/models/associations.js +852 -0
- package/dist/models/constraints.js +288 -0
- package/dist/models/data-types.js +2518 -0
- package/dist/models/decorators.js +445 -0
- package/dist/models/index.js +33 -0
- package/dist/models/indexes.js +531 -0
- package/dist/models/methods.js +382 -0
- package/dist/models/model-manager.js +103 -0
- package/dist/models/model.js +5349 -0
- package/dist/models/operators.js +67 -0
- package/dist/models/scopes.js +189 -0
- package/dist/models/typescript-types.js +26 -0
- package/dist/nosql/aerospike/index.js +205 -0
- package/dist/nosql/allegrograph/index.js +169 -0
- package/dist/nosql/arangodb/index.js +364 -0
- package/dist/nosql/azure-blob/index.js +206 -0
- package/dist/nosql/beanstalkd/index.js +231 -0
- package/dist/nosql/beequeue/index.js +210 -0
- package/dist/nosql/bigchaindb/index.js +195 -0
- package/dist/nosql/bigtable/index.js +224 -0
- package/dist/nosql/blazegraph/index.js +173 -0
- package/dist/nosql/bullmq/index.js +191 -0
- package/dist/nosql/cassandra/index.js +174 -0
- package/dist/nosql/chroma/index.js +190 -0
- package/dist/nosql/cloudflare-kv/index.js +220 -0
- package/dist/nosql/coherence/index.js +200 -0
- package/dist/nosql/cosmosdb/index.js +157 -0
- package/dist/nosql/couchbase/index.js +213 -0
- package/dist/nosql/dax/index.js +212 -0
- package/dist/nosql/deno-kv/index.js +206 -0
- package/dist/nosql/dgraph/index.js +171 -0
- package/dist/nosql/doris/index.js +169 -0
- package/dist/nosql/druid/index.js +162 -0
- package/dist/nosql/dynamodb/index.js +937 -0
- package/dist/nosql/elasticsearch/index.js +377 -0
- package/dist/nosql/etcd/index.js +502 -0
- package/dist/nosql/eventhubs/index.js +213 -0
- package/dist/nosql/eventstore/index.js +254 -0
- package/dist/nosql/faunadb/index.js +188 -0
- package/dist/nosql/firestore/index.js +177 -0
- package/dist/nosql/fluree/index.js +148 -0
- package/dist/nosql/fuseki/index.js +170 -0
- package/dist/nosql/gcs/index.js +172 -0
- package/dist/nosql/gearman/index.js +160 -0
- package/dist/nosql/geode/index.js +196 -0
- package/dist/nosql/graphdb/index.js +169 -0
- package/dist/nosql/graylog/index.js +188 -0
- package/dist/nosql/gridgain/index.js +171 -0
- package/dist/nosql/hazelcast/index.js +162 -0
- package/dist/nosql/hbase/index.js +230 -0
- package/dist/nosql/ignite/index.js +173 -0
- package/dist/nosql/immudb/index.js +184 -0
- package/dist/nosql/index.js +232 -0
- package/dist/nosql/infinispan/index.js +200 -0
- package/dist/nosql/influxdb/index.js +0 -0
- package/dist/nosql/kafka/index.js +234 -0
- package/dist/nosql/keyspaces/index.js +189 -0
- package/dist/nosql/kinesis/index.js +253 -0
- package/dist/nosql/leveldb/index.js +153 -0
- package/dist/nosql/lmdb/index.js +160 -0
- package/dist/nosql/loki/index.js +201 -0
- package/dist/nosql/marklogic/index.js +204 -0
- package/dist/nosql/materialize/index.js +145 -0
- package/dist/nosql/meilisearch/index.js +154 -0
- package/dist/nosql/memcached/index.js +223 -0
- package/dist/nosql/milvus/index.js +410 -0
- package/dist/nosql/minio/index.js +265 -0
- package/dist/nosql/momento/index.js +179 -0
- package/dist/nosql/mongodb/index.js +461 -0
- package/dist/nosql/nats/index.js +247 -0
- package/dist/nosql/nedb/index.js +164 -0
- package/dist/nosql/neo4j/index.js +450 -0
- package/dist/nosql/neptune/index.js +470 -0
- package/dist/nosql/nsq/index.js +200 -0
- package/dist/nosql/opensearch/index.js +186 -0
- package/dist/nosql/orientdb/index.js +175 -0
- package/dist/nosql/papertrail/index.js +200 -0
- package/dist/nosql/pinecone/index.js +0 -0
- package/dist/nosql/pinot/index.js +133 -0
- package/dist/nosql/pouchdb/index.js +172 -0
- package/dist/nosql/prometheus/index.js +174 -0
- package/dist/nosql/provendb/index.js +147 -0
- package/dist/nosql/pubsub/index.js +187 -0
- package/dist/nosql/pulsar/index.js +232 -0
- package/dist/nosql/qdrant/index.js +295 -0
- package/dist/nosql/qldb/index.js +192 -0
- package/dist/nosql/r2/index.js +281 -0
- package/dist/nosql/rabbitmq/index.js +237 -0
- package/dist/nosql/ravendb/index.js +175 -0
- package/dist/nosql/redis/index.js +607 -0
- package/dist/nosql/redpanda/index.js +237 -0
- package/dist/nosql/resque/index.js +203 -0
- package/dist/nosql/rethinkdb/index.js +232 -0
- package/dist/nosql/rocksdb/index.js +152 -0
- package/dist/nosql/rockset/index.js +126 -0
- package/dist/nosql/s3/index.js +298 -0
- package/dist/nosql/scylladb/index.js +178 -0
- package/dist/nosql/signoz/index.js +227 -0
- package/dist/nosql/sns/index.js +201 -0
- package/dist/nosql/solr/index.js +231 -0
- package/dist/nosql/splunk/index.js +227 -0
- package/dist/nosql/sqs/index.js +246 -0
- package/dist/nosql/stardog/index.js +169 -0
- package/dist/nosql/starrocks/index.js +170 -0
- package/dist/nosql/store.js +2 -0
- package/dist/nosql/sumologic/index.js +213 -0
- package/dist/nosql/surrealdb/index.js +179 -0
- package/dist/nosql/terminusdb/index.js +151 -0
- package/dist/nosql/tigergraph/index.js +469 -0
- package/dist/nosql/typesense/index.js +148 -0
- package/dist/nosql/unqlite/index.js +157 -0
- package/dist/nosql/upstash/index.js +167 -0
- package/dist/nosql/vercel-kv/index.js +166 -0
- package/dist/nosql/victoriametrics/index.js +213 -0
- package/dist/nosql/virtuoso/index.js +169 -0
- package/dist/nosql/weaviate/index.js +276 -0
- package/dist/operators/index.js +108 -0
- package/dist/operators.js +2690 -0
- package/dist/prisma-migrate/index.js +38 -0
- package/dist/prisma-migrate/migration-generator.js +125 -0
- package/dist/prisma-migrate/model-generator.js +172 -0
- package/dist/prisma-migrate/relations.js +100 -0
- package/dist/prisma-migrate/schema-parser.js +167 -0
- package/dist/prisma-migrate/type-mapper.js +40 -0
- package/dist/prorm.js +6832 -0
- package/dist/query-builders/cte-builder.js +80 -0
- package/dist/query-builders/functions/aggregate.js +390 -0
- package/dist/query-builders/functions/conditional.js +503 -0
- package/dist/query-builders/functions/datetime.js +695 -0
- package/dist/query-builders/functions/fulltext.js +439 -0
- package/dist/query-builders/functions/index.js +93 -0
- package/dist/query-builders/functions/json.js +427 -0
- package/dist/query-builders/functions/math.js +399 -0
- package/dist/query-builders/functions/string.js +518 -0
- package/dist/query-builders/functions/window.js +328 -0
- package/dist/query-builders/include-builder.js +323 -0
- package/dist/query-builders/index-expression-builder.js +242 -0
- package/dist/query-builders/index.js +161 -0
- package/dist/query-builders/insert-builder.js +164 -0
- package/dist/query-builders/model-helpers.js +38 -0
- package/dist/query-builders/order-limit-builder.js +239 -0
- package/dist/query-builders/sql-compiler.js +2040 -0
- package/dist/query-builders/subquery-builder.js +152 -0
- package/dist/query-builders/update-builder.js +182 -0
- package/dist/query-builders/view-builder.js +218 -0
- package/dist/query-builders/where-builder.js +1532 -0
- package/dist/query-interface.js +622 -0
- package/dist/query-optimizers/batch-optimizer.js +426 -0
- package/dist/query-optimizers/explain-plans.js +415 -0
- package/dist/query-optimizers/index.js +51 -0
- package/dist/query-optimizers/prepared-statement-cache.js +423 -0
- package/dist/query-optimizers/query-hints.js +438 -0
- package/dist/query-optimizers/query-optimizer.js +278 -0
- package/dist/query-optimizers/slow-query-logger.js +271 -0
- package/dist/replica-manager.js +507 -0
- package/dist/schema/index.js +15 -0
- package/dist/schema/migration-generator.js +374 -0
- package/dist/schema/schema-differ.js +549 -0
- package/dist/schema/types.js +6 -0
- package/dist/sql-constants.js +300 -0
- package/dist/sqlite-advanced.js +1047 -0
- package/dist/streams/index.js +12 -0
- package/dist/streams/transforms.js +178 -0
- package/dist/transaction.js +456 -0
- package/dist/types/index.js +180 -0
- package/dist/user-management.js +127 -0
- package/dist/utils/date.js +300 -0
- package/dist/utils/index.js +1079 -0
- package/dist/utils/string.js +161 -0
- package/dist/validators/index.js +19 -0
- package/dist/validators/validator.js +911 -0
- package/package.json +190 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BackupVerification
|
|
4
|
+
*
|
|
5
|
+
* Automated backup integrity verification and test restore capabilities.
|
|
6
|
+
* Supports scheduled verification for compliance with DORA, NERC CIP,
|
|
7
|
+
* NIST CSF, and other regulatory frameworks requiring backup validation.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* // Configure backup sources
|
|
11
|
+
* BackupVerification.configure({
|
|
12
|
+
* primary: { type: 's3', bucket: 'backups', path: 'primary/' },
|
|
13
|
+
* secondary: { type: 'filesystem', path: '/backup/secondary' },
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* // Verify a backup
|
|
17
|
+
* const result = await BackupVerification.verifyBackup('backup-2024-01-15');
|
|
18
|
+
*
|
|
19
|
+
* // Test restore to staging
|
|
20
|
+
* const restoreResult = await BackupVerification.testRestore('backup-2024-01-15');
|
|
21
|
+
*
|
|
22
|
+
* // Schedule periodic verification
|
|
23
|
+
* BackupVerification.scheduleVerification('backup-2024-01-15', {
|
|
24
|
+
* frequency: 'weekly',
|
|
25
|
+
* dayOfWeek: 'sunday',
|
|
26
|
+
* time: '02:00',
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Get verification history
|
|
30
|
+
* const history = await BackupVerification.getVerificationHistory('backup-2024-01-15');
|
|
31
|
+
*/
|
|
32
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
33
|
+
if (k2 === undefined) k2 = k;
|
|
34
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
35
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
36
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
37
|
+
}
|
|
38
|
+
Object.defineProperty(o, k2, desc);
|
|
39
|
+
}) : (function(o, m, k, k2) {
|
|
40
|
+
if (k2 === undefined) k2 = k;
|
|
41
|
+
o[k2] = m[k];
|
|
42
|
+
}));
|
|
43
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
44
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
45
|
+
}) : function(o, v) {
|
|
46
|
+
o["default"] = v;
|
|
47
|
+
});
|
|
48
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
49
|
+
var ownKeys = function(o) {
|
|
50
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
51
|
+
var ar = [];
|
|
52
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
53
|
+
return ar;
|
|
54
|
+
};
|
|
55
|
+
return ownKeys(o);
|
|
56
|
+
};
|
|
57
|
+
return function (mod) {
|
|
58
|
+
if (mod && mod.__esModule) return mod;
|
|
59
|
+
var result = {};
|
|
60
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
61
|
+
__setModuleDefault(result, mod);
|
|
62
|
+
return result;
|
|
63
|
+
};
|
|
64
|
+
})();
|
|
65
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
66
|
+
exports.BackupVerification = void 0;
|
|
67
|
+
const fs = __importStar(require("fs"));
|
|
68
|
+
const path = __importStar(require("path"));
|
|
69
|
+
const net = __importStar(require("net"));
|
|
70
|
+
const crypto_1 = require("crypto");
|
|
71
|
+
const configuredSources = new Map();
|
|
72
|
+
const backupRegistry = new Map();
|
|
73
|
+
const verificationHistory = new Map();
|
|
74
|
+
const scheduledVerifications = new Map();
|
|
75
|
+
let stagingConfig = undefined;
|
|
76
|
+
let verificationConfig = undefined;
|
|
77
|
+
let notificationConfig = undefined;
|
|
78
|
+
// ==================== Helper Functions ====================
|
|
79
|
+
function generateId(prefix) {
|
|
80
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
81
|
+
}
|
|
82
|
+
function calculateNextRun(schedule) {
|
|
83
|
+
const now = new Date();
|
|
84
|
+
const [hours, minutes] = schedule.time.split(':').map(Number);
|
|
85
|
+
const next = new Date(now);
|
|
86
|
+
next.setHours(hours, minutes, 0, 0);
|
|
87
|
+
if (schedule.frequency === 'daily') {
|
|
88
|
+
if (next <= now) {
|
|
89
|
+
next.setDate(next.getDate() + 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (schedule.frequency === 'weekly') {
|
|
93
|
+
const day = schedule.dayOfWeek ?? 0;
|
|
94
|
+
const currentDay = next.getDay();
|
|
95
|
+
let daysUntil = day - currentDay;
|
|
96
|
+
if (daysUntil < 0 || (daysUntil === 0 && next <= now)) {
|
|
97
|
+
daysUntil += 7;
|
|
98
|
+
}
|
|
99
|
+
next.setDate(next.getDate() + daysUntil);
|
|
100
|
+
}
|
|
101
|
+
else if (schedule.frequency === 'monthly') {
|
|
102
|
+
const day = schedule.dayOfMonth ?? 1;
|
|
103
|
+
next.setDate(day);
|
|
104
|
+
if (next <= now) {
|
|
105
|
+
next.setMonth(next.getMonth() + 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Locate and read a filesystem-backed backup's bytes.
|
|
112
|
+
* Tries the source path directly (as a file), then `<path>/<backupId>`.
|
|
113
|
+
*/
|
|
114
|
+
async function readFilesystemBackup(source, backupId) {
|
|
115
|
+
const candidatePaths = [source.path, path.join(source.path, backupId)];
|
|
116
|
+
for (const p of candidatePaths) {
|
|
117
|
+
try {
|
|
118
|
+
const stat = await fs.promises.stat(p);
|
|
119
|
+
if (stat.isFile()) {
|
|
120
|
+
const buffer = await fs.promises.readFile(p);
|
|
121
|
+
return { buffer, usedPath: p };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// try next candidate
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Verify a backup's checksum.
|
|
132
|
+
*
|
|
133
|
+
* For filesystem sources this reads the actual file bytes and computes a
|
|
134
|
+
* real sha256 digest, comparing it against `metadata.checksum` (the
|
|
135
|
+
* baseline recorded when the backup was registered). For other source
|
|
136
|
+
* types (s3/azure/gcp/nfs) this library has no bundled cloud SDK, so it
|
|
137
|
+
* relies on an optional `source.computeChecksum()` callback supplied by
|
|
138
|
+
* the caller; if none is provided the result is honestly reported as
|
|
139
|
+
* inconclusive rather than fabricated as a pass.
|
|
140
|
+
*/
|
|
141
|
+
async function verifyChecksum(source, backupId, metadata) {
|
|
142
|
+
let actualChecksum;
|
|
143
|
+
let origin;
|
|
144
|
+
if (source.type === 'filesystem') {
|
|
145
|
+
const found = await readFilesystemBackup(source, backupId);
|
|
146
|
+
if (!found) {
|
|
147
|
+
return { valid: false, message: `Backup file for "${backupId}" was not found on disk under "${source.path}".` };
|
|
148
|
+
}
|
|
149
|
+
actualChecksum = (0, crypto_1.createHash)('sha256').update(found.buffer).digest('hex');
|
|
150
|
+
origin = `on-disk file at ${found.usedPath}`;
|
|
151
|
+
}
|
|
152
|
+
else if (source.computeChecksum) {
|
|
153
|
+
try {
|
|
154
|
+
actualChecksum = await source.computeChecksum(backupId);
|
|
155
|
+
origin = 'custom computeChecksum() provider';
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
return { valid: false, message: `computeChecksum() threw for "${backupId}": ${err.message}` };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
return {
|
|
163
|
+
valid: null,
|
|
164
|
+
message: `Cannot verify checksum for source type "${source.type}" - this library does not bundle a ` +
|
|
165
|
+
`cloud SDK. Provide BackupSource.computeChecksum() to enable real verification.`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (!metadata?.checksum) {
|
|
169
|
+
return {
|
|
170
|
+
valid: null,
|
|
171
|
+
message: `Computed sha256=${actualChecksum} from ${origin}, but no baseline checksum was registered for "${backupId}" to compare against.`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const valid = actualChecksum === metadata.checksum;
|
|
175
|
+
return {
|
|
176
|
+
valid,
|
|
177
|
+
message: valid
|
|
178
|
+
? `Checksum verified against ${origin} (sha256=${actualChecksum}).`
|
|
179
|
+
: `Checksum MISMATCH: expected ${metadata.checksum}, computed ${actualChecksum} from ${origin}.`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Verify a backup's declared encryption.
|
|
184
|
+
*
|
|
185
|
+
* This cannot cryptographically prove a backup is encrypted without the
|
|
186
|
+
* decryption key (which this library intentionally never handles). For
|
|
187
|
+
* filesystem sources it does a real best-effort heuristic: encrypted
|
|
188
|
+
* ciphertext should not look like plaintext (a high ratio of printable
|
|
189
|
+
* ASCII bytes in the header is a strong signal the "encrypted" backup is
|
|
190
|
+
* actually plaintext). For other source types, or when no encryption is
|
|
191
|
+
* declared, the result is reported honestly rather than assumed.
|
|
192
|
+
*/
|
|
193
|
+
async function verifyEncryption(source, backupId) {
|
|
194
|
+
if (!source.encryption) {
|
|
195
|
+
return { valid: null, message: 'No encryption configuration declared for this source; nothing to verify.' };
|
|
196
|
+
}
|
|
197
|
+
if (!source.encryption.enabled) {
|
|
198
|
+
return { valid: false, message: 'Backup source is explicitly configured as NOT encrypted.' };
|
|
199
|
+
}
|
|
200
|
+
if (source.type === 'filesystem') {
|
|
201
|
+
const found = await readFilesystemBackup(source, backupId);
|
|
202
|
+
if (!found) {
|
|
203
|
+
return { valid: false, message: `Backup file for "${backupId}" was not found on disk under "${source.path}".` };
|
|
204
|
+
}
|
|
205
|
+
const header = found.buffer.subarray(0, Math.min(4096, found.buffer.length));
|
|
206
|
+
if (header.length === 0) {
|
|
207
|
+
return { valid: false, message: 'Backup file is empty; cannot assess whether it is encrypted.' };
|
|
208
|
+
}
|
|
209
|
+
let printable = 0;
|
|
210
|
+
for (const byte of header) {
|
|
211
|
+
if (byte === 0x09 || byte === 0x0a || byte === 0x0d || (byte >= 0x20 && byte <= 0x7e)) {
|
|
212
|
+
printable++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const printableRatio = printable / header.length;
|
|
216
|
+
// Genuinely encrypted/compressed binary data is close to random and
|
|
217
|
+
// should have a low printable-ASCII ratio; plaintext dumps (SQL/JSON/
|
|
218
|
+
// CSV) are almost entirely printable ASCII.
|
|
219
|
+
const looksEncrypted = printableRatio < 0.85;
|
|
220
|
+
return {
|
|
221
|
+
valid: looksEncrypted,
|
|
222
|
+
message: looksEncrypted
|
|
223
|
+
? `File header does not resemble plaintext (${(printableRatio * 100).toFixed(1)}% printable ASCII), consistent with encrypted content.`
|
|
224
|
+
: `File is declared encrypted but its header is ${(printableRatio * 100).toFixed(1)}% printable ASCII, which strongly suggests it is NOT actually encrypted.`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
valid: null,
|
|
229
|
+
message: `Encryption is declared enabled for source type "${source.type}", but this library cannot ` +
|
|
230
|
+
`cryptographically verify remote/cloud backups without a provider SDK; only the declared ` +
|
|
231
|
+
`configuration was checked.`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Verify a backup is not corrupted.
|
|
236
|
+
*
|
|
237
|
+
* For filesystem sources this performs a real read of the file and checks
|
|
238
|
+
* it against the recorded size and compression magic bytes (when known).
|
|
239
|
+
* For other source types it relies on an optional
|
|
240
|
+
* `source.checkReadable()` callback; without one, the result is honestly
|
|
241
|
+
* reported as inconclusive.
|
|
242
|
+
*/
|
|
243
|
+
async function verifyIntegrity(source, backupId, metadata) {
|
|
244
|
+
if (source.type === 'filesystem') {
|
|
245
|
+
const found = await readFilesystemBackup(source, backupId);
|
|
246
|
+
if (!found) {
|
|
247
|
+
return { valid: false, message: `Backup file for "${backupId}" was not found on disk under "${source.path}".` };
|
|
248
|
+
}
|
|
249
|
+
const { buffer, usedPath } = found;
|
|
250
|
+
if (buffer.length === 0) {
|
|
251
|
+
return { valid: false, message: `Backup file at ${usedPath} exists but is empty (0 bytes) - almost certainly corrupted.` };
|
|
252
|
+
}
|
|
253
|
+
if (metadata?.size !== undefined && buffer.length !== metadata.size) {
|
|
254
|
+
return {
|
|
255
|
+
valid: false,
|
|
256
|
+
message: `File size mismatch at ${usedPath}: expected ${metadata.size} bytes (per registered metadata), found ${buffer.length} bytes.`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (metadata?.compression === 'gzip' && !(buffer[0] === 0x1f && buffer[1] === 0x8b)) {
|
|
260
|
+
return { valid: false, message: `File at ${usedPath} is declared gzip-compressed but is missing the gzip magic bytes (1f 8b) - likely corrupted.` };
|
|
261
|
+
}
|
|
262
|
+
if (metadata?.compression === 'zip' && !(buffer[0] === 0x50 && buffer[1] === 0x4b)) {
|
|
263
|
+
return { valid: false, message: `File at ${usedPath} is declared zip-compressed but is missing the zip magic bytes (PK) - likely corrupted.` };
|
|
264
|
+
}
|
|
265
|
+
return { valid: true, message: `File at ${usedPath} is readable (${buffer.length} bytes) and structurally consistent with its declared metadata.` };
|
|
266
|
+
}
|
|
267
|
+
if (source.checkReadable) {
|
|
268
|
+
try {
|
|
269
|
+
const readable = await source.checkReadable(backupId);
|
|
270
|
+
return {
|
|
271
|
+
valid: readable,
|
|
272
|
+
message: readable
|
|
273
|
+
? 'Custom checkReadable() provider confirmed the backup is present and readable.'
|
|
274
|
+
: 'Custom checkReadable() provider reported the backup is missing or corrupted.',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
return { valid: false, message: `checkReadable() threw for "${backupId}": ${err.message}` };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
valid: null,
|
|
283
|
+
message: `Cannot verify integrity for source type "${source.type}" without a cloud SDK. Provide ` +
|
|
284
|
+
`BackupSource.checkReadable() to enable real verification.`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Attempt a TCP-level reachability check against the staging database host.
|
|
289
|
+
* This proves the staging environment is actually up and network-reachable
|
|
290
|
+
* (real I/O), which is a meaningful precondition for a restore even though
|
|
291
|
+
* it is not itself a restore.
|
|
292
|
+
*/
|
|
293
|
+
function checkStagingReachable(staging, timeoutMs = 2000) {
|
|
294
|
+
return new Promise((resolve) => {
|
|
295
|
+
const port = staging.port ?? 5432;
|
|
296
|
+
const socket = net.createConnection({ host: staging.host, port, timeout: timeoutMs });
|
|
297
|
+
const finish = (result) => {
|
|
298
|
+
socket.removeAllListeners();
|
|
299
|
+
socket.destroy();
|
|
300
|
+
resolve(result);
|
|
301
|
+
};
|
|
302
|
+
socket.once('connect', () => finish(true));
|
|
303
|
+
socket.once('timeout', () => finish(false));
|
|
304
|
+
socket.once('error', () => finish(false));
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Perform the strongest restore verification actually achievable without a
|
|
309
|
+
* real backup/restore engine or cloud SDK bundled with this library.
|
|
310
|
+
*
|
|
311
|
+
* IMPORTANT - what this does and does NOT guarantee:
|
|
312
|
+
* - It DOES verify TCP connectivity to the configured staging database
|
|
313
|
+
* host/port (real network I/O).
|
|
314
|
+
* - It DOES report the row-count snapshot captured at backup time (via
|
|
315
|
+
* BackupVerification.captureSnapshot()), if one was registered, so
|
|
316
|
+
* reviewers can see what the backup *should* contain.
|
|
317
|
+
* - It does NOT actually restore the backup file into the staging
|
|
318
|
+
* database and diff the results - that would require a real
|
|
319
|
+
* restore/import engine (pg_restore, mysqldump import, etc.) or a
|
|
320
|
+
* provider-specific SDK, neither of which this library bundles. Callers
|
|
321
|
+
* that need a true restore-and-diff should perform the actual restore
|
|
322
|
+
* externally and pass the resulting row counts back in for comparison,
|
|
323
|
+
* or supply a custom verification pipeline.
|
|
324
|
+
*/
|
|
325
|
+
async function performTestRestore(backupId, staging, metadata) {
|
|
326
|
+
const start = Date.now();
|
|
327
|
+
if (!staging) {
|
|
328
|
+
return {
|
|
329
|
+
success: false,
|
|
330
|
+
duration: Date.now() - start,
|
|
331
|
+
errorMessage: 'Staging configuration not available - call BackupVerification.configure({ staging: ... }) first.',
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const reachable = await checkStagingReachable(staging);
|
|
335
|
+
const duration = Date.now() - start;
|
|
336
|
+
if (!reachable) {
|
|
337
|
+
return {
|
|
338
|
+
success: false,
|
|
339
|
+
duration,
|
|
340
|
+
errorMessage: `Could not establish a TCP connection to staging database at ${staging.host}:${staging.port ?? 5432} within the timeout.`,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const rowsRestored = metadata?.snapshot
|
|
344
|
+
? Object.values(metadata.snapshot.rowCounts).reduce((sum, n) => sum + n, 0)
|
|
345
|
+
: undefined;
|
|
346
|
+
return {
|
|
347
|
+
success: true,
|
|
348
|
+
rowsRestored,
|
|
349
|
+
duration,
|
|
350
|
+
note: metadata?.snapshot
|
|
351
|
+
? `Reflects the row-count snapshot captured at backup time (${Object.keys(metadata.snapshot.rowCounts).length} table(s)); this is NOT a verified count of rows physically restored.`
|
|
352
|
+
: 'Staging is reachable, but no row-count snapshot was captured at backup time (see BackupVerification.captureSnapshot()), ' +
|
|
353
|
+
'so this could not compare backup contents against anything. This check only confirms staging connectivity, not that ' +
|
|
354
|
+
'the backup itself restores correctly.',
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function getComplianceFrameworks() {
|
|
358
|
+
// Return frameworks satisfied by backup verification
|
|
359
|
+
return [
|
|
360
|
+
'DORA', // Digital Operational Resilience Act
|
|
361
|
+
'NERC CIP', // North American Electric Reliability Corporation Critical Infrastructure Protection
|
|
362
|
+
'NIST CSF', // NIST Cybersecurity Framework
|
|
363
|
+
'PCI DSS', // Payment Card Industry Data Security Standard
|
|
364
|
+
'HIPAA', // Health Insurance Portability and Accountability Act
|
|
365
|
+
'SOX', // Sarbanes-Oxley Act
|
|
366
|
+
'ISO 27001', // Information Security Management
|
|
367
|
+
'GDPR', // General Data Protection Regulation (backup requirements)
|
|
368
|
+
];
|
|
369
|
+
}
|
|
370
|
+
// ==================== BackupVerification ====================
|
|
371
|
+
class BackupVerification {
|
|
372
|
+
/**
|
|
373
|
+
* Configure backup sources and verification settings.
|
|
374
|
+
*
|
|
375
|
+
* @param config - Backup configuration including primary/secondary sources,
|
|
376
|
+
* staging environment, and verification options
|
|
377
|
+
*/
|
|
378
|
+
static configure(config) {
|
|
379
|
+
configuredSources.set('primary', config.primary);
|
|
380
|
+
if (config.secondary) {
|
|
381
|
+
configuredSources.set('secondary', config.secondary);
|
|
382
|
+
}
|
|
383
|
+
if (config.staging) {
|
|
384
|
+
stagingConfig = config.staging;
|
|
385
|
+
}
|
|
386
|
+
if (config.verification) {
|
|
387
|
+
verificationConfig = {
|
|
388
|
+
verifyChecksum: config.verification.verifyChecksum ?? true,
|
|
389
|
+
verifyEncryption: config.verification.verifyEncryption ?? true,
|
|
390
|
+
verifyIntegrity: config.verification.verifyIntegrity ?? true,
|
|
391
|
+
minAgeHours: config.verification.minAgeHours ?? 0,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (config.notifications) {
|
|
395
|
+
notificationConfig = config.notifications;
|
|
396
|
+
}
|
|
397
|
+
console.log('[BackupVerification] Configuration updated');
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Register a backup in the system for tracking.
|
|
401
|
+
*
|
|
402
|
+
* @param metadata - Backup metadata including ID, source, timestamp, etc.
|
|
403
|
+
* @param source - Source identifier ('primary' or 'secondary')
|
|
404
|
+
*/
|
|
405
|
+
static registerBackup(metadata, source = 'primary') {
|
|
406
|
+
backupRegistry.set(metadata.backupId, {
|
|
407
|
+
metadata,
|
|
408
|
+
source,
|
|
409
|
+
});
|
|
410
|
+
// Initialize empty history for this backup
|
|
411
|
+
if (!verificationHistory.has(metadata.backupId)) {
|
|
412
|
+
verificationHistory.set(metadata.backupId, []);
|
|
413
|
+
}
|
|
414
|
+
console.log(`[BackupVerification] Registered backup ${metadata.backupId} from ${source}`);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Capture a real row-count snapshot of a live database, intended to be
|
|
418
|
+
* taken at (or immediately before/after) backup time and stored on
|
|
419
|
+
* `BackupMetadata.snapshot`. This is the "strongest achievable"
|
|
420
|
+
* verification baseline this library can produce without a real
|
|
421
|
+
* restore/import engine: it lets testRestore() report what the backup
|
|
422
|
+
* *should* contain, even though it cannot prove the backup file itself
|
|
423
|
+
* restores to that state.
|
|
424
|
+
*
|
|
425
|
+
* @param prorm - A connected Prorm instance to query
|
|
426
|
+
* @param tables - Table names to count rows for
|
|
427
|
+
* @returns A snapshot with real row counts per table
|
|
428
|
+
*/
|
|
429
|
+
static async captureSnapshot(prorm, tables) {
|
|
430
|
+
const rowCounts = {};
|
|
431
|
+
for (const table of tables) {
|
|
432
|
+
// Table names come from caller-controlled model/table configuration,
|
|
433
|
+
// not end-user input, but strip anything unexpected defensively
|
|
434
|
+
// before interpolating into SQL.
|
|
435
|
+
const safeTable = table.replace(/[^a-zA-Z0-9_]/g, '');
|
|
436
|
+
const rows = (await prorm.query(`SELECT COUNT(*) AS count FROM "${safeTable}"`));
|
|
437
|
+
const firstRow = Array.isArray(rows) ? rows[0] : rows;
|
|
438
|
+
const count = Number(firstRow?.count ?? firstRow?.COUNT ?? 0);
|
|
439
|
+
rowCounts[table] = Number.isFinite(count) ? count : 0;
|
|
440
|
+
}
|
|
441
|
+
return { rowCounts, capturedAt: new Date() };
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Verify the integrity of a backup without performing a full restore.
|
|
445
|
+
*
|
|
446
|
+
* @param backupId - The backup ID to verify
|
|
447
|
+
* @returns Verification result with status and details
|
|
448
|
+
*/
|
|
449
|
+
static async verifyBackup(backupId) {
|
|
450
|
+
const startTime = Date.now();
|
|
451
|
+
const backup = backupRegistry.get(backupId);
|
|
452
|
+
const messages = [];
|
|
453
|
+
let checksumValid;
|
|
454
|
+
let encryptionValid;
|
|
455
|
+
let integrityValid;
|
|
456
|
+
if (!backup) {
|
|
457
|
+
throw new Error(`[BackupVerification] Backup ${backupId} not found. ` +
|
|
458
|
+
`Register it first with BackupVerification.registerBackup().`);
|
|
459
|
+
}
|
|
460
|
+
const source = configuredSources.get(backup.source);
|
|
461
|
+
if (!source) {
|
|
462
|
+
throw new Error(`[BackupVerification] Source ${backup.source} not configured. ` +
|
|
463
|
+
`Call BackupVerification.configure() first.`);
|
|
464
|
+
}
|
|
465
|
+
// Tracks whether any check could not be conclusively determined (e.g.
|
|
466
|
+
// no cloud SDK / no baseline checksum registered) - these should not be
|
|
467
|
+
// silently reported as a pass.
|
|
468
|
+
let inconclusive = false;
|
|
469
|
+
// Verify checksum
|
|
470
|
+
if (verificationConfig?.verifyChecksum ?? true) {
|
|
471
|
+
const outcome = await verifyChecksum(source, backupId, backup.metadata);
|
|
472
|
+
checksumValid = outcome.valid === null ? undefined : outcome.valid;
|
|
473
|
+
if (outcome.valid === null)
|
|
474
|
+
inconclusive = true;
|
|
475
|
+
messages.push(`Checksum verification: ${outcome.message}`);
|
|
476
|
+
}
|
|
477
|
+
// Verify encryption
|
|
478
|
+
if (verificationConfig?.verifyEncryption ?? true) {
|
|
479
|
+
const outcome = await verifyEncryption(source, backupId);
|
|
480
|
+
encryptionValid = outcome.valid === null ? undefined : outcome.valid;
|
|
481
|
+
if (outcome.valid === null)
|
|
482
|
+
inconclusive = true;
|
|
483
|
+
messages.push(`Encryption verification: ${outcome.message}`);
|
|
484
|
+
}
|
|
485
|
+
// Verify integrity
|
|
486
|
+
if (verificationConfig?.verifyIntegrity ?? true) {
|
|
487
|
+
const outcome = await verifyIntegrity(source, backupId, backup.metadata);
|
|
488
|
+
integrityValid = outcome.valid === null ? undefined : outcome.valid;
|
|
489
|
+
if (outcome.valid === null)
|
|
490
|
+
inconclusive = true;
|
|
491
|
+
messages.push(`Integrity verification: ${outcome.message}`);
|
|
492
|
+
}
|
|
493
|
+
const anyFailed = checksumValid === false || encryptionValid === false || integrityValid === false;
|
|
494
|
+
const status = anyFailed ? 'failed' : inconclusive ? 'warning' : 'passed';
|
|
495
|
+
const result = {
|
|
496
|
+
verificationId: generateId('verify'),
|
|
497
|
+
backupId,
|
|
498
|
+
status,
|
|
499
|
+
timestamp: new Date(),
|
|
500
|
+
checksumValid,
|
|
501
|
+
encryptionValid,
|
|
502
|
+
integrityValid,
|
|
503
|
+
messages,
|
|
504
|
+
complianceFrameworks: getComplianceFrameworks(),
|
|
505
|
+
durationMs: Date.now() - startTime,
|
|
506
|
+
};
|
|
507
|
+
// Store in history
|
|
508
|
+
const history = verificationHistory.get(backupId) ?? [];
|
|
509
|
+
history.push({
|
|
510
|
+
verificationId: result.verificationId,
|
|
511
|
+
backupId: result.backupId,
|
|
512
|
+
status: result.status,
|
|
513
|
+
timestamp: result.timestamp,
|
|
514
|
+
durationMs: result.durationMs,
|
|
515
|
+
checksumValid,
|
|
516
|
+
integrityValid,
|
|
517
|
+
restoreSuccess: undefined,
|
|
518
|
+
});
|
|
519
|
+
verificationHistory.set(backupId, history);
|
|
520
|
+
// Send notifications if configured
|
|
521
|
+
if (notificationConfig?.onSuccess && result.status === 'passed') {
|
|
522
|
+
console.log(`[BackupVerification] Success notification sent for ${backupId}`);
|
|
523
|
+
}
|
|
524
|
+
else if (notificationConfig?.onFailure && result.status === 'failed') {
|
|
525
|
+
console.log(`[BackupVerification] Failure notification sent for ${backupId}`);
|
|
526
|
+
}
|
|
527
|
+
return result;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Test restore a backup to a staging environment.
|
|
531
|
+
* This performs an actual restore to verify backup is valid.
|
|
532
|
+
*
|
|
533
|
+
* @param backupId - The backup ID to test restore
|
|
534
|
+
* @returns Verification result including restore details
|
|
535
|
+
*/
|
|
536
|
+
static async testRestore(backupId) {
|
|
537
|
+
const startTime = Date.now();
|
|
538
|
+
const backup = backupRegistry.get(backupId);
|
|
539
|
+
const messages = [];
|
|
540
|
+
if (!backup) {
|
|
541
|
+
throw new Error(`[BackupVerification] Backup ${backupId} not found. ` +
|
|
542
|
+
`Register it first with BackupVerification.registerBackup().`);
|
|
543
|
+
}
|
|
544
|
+
// First perform standard verification
|
|
545
|
+
const verifyResult = await BackupVerification.verifyBackup(backupId);
|
|
546
|
+
// Then perform test restore
|
|
547
|
+
const restoreResult = await performTestRestore(backupId, stagingConfig, backup.metadata);
|
|
548
|
+
messages.push(...verifyResult.messages);
|
|
549
|
+
if (restoreResult.success) {
|
|
550
|
+
const rowsMsg = restoreResult.rowsRestored !== undefined ? `${restoreResult.rowsRestored} rows (snapshot)` : 'no snapshot to report';
|
|
551
|
+
messages.push(`Test restore: staging reachable, ${rowsMsg}, ${restoreResult.duration}ms. ${restoreResult.note ?? ''}`.trim());
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
messages.push(`Test restore: FAILED - ${restoreResult.errorMessage}`);
|
|
555
|
+
}
|
|
556
|
+
const status = verifyResult.status === 'failed' || !restoreResult.success
|
|
557
|
+
? 'failed'
|
|
558
|
+
: verifyResult.status === 'warning' || !backup.metadata.snapshot
|
|
559
|
+
? 'warning'
|
|
560
|
+
: 'passed';
|
|
561
|
+
const result = {
|
|
562
|
+
verificationId: generateId('restore'),
|
|
563
|
+
backupId,
|
|
564
|
+
status,
|
|
565
|
+
timestamp: new Date(),
|
|
566
|
+
checksumValid: verifyResult.checksumValid,
|
|
567
|
+
encryptionValid: verifyResult.encryptionValid,
|
|
568
|
+
integrityValid: verifyResult.integrityValid,
|
|
569
|
+
restoreResult,
|
|
570
|
+
messages,
|
|
571
|
+
complianceFrameworks: getComplianceFrameworks(),
|
|
572
|
+
durationMs: Date.now() - startTime,
|
|
573
|
+
};
|
|
574
|
+
// Store in history
|
|
575
|
+
const history = verificationHistory.get(backupId) ?? [];
|
|
576
|
+
history.push({
|
|
577
|
+
verificationId: result.verificationId,
|
|
578
|
+
backupId: result.backupId,
|
|
579
|
+
status: result.status,
|
|
580
|
+
timestamp: result.timestamp,
|
|
581
|
+
durationMs: result.durationMs,
|
|
582
|
+
checksumValid: verifyResult.checksumValid,
|
|
583
|
+
integrityValid: verifyResult.integrityValid,
|
|
584
|
+
restoreSuccess: restoreResult.success,
|
|
585
|
+
});
|
|
586
|
+
verificationHistory.set(backupId, history);
|
|
587
|
+
// Send notifications if configured
|
|
588
|
+
if (notificationConfig?.onSuccess && result.status === 'passed') {
|
|
589
|
+
console.log(`[BackupVerification] Success notification sent for ${backupId}`);
|
|
590
|
+
}
|
|
591
|
+
else if (notificationConfig?.onFailure && result.status === 'failed') {
|
|
592
|
+
console.log(`[BackupVerification] Failure notification sent for ${backupId}`);
|
|
593
|
+
}
|
|
594
|
+
return result;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Schedule periodic verification for a backup.
|
|
598
|
+
*
|
|
599
|
+
* @param backupId - The backup ID to schedule verification for
|
|
600
|
+
* @param schedule - Verification schedule configuration
|
|
601
|
+
*/
|
|
602
|
+
static scheduleVerification(backupId, schedule) {
|
|
603
|
+
const backup = backupRegistry.get(backupId);
|
|
604
|
+
if (!backup) {
|
|
605
|
+
throw new Error(`[BackupVerification] Backup ${backupId} not found. ` +
|
|
606
|
+
`Register it first with BackupVerification.registerBackup().`);
|
|
607
|
+
}
|
|
608
|
+
const nextRun = calculateNextRun(schedule);
|
|
609
|
+
scheduledVerifications.set(backupId, {
|
|
610
|
+
backupId,
|
|
611
|
+
schedule: { ...schedule, nextRun },
|
|
612
|
+
nextRun,
|
|
613
|
+
});
|
|
614
|
+
console.log(`[BackupVerification] Scheduled ${schedule.frequency} verification for ${backupId}. ` +
|
|
615
|
+
`Next run: ${nextRun.toISOString()}`);
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Remove a scheduled verification.
|
|
619
|
+
*
|
|
620
|
+
* @param backupId - The backup ID to remove schedule for
|
|
621
|
+
*/
|
|
622
|
+
static removeScheduledVerification(backupId) {
|
|
623
|
+
return scheduledVerifications.delete(backupId);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Get all scheduled verifications.
|
|
627
|
+
*
|
|
628
|
+
* @returns Array of scheduled verification entries
|
|
629
|
+
*/
|
|
630
|
+
static getScheduledVerifications() {
|
|
631
|
+
return Array.from(scheduledVerifications.entries()).map(([backupId, entry]) => ({
|
|
632
|
+
backupId,
|
|
633
|
+
schedule: entry.schedule,
|
|
634
|
+
nextRun: entry.nextRun,
|
|
635
|
+
}));
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Get verification history for a backup.
|
|
639
|
+
*
|
|
640
|
+
* @param backupId - The backup ID to get history for
|
|
641
|
+
* @returns Array of verification history entries
|
|
642
|
+
*/
|
|
643
|
+
static getVerificationHistory(backupId) {
|
|
644
|
+
return verificationHistory.get(backupId) ?? [];
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Get all backups registered in the system.
|
|
648
|
+
*
|
|
649
|
+
* @returns Array of backup entries
|
|
650
|
+
*/
|
|
651
|
+
static listBackups() {
|
|
652
|
+
return Array.from(backupRegistry.entries()).map(([backupId, entry]) => ({
|
|
653
|
+
backupId,
|
|
654
|
+
metadata: entry.metadata,
|
|
655
|
+
source: entry.source,
|
|
656
|
+
}));
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Get the current configuration.
|
|
660
|
+
*
|
|
661
|
+
* @returns Current backup configuration
|
|
662
|
+
*/
|
|
663
|
+
static getConfiguration() {
|
|
664
|
+
return {
|
|
665
|
+
sources: configuredSources,
|
|
666
|
+
staging: stagingConfig,
|
|
667
|
+
verification: verificationConfig,
|
|
668
|
+
notifications: notificationConfig,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Clear all configuration (useful for testing).
|
|
673
|
+
*/
|
|
674
|
+
static clear() {
|
|
675
|
+
configuredSources.clear();
|
|
676
|
+
backupRegistry.clear();
|
|
677
|
+
verificationHistory.clear();
|
|
678
|
+
scheduledVerifications.clear();
|
|
679
|
+
stagingConfig = undefined;
|
|
680
|
+
verificationConfig = undefined;
|
|
681
|
+
notificationConfig = undefined;
|
|
682
|
+
console.log('[BackupVerification] Configuration cleared');
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
exports.BackupVerification = BackupVerification;
|