321 lines
11 KiB
JavaScript
321 lines
11 KiB
JavaScript
|
|
/**
|
||
|
|
* Rate Limiting Augmentation
|
||
|
|
* Provides configurable rate limiting for Brainy operations
|
||
|
|
*/
|
||
|
|
import { BaseAugmentation } from './brainyAugmentation.js';
|
||
|
|
/**
|
||
|
|
* Rate Limit Augmentation
|
||
|
|
*/
|
||
|
|
export class RateLimitAugmentation extends BaseAugmentation {
|
||
|
|
constructor(config = {}) {
|
||
|
|
super(config);
|
||
|
|
this.name = 'rateLimiter';
|
||
|
|
this.timing = 'before';
|
||
|
|
this.metadata = 'none';
|
||
|
|
this.operations = ['search', 'find', 'add', 'update', 'delete', 'get'];
|
||
|
|
this.priority = 10; // High priority, runs early
|
||
|
|
// Augmentation metadata
|
||
|
|
this.category = 'core'; // Use 'core' as security isn't a valid category
|
||
|
|
this.description = 'Provides rate limiting for Brainy operations';
|
||
|
|
this.limiters = new Map();
|
||
|
|
// Merge with defaults
|
||
|
|
this.config = {
|
||
|
|
enabled: config.enabled ?? true,
|
||
|
|
limits: {
|
||
|
|
searches: config.limits?.searches ?? 1000,
|
||
|
|
writes: config.limits?.writes ?? 100,
|
||
|
|
reads: config.limits?.reads ?? 5000,
|
||
|
|
deletes: config.limits?.deletes ?? 50
|
||
|
|
},
|
||
|
|
windowMs: config.windowMs ?? 60000, // 1 minute default
|
||
|
|
skipSuccessfulRequests: config.skipSuccessfulRequests ?? false,
|
||
|
|
skipFailedRequests: config.skipFailedRequests ?? true,
|
||
|
|
keyGenerator: config.keyGenerator || this.defaultKeyGenerator
|
||
|
|
};
|
||
|
|
this.windowMs = this.config.windowMs;
|
||
|
|
// Initialize operation limiters
|
||
|
|
this.initializeLimiters();
|
||
|
|
}
|
||
|
|
getManifest() {
|
||
|
|
return {
|
||
|
|
id: 'rate-limiter',
|
||
|
|
name: 'Rate Limiter',
|
||
|
|
version: '1.0.0',
|
||
|
|
description: 'Configurable rate limiting for API operations',
|
||
|
|
longDescription: 'Provides per-operation rate limiting with configurable windows and limits. Helps prevent abuse and ensures fair resource usage.',
|
||
|
|
category: 'core',
|
||
|
|
configSchema: {
|
||
|
|
type: 'object',
|
||
|
|
properties: {
|
||
|
|
enabled: {
|
||
|
|
type: 'boolean',
|
||
|
|
default: true,
|
||
|
|
description: 'Enable or disable rate limiting'
|
||
|
|
},
|
||
|
|
limits: {
|
||
|
|
type: 'object',
|
||
|
|
properties: {
|
||
|
|
searches: {
|
||
|
|
type: 'number',
|
||
|
|
default: 1000,
|
||
|
|
description: 'Search operations per minute'
|
||
|
|
},
|
||
|
|
writes: {
|
||
|
|
type: 'number',
|
||
|
|
default: 100,
|
||
|
|
description: 'Write operations per minute'
|
||
|
|
},
|
||
|
|
reads: {
|
||
|
|
type: 'number',
|
||
|
|
default: 5000,
|
||
|
|
description: 'Read operations per minute'
|
||
|
|
},
|
||
|
|
deletes: {
|
||
|
|
type: 'number',
|
||
|
|
default: 50,
|
||
|
|
description: 'Delete operations per minute'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
windowMs: {
|
||
|
|
type: 'number',
|
||
|
|
default: 60000,
|
||
|
|
description: 'Time window in milliseconds'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
configDefaults: {
|
||
|
|
enabled: true,
|
||
|
|
limits: {
|
||
|
|
searches: 1000,
|
||
|
|
writes: 100,
|
||
|
|
reads: 5000,
|
||
|
|
deletes: 50
|
||
|
|
},
|
||
|
|
windowMs: 60000
|
||
|
|
},
|
||
|
|
minBrainyVersion: '3.0.0',
|
||
|
|
keywords: ['rate-limit', 'security', 'throttle'],
|
||
|
|
documentation: 'https://docs.brainy.dev/augmentations/rate-limit',
|
||
|
|
status: 'stable',
|
||
|
|
performance: {
|
||
|
|
memoryUsage: 'low',
|
||
|
|
cpuUsage: 'low',
|
||
|
|
networkUsage: 'none'
|
||
|
|
},
|
||
|
|
features: ['per-operation-limits', 'configurable-windows', 'key-based-limiting'],
|
||
|
|
enhancedOperations: ['search', 'add', 'update', 'delete', 'get'],
|
||
|
|
metrics: [
|
||
|
|
{
|
||
|
|
name: 'rate_limit_exceeded',
|
||
|
|
type: 'counter',
|
||
|
|
description: 'Number of rate limit violations'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'rate_limit_requests',
|
||
|
|
type: 'counter',
|
||
|
|
description: 'Total requests checked'
|
||
|
|
}
|
||
|
|
]
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Initialize rate limiters for each operation type
|
||
|
|
*/
|
||
|
|
initializeLimiters() {
|
||
|
|
const operations = ['searches', 'writes', 'reads', 'deletes'];
|
||
|
|
for (const op of operations) {
|
||
|
|
this.limiters.set(op, new Map());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Default key generator (could be IP, user ID, etc.)
|
||
|
|
*/
|
||
|
|
defaultKeyGenerator(_context) {
|
||
|
|
// In a real implementation, this would extract IP or user ID
|
||
|
|
return 'default';
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if request should be rate limited
|
||
|
|
*/
|
||
|
|
checkRateLimit(operation, key) {
|
||
|
|
const limiter = this.limiters.get(operation);
|
||
|
|
if (!limiter)
|
||
|
|
return false;
|
||
|
|
const limit = this.config.limits[operation];
|
||
|
|
if (!limit)
|
||
|
|
return false;
|
||
|
|
const now = Date.now();
|
||
|
|
let entry = limiter.get(key);
|
||
|
|
// Initialize or reset entry
|
||
|
|
if (!entry || now >= entry.resetTime) {
|
||
|
|
entry = {
|
||
|
|
count: 0,
|
||
|
|
resetTime: now + this.windowMs
|
||
|
|
};
|
||
|
|
limiter.set(key, entry);
|
||
|
|
}
|
||
|
|
// Check if limit exceeded
|
||
|
|
if (entry.count >= limit) {
|
||
|
|
return true; // Rate limited
|
||
|
|
}
|
||
|
|
// Increment counter
|
||
|
|
entry.count++;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get remaining requests for an operation
|
||
|
|
*/
|
||
|
|
getRemainingRequests(operation, key) {
|
||
|
|
const limiter = this.limiters.get(operation);
|
||
|
|
if (!limiter)
|
||
|
|
return -1;
|
||
|
|
const limit = this.config.limits[operation];
|
||
|
|
if (!limit)
|
||
|
|
return -1;
|
||
|
|
const entry = limiter.get(key);
|
||
|
|
if (!entry)
|
||
|
|
return limit;
|
||
|
|
const now = Date.now();
|
||
|
|
if (now >= entry.resetTime)
|
||
|
|
return limit;
|
||
|
|
return Math.max(0, limit - entry.count);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get time until reset
|
||
|
|
*/
|
||
|
|
getResetTime(operation, key) {
|
||
|
|
const limiter = this.limiters.get(operation);
|
||
|
|
if (!limiter)
|
||
|
|
return 0;
|
||
|
|
const entry = limiter.get(key);
|
||
|
|
if (!entry)
|
||
|
|
return 0;
|
||
|
|
const now = Date.now();
|
||
|
|
return Math.max(0, entry.resetTime - now);
|
||
|
|
}
|
||
|
|
async onInitialize() {
|
||
|
|
if (!this.config.enabled) {
|
||
|
|
this.log('Rate limiter disabled by configuration');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.log(`Rate limiter initialized (window: ${this.windowMs}ms)`);
|
||
|
|
// Start cleanup timer
|
||
|
|
setInterval(() => {
|
||
|
|
this.cleanup();
|
||
|
|
}, this.windowMs);
|
||
|
|
}
|
||
|
|
async onShutdown() {
|
||
|
|
this.clear();
|
||
|
|
this.log('Rate limiter shut down');
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Execute augmentation - apply rate limiting
|
||
|
|
*/
|
||
|
|
async execute(operation, params, next) {
|
||
|
|
// If rate limiting is disabled, just pass through
|
||
|
|
if (!this.config.enabled) {
|
||
|
|
return next();
|
||
|
|
}
|
||
|
|
// Map operations to rate limit categories
|
||
|
|
let rateLimitOperation;
|
||
|
|
switch (operation) {
|
||
|
|
case 'search':
|
||
|
|
case 'find':
|
||
|
|
case 'similar':
|
||
|
|
rateLimitOperation = 'searches';
|
||
|
|
break;
|
||
|
|
case 'add':
|
||
|
|
case 'update':
|
||
|
|
rateLimitOperation = 'writes';
|
||
|
|
break;
|
||
|
|
case 'delete':
|
||
|
|
rateLimitOperation = 'deletes';
|
||
|
|
break;
|
||
|
|
case 'get':
|
||
|
|
rateLimitOperation = 'reads';
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
return next(); // Don't rate limit unknown operations
|
||
|
|
}
|
||
|
|
const key = this.config.keyGenerator(params);
|
||
|
|
if (this.checkRateLimit(rateLimitOperation, key)) {
|
||
|
|
const error = new Error(`Rate limit exceeded for ${operation}`);
|
||
|
|
error.statusCode = 429;
|
||
|
|
error.retryAfter = this.getResetTime(rateLimitOperation, key);
|
||
|
|
error.rateLimit = {
|
||
|
|
limit: this.config.limits[rateLimitOperation],
|
||
|
|
remaining: 0,
|
||
|
|
reset: Date.now() + this.getResetTime(rateLimitOperation, key)
|
||
|
|
};
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const result = await next();
|
||
|
|
// Add rate limit info to result if possible
|
||
|
|
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
||
|
|
result._rateLimit = {
|
||
|
|
limit: this.config.limits[rateLimitOperation],
|
||
|
|
remaining: this.getRemainingRequests(rateLimitOperation, key),
|
||
|
|
reset: Date.now() + this.getResetTime(rateLimitOperation, key)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
catch (error) {
|
||
|
|
// Optionally don't count failed requests
|
||
|
|
if (this.config.skipFailedRequests) {
|
||
|
|
const limiter = this.limiters.get(rateLimitOperation);
|
||
|
|
const entry = limiter.get(key);
|
||
|
|
if (entry && entry.count > 0)
|
||
|
|
entry.count--;
|
||
|
|
}
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get rate limit statistics
|
||
|
|
*/
|
||
|
|
getStats() {
|
||
|
|
const stats = { operations: {} };
|
||
|
|
for (const [operation, limiter] of this.limiters) {
|
||
|
|
let totalRequests = 0;
|
||
|
|
for (const entry of limiter.values()) {
|
||
|
|
totalRequests += entry.count;
|
||
|
|
}
|
||
|
|
stats.operations[operation] = {
|
||
|
|
activeKeys: limiter.size,
|
||
|
|
totalRequests
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return stats;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clear all rate limit entries
|
||
|
|
*/
|
||
|
|
clear() {
|
||
|
|
for (const limiter of this.limiters.values()) {
|
||
|
|
limiter.clear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clear expired entries (cleanup)
|
||
|
|
*/
|
||
|
|
cleanup() {
|
||
|
|
const now = Date.now();
|
||
|
|
for (const limiter of this.limiters.values()) {
|
||
|
|
for (const [key, entry] of limiter) {
|
||
|
|
if (now >= entry.resetTime) {
|
||
|
|
limiter.delete(key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Create rate limit augmentation
|
||
|
|
*/
|
||
|
|
export function createRateLimitAugmentation(config) {
|
||
|
|
return new RateLimitAugmentation(config);
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=rateLimitAugmentation.js.map
|