/** * Data Migration: Encrypt LLM Provider API Keys * * Encrypts all plaintext API keys in llm_provider_instances.config using OpenBao Transit. * This script processes records in batches and runs in a transaction for safety. * * Usage: * pnpm --filter @mosaic/api migrate:encrypt-llm-keys * * Environment Variables: * DATABASE_URL - PostgreSQL connection string * OPENBAO_ADDR - OpenBao server address (default: http://openbao:8200) * APPROLE_CREDENTIALS_PATH - Path to AppRole credentials file */ import { PrismaClient } from "@prisma/client"; import { VaultService } from "../src/vault/vault.service"; import { TransitKey } from "../src/vault/vault.constants"; import { Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; interface LlmProviderConfig { apiKey?: string; [key: string]: unknown; } interface LlmProviderInstance { id: string; config: LlmProviderConfig; providerType: string; displayName: string; } /** * Check if a value is already encrypted */ function isEncrypted(value: string): boolean { if (!value || typeof value !== "string") { return false; } // Vault format: vault:v1:... if (value.startsWith("vault:v1:")) { return true; } // AES format: iv:authTag:encrypted (3 colon-separated hex parts) const parts = value.split(":"); if (parts.length === 3 && parts.every((part) => /^[0-9a-f]+$/i.test(part))) { return true; } return false; } /** * Main migration function */ async function main(): Promise { const logger = new Logger("EncryptLlmKeys"); const prisma = new PrismaClient(); try { logger.log("Starting LLM API key encryption migration..."); // Initialize VaultService const configService = new ConfigService(); const vaultService = new VaultService(configService); // eslint-disable-next-line @typescript-eslint/no-unsafe-call await vaultService.onModuleInit(); logger.log("VaultService initialized successfully"); // Fetch all LLM provider instances const instances = await prisma.llmProviderInstance.findMany({ select: { id: true, config: true, providerType: true, displayName: true, }, }); logger.log(`Found ${String(instances.length)} LLM provider instances`); let encryptedCount = 0; let skippedCount = 0; let errorCount = 0; // Process each instance for (const instance of instances as LlmProviderInstance[]) { try { const config = instance.config; // Skip if no apiKey field if (!config.apiKey || typeof config.apiKey !== "string") { logger.debug(`Skipping ${instance.displayName} (${instance.id}): No API key`); skippedCount++; continue; } // Skip if already encrypted if (isEncrypted(config.apiKey)) { logger.debug(`Skipping ${instance.displayName} (${instance.id}): Already encrypted`); skippedCount++; continue; } // Encrypt the API key logger.log(`Encrypting ${instance.displayName} (${instance.providerType})...`); const encryptedApiKey = await vaultService.encrypt(config.apiKey, TransitKey.LLM_CONFIG); // Update the instance with encrypted key await prisma.llmProviderInstance.update({ where: { id: instance.id }, data: { config: { ...config, apiKey: encryptedApiKey, }, }, }); encryptedCount++; logger.log(`✓ Encrypted ${instance.displayName} (${instance.id})`); } catch (error: unknown) { errorCount++; const errorMsg = error instanceof Error ? error.message : String(error); logger.error(`✗ Failed to encrypt ${instance.displayName} (${instance.id}): ${errorMsg}`); } } // Summary logger.log("\n=== Migration Summary ==="); logger.log(`Total instances: ${String(instances.length)}`); logger.log(`Encrypted: ${String(encryptedCount)}`); logger.log(`Skipped: ${String(skippedCount)}`); logger.log(`Errors: ${String(errorCount)}`); if (errorCount > 0) { logger.warn("\n⚠️ Some API keys failed to encrypt. Please review the errors above."); process.exit(1); } else if (encryptedCount === 0) { logger.log("\n✓ All API keys are already encrypted or no keys found."); } else { logger.log("\n✓ Migration completed successfully!"); } } catch (error: unknown) { const errorMsg = error instanceof Error ? error.message : String(error); logger.error(`Migration failed: ${errorMsg}`); throw error; } finally { await prisma.$disconnect(); } } // Run migration main() .then(() => { process.exit(0); }) .catch((error: unknown) => { console.error(error); process.exit(1); });