feat(integration): components now play nicer with each other

This commit is contained in:
2025-05-30 05:30:06 +00:00
parent 2c244c4a9a
commit 40db395591
19 changed files with 2849 additions and 264 deletions

View File

@ -28,6 +28,9 @@ export interface IHierarchicalRateLimits {
// IP-specific rate limits (applied to specific IPs)
ips?: Record<string, IRateLimitConfig>;
// Domain-specific rate limits (applied to specific email domains)
domains?: Record<string, IRateLimitConfig>;
// Temporary blocks list and their expiry times
blocks?: Record<string, number>; // IP to expiry timestamp
}
@ -86,6 +89,7 @@ export class UnifiedRateLimiter extends EventEmitter {
private counters: Map<string, ILimitCounter> = new Map();
private patternCounters: Map<string, ILimitCounter> = new Map();
private ipCounters: Map<string, ILimitCounter> = new Map();
private domainCounters: Map<string, ILimitCounter> = new Map();
private cleanupInterval?: NodeJS.Timeout;
private stats: IRateLimiterStats;
@ -221,6 +225,13 @@ export class UnifiedRateLimiter extends EventEmitter {
}
}
// Clean domain counters
for (const [key, counter] of this.domainCounters.entries()) {
if (counter.lastReset < cutoff) {
this.domainCounters.delete(key);
}
}
// Update statistics
this.updateStats();
}
@ -231,9 +242,10 @@ export class UnifiedRateLimiter extends EventEmitter {
* @param ip IP address
* @param recipients Number of recipients
* @param pattern Matched pattern
* @param domain Domain name for domain-specific limits
* @returns Result of rate limit check
*/
public checkMessageLimit(email: string, ip: string, recipients: number, pattern?: string): IRateLimitResult {
public checkMessageLimit(email: string, ip: string, recipients: number, pattern?: string, domain?: string): IRateLimitResult {
// Check if IP is blocked
if (this.isIpBlocked(ip)) {
return {
@ -257,6 +269,14 @@ export class UnifiedRateLimiter extends EventEmitter {
}
}
// Check domain-specific limit if domain is provided
if (domain) {
const domainResult = this.checkDomainMessageLimit(domain);
if (!domainResult.allowed) {
return domainResult;
}
}
// Check IP-specific limit
const ipResult = this.checkIpMessageLimit(ip);
if (!ipResult.allowed) {
@ -264,7 +284,7 @@ export class UnifiedRateLimiter extends EventEmitter {
}
// Check recipient limit
const recipientResult = this.checkRecipientLimit(email, recipients, pattern);
const recipientResult = this.checkRecipientLimit(email, recipients, pattern, domain);
if (!recipientResult.allowed) {
return recipientResult;
}
@ -403,6 +423,64 @@ export class UnifiedRateLimiter extends EventEmitter {
return { allowed: true };
}
/**
* Check domain-specific message rate limit
* @param domain Domain to check
*/
private checkDomainMessageLimit(domain: string): IRateLimitResult {
const now = Date.now();
// Get domain-specific limit or use global
const domainConfig = this.config.domains?.[domain];
const limit = domainConfig?.maxMessagesPerMinute || this.config.global.maxMessagesPerMinute!;
if (!limit) {
return { allowed: true };
}
// Get or create counter
let counter = this.domainCounters.get(domain);
if (!counter) {
counter = {
count: 0,
lastReset: now,
recipients: 0,
errors: 0,
authFailures: 0,
connections: 0
};
this.domainCounters.set(domain, counter);
}
// Check if counter needs to be reset
if (now - counter.lastReset >= 60000) {
counter.count = 0;
counter.lastReset = now;
}
// Check if limit is exceeded
if (counter.count >= limit) {
// Calculate reset time
const resetIn = 60000 - (now - counter.lastReset);
logger.log('warn', `Domain ${domain} rate limit exceeded: ${counter.count}/${limit} messages per minute`);
return {
allowed: false,
reason: `Domain "${domain}" message rate limit exceeded`,
limit,
current: counter.count,
resetIn
};
}
// Increment counter
counter.count++;
return { allowed: true };
}
/**
* Check IP-specific message rate limit
* @param ip IP address
@ -485,15 +563,22 @@ export class UnifiedRateLimiter extends EventEmitter {
* @param email Email address
* @param recipients Number of recipients
* @param pattern Matched pattern
* @param domain Domain name
*/
private checkRecipientLimit(email: string, recipients: number, pattern?: string): IRateLimitResult {
// Get pattern-specific limit if available
private checkRecipientLimit(email: string, recipients: number, pattern?: string, domain?: string): IRateLimitResult {
// Get the most specific limit available
let limit = this.config.global.maxRecipientsPerMessage!;
// Check pattern-specific limit
if (pattern && this.config.patterns?.[pattern]?.maxRecipientsPerMessage) {
limit = this.config.patterns[pattern].maxRecipientsPerMessage!;
}
// Check domain-specific limit (overrides pattern if present)
if (domain && this.config.domains?.[domain]?.maxRecipientsPerMessage) {
limit = this.config.domains[domain].maxRecipientsPerMessage!;
}
if (!limit) {
return { allowed: true };
}
@ -923,4 +1008,46 @@ export class UnifiedRateLimiter extends EventEmitter {
public getConfig(): IHierarchicalRateLimits {
return { ...this.config };
}
/**
* Apply domain-specific rate limits
* Merges domain limits with existing configuration
* @param domain Domain name
* @param limits Rate limit configuration for the domain
*/
public applyDomainLimits(domain: string, limits: IRateLimitConfig): void {
if (!this.config.domains) {
this.config.domains = {};
}
// Merge the limits with any existing domain config
this.config.domains[domain] = {
...this.config.domains[domain],
...limits
};
logger.log('info', `Applied rate limits for domain ${domain}:`, limits);
}
/**
* Remove domain-specific rate limits
* @param domain Domain name
*/
public removeDomainLimits(domain: string): void {
if (this.config.domains && this.config.domains[domain]) {
delete this.config.domains[domain];
// Also remove the counter
this.domainCounters.delete(domain);
logger.log('info', `Removed rate limits for domain ${domain}`);
}
}
/**
* Get domain-specific rate limits
* @param domain Domain name
* @returns Domain rate limit config or undefined
*/
public getDomainLimits(domain: string): IRateLimitConfig | undefined {
return this.config.domains?.[domain];
}
}