diff --git a/src/server/services/backupService.test.ts b/src/server/services/backupService.test.ts index 4da9f00..71e068c 100644 --- a/src/server/services/backupService.test.ts +++ b/src/server/services/backupService.test.ts @@ -290,6 +290,52 @@ describe('backupService', () => { ]); }); + it('does not export runtime database config in preferences backups', async () => { + await db.insert(schema.settings).values([ + { key: 'db_type', value: JSON.stringify('postgres') }, + { key: 'db_url', value: JSON.stringify('postgres://metapi:secret@db.example.com:5432/metapi') }, + { key: 'db_ssl', value: JSON.stringify(true) }, + { key: 'routing_fallback_unit_cost', value: JSON.stringify(0.25) }, + ]).run(); + + const exported = await backupService.exportBackup('preferences') as any; + const exportedSettingKeys = exported.preferences.settings.map((row: { key: string }) => row.key); + + expect(exportedSettingKeys).toContain('routing_fallback_unit_cost'); + expect(exportedSettingKeys).not.toContain('db_type'); + expect(exportedSettingKeys).not.toContain('db_url'); + expect(exportedSettingKeys).not.toContain('db_ssl'); + }); + + it('ignores imported runtime database config settings', async () => { + const result = await backupService.importBackup({ + version: '2.1', + timestamp: Date.now(), + type: 'preferences', + preferences: { + settings: [ + { key: 'db_type', value: 'postgres' }, + { key: 'db_url', value: 'postgres://metapi:secret@db.example.com:5432/metapi' }, + { key: 'db_ssl', value: true }, + { key: 'routing_fallback_unit_cost', value: 0.25 }, + ], + }, + }); + + expect(result.sections.preferences).toBe(true); + expect(result.appliedSettings).toEqual([ + { key: 'routing_fallback_unit_cost', value: 0.25 }, + ]); + + const settingsRows = await db.select().from(schema.settings).all(); + const savedKeys = settingsRows.map((row) => row.key); + + expect(savedKeys).toContain('routing_fallback_unit_cost'); + expect(savedKeys).not.toContain('db_type'); + expect(savedKeys).not.toContain('db_url'); + expect(savedKeys).not.toContain('db_ssl'); + }); + it('preserves local logs and runtime stats when importing account backups', async () => { const exportedAt = '2026-03-20T09:00:00.000Z'; const localRuntimeAt = '2026-03-21T10:30:00.000Z'; diff --git a/src/server/services/backupService.ts b/src/server/services/backupService.ts index 3badddc..1df030d 100644 --- a/src/server/services/backupService.ts +++ b/src/server/services/backupService.ts @@ -228,6 +228,10 @@ interface BackupImportResult { const EXCLUDED_SETTING_KEYS = new Set([ // Keep current admin login credential unchanged to avoid accidental lock-out. 'auth_token', + // Runtime database selection is environment-bound and must not be propagated by backups. + 'db_type', + 'db_url', + 'db_ssl', ]); const BACKUP_WEBDAV_CONFIG_SETTING_KEY = 'backup_webdav_config_v1'; const BACKUP_WEBDAV_STATE_SETTING_KEY = 'backup_webdav_state_v1'; diff --git a/src/server/services/databaseMigrationService.test.ts b/src/server/services/databaseMigrationService.test.ts index 223ad59..54a11ee 100644 --- a/src/server/services/databaseMigrationService.test.ts +++ b/src/server/services/databaseMigrationService.test.ts @@ -465,4 +465,46 @@ describe('databaseMigrationService', () => { vi.resetModules(); } }); + + it('excludes runtime database config settings from migration statements', () => { + const statements = __databaseMigrationServiceTestUtils.buildStatements({ + version: 'test', + timestamp: Date.now(), + accounts: { + sites: [], + siteAnnouncements: [], + siteDisabledModels: [], + accounts: [], + accountTokens: [], + checkinLogs: [], + modelAvailability: [], + tokenModelAvailability: [], + tokenRoutes: [], + routeChannels: [], + routeGroupSources: [], + proxyLogs: [], + proxyVideoTasks: [], + proxyFiles: [], + downstreamApiKeys: [], + events: [], + }, + preferences: { + settings: [ + { key: 'db_type', value: 'sqlite' }, + { key: 'db_url', value: '/app/data/hub.db' }, + { key: 'db_ssl', value: false }, + { key: 'routing_fallback_unit_cost', value: 0.25 }, + ], + }, + } as any); + + const migratedSettingKeys = statements + .filter((statement) => statement.table === 'settings') + .map((statement) => statement.values[0]); + + expect(migratedSettingKeys).toContain('routing_fallback_unit_cost'); + expect(migratedSettingKeys).not.toContain('db_type'); + expect(migratedSettingKeys).not.toContain('db_url'); + expect(migratedSettingKeys).not.toContain('db_ssl'); + }); }); diff --git a/src/server/services/databaseMigrationService.ts b/src/server/services/databaseMigrationService.ts index d03f0b9..82e8a35 100644 --- a/src/server/services/databaseMigrationService.ts +++ b/src/server/services/databaseMigrationService.ts @@ -85,6 +85,7 @@ interface InsertStatement { } const DIALECTS: MigrationDialect[] = ['sqlite', 'mysql', 'postgres']; +const RUNTIME_DATABASE_SETTING_KEYS = new Set(['db_type', 'db_url', 'db_ssl']); function asString(value: unknown): string { return typeof value === 'string' ? value.trim() : ''; @@ -628,6 +629,9 @@ function buildStatements(snapshot: BackupSnapshot): InsertStatement[] { } for (const row of snapshot.preferences.settings) { + if (RUNTIME_DATABASE_SETTING_KEYS.has(row.key)) { + continue; + } statements.push({ table: 'settings', columns: ['key', 'value'],