feat: add backup and restore with encrypted secrets
Some checks are pending
CI / lint (push) Waiting to run
CI / typecheck (push) Waiting to run
CI / test (push) Waiting to run
CI / build (push) Blocked by required conditions

BackupService exports servers/profiles/projects to JSON bundle.
RestoreService imports with skip/overwrite/fail conflict strategies.
AES-256-GCM encryption for sensitive env vars via scrypt-derived keys.
REST endpoints and CLI commands for backup/restore operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-21 05:40:46 +00:00
parent 9a67e51307
commit 6da4ae495c
11 changed files with 1054 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
import type { FastifyInstance } from 'fastify';
import type { BackupService } from '../services/backup/backup-service.js';
import type { RestoreService } from '../services/backup/restore-service.js';
import type { BackupBundle, BackupOptions } from '../services/backup/backup-service.js';
import type { ConflictStrategy, RestoreOptions } from '../services/backup/restore-service.js';
export interface BackupDeps {
backupService: BackupService;
restoreService: RestoreService;
}
export function registerBackupRoutes(app: FastifyInstance, deps: BackupDeps): void {
app.post<{
Body: {
password?: string;
resources?: Array<'servers' | 'profiles' | 'projects'>;
};
}>('/api/v1/backup', async (request) => {
const opts: BackupOptions = {};
if (request.body?.password) {
opts.password = request.body.password;
}
if (request.body?.resources) {
opts.resources = request.body.resources;
}
const bundle = await deps.backupService.createBackup(opts);
return bundle;
});
app.post<{
Body: {
bundle: BackupBundle;
password?: string;
conflictStrategy?: ConflictStrategy;
};
}>('/api/v1/restore', async (request, reply) => {
const { bundle, password, conflictStrategy } = request.body;
if (!deps.restoreService.validateBundle(bundle)) {
reply.code(400);
return { error: 'Invalid backup bundle format', statusCode: 400 };
}
const restoreOpts: RestoreOptions = {};
if (password) {
restoreOpts.password = password;
}
if (conflictStrategy) {
restoreOpts.conflictStrategy = conflictStrategy;
}
const result = await deps.restoreService.restore(bundle, restoreOpts);
if (result.errors.length > 0 && result.serversCreated === 0 && result.profilesCreated === 0 && result.projectsCreated === 0) {
reply.code(422);
}
return result;
});
}