/** * Mail-Retention-Cron — Phase 1 Data-Foundation. * * Zweck: DSGVO-konforme Datensparsamkeit (Art. 5 Abs. 1e — Speicherbegrenzung). * * Zwei Retention-Jobs: * * 1. Subject-Nullification (täglich): * Raw-Subject in MailClassificationSample auf null setzen wenn älter als 30 Tage. * Das sanitisierte Subject (features.subjectSanitized) bleibt erhalten — * nur der raw-Text wird entfernt. Begründung: raw-Subject kann noch PII * enthalten die sanitizeSubjectForTraining() übersehen hat (z.B. Names die * nicht ALL-CAPS sind). Nach 30 Tagen ist der Klassifikations-Nutzen gering. * * 2. Sample-Purge (monatlich): * MailClassificationSamples älter als 12 Monate werden hart gelöscht. * Begründung: Trainingsdaten werden im Phase-2-Export batch-extrahiert * (Colab). Nach 12 Monaten sind ältere Samples für Modell-Updates weniger * relevant als neue Samples. User-Lösch-Recht (Art. 17) ist davon unabhängig * und wird via deleteUserMailClassificationSamples() behandelt. * * Läuft nur in Production (import.meta.dev guard). * Beide Jobs laufen beim Server-Start einmalig (initial sweep) + dann periodisch. */ import { consola } from "consola"; import { usePrisma } from "../utils/prisma"; // Subject-Nullification: täglich const SUBJECT_NULLIFY_INTERVAL_MS = 24 * 60 * 60 * 1000; // Subject wird nach 30 Tagen auf null gesetzt const SUBJECT_NULLIFY_AGE_DAYS = 30; // Sample-Purge: alle 30 Tage (monatlich) const SAMPLE_PURGE_INTERVAL_MS = 30 * 24 * 60 * 60 * 1000; // Samples älter als 12 Monate werden gelöscht const SAMPLE_PURGE_AGE_DAYS = 365; export default defineNitroPlugin((nitro) => { if (import.meta.dev) return; consola.info("[mail-retention-cron] Starting — subject-nullify daily, sample-purge monthly"); // Einmaliger initialer Sweep bei Server-Start (deckt verpasste Runs ab) void runSubjectNullification().catch(() => {}); void runSamplePurge().catch(() => {}); const nullifyInterval = setInterval(() => { void runSubjectNullification().catch(() => {}); }, SUBJECT_NULLIFY_INTERVAL_MS); const purgeInterval = setInterval(() => { void runSamplePurge().catch(() => {}); }, SAMPLE_PURGE_INTERVAL_MS); nitro.hooks.hook("close", () => { clearInterval(nullifyInterval); clearInterval(purgeInterval); }); }); /** * Setzt raw-Subject-Feld auf null für Samples älter als 30 Tage. * features.subjectSanitized bleibt erhalten (wurde bei Insert geschrieben). * * Verwendet $executeRaw weil Prisma updateMany kein * WHERE subject IS NOT NULL direkt unterstützt (kein affected-rows-count). */ async function runSubjectNullification(): Promise { const db = usePrisma(); const cutoff = new Date(Date.now() - SUBJECT_NULLIFY_AGE_DAYS * 86_400_000); const result = await db.$executeRaw` UPDATE "rebreak"."mail_classification_samples" SET "subject" = NULL WHERE "subject" IS NOT NULL AND "created_at" < ${cutoff}::timestamptz `; if (result > 0) { consola.info(`[mail-retention-cron] subject-nullify: ${result} samples anonymized`); } } /** * Löscht MailClassificationSamples älter als 12 Monate. * Gibt die Anzahl gelöschter Rows zurück (für Logging). */ async function runSamplePurge(): Promise { const db = usePrisma(); const cutoff = new Date(Date.now() - SAMPLE_PURGE_AGE_DAYS * 86_400_000); const result = await db.mailClassificationSample.deleteMany({ where: { createdAt: { lt: cutoff } }, }); if (result.count > 0) { consola.info(`[mail-retention-cron] sample-purge: ${result.count} old samples deleted`); } }