feat(api): add SSE subscriber isolation, token pruning and E2E rate-limit guard

- event-bus: wrap each subscriber.fn call in try/catch so one throwing subscriber cannot kill delivery to all others
- event-bus: log Redis parse errors instead of swallowing them silently; add .catch() on Redis publish promise for async fallback to local delivery
- pruning.ts: new runPruning() deletes expired invite tokens, expired password-reset tokens, and read notifications older than 90 days
- settings.runPruning: expose pruning as adminProcedure mutation
- trpc.ts: E2E_TEST_MODE rate-limit bypass is now a no-op in production (NODE_ENV=production); logs a startup warning if misconfigured

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 08:35:39 +02:00
parent 472d87c829
commit 60d267fa0a
4 changed files with 56 additions and 5 deletions
+36
View File
@@ -0,0 +1,36 @@
import type { PrismaClient } from "@capakraken/db";
const NOTIFICATION_RETENTION_DAYS = 90;
/**
* Deletes expired invite tokens, expired password-reset tokens, and read
* notifications older than NOTIFICATION_RETENTION_DAYS.
*
* Designed to be called from an admin procedure or a scheduled job.
* Returns counts of deleted rows for observability.
*/
export async function runPruning(db: PrismaClient): Promise<{
inviteTokensDeleted: number;
passwordResetTokensDeleted: number;
notificationsDeleted: number;
}> {
const now = new Date();
const notificationCutoff = new Date(now.getTime() - NOTIFICATION_RETENTION_DAYS * 24 * 60 * 60 * 1000);
const [inviteResult, resetResult, notificationResult] = await Promise.all([
db.inviteToken.deleteMany({ where: { expiresAt: { lt: now } } }),
db.passwordResetToken.deleteMany({ where: { expiresAt: { lt: now } } }),
db.notification.deleteMany({
where: {
readAt: { not: null },
createdAt: { lt: notificationCutoff },
},
}),
]);
return {
inviteTokensDeleted: inviteResult.count,
passwordResetTokensDeleted: resetResult.count,
notificationsDeleted: notificationResult.count,
};
}