- Create API gateway service with Express.js - Implement sliding window rate limiting via Redis - Add API key management with tiered access (free/developer/enterprise) - Track usage analytics per key and globally - Add RPC proxy to blockchain nodes - Configure Docker Compose with api-gateway and redis services - Free tier: 100 req/min, Developer: 1000 req/min, Enterprise: unlimited
121 lines
3.2 KiB
TypeScript
121 lines
3.2 KiB
TypeScript
/**
|
|
* Rate limiting using Redis sliding window algorithm.
|
|
*
|
|
* Tiers:
|
|
* - free: 100 requests/minute
|
|
* - developer: 1000 requests/minute
|
|
* - enterprise: unlimited
|
|
*/
|
|
|
|
// Use require for ioredis to avoid ESM import issues
|
|
import type { Redis } from 'ioredis';
|
|
import IORedisModule from 'ioredis';
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const IORedis = (IORedisModule as any).default || IORedisModule;
|
|
|
|
export type RateLimitTier = 'free' | 'developer' | 'enterprise';
|
|
|
|
interface TierConfig {
|
|
requestsPerMinute: number;
|
|
burstLimit: number;
|
|
}
|
|
|
|
const TIER_CONFIGS: Record<RateLimitTier, TierConfig> = {
|
|
free: { requestsPerMinute: 100, burstLimit: 20 },
|
|
developer: { requestsPerMinute: 1000, burstLimit: 100 },
|
|
enterprise: { requestsPerMinute: Infinity, burstLimit: Infinity },
|
|
};
|
|
|
|
export interface RateLimitResult {
|
|
allowed: boolean;
|
|
remaining: number;
|
|
resetAt: number;
|
|
tier: RateLimitTier;
|
|
}
|
|
|
|
export class RateLimiter {
|
|
private redis: Redis;
|
|
private windowMs = 60_000; // 1 minute
|
|
|
|
constructor(redisUrl: string) {
|
|
this.redis = new IORedis(redisUrl);
|
|
}
|
|
|
|
async checkLimit(key: string, tier: RateLimitTier): Promise<RateLimitResult> {
|
|
const config = TIER_CONFIGS[tier];
|
|
|
|
// Enterprise tier bypasses rate limiting
|
|
if (tier === 'enterprise') {
|
|
return {
|
|
allowed: true,
|
|
remaining: Infinity,
|
|
resetAt: 0,
|
|
tier,
|
|
};
|
|
}
|
|
|
|
const now = Date.now();
|
|
const windowStart = now - this.windowMs;
|
|
const redisKey = `ratelimit:${key}`;
|
|
|
|
// Use Redis sorted set for sliding window
|
|
const pipeline = this.redis.pipeline();
|
|
|
|
// Remove expired entries
|
|
pipeline.zremrangebyscore(redisKey, 0, windowStart);
|
|
|
|
// Count current requests in window
|
|
pipeline.zcard(redisKey);
|
|
|
|
// Add current request
|
|
pipeline.zadd(redisKey, now, `${now}-${Math.random()}`);
|
|
|
|
// Set expiry on the key
|
|
pipeline.expire(redisKey, 120);
|
|
|
|
const results = await pipeline.exec();
|
|
const currentCount = (results?.[1]?.[1] as number) || 0;
|
|
|
|
const allowed = currentCount < config.requestsPerMinute;
|
|
const remaining = Math.max(0, config.requestsPerMinute - currentCount - 1);
|
|
|
|
// Calculate reset time
|
|
const oldestRequest = await this.redis.zrange(redisKey, 0, 0, 'WITHSCORES');
|
|
const resetAt = oldestRequest.length >= 2
|
|
? parseInt(oldestRequest[1]) + this.windowMs
|
|
: now + this.windowMs;
|
|
|
|
return {
|
|
allowed,
|
|
remaining: allowed ? remaining : 0,
|
|
resetAt,
|
|
tier,
|
|
};
|
|
}
|
|
|
|
async getUsageStats(key: string): Promise<{
|
|
requestsLastMinute: number;
|
|
requestsLastHour: number;
|
|
requestsLastDay: number;
|
|
}> {
|
|
const now = Date.now();
|
|
const redisKey = `ratelimit:${key}`;
|
|
|
|
const pipeline = this.redis.pipeline();
|
|
pipeline.zcount(redisKey, now - 60_000, now);
|
|
pipeline.zcount(redisKey, now - 3600_000, now);
|
|
pipeline.zcount(redisKey, now - 86400_000, now);
|
|
|
|
const results = await pipeline.exec();
|
|
|
|
return {
|
|
requestsLastMinute: (results?.[0]?.[1] as number) || 0,
|
|
requestsLastHour: (results?.[1]?.[1] as number) || 0,
|
|
requestsLastDay: (results?.[2]?.[1] as number) || 0,
|
|
};
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
await this.redis.quit();
|
|
}
|
|
}
|