From 5e62f517fc896849c270e795cd31a110b83707cc Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Thu, 16 Apr 2026 08:05:56 +0200 Subject: [PATCH] feat: Update new project bounce thresholds for stricter email handling --- apps/api/src/services/SecurityService.ts | 24 +++++++++---------- .../__tests__/SecurityService.test.ts | 20 ++++++++-------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/api/src/services/SecurityService.ts b/apps/api/src/services/SecurityService.ts index aa96993..d75cc26 100644 --- a/apps/api/src/services/SecurityService.ts +++ b/apps/api/src/services/SecurityService.ts @@ -54,14 +54,14 @@ const SECURITY_THRESHOLDS = { // === New project thresholds (projects < 30 days old) === // Legitimate senders ramp up gradually; spammers blast immediately NEW_PROJECT_AGE_DAYS: 30, - NEW_PROJECT_BOUNCE_24H_CEILING_WARNING: 20, - NEW_PROJECT_BOUNCE_24H_CEILING_CRITICAL: 50, - NEW_PROJECT_BOUNCE_7DAY_CEILING_WARNING: 75, - NEW_PROJECT_BOUNCE_7DAY_CEILING_CRITICAL: 150, - NEW_PROJECT_COMPLAINT_24H_CEILING_WARNING: 5, - NEW_PROJECT_COMPLAINT_24H_CEILING_CRITICAL: 10, - NEW_PROJECT_COMPLAINT_7DAY_CEILING_WARNING: 15, - NEW_PROJECT_COMPLAINT_7DAY_CEILING_CRITICAL: 30, + NEW_PROJECT_BOUNCE_24H_CEILING_WARNING: 10, + NEW_PROJECT_BOUNCE_24H_CEILING_CRITICAL: 25, + NEW_PROJECT_BOUNCE_7DAY_CEILING_WARNING: 25, + NEW_PROJECT_BOUNCE_7DAY_CEILING_CRITICAL: 50, + NEW_PROJECT_COMPLAINT_24H_CEILING_WARNING: 3, + NEW_PROJECT_COMPLAINT_24H_CEILING_CRITICAL: 7, + NEW_PROJECT_COMPLAINT_7DAY_CEILING_WARNING: 10, + NEW_PROJECT_COMPLAINT_7DAY_CEILING_CRITICAL: 20, } as const; interface RateData { @@ -179,7 +179,9 @@ export class SecurityService { `[SECURITY] All-time stats: ${status.allTime.bounces} bounces, ${status.allTime.complaints} complaints out of ${status.allTime.total} emails`, ); if (status.isNewProject) { - signale.info(`[SECURITY] Project is under ${SECURITY_THRESHOLDS.NEW_PROJECT_AGE_DAYS} days old — stricter ceilings apply`); + signale.info( + `[SECURITY] Project is under ${SECURITY_THRESHOLDS.NEW_PROJECT_AGE_DAYS} days old — stricter ceilings apply`, + ); } // Send notification about critical security violations @@ -369,9 +371,7 @@ export class SecurityService { select: {createdAt: true}, }); - const projectAgeDays = project - ? (now.getTime() - project.createdAt.getTime()) / (1000 * 60 * 60 * 24) - : Infinity; + const projectAgeDays = project ? (now.getTime() - project.createdAt.getTime()) / (1000 * 60 * 60 * 24) : Infinity; const isNewProject = projectAgeDays < SECURITY_THRESHOLDS.NEW_PROJECT_AGE_DAYS; // Get 24-hour, 7-day and all-time rates in parallel diff --git a/apps/api/src/services/__tests__/SecurityService.test.ts b/apps/api/src/services/__tests__/SecurityService.test.ts index 0dec4be..f781ed8 100644 --- a/apps/api/src/services/__tests__/SecurityService.test.ts +++ b/apps/api/src/services/__tests__/SecurityService.test.ts @@ -169,8 +169,8 @@ describe('SecurityService', () => { describe('New project stricter thresholds', () => { it('should apply stricter ceilings for projects under 30 days old', async () => { // Default project is created "now", so it's a new project - // 10,000 emails, 51 bounces (above 50 new project 24h critical ceiling) - await createEmails(10000, {bouncedCount: 51}); + // 10,000 emails, 26 bounces (above 25 new project 24h critical ceiling) + await createEmails(10000, {bouncedCount: 26}); const status = await SecurityService.getSecurityStatus(projectId); expect(status.isNewProject).toBe(true); @@ -186,26 +186,26 @@ describe('SecurityService', () => { data: {createdAt: oldDate}, }); - // 10,000 emails, 51 bounces (above 50 new project ceiling, below 100 standard ceiling) - await createEmails(10000, {bouncedCount: 51}); + // 10,000 emails, 26 bounces (above 25 new project ceiling, below 50 standard warning ceiling) + await createEmails(10000, {bouncedCount: 26}); const status = await SecurityService.getSecurityStatus(projectId); expect(status.isNewProject).toBe(false); - // 51 is above the 50-bounce 24h warning ceiling for established projects - expect(status.warnings.some(w => w.includes('24-hour bounce count'))).toBe(true); - // But below the 100-bounce 24h critical ceiling + // 26 is below the 50-bounce 24h warning ceiling for established projects + expect(status.warnings.some(w => w.includes('24-hour bounce count'))).toBe(false); + // And below the 100-bounce 24h critical ceiling expect(status.violations.some(v => v.includes('24-hour bounce count'))).toBe(false); }); it('should catch new project blasting emails with delayed bounces', async () => { // Simulate the spammer scenario: new project sends 20K emails, - // only 55 bounces have come back so far (rate is tiny: 0.275%) - await createEmails(20000, {bouncedCount: 55}); + // only 30 bounces have come back so far (rate is tiny: 0.15%) + await createEmails(20000, {bouncedCount: 30}); const status = await SecurityService.getSecurityStatus(projectId); expect(status.isNewProject).toBe(true); expect(status.shouldDisable).toBe(true); - // 55 > 50 new project 24h critical ceiling + // 30 > 25 new project 24h critical ceiling expect(status.violations.some(v => v.includes('24-hour bounce count'))).toBe(true); }); });