/** * One-time migration: encrypt existing plaintext kycData records. * * Usage: * KYC_ENCRYPTION_KEY= npx tsx scripts/encrypt-existing-kyc.ts [--dry-run] * * This script: * 1. Reads all User rows where kycData is not null * 2. Skips rows that are already encrypted (have the `enc:` prefix) * 3. Encrypts plaintext kycData using AES-256-GCM * 4. Updates each row in a transaction */ import { PrismaClient } from '@prisma/client'; import { encryptField, isEncrypted, type FieldEncryptionConfig, } from '../apps/api/src/modules/shared/infrastructure/field-encryption'; async function main() { const key = process.env['KYC_ENCRYPTION_KEY']; if (!key) { console.error('ERROR: KYC_ENCRYPTION_KEY env var is required.'); process.exit(1); } const dryRun = process.argv.includes('--dry-run'); const config: FieldEncryptionConfig = { key, keyVersion: parseInt(process.env['KYC_ENCRYPTION_KEY_VERSION'] ?? '1', 10), }; // Use raw PrismaClient without encryption middleware to read plaintext const prisma = new PrismaClient(); await prisma.$connect(); try { const users = await prisma.user.findMany({ where: { kycData: { not: null } }, select: { id: true, kycData: true }, }); console.warn(`Found ${users.length} users with kycData.`); let encrypted = 0; let skipped = 0; for (const user of users) { if (isEncrypted(user.kycData)) { skipped++; continue; } const encryptedValue = encryptField(user.kycData, config); if (dryRun) { console.warn(`[DRY RUN] Would encrypt kycData for user ${user.id}`); } else { await prisma.user.update({ where: { id: user.id }, data: { kycData: encryptedValue }, }); } encrypted++; } console.warn( `${dryRun ? '[DRY RUN] ' : ''}Done. Encrypted: ${encrypted}, Already encrypted: ${skipped}`, ); } finally { await prisma.$disconnect(); } } main().catch((err) => { console.error('Migration failed:', err); process.exit(1); });