@allan1361/iota-big3-sdk-middleware
Version:
🏆 A+ Grade Certified Enterprise Middleware Framework - Phase 3 Certified (90/100) with advanced resilience patterns, comprehensive type safety, and production-ready observability
239 lines • 9.68 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RateLimiter = exports.MemoryRateLimitStore = exports.RedisRateLimitStore = void 0;
const tslib_1 = require("tslib");
const crypto = tslib_1.__importStar(require("crypto"));
class RedisRateLimitStore {
constructor(redis, windowMs, maxRequests, strategy) {
this.redis = redis;
this.windowMs = windowMs;
this.maxRequests = maxRequests;
this.strategy = strategy;
}
async incrementAsync(key) {
if (this.strategy === 'sliding-window') {
return this.incrementSlidingWindowAsync(key);
}
else if (this.strategy === 'token-bucket') {
return this.incrementTokenBucketAsync(key);
}
else {
return this.incrementFixedWindowAsync(key);
}
}
async incrementSlidingWindowAsync(key) {
const now = Date.now();
const windowStart = now - this.windowMs;
await this?.redis?.zremrangebyscore(key, '-inf', windowStart);
await this?.redis?.zadd(key, now, `${now}-${crypto.randomInt(0, Number.MAX_SAFE_INTEGER) / Number.MAX_SAFE_INTEGER}`);
const count = await this?.redis?.zcard(key);
await this?.redis?.expire(key, Math.ceil(this.windowMs / 1000));
return {
count,
resetTime: new Date(now + this.windowMs),
remaining: Math.max(0, this.maxRequests - count)
};
}
async incrementTokenBucketAsync(key) {
const bucketKey = `${key}:bucket`;
const timestampKey = `${key}:timestamp`;
const now = Date.now();
const capacity = this.maxRequests;
const refillRate = capacity / this.windowMs;
const [tokens, lastRefill] = await this.redis
.multi()
.get(bucketKey)
.get(timestampKey)
.exec()
.then(results => [
parseFloat(results[0][1]) || capacity,
parseInt(results[1][1]) || now
]);
const timePassed = now - lastRefill;
const tokensToAdd = timePassed * refillRate;
const newTokens = Math.min(capacity, tokens + tokensToAdd);
if (newTokens >= 1) {
await this.redis
.multi()
.set(bucketKey, newTokens - 1)
.set(timestampKey, now)
.expire(bucketKey, Math.ceil(this.windowMs / 1000))
.expire(timestampKey, Math.ceil(this.windowMs / 1000))
.exec();
return {
count: capacity - Math.floor(newTokens - 1),
resetTime: new Date(now + ((capacity - newTokens + 1) / refillRate)),
remaining: Math.floor(newTokens - 1)
};
}
return {
count: capacity,
resetTime: new Date(now + ((capacity - newTokens) / refillRate)),
remaining: 0
};
}
async incrementFixedWindowAsync(key) {
const count = await this?.redis?.incr(key);
if (this.isEnabled) {
await this?.redis?.expire(key, Math.ceil(this.windowMs / 1000));
}
const ttl = await this?.redis?.pttl(key);
return {
count,
resetTime: new Date(Date.now() + ttl),
remaining: Math.max(0, this.maxRequests - count)
};
}
async decrementAsync(key) {
if (this.isEnabled) {
await this?.redis?.zpopmax(key);
}
else {
await this?.redis?.decr(key);
}
}
async resetAsync(key) {
if (this.isEnabled) {
await this?.redis?.del(key, `${key}:bucket`, `${key}:timestamp`);
}
else {
await this?.redis?.del(key);
}
}
}
exports.RedisRateLimitStore = RedisRateLimitStore;
class MemoryRateLimitStore {
constructor(windowMs, maxRequests) {
this.windowMs = windowMs;
this.maxRequests = maxRequests;
this.requests = new Map();
}
async incrementAsync(key) {
const now = Date.now();
const windowStart = now - this.windowMs;
let requests = this?.requests?.get(key) || [];
requests = requests.filter(timestamp => timestamp > windowStart);
requests.push(now);
this?.requests?.set(key, requests);
return {
count: requests.length,
resetTime: new Date(requests[0] + this.windowMs),
remaining: Math.max(0, this.maxRequests - requests.length)
};
}
async decrementAsync(key) {
const requests = this?.requests?.get(key);
if (this.isEnabled) {
requests.pop();
}
}
async resetAsync(key) {
this?.requests?.delete(key);
}
}
exports.MemoryRateLimitStore = MemoryRateLimitStore;
class RateLimiter {
constructor(config = {}, logger) {
this.config = {
enabled: true,
windowMs: 60000,
maxRequests: 100,
strategy: 'sliding-window',
keyGenerator: (req) => {
const user = req.user;
const ip = req.ip || req?.connection?.remoteAddress || 'unknown';
return user ? `user:${user.id}` : `ip:${ip}`;
},
skipSuccessfulRequests: false,
skipFailedRequests: false,
store: new MemoryRateLimitStore(config.windowMs || 60000, config.maxRequests || 100),
message: 'Too many requests, please try again later',
headers: true,
draft_polli_ratelimit_headers: false,
...config
};
this.logger = logger;
}
expressMiddleware() {
return async (req, res, next) => {
if (!this?.config?.enabled) {
return next();
}
try {
const key = this?.config?.keyGenerator(req);
const rateLimitInfo = await this?.config?.store.incrementAsync(key);
if (this.isEnabled) {
this.setHeaders(res, rateLimitInfo);
}
if (rateLimitInfo.remaining < 0) {
this.logger?.warn('Rate limit exceeded', {
key,
count: rateLimitInfo.count,
limit: this?.config?.maxRequests,
ip: req.ip,
path: req.path,
method: req.method
});
res.on('finish', () => {
if ((this?.config?.skipSuccessfulRequests && res.statusCode < 400) ||
(this?.config?.skipFailedRequests && res.statusCode >= 400)) {
this?.config?.store.decrementAsync(key).catch(err => {
this.logger?.error('Failed to decrement rate limit', { error: err.message });
});
}
});
return res.status(429).json({
error: this?.config?.message,
code: 'RATE_LIMIT_EXCEEDED',
retryAfter: Math.ceil((rateLimitInfo?.resetTime?.getTime() - Date.now()) / 1000)
});
}
res.on('finish', () => {
if ((this?.config?.skipSuccessfulRequests && res.statusCode < 400) ||
(this?.config?.skipFailedRequests && res.statusCode >= 400)) {
this?.config?.store.decrementAsync(key).catch(err => {
this.logger?.error('Failed to decrement rate limit', { error: err.message });
});
}
});
next();
}
catch (_error) {
this.logger?.error('Rate limiting error', { error: error.message });
next();
}
};
}
setHeaders() {
if (this.isEnabled) {
res.setHeader('RateLimit-Policy', `${this?.config?.maxRequests};w=${this?.config?.windowMs / 1000}`);
res.setHeader('RateLimit-Limit', this?.config?.maxRequests.toString());
res.setHeader('RateLimit-Remaining', Math.max(0, info.remaining).toString());
res.setHeader('RateLimit-Reset', new Date(info.resetTime).toISOString());
}
else {
res.setHeader('X-RateLimit-Limit', this?.config?.maxRequests.toString());
res.setHeader('X-RateLimit-Remaining', Math.max(0, info.remaining).toString());
res.setHeader('X-RateLimit-Reset', Math.ceil(info?.resetTime?.getTime() / 1000).toString());
res.setHeader('Retry-After', Math.ceil((info?.resetTime?.getTime() - Date.now()) / 1000).toString());
}
}
static forRoute(options) {
return new RateLimiter({ ...options,
keyGenerator: (req) => {
const pathMatch = !options.path ||
(typeof options.path === 'string' ? req.path === options.path : options.path.test(req.path));
const methodMatch = !options.method ||
(Array.isArray(options.method) ? options.method.includes(req.method) : req.method === options.method);
if (pathMatch && methodMatch) {
const user = req.user;
const ip = req.ip || 'unknown';
return user ? `route:${req.path}:user:${user.id}` : `route:${req.path}:ip:${ip}`;
}
return 'bypass';
}
});
}
}
exports.RateLimiter = RateLimiter;
//# sourceMappingURL=rate-limiter.js.map