forked from Lilly-Protocol/lily-backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrate-limit.ts
More file actions
128 lines (111 loc) · 3.56 KB
/
Copy pathrate-limit.ts
File metadata and controls
128 lines (111 loc) · 3.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import type { Request, Response } from "express";
import rateLimit from "express-rate-limit";
import { env, securityConfig } from "./env";
interface RateLimitLocals {
resetTime?: Date | null;
}
/**
* Normalizes and determines whether a given path is an operational endpoint
* (root, health probes, or metrics) that should not consume the public rate limit.
*/
export const isOperationalPath = (
pathname: string,
apiPrefix: string = env.API_PREFIX,
): boolean => {
const normalized = pathname.split("?")[0] ?? "";
const healthPrefix = `${apiPrefix}/health`;
const metricsPrefix = `${apiPrefix}/metrics`;
return (
normalized === "/" ||
normalized === healthPrefix ||
normalized.startsWith(`${healthPrefix}/`) ||
normalized === metricsPrefix ||
normalized.startsWith(`${metricsPrefix}/`) ||
normalized === "/health" ||
normalized.startsWith("/health/") ||
normalized === "/metrics" ||
normalized.startsWith("/metrics/")
);
};
/**
* Predicate evaluating whether an incoming request should skip the API rate limiter.
*/
export const shouldSkipApiRateLimit = (
request: Request,
apiPrefix: string = env.API_PREFIX,
): boolean => {
const isTest = process.env.NODE_ENV === "test";
const forceRateLimit =
process.env.ENABLE_RATE_LIMIT_TESTS === "true" ||
request.headers?.["x-test-rate-limit"] === "true";
if (isTest && !forceRateLimit) {
return true;
}
if (request.path && isOperationalPath(request.path, apiPrefix)) {
return true;
}
if (request.originalUrl) {
const originalPathname = new URL(request.originalUrl, "http://localhost")
.pathname;
if (isOperationalPath(originalPathname, apiPrefix)) {
return true;
}
}
if (request.url) {
const urlPathname = new URL(request.url, "http://localhost").pathname;
if (isOperationalPath(urlPathname, apiPrefix)) {
return true;
}
}
return false;
};
/**
* Custom 429 handler used when an express-rate-limit limiter is exceeded.
* Reads the limiter-provided reset time from `res.locals.rateLimit` so the
* Retry-After header and the response envelope reflect when the window resets.
*/
export const rateLimitHandler = (
_request: Request,
response: Response,
): void => {
const resetTime =
(response.locals as { rateLimit?: RateLimitLocals }).rateLimit?.resetTime ??
null;
if (resetTime && resetTime.getTime() > Date.now()) {
const retryAfterSeconds = Math.max(
1,
Math.ceil((resetTime.getTime() - Date.now()) / 1000),
);
response.setHeader("Retry-After", String(retryAfterSeconds));
}
response.status(429).json({
success: false,
message: "Too many requests, please try again later.",
details: { resetTime: resetTime ? resetTime.toISOString() : null },
});
};
export interface CreateRateLimiterOptions {
apiPrefix?: string;
limit?: number;
windowMs?: number;
}
export const createApiRateLimiter = (options?: CreateRateLimiterOptions) => {
return rateLimit({
windowMs: options?.windowMs ?? securityConfig.rateLimitWindowMs,
limit: options?.limit ?? securityConfig.rateLimitMaxRequests,
standardHeaders: true,
legacyHeaders: false,
skip: (request: Request) =>
shouldSkipApiRateLimit(request, options?.apiPrefix ?? env.API_PREFIX),
handler: rateLimitHandler,
});
};
export const apiRateLimiter = createApiRateLimiter();
export const writeRateLimiter = rateLimit({
windowMs: 60_000,
limit: 20,
standardHeaders: true,
legacyHeaders: false,
skip: (req) => process.env.NODE_ENV === "test" || isOperationalPath(req),
handler: rateLimitHandler,
});