feat(mail/delivery): add error-count based blocking to rate limiter; improve test SMTP server port selection; add tsbuild scripts and devDependency; remove stale backup file

This commit is contained in:
2026-02-10 14:44:45 +00:00
parent f262f602a0
commit 14be3cdb9a
8 changed files with 9718 additions and 709 deletions

View File

@@ -29,15 +29,21 @@ export interface IRateLimitConfig {
interface TokenBucket {
/** Current number of tokens */
tokens: number;
/** Last time tokens were refilled */
lastRefill: number;
/** Total allowed requests */
allowed: number;
/** Total denied requests */
denied: number;
/** Error count for blocking decisions */
errors: number;
/** Timestamp of first error in current window */
firstErrorTime: number;
}
/**
@@ -74,7 +80,9 @@ export class RateLimiter {
tokens: this.config.initialTokens,
lastRefill: Date.now(),
allowed: 0,
denied: 0
denied: 0,
errors: 0,
firstErrorTime: 0
};
// Log initialization
@@ -196,7 +204,9 @@ export class RateLimiter {
tokens: this.config.initialTokens,
lastRefill: Date.now(),
allowed: 0,
denied: 0
denied: 0,
errors: 0,
firstErrorTime: 0
});
}
@@ -266,16 +276,50 @@ export class RateLimiter {
public cleanup(maxAge: number = 24 * 60 * 60 * 1000): void {
const now = Date.now();
let removed = 0;
for (const [key, bucket] of this.buckets.entries()) {
if (now - bucket.lastRefill > maxAge) {
this.buckets.delete(key);
removed++;
}
}
if (removed > 0) {
logger.log('debug', `Cleaned up ${removed} stale rate limit buckets`);
}
}
/**
* Record an error for a key (e.g., IP address) and determine if blocking is needed
* RFC 5321 Section 4.5.4.1 suggests limiting errors to prevent abuse
*
* @param key Key to record error for (typically an IP address)
* @param errorWindow Time window for error tracking in ms (default: 5 minutes)
* @param errorThreshold Maximum errors before blocking (default: 10)
* @returns true if the key should be blocked due to excessive errors
*/
public recordError(key: string, errorWindow: number = 5 * 60 * 1000, errorThreshold: number = 10): boolean {
const bucket = this.getBucket(key);
const now = Date.now();
// Reset error count if the time window has expired
if (bucket.firstErrorTime === 0 || now - bucket.firstErrorTime > errorWindow) {
bucket.errors = 0;
bucket.firstErrorTime = now;
}
// Increment error count
bucket.errors++;
// Log error tracking
logger.log('debug', `Error recorded for ${key}: ${bucket.errors}/${errorThreshold} in window`);
// Check if threshold exceeded
if (bucket.errors >= errorThreshold) {
logger.log('warn', `Error threshold exceeded for ${key}: ${bucket.errors} errors`);
return true; // Should block
}
return false; // Continue allowing
}
}