Task B — linguistische FP-Fix: - mail-classifier.ts: Subject-Keyword-Loop überspringt Keyword-Score wenn Subject das Keyword als Sucht-Compound enthält (z.B. "glücksspiel" in "Glücksspielsucht" → kein +50 Score). Globale linguistische Invariante Deutsch — Gambling-Marketer schreiben nie "Glücksspielsucht-Bonus". - gambling-keywords.mjs: GAMBLING_WHITELIST erweitert um Stamm-Varianten (wettsucht, spielsucht, suchtberatung, suchthilfe) als Fallback für Compounds wo keyword ≠ exakter Stamm. - 4 neue Tests: Forum Glücksspielsucht → PASS, Hilfe bei Spielsucht → PASS, Wettsucht-Selbsthilfe → PASS, Glücksspiel-Bonus 100€ → BLOCK. Task C — Phase-1-Data-Foundation: - mail-training-utils.ts: sanitizeSubjectForTraining() (PII-Stripping via Regex: EMAIL/URL/NUM/Greeting/ALL-CAPS) + detectSubjectLanguage() via franc (iso639-3). 26 Unit-Tests. - franc@6.2.0 installiert (~50KB ESM). - mail.ts insertMailClassificationSample(): ruft sanitizeSubjectForTraining() auf, schreibt detectedLang + subjectSanitized in features-JSON (Interim bis Schema-Migration). - mail-retention-cron.ts: Subject-Nullification nach 30 Tagen (täglich) + Sample-Purge nach 12 Monaten (monatlich). DSGVO Art. 5 Abs. 1e. 105 Tests grün (58 classifier + 26 training-utils + 11 display-name + 10 gmail). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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<void> {
|
|
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`);
|
|
}
|
|
}
|