brainy/.recovery-workspace/dist-backup-20250910-141917/utils/rateLimiter.js
David Snelling 8ff382ca3b chore: recovery checkpoint - v3.0 API successfully recovered
CRITICAL CHECKPOINT - DO NOT PUSH TO GITHUB

Recovery Status:
- Successfully recovered brainy.ts from compiled JavaScript
- All core v3.0 API methods functional (add, get, update, delete, relate, find, etc.)
- Neural subsystem intact (562KB embedded patterns, NLP working)
- Augmentation pipeline operational (20+ augmentations)
- HNSW clustering system complete
- Triple Intelligence compiled (needs constructor fix)
- Test suite validates functionality

Changes preserved:
- 898 files with changes from last 3 days
- 144,475 insertions
- All augmentation improvements
- All test coverage enhancements
- Complete v3.0 feature set

This is a LOCAL checkpoint only - contains recovered work after corruption incident.
Created backup in .backups/brainy-full-20250910-151314.tar.gz

Branch: recovery-checkpoint-20250910-151433
Date: Wed Sep 10 03:18:04 PM PDT 2025
2025-09-10 15:18:04 -07:00

271 lines
No EOL
8.1 KiB
JavaScript

/**
* Rate Limiter for Brainy API
*
* Provides rate limiting without external dependencies like Redis.
* - Uses in-memory storage for single instances
* - Can use S3/R2 for distributed rate limiting
*
* @module rateLimiter
*/
/**
* Simple in-memory rate limiter
*/
export class RateLimiter {
constructor(config) {
this.config = config;
this.requests = new Map();
this.cleanupInterval = null;
// Start cleanup interval to remove expired entries
this.startCleanup();
}
/**
* Check if a request is allowed and update the rate limit
*/
async checkLimit(identifier) {
const now = Date.now();
if (this.config.distributed && this.config.storage) {
return this.checkDistributedLimit(identifier, now);
}
return this.checkMemoryLimit(identifier, now);
}
/**
* Check rate limit using in-memory storage
*/
checkMemoryLimit(identifier, now) {
const entry = this.requests.get(identifier);
const resetTime = now + this.config.windowMs;
if (!entry || entry.resetTime <= now) {
// New window or expired window
this.requests.set(identifier, {
count: 1,
resetTime
});
return {
allowed: true,
remaining: this.config.maxRequests - 1,
resetTime,
limit: this.config.maxRequests
};
}
// Existing window
if (entry.count < this.config.maxRequests) {
entry.count++;
return {
allowed: true,
remaining: this.config.maxRequests - entry.count,
resetTime: entry.resetTime,
limit: this.config.maxRequests
};
}
// Rate limit exceeded
return {
allowed: false,
remaining: 0,
resetTime: entry.resetTime,
limit: this.config.maxRequests
};
}
/**
* Check rate limit using distributed storage (S3/R2)
*/
async checkDistributedLimit(identifier, now) {
const storage = this.config.storage;
const key = `ratelimit_${identifier}`;
const resetTime = now + this.config.windowMs;
try {
// Try to get existing rate limit data from metadata storage
const existing = await storage.getMetadata(key);
if (!existing || !existing.resetTime ||
Number(existing.resetTime) <= now) {
// New window or expired window
await storage.saveMetadata(key, {
count: 1,
resetTime: resetTime,
identifier
});
return {
allowed: true,
remaining: this.config.maxRequests - 1,
resetTime,
limit: this.config.maxRequests
};
}
const count = Number(existing.count || 0);
if (count < this.config.maxRequests) {
// Update count
await storage.saveMetadata(key, {
count: count + 1,
resetTime: existing.resetTime,
identifier
});
return {
allowed: true,
remaining: this.config.maxRequests - count - 1,
resetTime: Number(existing.resetTime),
limit: this.config.maxRequests
};
}
// Rate limit exceeded
return {
allowed: false,
remaining: 0,
resetTime: Number(existing.resetTime),
limit: this.config.maxRequests
};
}
catch (error) {
// On error, fail open (allow the request)
console.warn('Rate limiter error, failing open:', error);
return {
allowed: true,
remaining: this.config.maxRequests,
resetTime,
limit: this.config.maxRequests
};
}
}
/**
* Reset rate limit for a specific identifier
*/
async reset(identifier) {
if (this.config.distributed && this.config.storage) {
const key = `ratelimit_${identifier}`;
// Reset by setting count to 0 and expired time
await this.config.storage.saveMetadata(key, {
count: 0,
resetTime: 0,
identifier
});
}
else {
this.requests.delete(identifier);
}
}
/**
* Start cleanup interval to remove expired entries
*/
startCleanup() {
// Run cleanup every minute
this.cleanupInterval = setInterval(() => {
const now = Date.now();
const expired = [];
for (const [key, entry] of this.requests) {
if (entry.resetTime <= now) {
expired.push(key);
}
}
for (const key of expired) {
this.requests.delete(key);
}
}, 60000); // 1 minute
// Don't keep Node.js process alive just for cleanup
if (this.cleanupInterval.unref) {
this.cleanupInterval.unref();
}
}
/**
* Stop the rate limiter and cleanup
*/
destroy() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
this.requests.clear();
}
}
/**
* Express/Connect middleware for rate limiting
*/
export function rateLimitMiddleware(config) {
const limiter = new RateLimiter(config);
return async (req, res, next) => {
// Use IP address as identifier (can be customized)
const identifier = req.ip || req.connection?.remoteAddress || 'unknown';
const result = await limiter.checkLimit(identifier);
// Set rate limit headers
res.setHeader('X-RateLimit-Limit', result.limit);
res.setHeader('X-RateLimit-Remaining', result.remaining);
res.setHeader('X-RateLimit-Reset', result.resetTime);
if (!result.allowed) {
res.status(429).json({
error: config.message || 'Too many requests, please try again later.',
retryAfter: Math.ceil((result.resetTime - Date.now()) / 1000)
});
return;
}
next();
};
}
/**
* Create a rate limiter for use with Brainy
*
* @example
* ```typescript
* // For single instance (in-memory)
* const limiter = createRateLimiter({
* maxRequests: 100,
* windowMs: 15 * 60 * 1000 // 15 minutes
* })
*
* // For distributed (using S3/R2)
* const limiter = createRateLimiter({
* maxRequests: 100,
* windowMs: 15 * 60 * 1000,
* distributed: true,
* storage: myS3Adapter
* })
*
* // Check rate limit
* const result = await limiter.checkLimit('user-123')
* if (!result.allowed) {
* throw new Error('Rate limit exceeded')
* }
* ```
*/
export function createRateLimiter(config) {
return new RateLimiter(config);
}
/**
* Preset configurations for common use cases
*/
export const RateLimitPresets = {
/**
* Default API rate limit: 100 requests per 15 minutes
*/
default: {
maxRequests: 100,
windowMs: 15 * 60 * 1000
},
/**
* Strict rate limit: 10 requests per minute
*/
strict: {
maxRequests: 10,
windowMs: 60 * 1000
},
/**
* Lenient rate limit: 1000 requests per hour
*/
lenient: {
maxRequests: 1000,
windowMs: 60 * 60 * 1000
},
/**
* Search endpoint: 30 requests per minute
*/
search: {
maxRequests: 30,
windowMs: 60 * 1000,
message: 'Search rate limit exceeded. Please wait before searching again.'
},
/**
* Write operations: 20 requests per minute
*/
write: {
maxRequests: 20,
windowMs: 60 * 1000,
message: 'Write rate limit exceeded. Please slow down your write operations.'
}
};
//# sourceMappingURL=rateLimiter.js.map