import type { CliPlugin, PluginConfigValue, PluginContext } from '../types.js' /** * Rate Limit Plugin * Provides configurable rate limiting with multiple strategies and storage options */ export const rateLimitPlugin: CliPlugin = { id: 'rate-limit', category: 'middleware', description: 'Configurable rate limiting with multiple strategies and storage options', config: { strategy: { type: 'select', description: 'Rate limiting strategy', options: [ { name: 'Simple (in-memory)', value: 'simple' }, { name: 'Optimized (sliding window)', value: 'optimized' }, ], default: 'optimized', }, windowMs: { type: 'number', description: 'Time window in milliseconds', default: 900000, // 15 minutes }, maxRequests: { type: 'number', description: 'Maximum requests per window', default: 100, }, keyGenerator: { type: 'select', description: 'How to identify clients for rate limiting', options: [ { name: 'IP Address', value: 'ip' }, { name: 'User ID (authenticated)', value: 'user' }, { name: 'API Key', value: 'apikey' }, { name: 'Custom Header', value: 'header' }, ], default: 'ip', }, customHeader: { type: 'string', description: 'Custom header name (when keyGenerator is "header")', default: 'x-client-id', }, skipSuccessfulRequests: { type: 'boolean', description: 'Only count failed requests (4xx/5xx)', default: false, }, skipFailedRequests: { type: 'boolean', description: 'Only count successful requests (2xx/3xx)', default: false, }, enableHeaders: { type: 'boolean', description: 'Include rate limit info in response headers', default: true, }, message: { type: 'string', description: 'Custom rate limit exceeded message', default: 'Too many requests, please try again later.', }, statusCode: { type: 'number', description: 'HTTP status code for rate limit exceeded', default: 429, }, whitelist: { type: 'string', description: 'Comma-separated list of IPs to whitelist', default: '', }, enableLogging: { type: 'boolean', description: 'Log rate limit violations', default: true, }, }, prepare(_ctx: PluginContext, _config: Record) { // No additional dependencies needed - uses built-in functionality }, async apply(ctx: PluginContext, config: Record): Promise { const strategy = config.strategy || 'optimized' const windowMs = config.windowMs || 900000 const maxRequests = config.maxRequests || 100 const keyGenerator = config.keyGenerator || 'ip' const customHeader = config.customHeader || 'x-client-id' const skipSuccessfulRequests = config.skipSuccessfulRequests || false const skipFailedRequests = config.skipFailedRequests || false const enableHeaders = config.enableHeaders !== false const message = config.message || 'Too many requests, please try again later.' const statusCode = config.statusCode || 429 const whitelist = config.whitelist ? String(config.whitelist) .split(',') .map(ip => ip.trim()) : [] const enableLogging = config.enableLogging !== false // Generate rate limit middleware file const rateLimitMiddlewareContent = `import type { Context, MiddlewareHandler } from 'hono' import { createMiddleware } from 'hono/factory' import { HTTPException } from 'hono/http-exception' import type { AppBindings } from './types.js' /** * Rate limiting middleware generated by Rate Limit Plugin */ interface RateLimitStore { get(key: string): Promise | number | null set(key: string, value: number, ttl?: number): Promise | void increment(key: string, ttl?: number): Promise | number reset(key: string): Promise | void } class MemoryStore implements RateLimitStore { private store = new Map() get(key: string): number | null { const entry = this.store.get(key) if (!entry || Date.now() > entry.resetTime) { this.store.delete(key) return null } return entry.count } set(key: string, value: number, ttl = ${windowMs}): void { this.store.set(key, { count: value, resetTime: Date.now() + ttl }) } increment(key: string, ttl = ${windowMs}): number { const existing = this.get(key) const newCount = (existing || 0) + 1 this.set(key, newCount, ttl) return newCount } reset(key: string): void { this.store.delete(key) } // Cleanup expired entries periodically cleanup(): void { const now = Date.now() for (const [key, entry] of this.store.entries()) { if (now > entry.resetTime) { this.store.delete(key) } } } } ${ strategy === 'optimized' ? ` class SlidingWindowStore implements RateLimitStore { private store = new Map() private readonly windowMs = ${windowMs} get(key: string): number | null { const timestamps = this.store.get(key) if (!timestamps) return null const now = Date.now() const validTimestamps = timestamps.filter(ts => now - ts < this.windowMs) if (validTimestamps.length !== timestamps.length) { this.store.set(key, validTimestamps) } return validTimestamps.length } set(key: string, value: number): void { // For sliding window, we don't directly set counts // This method is mainly for compatibility const now = Date.now() const timestamps = Array(value).fill(now) this.store.set(key, timestamps) } increment(key: string): number { const now = Date.now() const timestamps = this.store.get(key) || [] // Remove expired timestamps const validTimestamps = timestamps.filter(ts => now - ts < this.windowMs) // Add new timestamp validTimestamps.push(now) this.store.set(key, validTimestamps) return validTimestamps.length } reset(key: string): void { this.store.delete(key) } cleanup(): void { const now = Date.now() for (const [key, timestamps] of this.store.entries()) { const validTimestamps = timestamps.filter(ts => now - ts < this.windowMs) if (validTimestamps.length === 0) { this.store.delete(key) } else if (validTimestamps.length !== timestamps.length) { this.store.set(key, validTimestamps) } } } } ` : '' } // Global store instance const store: RateLimitStore = ${strategy === 'optimized' ? 'new SlidingWindowStore()' : 'new MemoryStore()'} // Cleanup expired entries every 5 minutes setInterval(() => { if ('cleanup' in store) { store.cleanup() } }, 5 * 60 * 1000) /** * Generate rate limit key based on configuration */ function generateKey(c: Context): string { ${ keyGenerator === 'ip' ? ` return c.req.header('cf-connecting-ip') || c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown' ` : keyGenerator === 'user' ? ` const user = c.get('user') return user ? \`user:\${user.id}\` : 'anonymous' ` : keyGenerator === 'apikey' ? ` const apiKey = c.req.header('x-api-key') || c.req.header('authorization') return apiKey ? \`apikey:\${apiKey}\` : 'no-key' ` : ` const headerValue = c.req.header('${customHeader}') return headerValue ? \`header:\${headerValue}\` : 'no-header' ` } } /** * Check if IP is whitelisted */ function isWhitelisted(ip: string): boolean { const whitelist = [${whitelist.map(ip => `'${ip}'`).join(', ')}] return whitelist.includes(ip) || whitelist.includes('*') } ${ strategy === 'simple' ? ` /** * Simple rate limiting middleware */ export function simpleRateLimit(): MiddlewareHandler { return createMiddleware(async (c, next) => { const key = generateKey(c) // Check whitelist if (isWhitelisted(key)) { await next() return } const current = await store.get(key) const count = current || 0 if (count >= ${maxRequests}) { ${ enableLogging ? ` console.warn(JSON.stringify({ type: 'rate_limit_exceeded', key, count, limit: ${maxRequests}, window: ${windowMs}, timestamp: new Date().toISOString() })) ` : '' } ${ enableHeaders ? ` c.header('X-RateLimit-Limit', '${maxRequests}') c.header('X-RateLimit-Remaining', '0') c.header('X-RateLimit-Reset', String(Math.ceil(Date.now() / 1000) + Math.ceil(${windowMs} / 1000))) ` : '' } throw new HTTPException(${statusCode}, { message: '${message}' }) } // Execute request let shouldCount = true let statusCode: number | undefined try { await next() statusCode = c.res.status ${ skipSuccessfulRequests ? ` if (statusCode >= 200 && statusCode < 400) { shouldCount = false } ` : '' } } catch (error) { statusCode = error instanceof HTTPException ? error.status : 500 ${ skipFailedRequests ? ` if (statusCode >= 400) { shouldCount = false } ` : '' } throw error } finally { if (shouldCount) { const newCount = await store.increment(key, ${windowMs}) ${ enableHeaders ? ` c.header('X-RateLimit-Limit', '${maxRequests}') c.header('X-RateLimit-Remaining', String(Math.max(0, ${maxRequests} - newCount))) c.header('X-RateLimit-Reset', String(Math.ceil(Date.now() / 1000) + Math.ceil(${windowMs} / 1000))) ` : '' } } } }) } ` : ` /** * Optimized rate limiting with sliding window */ export function rateLimitOptimized(): MiddlewareHandler { return createMiddleware(async (c, next) => { const key = generateKey(c) // Check whitelist if (isWhitelisted(key)) { await next() return } const current = await store.get(key) || 0 if (current >= ${maxRequests}) { ${ enableLogging ? ` console.warn(JSON.stringify({ type: 'rate_limit_exceeded', key, count: current, limit: ${maxRequests}, window: ${windowMs}, timestamp: new Date().toISOString() })) ` : '' } ${ enableHeaders ? ` c.header('X-RateLimit-Limit', '${maxRequests}') c.header('X-RateLimit-Remaining', '0') c.header('X-RateLimit-Reset', String(Math.ceil(Date.now() / 1000) + Math.ceil(${windowMs} / 1000))) ` : '' } throw new HTTPException(${statusCode}, { message: '${message}' }) } // Execute request let shouldCount = true let statusCode: number | undefined try { await next() statusCode = c.res.status ${ skipSuccessfulRequests ? ` if (statusCode >= 200 && statusCode < 400) { shouldCount = false } ` : '' } } catch (error) { statusCode = error instanceof HTTPException ? error.status : 500 ${ skipFailedRequests ? ` if (statusCode >= 400) { shouldCount = false } ` : '' } throw error } finally { if (shouldCount) { const newCount = await store.increment(key) ${ enableHeaders ? ` c.header('X-RateLimit-Limit', '${maxRequests}') c.header('X-RateLimit-Remaining', String(Math.max(0, ${maxRequests} - newCount))) c.header('X-RateLimit-Reset', String(Math.ceil(Date.now() / 1000) + Math.ceil(${windowMs} / 1000))) ` : '' } } } }) } ` } // Export the configured rate limit function export const rateLimit = ${strategy === 'simple' ? 'simpleRateLimit' : 'rateLimitOptimized'} ` await ctx.addFile('src/lib/rate-limit.middleware.ts', rateLimitMiddlewareContent) // Update main app file to include rate limiting middleware if (await ctx.fileExists('src/index.ts')) { await ctx.modifyFile('src/index.ts', (content: string) => { // Add import if not already present if (!content.includes('rate-limit.middleware')) { const importLine = `import { rateLimit } from './lib/rate-limit.middleware.js'` // Find the last import statement const lines = content.split('\\n') let lastImportIndex = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].trim().startsWith('import ')) { lastImportIndex = i } } if (lastImportIndex >= 0) { lines.splice(lastImportIndex + 1, 0, importLine) } else { lines.unshift(importLine) } content = lines.join('\\n') } // Add middleware usage if not already present if (!content.includes('rateLimit()')) { // Find where to insert middleware calls if (content.includes('const app = ') || content.includes('export const app = ')) { const lines = content.split('\\n') let insertIndex = -1 for (let i = 0; i < lines.length; i++) { if (lines[i].includes('const app = ') || lines[i].includes('export const app = ')) { insertIndex = i + 1 break } } if (insertIndex >= 0) { lines.splice( insertIndex, 0, '', '// Rate limiting middleware', "app.use('*', rateLimit())" ) content = lines.join('\\n') } } } return content }) } }, }