From b391d45c0f14a719fc2b9644bad91ebe85886c4e Mon Sep 17 00:00:00 2001 From: Bellabuks Date: Fri, 26 Jun 2026 20:16:00 +0100 Subject: [PATCH 1/4] Add tenant-scoped rate limiting and abuse signal aggregation --- .../guards/tenant-quota.guard.ts | 36 +++++++++++++++++++ src/security/abuse/abuse-score.service.ts | 21 +++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/rate-limiting/guards/tenant-quota.guard.ts create mode 100644 src/security/abuse/abuse-score.service.ts diff --git a/src/rate-limiting/guards/tenant-quota.guard.ts b/src/rate-limiting/guards/tenant-quota.guard.ts new file mode 100644 index 00000000..c683dd62 --- /dev/null +++ b/src/rate-limiting/guards/tenant-quota.guard.ts @@ -0,0 +1,36 @@ +import { Injectable, CanActivate, ExecutionContext, HttpException } from '@nestjs/common'; + +@Injectable() +export class TenantQuotaGuard implements CanActivate { + private readonly tiers: Record = { + FREE: 100, + PRO: 1000, + ENTERPRISE: -1, + }; + private counters = new Map(); + + canActivate(context: ExecutionContext): boolean { + const req = context.switchToHttp().getRequest(); + const tenantId = req.headers['x-tenant-id'] as string; + if (!tenantId) return true; + + const tier = (req as any).tenantTier || 'FREE'; + const limit = this.tiers[tier] || this.tiers.FREE; + if (limit === -1) return true; + + const now = Date.now(); + const key = enant:; + const entry = this.counters.get(key); + + if (!entry || now > entry.resetAt) { + this.counters.set(key, { count: 1, resetAt: now + 60000 }); + return true; + } + + entry.count++; + if (entry.count > limit) { + throw new HttpException('Tenant rate limit exceeded', 429); + } + return true; + } +} \ No newline at end of file diff --git a/src/security/abuse/abuse-score.service.ts b/src/security/abuse/abuse-score.service.ts new file mode 100644 index 00000000..ca7497ee --- /dev/null +++ b/src/security/abuse/abuse-score.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AbuseScoreService { + private scores = new Map(); + + async getCompositeScore(userId: string, ip: string): Promise { + const key = ${userId}:; + const entry = this.scores.get(key); + if (!entry || Date.now() > entry.expiresAt) return 0; + return entry.score; + } + + addSignal(userId: string, ip: string, weight: number): void { + const key = ${userId}:; + const entry = this.scores.get(key); + const now = Date.now(); + const newScore = (entry && now <= entry.expiresAt ? entry.score : 0) + weight; + this.scores.set(key, { score: newScore, expiresAt: now + 60000 }); + } +} \ No newline at end of file From 11517464d47625a57600a56a2226a89fdd54e78c Mon Sep 17 00:00:00 2001 From: Bellabuks Date: Fri, 26 Jun 2026 21:59:44 +0100 Subject: [PATCH 2/4] fix: syntax errors in guards and services --- src/rate-limiting/guards/tenant-quota.guard.ts | 4 ++-- src/security/abuse/abuse-score.service.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rate-limiting/guards/tenant-quota.guard.ts b/src/rate-limiting/guards/tenant-quota.guard.ts index c683dd62..829121a3 100644 --- a/src/rate-limiting/guards/tenant-quota.guard.ts +++ b/src/rate-limiting/guards/tenant-quota.guard.ts @@ -19,7 +19,7 @@ export class TenantQuotaGuard implements CanActivate { if (limit === -1) return true; const now = Date.now(); - const key = enant:; + const key = tenantId; const entry = this.counters.get(key); if (!entry || now > entry.resetAt) { @@ -33,4 +33,4 @@ export class TenantQuotaGuard implements CanActivate { } return true; } -} \ No newline at end of file +} diff --git a/src/security/abuse/abuse-score.service.ts b/src/security/abuse/abuse-score.service.ts index ca7497ee..397cfcc1 100644 --- a/src/security/abuse/abuse-score.service.ts +++ b/src/security/abuse/abuse-score.service.ts @@ -5,17 +5,17 @@ export class AbuseScoreService { private scores = new Map(); async getCompositeScore(userId: string, ip: string): Promise { - const key = ${userId}:; + const key = userId + ':' + ip; const entry = this.scores.get(key); if (!entry || Date.now() > entry.expiresAt) return 0; return entry.score; } addSignal(userId: string, ip: string, weight: number): void { - const key = ${userId}:; + const key = userId + ':' + ip; const entry = this.scores.get(key); const now = Date.now(); const newScore = (entry && now <= entry.expiresAt ? entry.score : 0) + weight; this.scores.set(key, { score: newScore, expiresAt: now + 60000 }); } -} \ No newline at end of file +} From 5e2fcd7a0b5c727fb742a5dfb4837550dde9eb3f Mon Sep 17 00:00:00 2001 From: Bellabuks Date: Sat, 27 Jun 2026 04:01:39 +0100 Subject: [PATCH 3/4] fix: use template literals --- src/security/abuse/abuse-score.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/security/abuse/abuse-score.service.ts b/src/security/abuse/abuse-score.service.ts index 397cfcc1..4f9bedf6 100644 --- a/src/security/abuse/abuse-score.service.ts +++ b/src/security/abuse/abuse-score.service.ts @@ -5,14 +5,14 @@ export class AbuseScoreService { private scores = new Map(); async getCompositeScore(userId: string, ip: string): Promise { - const key = userId + ':' + ip; + const key = `${userId}:${ip}`; const entry = this.scores.get(key); if (!entry || Date.now() > entry.expiresAt) return 0; return entry.score; } addSignal(userId: string, ip: string, weight: number): void { - const key = userId + ':' + ip; + const key = `${userId}:${ip}`; const entry = this.scores.get(key); const now = Date.now(); const newScore = (entry && now <= entry.expiresAt ? entry.score : 0) + weight; From 1489267b6613d43e6daccb0f752d88ccc2c9dd79 Mon Sep 17 00:00:00 2001 From: Bellabuks Date: Sat, 27 Jun 2026 04:10:52 +0100 Subject: [PATCH 4/4] fix: use template literals