import { credentialEncryption } from '../classes/encryption.ts'; import type { AuthRepository } from './repositories/auth.repository.ts'; const encryptedSecretPrefix = 'enc:v1:'; const secretSettingAliases = { backupPassword: ['backup_encryption_password'], cloudflareToken: ['cloudflareAPIKey'], } as const; type TCanonicalSecretSettingKey = keyof typeof secretSettingAliases; export class SecretSettingsManager { constructor(private authRepo: AuthRepository) {} public isSecretKey(key: string): boolean { return this.resolveCanonicalKey(key) !== null; } public getCanonicalKeys(): TCanonicalSecretSettingKey[] { return Object.keys(secretSettingAliases) as TCanonicalSecretSettingKey[]; } public async get(key: string): Promise { const canonicalKey = this.resolveCanonicalKey(key); if (!canonicalKey) { return null; } for (const candidateKey of this.getCandidateKeys(canonicalKey)) { const secretValue = this.authRepo.getSecretSetting(candidateKey); if (secretValue !== null) { const decryptedValue = await this.decodeStoredValue(secretValue); await this.normalizeStoredSecret(canonicalKey, candidateKey, secretValue, decryptedValue); return decryptedValue; } const legacyValue = this.authRepo.getSetting(candidateKey); if (legacyValue !== null) { await this.set(canonicalKey, legacyValue); if (candidateKey !== canonicalKey) { this.authRepo.deleteSetting(candidateKey); } this.authRepo.deleteSetting(canonicalKey); return legacyValue; } } return null; } public async set(key: string, value: string | null): Promise { const canonicalKey = this.resolveCanonicalKey(key); if (!canonicalKey) { throw new Error(`Unsupported secret setting key: ${key}`); } if (!value) { this.clear(canonicalKey); return; } const encryptedValue = await credentialEncryption.encrypt({ value }); this.authRepo.setSecretSetting(canonicalKey, `${encryptedSecretPrefix}${encryptedValue}`); for (const aliasKey of secretSettingAliases[canonicalKey]) { this.authRepo.deleteSecretSetting(aliasKey); this.authRepo.deleteSetting(aliasKey); } this.authRepo.deleteSetting(canonicalKey); } public async has(key: string): Promise { return (await this.get(key)) !== null; } public clear(key: string): void { const canonicalKey = this.resolveCanonicalKey(key); if (!canonicalKey) { return; } this.authRepo.deleteSecretSetting(canonicalKey); this.authRepo.deleteSetting(canonicalKey); for (const aliasKey of secretSettingAliases[canonicalKey]) { this.authRepo.deleteSecretSetting(aliasKey); this.authRepo.deleteSetting(aliasKey); } } private resolveCanonicalKey(key: string): TCanonicalSecretSettingKey | null { if (key in secretSettingAliases) { return key as TCanonicalSecretSettingKey; } for (const [canonicalKey, aliases] of Object.entries(secretSettingAliases)) { if ((aliases as readonly string[]).includes(key)) { return canonicalKey as TCanonicalSecretSettingKey; } } return null; } private getCandidateKeys(canonicalKey: TCanonicalSecretSettingKey): string[] { return [canonicalKey, ...secretSettingAliases[canonicalKey]]; } private async decodeStoredValue(value: string): Promise { if (value.startsWith(encryptedSecretPrefix)) { const decrypted = await credentialEncryption.decrypt<{ value: string }>( value.slice(encryptedSecretPrefix.length), ); return decrypted.value; } // Compatibility for any earlier secret_settings rows stored without encryption. return value; } private async normalizeStoredSecret( canonicalKey: TCanonicalSecretSettingKey, sourceKey: string, storedValue: string, decryptedValue: string, ): Promise { if (sourceKey !== canonicalKey || !storedValue.startsWith(encryptedSecretPrefix)) { await this.set(canonicalKey, decryptedValue); if (sourceKey !== canonicalKey) { this.authRepo.deleteSecretSetting(sourceKey); } } this.authRepo.deleteSetting(canonicalKey); for (const aliasKey of secretSettingAliases[canonicalKey]) { this.authRepo.deleteSetting(aliasKey); } } }