UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

185 lines (184 loc) 6.78 kB
import { $atom, $hook, $inject, $module, $state, createMiddleware, z } from "alepha"; import { AlephaServer, HttpError, ServerRouterProvider } from "alepha/server"; import { CacheProvider } from "alepha/cache"; import { DateTimeProvider } from "alepha/datetime"; import { $logger } from "alepha/logger"; //#region ../../src/server/rate-limit/providers/ServerRateLimitProvider.ts /** * Rate limit configuration atom (global defaults) */ const rateLimitOptions = $atom({ name: "alepha.server.rate-limit.options", schema: z.object({ windowMs: z.number().describe("Window duration in milliseconds").default(900 * 1e3), max: z.number().describe("Maximum number of requests per window").default(100), skipFailedRequests: z.boolean().describe("Skip rate limiting for failed requests").optional(), skipSuccessfulRequests: z.boolean().describe("Skip rate limiting for successful requests").optional() }), default: { windowMs: 900 * 1e3, max: 100 } }); var ServerRateLimitProvider = class ServerRateLimitProvider { log = $logger(); dateTime = $inject(DateTimeProvider); serverRouterProvider = $inject(ServerRouterProvider); cacheProvider = $inject(CacheProvider); globalOptions = $state(rateLimitOptions); static CACHE_NAME = "rate-limit"; /** * Registered rate limit configurations with their path patterns */ registeredConfigs = []; /** * Register a rate limit configuration (called by primitives) */ registerRateLimit(config) { this.registeredConfigs.push(config); } onStart = $hook({ on: "start", handler: async () => { for (const config of this.registeredConfigs) if (config.paths) for (const pattern of config.paths) { const matchedRoutes = this.serverRouterProvider.getRoutes(pattern); for (const route of matchedRoutes) route.rateLimit = this.buildRateLimitOptions(config); } if (this.registeredConfigs.length > 0) this.log.info(`Initialized with ${this.registeredConfigs.length} registered rate-limit configurations.`); } }); onRequest = $hook({ on: "server:onRequest", handler: async ({ route, request }) => { const rateLimitConfig = route.rateLimit ?? this.globalOptions; if (!rateLimitConfig.max && !rateLimitConfig.windowMs) return; const result = await this.checkLimit(request, rateLimitConfig); this.setRateLimitHeaders(request, result); if (!result.allowed) throw new HttpError({ status: 429, message: "Too Many Requests" }); } }); onActionRequest = $hook({ on: "action:onRequest", handler: async ({ action, request }) => { const rateLimit = action.options?.rateLimit; if (!rateLimit) return; if (!(await this.checkLimit(request, rateLimit)).allowed) throw new HttpError({ status: 429, message: "Too Many Requests" }); } }); /** * Build complete rate limit options by merging with global defaults */ buildRateLimitOptions(config) { return { max: config.max ?? this.globalOptions.max, windowMs: config.windowMs ?? this.globalOptions.windowMs, keyGenerator: config.keyGenerator, skipFailedRequests: config.skipFailedRequests ?? this.globalOptions.skipFailedRequests, skipSuccessfulRequests: config.skipSuccessfulRequests ?? this.globalOptions.skipSuccessfulRequests }; } /** * Set rate limit headers on the response */ setRateLimitHeaders(request, result) { request.reply.setHeader("X-RateLimit-Limit", result.limit.toString()); request.reply.setHeader("X-RateLimit-Remaining", result.remaining.toString()); request.reply.setHeader("X-RateLimit-Reset", Math.ceil(result.resetTime / 1e3).toString()); if (!result.allowed && result.retryAfter) request.reply.setHeader("Retry-After", result.retryAfter.toString()); } async checkLimit(req, options = {}) { const baseKey = this.generateKey(req); return this.checkLimitByKey(baseKey, options); } /** * Check rate limit by an explicit key string. * Useful when no request context is available (e.g. `$job`, `$pipeline`). */ async checkLimitByKey(baseKey, options = {}) { const windowMs = options.windowMs ?? this.globalOptions.windowMs; const max = options.max ?? this.globalOptions.max; const now = this.dateTime.nowMillis(); const windowStart = Math.floor(now / windowMs) * windowMs; const resetTime = windowStart + windowMs; const key = `${baseKey}:${windowStart}`; const count = await this.cacheProvider.incr(ServerRateLimitProvider.CACHE_NAME, key, 1); const allowed = count <= max; const result = { allowed, limit: max, remaining: Math.max(0, max - count), resetTime }; if (!allowed) result.retryAfter = Math.ceil((resetTime - now) / 1e3); return result; } generateKey(req) { return `ip:${req.ip || "unknown"}`; } }; //#endregion //#region ../../src/server/rate-limit/primitives/$rateLimit.ts /** * Middleware that enforces rate limiting. * * **Key resolution** (in order): * 1. Explicit `key` function — user controls the key. Works anywhere (`$action`, `$job`, `$pipeline`). * 2. Auto-detect `request.ip` from ALS — default for `$action` context. * 3. `"global"` fallback — when no request context and no `key`. All calls share one bucket. * * Sets `X-RateLimit-*` response headers when a request context is available. * Throws `HttpError(429)` when the limit is exceeded. * * ```typescript * // In $action: automatically rate limits by IP * $action({ use: [$rateLimit({ max: 100, windowMs: 60000 })] }) * * // In $action: rate limit by custom key * $action({ use: [$rateLimit({ max: 10, windowMs: 60000, key: (req) => req.user?.id })] }) * * // In $job: rate limit all executions globally * $job({ use: [$rateLimit({ max: 5, windowMs: 3600000 })] }) * ``` */ const $rateLimit = (options) => { return createMiddleware({ name: "$rateLimit", options, handler: ({ alepha, next }) => { const rateLimitProvider = alepha.inject(ServerRateLimitProvider); return async (...args) => { const request = alepha.get("alepha.http.request"); const result = options?.key ? await rateLimitProvider.checkLimitByKey(options.key(...args), options) : await rateLimitProvider.checkLimit(request ?? { ip: "global" }, options); if (request) rateLimitProvider.setRateLimitHeaders(request, result); if (!result.allowed) throw new HttpError({ status: 429, message: "Too Many Requests" }); return next(...args); }; } }); }; //#endregion //#region ../../src/server/rate-limit/index.ts /** * Request rate limiting on actions. * * **Features:** * - Rate limit configuration per action * * @module alepha.server.rate-limit */ const AlephaServerRateLimit = $module({ name: "alepha.server.rate-limit", services: [AlephaServer, ServerRateLimitProvider] }); //#endregion export { $rateLimit, AlephaServerRateLimit, ServerRateLimitProvider, rateLimitOptions }; //# sourceMappingURL=index.js.map