alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
185 lines (184 loc) • 6.78 kB
JavaScript
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