synor/apps/api-gateway/src/rate-limiter.ts
Gulshan Yadav 4a2825f516 feat(api): add public API gateway with rate limiting
- 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
2026-01-10 06:19:08 +05:30

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();
}
}