feat(backup): full DR bundle export + admin-configurable offsite destinations
Backend-agnostic disaster-recovery backup engine that runs on the current storage backend (no storage cutover required): - Full-bundle export: db.dump (pg_dump custom) + every storage blob + manifest.json with per-object SHA-256, streamed as a tar. Entry points: admin UI download, GET /api/v1/admin/backup/export, scripts/create-full-backup.ts. - Admin-configurable push destinations (backup_destinations table, migration 0091): SFTP/SSH, S3-compatible (reuses the minio client), and mounted path/NAS behind one transport interface (test/push/prune). Secrets AES-GCM at rest; API returns only *IsSet markers. - Opt-in per-destination AES-256 bundle encryption (scrypt KDF, streamed) + scripts/decrypt-backup.ts for restore. - Wired the previously-dead database-backup cron to runScheduledBackupPush (push to enabled destinations, prune to retention, alert super-admins on failure). Tests: 1608 unit/integration pass; tsc + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
31
scripts/decrypt-backup.ts
Normal file
31
scripts/decrypt-backup.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Decrypt an encrypted backup bundle (`*.tar.enc`) produced when a destination
|
||||
* has bundle encryption enabled. Restore step — see
|
||||
* docs/backup-restore-runbook.md.
|
||||
*
|
||||
* BACKUP_PASSPHRASE='…' pnpm tsx scripts/decrypt-backup.ts <in.tar.enc> <out.tar>
|
||||
*
|
||||
* The passphrase is read from $BACKUP_PASSPHRASE (not argv, to keep it out of
|
||||
* shell history / the process list).
|
||||
*/
|
||||
import { decryptFileToFile } from '@/lib/services/backup-destinations/bundle-encryption';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const [input, output] = process.argv.slice(2);
|
||||
const passphrase = process.env.BACKUP_PASSPHRASE;
|
||||
if (!input || !output) {
|
||||
throw new Error(
|
||||
'Usage: BACKUP_PASSPHRASE=… pnpm tsx scripts/decrypt-backup.ts <in.tar.enc> <out.tar>',
|
||||
);
|
||||
}
|
||||
if (!passphrase) throw new Error('Set BACKUP_PASSPHRASE in the environment');
|
||||
await decryptFileToFile(input, output, passphrase);
|
||||
process.stdout.write(`Decrypted → ${output}\n`, () => process.exit(0));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(
|
||||
`Decrypt failed: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
() => process.exit(1),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user