@periodic/titanium 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +608 -0
- package/dist/adapters/express.d.ts +71 -0
- package/dist/adapters/express.d.ts.map +1 -0
- package/dist/adapters/express.js +194 -0
- package/dist/adapters/express.js.map +1 -0
- package/dist/core/limiter.d.ts +64 -0
- package/dist/core/limiter.d.ts.map +1 -0
- package/dist/core/limiter.js +156 -0
- package/dist/core/limiter.js.map +1 -0
- package/dist/core/types.d.ts +145 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/ip.d.ts +31 -0
- package/dist/utils/ip.d.ts.map +1 -0
- package/dist/utils/ip.js +68 -0
- package/dist/utils/ip.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rateLimit = rateLimit;
|
|
4
|
+
exports.createRateLimiter = createRateLimiter;
|
|
5
|
+
const limiter_1 = require("../core/limiter");
|
|
6
|
+
const ip_1 = require("../utils/ip");
|
|
7
|
+
/**
|
|
8
|
+
* Express rate limiting middleware factory
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Framework-agnostic core with Express adapter
|
|
12
|
+
* - Configurable identifier extraction (user ID, API key, IP, etc.)
|
|
13
|
+
* - Fail-open or fail-closed strategies
|
|
14
|
+
* - Standard HTTP rate limit headers
|
|
15
|
+
* - Skip function for conditional rate limiting
|
|
16
|
+
*
|
|
17
|
+
* @param options - Express rate limit configuration
|
|
18
|
+
* @returns Express middleware function
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { createClient } from 'redis';
|
|
23
|
+
* import { rateLimit } from '@periodic/titanium';
|
|
24
|
+
*
|
|
25
|
+
* const redis = createClient();
|
|
26
|
+
* await redis.connect();
|
|
27
|
+
*
|
|
28
|
+
* // Basic usage with IP-based rate limiting
|
|
29
|
+
* app.use(rateLimit({
|
|
30
|
+
* redis,
|
|
31
|
+
* limit: 100,
|
|
32
|
+
* window: 60,
|
|
33
|
+
* keyPrefix: 'api'
|
|
34
|
+
* }));
|
|
35
|
+
*
|
|
36
|
+
* // User-based rate limiting with JWT
|
|
37
|
+
* app.post('/api/resource',
|
|
38
|
+
* authMiddleware,
|
|
39
|
+
* rateLimit({
|
|
40
|
+
* redis,
|
|
41
|
+
* limit: 10,
|
|
42
|
+
* window: 60,
|
|
43
|
+
* keyPrefix: 'create-resource',
|
|
44
|
+
* identifier: (req) => req.user?.id?.toString() || null
|
|
45
|
+
* }),
|
|
46
|
+
* handler
|
|
47
|
+
* );
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
function rateLimit(options) {
|
|
51
|
+
// Validate required options
|
|
52
|
+
if (!options.redis) {
|
|
53
|
+
throw new Error("Redis client is required");
|
|
54
|
+
}
|
|
55
|
+
if (!options.limit || options.limit <= 0) {
|
|
56
|
+
throw new Error("Limit must be a positive number");
|
|
57
|
+
}
|
|
58
|
+
if (!options.window || options.window <= 0) {
|
|
59
|
+
throw new Error("Window must be a positive number");
|
|
60
|
+
}
|
|
61
|
+
if (!options.keyPrefix || options.keyPrefix.trim() === "") {
|
|
62
|
+
throw new Error("Key prefix is required");
|
|
63
|
+
}
|
|
64
|
+
// Extract options with defaults
|
|
65
|
+
const { redis, limit, window, keyPrefix, algorithm = "fixed-window", identifier, message = "Too many requests. Please try again later.", skip, failStrategy = "open", standardHeaders = true, logger, } = options;
|
|
66
|
+
// Create logger instance
|
|
67
|
+
const log = {
|
|
68
|
+
info: logger?.info || console.log,
|
|
69
|
+
warn: logger?.warn || console.warn,
|
|
70
|
+
error: logger?.error || console.error,
|
|
71
|
+
};
|
|
72
|
+
// Create core rate limiter
|
|
73
|
+
const limiter = new limiter_1.RateLimiter({
|
|
74
|
+
redis,
|
|
75
|
+
limit,
|
|
76
|
+
window,
|
|
77
|
+
keyPrefix,
|
|
78
|
+
algorithm,
|
|
79
|
+
logger: log,
|
|
80
|
+
});
|
|
81
|
+
// Return Express middleware
|
|
82
|
+
return async (req, res, next) => {
|
|
83
|
+
try {
|
|
84
|
+
// Check if rate limiting should be skipped
|
|
85
|
+
if (skip && skip(req)) {
|
|
86
|
+
return next();
|
|
87
|
+
}
|
|
88
|
+
// Extract identifier
|
|
89
|
+
let clientIdentifier;
|
|
90
|
+
if (identifier) {
|
|
91
|
+
const customId = identifier(req);
|
|
92
|
+
clientIdentifier = customId || (0, ip_1.getDefaultIdentifier)(req);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
clientIdentifier = (0, ip_1.getDefaultIdentifier)(req);
|
|
96
|
+
}
|
|
97
|
+
// Attempt rate limiting
|
|
98
|
+
let result;
|
|
99
|
+
try {
|
|
100
|
+
result = await limiter.limit(clientIdentifier);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Redis error - apply fail strategy
|
|
104
|
+
log.error?.("Rate limiter error:", error);
|
|
105
|
+
if (failStrategy === "closed") {
|
|
106
|
+
log.warn?.("Redis unavailable with fail-closed strategy - blocking request");
|
|
107
|
+
res.status(503).json({
|
|
108
|
+
error: "Service temporarily unavailable. Please try again later.",
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Fail-open strategy
|
|
113
|
+
log.warn?.("Redis unavailable with fail-open strategy - allowing request");
|
|
114
|
+
return next();
|
|
115
|
+
}
|
|
116
|
+
// Set standard headers if enabled
|
|
117
|
+
if (standardHeaders) {
|
|
118
|
+
setRateLimitHeaders(res, {
|
|
119
|
+
limit: result.limit,
|
|
120
|
+
remaining: result.remaining,
|
|
121
|
+
reset: result.reset,
|
|
122
|
+
retryAfter: result.allowed ? undefined : result.ttl,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Check if request is allowed
|
|
126
|
+
if (!result.allowed) {
|
|
127
|
+
log.warn?.(`Rate limit exceeded for identifier: ${clientIdentifier} (${keyPrefix})`);
|
|
128
|
+
res.status(429).json({
|
|
129
|
+
error: message,
|
|
130
|
+
retryAfter: result.ttl,
|
|
131
|
+
limit: result.limit,
|
|
132
|
+
remaining: result.remaining,
|
|
133
|
+
reset: result.reset,
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
// Log warning when approaching limit (80% threshold)
|
|
138
|
+
if (result.remaining < result.limit * 0.2) {
|
|
139
|
+
log.info?.(`Rate limit warning for identifier: ${clientIdentifier} (${keyPrefix}) - ${result.remaining} remaining`);
|
|
140
|
+
}
|
|
141
|
+
next();
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
// Unexpected error - apply fail strategy
|
|
145
|
+
log.error?.("Unexpected rate limit middleware error:", error);
|
|
146
|
+
if (failStrategy === "closed") {
|
|
147
|
+
res.status(503).json({
|
|
148
|
+
error: "Service temporarily unavailable. Please try again later.",
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Fail-open
|
|
153
|
+
log.warn?.("Allowing request due to unexpected error (fail-open mode)");
|
|
154
|
+
next();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Set standard rate limit headers on response
|
|
160
|
+
*/
|
|
161
|
+
function setRateLimitHeaders(res, info) {
|
|
162
|
+
res.setHeader("X-RateLimit-Limit", info.limit.toString());
|
|
163
|
+
res.setHeader("X-RateLimit-Remaining", info.remaining.toString());
|
|
164
|
+
res.setHeader("X-RateLimit-Reset", info.reset.toString());
|
|
165
|
+
if (info.retryAfter !== undefined) {
|
|
166
|
+
res.setHeader("Retry-After", info.retryAfter.toString());
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Create a rate limiter instance for manual control
|
|
171
|
+
* Useful for custom implementations or non-Express frameworks
|
|
172
|
+
*
|
|
173
|
+
* @param options - Rate limiter configuration
|
|
174
|
+
* @returns RateLimiter instance
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* const limiter = createRateLimiter({
|
|
179
|
+
* redis,
|
|
180
|
+
* limit: 100,
|
|
181
|
+
* window: 60,
|
|
182
|
+
* keyPrefix: 'api'
|
|
183
|
+
* });
|
|
184
|
+
*
|
|
185
|
+
* const result = await limiter.limit('user-123');
|
|
186
|
+
* if (!result.allowed) {
|
|
187
|
+
* // Handle rate limit exceeded
|
|
188
|
+
* }
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
function createRateLimiter(options) {
|
|
192
|
+
return new limiter_1.RateLimiter(options);
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=express.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/adapters/express.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { RateLimiterConfig, RateLimitResult } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Core rate limiter implementation
|
|
4
|
+
* Framework-agnostic, pure Redis-based rate limiting
|
|
5
|
+
*/
|
|
6
|
+
export declare class RateLimiter {
|
|
7
|
+
private redis;
|
|
8
|
+
private requestLimit;
|
|
9
|
+
private window;
|
|
10
|
+
private keyPrefix;
|
|
11
|
+
private algorithm;
|
|
12
|
+
private logger;
|
|
13
|
+
constructor(config: RateLimiterConfig);
|
|
14
|
+
/**
|
|
15
|
+
* Validate configuration options
|
|
16
|
+
*/
|
|
17
|
+
private validateConfig;
|
|
18
|
+
/**
|
|
19
|
+
* Create logger with fallback to console
|
|
20
|
+
*/
|
|
21
|
+
private createLogger;
|
|
22
|
+
/**
|
|
23
|
+
* Build Redis key for the identifier
|
|
24
|
+
*/
|
|
25
|
+
private buildKey;
|
|
26
|
+
/**
|
|
27
|
+
* Check if Redis client is available
|
|
28
|
+
*/
|
|
29
|
+
private isRedisAvailable;
|
|
30
|
+
/**
|
|
31
|
+
* Attempt to consume a request for the given identifier
|
|
32
|
+
* Returns rate limit information
|
|
33
|
+
*
|
|
34
|
+
* @param identifier - Unique identifier for the client (user ID, IP, API key, etc.)
|
|
35
|
+
* @returns Promise resolving to rate limit result
|
|
36
|
+
*
|
|
37
|
+
* @throws Error if Redis operations fail (caller should handle)
|
|
38
|
+
*/
|
|
39
|
+
limit(identifier: string): Promise<RateLimitResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Fixed window rate limiting implementation
|
|
42
|
+
* Uses true fixed window semantics with SET NX EX
|
|
43
|
+
*/
|
|
44
|
+
private fixedWindowLimit;
|
|
45
|
+
/**
|
|
46
|
+
* Reset rate limit for a specific identifier
|
|
47
|
+
* Useful for testing or manual intervention
|
|
48
|
+
*
|
|
49
|
+
* @param identifier - Unique identifier to reset
|
|
50
|
+
* @returns Promise resolving to true if key was deleted, false otherwise
|
|
51
|
+
*/
|
|
52
|
+
reset(identifier: string): Promise<boolean>;
|
|
53
|
+
/**
|
|
54
|
+
* Get current rate limit status for an identifier
|
|
55
|
+
*
|
|
56
|
+
* @param identifier - Unique identifier to check
|
|
57
|
+
* @returns Promise resolving to current count and TTL, or null if no limit exists
|
|
58
|
+
*/
|
|
59
|
+
getStatus(identifier: string): Promise<{
|
|
60
|
+
current: number;
|
|
61
|
+
ttl: number;
|
|
62
|
+
} | null>;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=limiter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limiter.d.ts","sourceRoot":"","sources":["../../src/core/limiter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAqB,MAAM,SAAS,CAAC;AAEhF;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAkB;IAC/B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,EAAE,iBAAiB;IAWrC;;OAEG;IACH,OAAO,CAAC,cAAc;IAkBtB;;OAEG;IACH,OAAO,CAAC,YAAY;IAQpB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAIhB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAIxB;;;;;;;;OAQG;IACG,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAkBzD;;;OAGG;YACW,gBAAgB;IAkC9B;;;;;;OAMG;IACG,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiBjD;;;;;OAKG;IACG,SAAS,CACb,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CA6BpD"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RateLimiter = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Core rate limiter implementation
|
|
6
|
+
* Framework-agnostic, pure Redis-based rate limiting
|
|
7
|
+
*/
|
|
8
|
+
class RateLimiter {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.validateConfig(config);
|
|
11
|
+
this.redis = config.redis;
|
|
12
|
+
this.requestLimit = config.limit;
|
|
13
|
+
this.window = config.window;
|
|
14
|
+
this.keyPrefix = config.keyPrefix;
|
|
15
|
+
this.algorithm = config.algorithm || "fixed-window";
|
|
16
|
+
this.logger = this.createLogger(config.logger);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Validate configuration options
|
|
20
|
+
*/
|
|
21
|
+
validateConfig(config) {
|
|
22
|
+
if (!config.redis) {
|
|
23
|
+
throw new Error("Redis client is required");
|
|
24
|
+
}
|
|
25
|
+
if (!config.limit || config.limit <= 0) {
|
|
26
|
+
throw new Error("Limit must be a positive number");
|
|
27
|
+
}
|
|
28
|
+
if (!config.window || config.window <= 0) {
|
|
29
|
+
throw new Error("Window must be a positive number (in seconds)");
|
|
30
|
+
}
|
|
31
|
+
if (!config.keyPrefix || config.keyPrefix.trim() === "") {
|
|
32
|
+
throw new Error("Key prefix is required");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create logger with fallback to console
|
|
37
|
+
*/
|
|
38
|
+
createLogger(customLogger) {
|
|
39
|
+
return {
|
|
40
|
+
info: customLogger?.info || console.log,
|
|
41
|
+
warn: customLogger?.warn || console.warn,
|
|
42
|
+
error: customLogger?.error || console.error,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build Redis key for the identifier
|
|
47
|
+
*/
|
|
48
|
+
buildKey(identifier) {
|
|
49
|
+
return `ratelimit:${this.keyPrefix}:${identifier}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if Redis client is available
|
|
53
|
+
*/
|
|
54
|
+
isRedisAvailable() {
|
|
55
|
+
return this.redis.isOpen && this.redis.isReady;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Attempt to consume a request for the given identifier
|
|
59
|
+
* Returns rate limit information
|
|
60
|
+
*
|
|
61
|
+
* @param identifier - Unique identifier for the client (user ID, IP, API key, etc.)
|
|
62
|
+
* @returns Promise resolving to rate limit result
|
|
63
|
+
*
|
|
64
|
+
* @throws Error if Redis operations fail (caller should handle)
|
|
65
|
+
*/
|
|
66
|
+
async limit(identifier) {
|
|
67
|
+
if (!identifier || identifier.trim() === "") {
|
|
68
|
+
throw new Error("Identifier cannot be empty");
|
|
69
|
+
}
|
|
70
|
+
if (!this.isRedisAvailable()) {
|
|
71
|
+
throw new Error("Redis client is not available");
|
|
72
|
+
}
|
|
73
|
+
const key = this.buildKey(identifier);
|
|
74
|
+
if (this.algorithm === "fixed-window") {
|
|
75
|
+
return this.fixedWindowLimit(key);
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Unsupported algorithm: ${this.algorithm}`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Fixed window rate limiting implementation
|
|
81
|
+
* Uses true fixed window semantics with SET NX EX
|
|
82
|
+
*/
|
|
83
|
+
async fixedWindowLimit(key) {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
// Try to initialize the window if it doesn't exist
|
|
86
|
+
// SET key 0 EX window NX - Only set if key doesn't exist
|
|
87
|
+
const initialized = await this.redis.set(key, "0", {
|
|
88
|
+
EX: this.window,
|
|
89
|
+
NX: true,
|
|
90
|
+
});
|
|
91
|
+
// Atomically increment the counter
|
|
92
|
+
const currentCount = await this.redis.incr(key);
|
|
93
|
+
// Get TTL to calculate reset time
|
|
94
|
+
const ttl = await this.redis.ttl(key);
|
|
95
|
+
// Calculate reset timestamp
|
|
96
|
+
const resetTime = ttl > 0 ? now + ttl * 1000 : now + this.window * 1000;
|
|
97
|
+
const remaining = Math.max(0, this.requestLimit - currentCount);
|
|
98
|
+
const allowed = currentCount <= this.requestLimit;
|
|
99
|
+
this.logger.info?.(`Rate limit check: identifier=${key}, count=${currentCount}/${this.requestLimit}, allowed=${allowed}`);
|
|
100
|
+
return {
|
|
101
|
+
allowed,
|
|
102
|
+
limit: this.requestLimit,
|
|
103
|
+
remaining,
|
|
104
|
+
reset: Math.ceil(resetTime / 1000),
|
|
105
|
+
ttl: ttl > 0 ? ttl : this.window,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Reset rate limit for a specific identifier
|
|
110
|
+
* Useful for testing or manual intervention
|
|
111
|
+
*
|
|
112
|
+
* @param identifier - Unique identifier to reset
|
|
113
|
+
* @returns Promise resolving to true if key was deleted, false otherwise
|
|
114
|
+
*/
|
|
115
|
+
async reset(identifier) {
|
|
116
|
+
if (!identifier || identifier.trim() === "") {
|
|
117
|
+
throw new Error("Identifier cannot be empty");
|
|
118
|
+
}
|
|
119
|
+
if (!this.isRedisAvailable()) {
|
|
120
|
+
throw new Error("Redis client is not available");
|
|
121
|
+
}
|
|
122
|
+
const key = this.buildKey(identifier);
|
|
123
|
+
const result = await this.redis.del(key);
|
|
124
|
+
this.logger.info?.(`Rate limit reset for: ${key}`);
|
|
125
|
+
return result > 0;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get current rate limit status for an identifier
|
|
129
|
+
*
|
|
130
|
+
* @param identifier - Unique identifier to check
|
|
131
|
+
* @returns Promise resolving to current count and TTL, or null if no limit exists
|
|
132
|
+
*/
|
|
133
|
+
async getStatus(identifier) {
|
|
134
|
+
if (!identifier || identifier.trim() === "") {
|
|
135
|
+
throw new Error("Identifier cannot be empty");
|
|
136
|
+
}
|
|
137
|
+
if (!this.isRedisAvailable()) {
|
|
138
|
+
throw new Error("Redis client is not available");
|
|
139
|
+
}
|
|
140
|
+
const key = this.buildKey(identifier);
|
|
141
|
+
// Use pipeline for atomic read
|
|
142
|
+
const pipeline = this.redis.multi();
|
|
143
|
+
pipeline.get(key);
|
|
144
|
+
pipeline.ttl(key);
|
|
145
|
+
const results = await pipeline.exec();
|
|
146
|
+
const currentValue = results?.[0];
|
|
147
|
+
const ttl = results?.[1] || -1;
|
|
148
|
+
if (!currentValue || ttl < 0) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const current = parseInt(currentValue, 10);
|
|
152
|
+
return { current, ttl };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.RateLimiter = RateLimiter;
|
|
156
|
+
//# sourceMappingURL=limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limiter.js","sourceRoot":"","sources":["../../src/core/limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { RedisClientType } from "redis";
|
|
2
|
+
import { Request } from "express";
|
|
3
|
+
/**
|
|
4
|
+
* Rate limiting algorithm types
|
|
5
|
+
* Currently only fixed-window is implemented
|
|
6
|
+
*/
|
|
7
|
+
export type Algorithm = "fixed-window";
|
|
8
|
+
/**
|
|
9
|
+
* Strategy for handling failures when Redis is unavailable
|
|
10
|
+
* - 'open': Allow requests through (recommended for availability)
|
|
11
|
+
* - 'closed': Block all requests (recommended for strict security)
|
|
12
|
+
*/
|
|
13
|
+
export type FailStrategy = "open" | "closed";
|
|
14
|
+
/**
|
|
15
|
+
* Logger interface for optional logging
|
|
16
|
+
* If not provided, console will be used as fallback
|
|
17
|
+
*/
|
|
18
|
+
export interface Logger {
|
|
19
|
+
info?: (...args: any[]) => void;
|
|
20
|
+
warn?: (...args: any[]) => void;
|
|
21
|
+
error?: (...args: any[]) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Result returned by the core rate limiter
|
|
25
|
+
*/
|
|
26
|
+
export interface RateLimitResult {
|
|
27
|
+
/**
|
|
28
|
+
* Whether the request should be allowed
|
|
29
|
+
*/
|
|
30
|
+
allowed: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Maximum number of requests allowed in the window
|
|
33
|
+
*/
|
|
34
|
+
limit: number;
|
|
35
|
+
/**
|
|
36
|
+
* Number of requests remaining in current window
|
|
37
|
+
*/
|
|
38
|
+
remaining: number;
|
|
39
|
+
/**
|
|
40
|
+
* Timestamp (in seconds) when the rate limit resets
|
|
41
|
+
*/
|
|
42
|
+
reset: number;
|
|
43
|
+
/**
|
|
44
|
+
* Time to live in seconds for the current window
|
|
45
|
+
*/
|
|
46
|
+
ttl: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Core rate limiter configuration
|
|
50
|
+
*/
|
|
51
|
+
export interface RateLimiterConfig {
|
|
52
|
+
/**
|
|
53
|
+
* Redis client instance (must be connected)
|
|
54
|
+
*/
|
|
55
|
+
redis: RedisClientType;
|
|
56
|
+
/**
|
|
57
|
+
* Maximum number of requests allowed in the time window
|
|
58
|
+
*/
|
|
59
|
+
limit: number;
|
|
60
|
+
/**
|
|
61
|
+
* Time window in seconds
|
|
62
|
+
*/
|
|
63
|
+
window: number;
|
|
64
|
+
/**
|
|
65
|
+
* Prefix for Redis keys (useful for different route groups)
|
|
66
|
+
*/
|
|
67
|
+
keyPrefix: string;
|
|
68
|
+
/**
|
|
69
|
+
* Rate limiting algorithm
|
|
70
|
+
* @default 'fixed-window'
|
|
71
|
+
*/
|
|
72
|
+
algorithm?: Algorithm;
|
|
73
|
+
/**
|
|
74
|
+
* Optional logger instance
|
|
75
|
+
* If not provided, falls back to console
|
|
76
|
+
*/
|
|
77
|
+
logger?: Logger;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Express middleware configuration
|
|
81
|
+
*/
|
|
82
|
+
export interface ExpressRateLimitOptions extends RateLimiterConfig {
|
|
83
|
+
/**
|
|
84
|
+
* Custom function to extract identifier from request
|
|
85
|
+
* If not provided, uses IP address from request
|
|
86
|
+
*
|
|
87
|
+
* @param req - Express request object
|
|
88
|
+
* @returns Unique identifier for the client, or null to use IP
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // Use user ID from JWT token
|
|
92
|
+
* identifier: (req) => req.user?.id?.toString() || null
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* // Use API key from headers
|
|
96
|
+
* identifier: (req) => req.headers['x-api-key'] as string || null
|
|
97
|
+
*/
|
|
98
|
+
identifier?: (req: Request) => string | null;
|
|
99
|
+
/**
|
|
100
|
+
* Custom error message when rate limit is exceeded
|
|
101
|
+
* @default 'Too many requests. Please try again later.'
|
|
102
|
+
*/
|
|
103
|
+
message?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Function to skip rate limiting for certain requests
|
|
106
|
+
*
|
|
107
|
+
* @param req - Express request object
|
|
108
|
+
* @returns true to skip rate limiting, false to apply it
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* // Skip rate limiting for admin users
|
|
112
|
+
* skip: (req) => req.user?.isAdmin === true
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* // Skip rate limiting for internal IPs
|
|
116
|
+
* skip: (req) => req.ip?.startsWith('192.168.')
|
|
117
|
+
*/
|
|
118
|
+
skip?: (req: Request) => boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Strategy for handling Redis failures
|
|
121
|
+
* - 'open': Allow requests when Redis is down (recommended)
|
|
122
|
+
* - 'closed': Block requests when Redis is down
|
|
123
|
+
* @default 'open'
|
|
124
|
+
*/
|
|
125
|
+
failStrategy?: FailStrategy;
|
|
126
|
+
/**
|
|
127
|
+
* Whether to include standard rate limit headers in responses
|
|
128
|
+
* - X-RateLimit-Limit
|
|
129
|
+
* - X-RateLimit-Remaining
|
|
130
|
+
* - X-RateLimit-Reset
|
|
131
|
+
* - Retry-After (when limit exceeded)
|
|
132
|
+
* @default true
|
|
133
|
+
*/
|
|
134
|
+
standardHeaders?: boolean;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Internal rate limit information for header setting
|
|
138
|
+
*/
|
|
139
|
+
export interface RateLimitInfo {
|
|
140
|
+
limit: number;
|
|
141
|
+
remaining: number;
|
|
142
|
+
reset: number;
|
|
143
|
+
retryAfter?: number;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,cAAc,CAAC;AAEvC;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE7C;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;IAChC,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,KAAK,EAAE,eAAe,CAAC;IAEvB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IAEtB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAwB,SAAQ,iBAAiB;IAChE;;;;;;;;;;;;;;OAcG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;IAE7C;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC;IAEjC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAE5B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @periodic/titanium
|
|
3
|
+
*
|
|
4
|
+
* Production-ready Redis-backed rate limiting middleware for Express
|
|
5
|
+
* with TypeScript support, fail-safe design, and flexible configuration.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
export { rateLimit, createRateLimiter } from "./adapters/express";
|
|
10
|
+
export { RateLimiter } from "./core/limiter";
|
|
11
|
+
export type { Algorithm, FailStrategy, Logger, RateLimitResult, RateLimiterConfig, ExpressRateLimitOptions, RateLimitInfo, } from "./core/types";
|
|
12
|
+
export { extractClientIp, normalizeIp, getDefaultIdentifier } from "./utils/ip";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAGlE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,YAAY,EACV,SAAS,EACT,YAAY,EACZ,MAAM,EACN,eAAe,EACf,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,GACd,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @periodic/titanium
|
|
4
|
+
*
|
|
5
|
+
* Production-ready Redis-backed rate limiting middleware for Express
|
|
6
|
+
* with TypeScript support, fail-safe design, and flexible configuration.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.getDefaultIdentifier = exports.normalizeIp = exports.extractClientIp = exports.RateLimiter = exports.createRateLimiter = exports.rateLimit = void 0;
|
|
12
|
+
// Export Express middleware
|
|
13
|
+
var express_1 = require("./adapters/express");
|
|
14
|
+
Object.defineProperty(exports, "rateLimit", { enumerable: true, get: function () { return express_1.rateLimit; } });
|
|
15
|
+
Object.defineProperty(exports, "createRateLimiter", { enumerable: true, get: function () { return express_1.createRateLimiter; } });
|
|
16
|
+
// Export core components for advanced usage
|
|
17
|
+
var limiter_1 = require("./core/limiter");
|
|
18
|
+
Object.defineProperty(exports, "RateLimiter", { enumerable: true, get: function () { return limiter_1.RateLimiter; } });
|
|
19
|
+
// Export utilities
|
|
20
|
+
var ip_1 = require("./utils/ip");
|
|
21
|
+
Object.defineProperty(exports, "extractClientIp", { enumerable: true, get: function () { return ip_1.extractClientIp; } });
|
|
22
|
+
Object.defineProperty(exports, "normalizeIp", { enumerable: true, get: function () { return ip_1.normalizeIp; } });
|
|
23
|
+
Object.defineProperty(exports, "getDefaultIdentifier", { enumerable: true, get: function () { return ip_1.getDefaultIdentifier; } });
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,4BAA4B;AAC5B,8CAAkE;AAAzD,oGAAA,SAAS,OAAA;AAAE,4GAAA,iBAAiB,OAAA;AAErC,4CAA4C;AAC5C,0CAA6C;AAApC,sGAAA,WAAW,OAAA;AAapB,mBAAmB;AACnB,iCAAgF;AAAvE,qGAAA,eAAe,OAAA;AAAE,iGAAA,WAAW,OAAA;AAAE,0GAAA,oBAAoB,OAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Request } from "express";
|
|
2
|
+
/**
|
|
3
|
+
* Extract client IP address from Express request
|
|
4
|
+
* Handles various proxy and load balancer scenarios
|
|
5
|
+
*
|
|
6
|
+
* Priority:
|
|
7
|
+
* 1. X-Forwarded-For header (first IP if multiple)
|
|
8
|
+
* 2. X-Real-IP header
|
|
9
|
+
* 3. Socket remote address
|
|
10
|
+
* 4. 'unknown' as fallback
|
|
11
|
+
*
|
|
12
|
+
* @param req - Express request object
|
|
13
|
+
* @returns IP address string
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractClientIp(req: Request): string;
|
|
16
|
+
/**
|
|
17
|
+
* Normalize IP address for consistent key generation
|
|
18
|
+
* Handles IPv6 to IPv4 mapping
|
|
19
|
+
*
|
|
20
|
+
* @param ip - IP address string
|
|
21
|
+
* @returns Normalized IP address
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizeIp(ip: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Extract and normalize client identifier from request
|
|
26
|
+
*
|
|
27
|
+
* @param req - Express request object
|
|
28
|
+
* @returns Normalized IP address
|
|
29
|
+
*/
|
|
30
|
+
export declare function getDefaultIdentifier(req: Request): string;
|
|
31
|
+
//# sourceMappingURL=ip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ip.d.ts","sourceRoot":"","sources":["../../src/utils/ip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CA0BpD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAQ9C;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAGzD"}
|