@juspay/neurolink 9.31.2 → 9.32.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/CHANGELOG.md +6 -0
- package/dist/auth/AuthProviderFactory.d.ts +71 -0
- package/dist/auth/AuthProviderFactory.js +111 -0
- package/dist/auth/AuthProviderRegistry.d.ts +33 -0
- package/dist/auth/AuthProviderRegistry.js +190 -0
- package/dist/auth/RequestContext.d.ts +23 -0
- package/dist/auth/RequestContext.js +78 -0
- package/dist/auth/authContext.d.ts +198 -0
- package/dist/auth/authContext.js +314 -0
- package/dist/auth/errors.d.ts +63 -0
- package/dist/auth/errors.js +39 -0
- package/dist/auth/index.d.ts +20 -8
- package/dist/auth/index.js +35 -7
- package/dist/auth/middleware/AuthMiddleware.d.ts +181 -0
- package/dist/auth/middleware/AuthMiddleware.js +519 -0
- package/dist/auth/middleware/rateLimitByUser.d.ts +282 -0
- package/dist/auth/middleware/rateLimitByUser.js +554 -0
- package/dist/auth/providers/BaseAuthProvider.d.ts +259 -0
- package/dist/auth/providers/BaseAuthProvider.js +723 -0
- package/dist/auth/providers/CognitoProvider.d.ts +61 -0
- package/dist/auth/providers/CognitoProvider.js +304 -0
- package/dist/auth/providers/KeycloakProvider.d.ts +61 -0
- package/dist/auth/providers/KeycloakProvider.js +393 -0
- package/dist/auth/providers/auth0.d.ts +59 -0
- package/dist/auth/providers/auth0.js +274 -0
- package/dist/auth/providers/betterAuth.d.ts +51 -0
- package/dist/auth/providers/betterAuth.js +182 -0
- package/dist/auth/providers/clerk.d.ts +65 -0
- package/dist/auth/providers/clerk.js +317 -0
- package/dist/auth/providers/custom.d.ts +64 -0
- package/dist/auth/providers/custom.js +112 -0
- package/dist/auth/providers/firebase.d.ts +63 -0
- package/dist/auth/providers/firebase.js +226 -0
- package/dist/auth/providers/jwt.d.ts +68 -0
- package/dist/auth/providers/jwt.js +212 -0
- package/dist/auth/providers/oauth2.d.ts +73 -0
- package/dist/auth/providers/oauth2.js +303 -0
- package/dist/auth/providers/supabase.d.ts +63 -0
- package/dist/auth/providers/supabase.js +259 -0
- package/dist/auth/providers/workos.d.ts +61 -0
- package/dist/auth/providers/workos.js +284 -0
- package/dist/auth/serverBridge.d.ts +14 -0
- package/dist/auth/serverBridge.js +25 -0
- package/dist/auth/sessionManager.d.ts +142 -0
- package/dist/auth/sessionManager.js +437 -0
- package/dist/cli/commands/authProviders.d.ts +43 -0
- package/dist/cli/commands/authProviders.js +399 -0
- package/dist/cli/factories/authCommandFactory.d.ts +23 -5
- package/dist/cli/factories/authCommandFactory.js +108 -5
- package/dist/cli/parser.js +1 -1
- package/dist/client/auth/AuthProviderFactory.js +111 -0
- package/dist/client/auth/AuthProviderRegistry.js +190 -0
- package/dist/client/auth/RequestContext.js +78 -0
- package/dist/client/auth/accountPool.js +178 -0
- package/dist/client/auth/authContext.js +314 -0
- package/dist/client/auth/errors.js +39 -0
- package/dist/client/auth/index.js +61 -0
- package/dist/client/auth/middleware/AuthMiddleware.js +519 -0
- package/dist/client/auth/middleware/rateLimitByUser.js +554 -0
- package/dist/client/auth/providers/BaseAuthProvider.js +723 -0
- package/dist/client/auth/providers/CognitoProvider.js +304 -0
- package/dist/client/auth/providers/KeycloakProvider.js +393 -0
- package/dist/client/auth/providers/auth0.js +274 -0
- package/dist/client/auth/providers/betterAuth.js +182 -0
- package/dist/client/auth/providers/clerk.js +317 -0
- package/dist/client/auth/providers/custom.js +112 -0
- package/dist/client/auth/providers/firebase.js +226 -0
- package/dist/client/auth/providers/jwt.js +212 -0
- package/dist/client/auth/providers/oauth2.js +303 -0
- package/dist/client/auth/providers/supabase.js +259 -0
- package/dist/client/auth/providers/workos.js +284 -0
- package/dist/client/auth/serverBridge.js +25 -0
- package/dist/client/auth/sessionManager.js +437 -0
- package/dist/client/core/infrastructure/baseRegistry.js +5 -1
- package/dist/client/index.js +25 -0
- package/dist/client/mcp/toolRegistry.js +11 -1
- package/dist/client/neurolink.js +218 -0
- package/dist/client/rag/ChunkerRegistry.js +2 -2
- package/dist/client/rag/metadata/MetadataExtractorRegistry.js +2 -2
- package/dist/client/rag/reranker/RerankerRegistry.js +2 -2
- package/dist/client/server/routes/agentRoutes.js +20 -2
- package/dist/client/types/authTypes.js +2 -1
- package/dist/core/infrastructure/baseRegistry.d.ts +3 -1
- package/dist/core/infrastructure/baseRegistry.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +25 -0
- package/dist/lib/auth/AuthProviderFactory.d.ts +71 -0
- package/dist/lib/auth/AuthProviderFactory.js +112 -0
- package/dist/lib/auth/AuthProviderRegistry.d.ts +33 -0
- package/dist/lib/auth/AuthProviderRegistry.js +191 -0
- package/dist/lib/auth/RequestContext.d.ts +23 -0
- package/dist/lib/auth/RequestContext.js +79 -0
- package/dist/lib/auth/authContext.d.ts +198 -0
- package/dist/lib/auth/authContext.js +315 -0
- package/dist/lib/auth/errors.d.ts +63 -0
- package/dist/lib/auth/errors.js +40 -0
- package/dist/lib/auth/index.d.ts +20 -8
- package/dist/lib/auth/index.js +35 -7
- package/dist/lib/auth/middleware/AuthMiddleware.d.ts +181 -0
- package/dist/lib/auth/middleware/AuthMiddleware.js +520 -0
- package/dist/lib/auth/middleware/rateLimitByUser.d.ts +282 -0
- package/dist/lib/auth/middleware/rateLimitByUser.js +555 -0
- package/dist/lib/auth/providers/BaseAuthProvider.d.ts +259 -0
- package/dist/lib/auth/providers/BaseAuthProvider.js +724 -0
- package/dist/lib/auth/providers/CognitoProvider.d.ts +61 -0
- package/dist/lib/auth/providers/CognitoProvider.js +305 -0
- package/dist/lib/auth/providers/KeycloakProvider.d.ts +61 -0
- package/dist/lib/auth/providers/KeycloakProvider.js +394 -0
- package/dist/lib/auth/providers/auth0.d.ts +59 -0
- package/dist/lib/auth/providers/auth0.js +275 -0
- package/dist/lib/auth/providers/betterAuth.d.ts +51 -0
- package/dist/lib/auth/providers/betterAuth.js +183 -0
- package/dist/lib/auth/providers/clerk.d.ts +65 -0
- package/dist/lib/auth/providers/clerk.js +318 -0
- package/dist/lib/auth/providers/custom.d.ts +64 -0
- package/dist/lib/auth/providers/custom.js +113 -0
- package/dist/lib/auth/providers/firebase.d.ts +63 -0
- package/dist/lib/auth/providers/firebase.js +227 -0
- package/dist/lib/auth/providers/jwt.d.ts +68 -0
- package/dist/lib/auth/providers/jwt.js +213 -0
- package/dist/lib/auth/providers/oauth2.d.ts +73 -0
- package/dist/lib/auth/providers/oauth2.js +304 -0
- package/dist/lib/auth/providers/supabase.d.ts +63 -0
- package/dist/lib/auth/providers/supabase.js +260 -0
- package/dist/lib/auth/providers/workos.d.ts +61 -0
- package/dist/lib/auth/providers/workos.js +285 -0
- package/dist/lib/auth/serverBridge.d.ts +14 -0
- package/dist/lib/auth/serverBridge.js +26 -0
- package/dist/lib/auth/sessionManager.d.ts +142 -0
- package/dist/lib/auth/sessionManager.js +438 -0
- package/dist/lib/core/infrastructure/baseRegistry.d.ts +3 -1
- package/dist/lib/core/infrastructure/baseRegistry.js +5 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +25 -0
- package/dist/lib/mcp/toolRegistry.js +11 -1
- package/dist/lib/neurolink.d.ts +42 -1
- package/dist/lib/neurolink.js +218 -0
- package/dist/lib/rag/ChunkerRegistry.js +2 -2
- package/dist/lib/rag/metadata/MetadataExtractorRegistry.js +2 -2
- package/dist/lib/rag/reranker/RerankerRegistry.js +2 -2
- package/dist/lib/server/routes/agentRoutes.js +20 -2
- package/dist/lib/types/authTypes.d.ts +937 -1
- package/dist/lib/types/authTypes.js +2 -1
- package/dist/lib/types/configTypes.d.ts +46 -0
- package/dist/lib/types/generateTypes.d.ts +6 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/streamTypes.d.ts +6 -0
- package/dist/mcp/toolRegistry.js +11 -1
- package/dist/neurolink.d.ts +42 -1
- package/dist/neurolink.js +218 -0
- package/dist/rag/ChunkerRegistry.js +2 -2
- package/dist/rag/metadata/MetadataExtractorRegistry.js +2 -2
- package/dist/rag/reranker/RerankerRegistry.js +2 -2
- package/dist/server/routes/agentRoutes.js +20 -2
- package/dist/types/authTypes.d.ts +937 -1
- package/dist/types/authTypes.js +2 -1
- package/dist/types/configTypes.d.ts +46 -0
- package/dist/types/generateTypes.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/streamTypes.d.ts +6 -0
- package/package.json +2 -1
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
// src/lib/auth/middleware/rateLimitByUser.ts
|
|
2
|
+
import { logger } from "../../utils/logger.js";
|
|
3
|
+
/** Mask a userId for safe log output (first 4 chars + "***"). */
|
|
4
|
+
function maskUserId(id) {
|
|
5
|
+
return id.length > 4 ? `${id.slice(0, 4)}***` : "***";
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* In-memory storage for rate limiting (single instance deployments)
|
|
9
|
+
*/
|
|
10
|
+
export class MemoryRateLimitStorage {
|
|
11
|
+
buckets = new Map();
|
|
12
|
+
cleanupInterval;
|
|
13
|
+
expiryMs;
|
|
14
|
+
constructor(cleanupIntervalMs = 60000, expiryMs = 3600000) {
|
|
15
|
+
this.expiryMs = expiryMs;
|
|
16
|
+
// Periodically cleanup expired buckets
|
|
17
|
+
this.cleanupInterval = setInterval(() => {
|
|
18
|
+
this.cleanupExpiredBuckets();
|
|
19
|
+
}, cleanupIntervalMs);
|
|
20
|
+
// Allow Node.js to exit gracefully even if the interval is still active
|
|
21
|
+
this.cleanupInterval.unref();
|
|
22
|
+
}
|
|
23
|
+
async getBucket(userId) {
|
|
24
|
+
return this.buckets.get(userId) || null;
|
|
25
|
+
}
|
|
26
|
+
async setBucket(userId, bucket) {
|
|
27
|
+
this.buckets.set(userId, bucket);
|
|
28
|
+
}
|
|
29
|
+
async deleteBucket(userId) {
|
|
30
|
+
this.buckets.delete(userId);
|
|
31
|
+
}
|
|
32
|
+
async healthCheck() {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
async cleanup() {
|
|
36
|
+
if (this.cleanupInterval) {
|
|
37
|
+
clearInterval(this.cleanupInterval);
|
|
38
|
+
}
|
|
39
|
+
this.buckets.clear();
|
|
40
|
+
}
|
|
41
|
+
cleanupExpiredBuckets() {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const expiryCutoff = now - this.expiryMs;
|
|
44
|
+
for (const [userId, bucket] of this.buckets.entries()) {
|
|
45
|
+
if (bucket.lastRefill < expiryCutoff) {
|
|
46
|
+
this.buckets.delete(userId);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Redis-backed storage for rate limiting (distributed deployments)
|
|
53
|
+
*/
|
|
54
|
+
export class RedisRateLimitStorage {
|
|
55
|
+
redisUrl;
|
|
56
|
+
prefix;
|
|
57
|
+
ttlSeconds;
|
|
58
|
+
client = null;
|
|
59
|
+
initPromise = null;
|
|
60
|
+
constructor(config) {
|
|
61
|
+
this.redisUrl = config.url;
|
|
62
|
+
this.prefix = config.prefix || "neurolink:ratelimit:";
|
|
63
|
+
const baseTtl = config.ttlSeconds || 3600; // 1 hour default TTL
|
|
64
|
+
const windowTtl = config.windowMs ? Math.ceil(config.windowMs / 1000) : 0;
|
|
65
|
+
this.ttlSeconds = Math.max(baseTtl, windowTtl);
|
|
66
|
+
}
|
|
67
|
+
async getClient() {
|
|
68
|
+
if (this.client) {
|
|
69
|
+
return this.client;
|
|
70
|
+
}
|
|
71
|
+
if (!this.initPromise) {
|
|
72
|
+
this.initPromise = this.createClient();
|
|
73
|
+
}
|
|
74
|
+
return this.initPromise;
|
|
75
|
+
}
|
|
76
|
+
async createClient() {
|
|
77
|
+
try {
|
|
78
|
+
// Dynamic import to avoid loading Redis unless needed
|
|
79
|
+
const { createClient } = await import("redis");
|
|
80
|
+
const client = createClient({ url: this.redisUrl });
|
|
81
|
+
await client.connect();
|
|
82
|
+
this.client = client;
|
|
83
|
+
return this.client;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
this.initPromise = null;
|
|
87
|
+
throw new Error("Redis client not available for rate limiting");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async getBucket(userId) {
|
|
91
|
+
try {
|
|
92
|
+
const client = await this.getClient();
|
|
93
|
+
const key = `${this.prefix}${userId}`;
|
|
94
|
+
const data = await client.get(key);
|
|
95
|
+
if (!data) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return JSON.parse(data);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
logger.warn("Redis rate limit getBucket failed:", error);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async setBucket(userId, bucket) {
|
|
106
|
+
try {
|
|
107
|
+
const client = await this.getClient();
|
|
108
|
+
const key = `${this.prefix}${userId}`;
|
|
109
|
+
await client.setEx(key, this.ttlSeconds, JSON.stringify(bucket));
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
logger.warn("Redis rate limit setBucket failed:", error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async deleteBucket(userId) {
|
|
116
|
+
try {
|
|
117
|
+
const client = await this.getClient();
|
|
118
|
+
const key = `${this.prefix}${userId}`;
|
|
119
|
+
await client.del(key);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
logger.warn("Redis rate limit deleteBucket failed:", error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Atomically refill and consume one token using a Redis Lua script.
|
|
127
|
+
*
|
|
128
|
+
* The entire read-modify-write cycle runs inside Redis as a single
|
|
129
|
+
* atomic operation, so two parallel requests for the same user can
|
|
130
|
+
* never read the same token count.
|
|
131
|
+
*/
|
|
132
|
+
async atomicConsume(userId, limit, windowMs, nowMs) {
|
|
133
|
+
try {
|
|
134
|
+
const client = await this.getClient();
|
|
135
|
+
const key = `${this.prefix}${userId}`;
|
|
136
|
+
// Lua script: refill tokens based on elapsed time, then try to consume one.
|
|
137
|
+
// KEYS[1] = bucket key
|
|
138
|
+
// ARGV[1] = limit (max tokens)
|
|
139
|
+
// ARGV[2] = windowMs
|
|
140
|
+
// ARGV[3] = nowMs (current timestamp)
|
|
141
|
+
// ARGV[4] = ttl in seconds
|
|
142
|
+
// ARGV[5] = userId
|
|
143
|
+
//
|
|
144
|
+
// Returns: [tokens (x1000), lastRefill, consumed (0/1)]
|
|
145
|
+
// – tokens are multiplied by 1000 to preserve 3 decimal places
|
|
146
|
+
// since Redis Lua returns only integers.
|
|
147
|
+
// – returns nil when the key does not exist (caller creates bucket).
|
|
148
|
+
const luaScript = `
|
|
149
|
+
local data = redis.call('GET', KEYS[1])
|
|
150
|
+
if not data then
|
|
151
|
+
return nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
local bucket = cjson.decode(data)
|
|
155
|
+
local limit = tonumber(ARGV[1])
|
|
156
|
+
local windowMs = tonumber(ARGV[2])
|
|
157
|
+
local nowMs = tonumber(ARGV[3])
|
|
158
|
+
local ttl = tonumber(ARGV[4])
|
|
159
|
+
local userId = ARGV[5]
|
|
160
|
+
|
|
161
|
+
-- Refill tokens
|
|
162
|
+
local elapsed = nowMs - bucket.lastRefill
|
|
163
|
+
local tokensToAdd = (elapsed / windowMs) * limit
|
|
164
|
+
local tokens = math.min(limit, bucket.tokens + tokensToAdd)
|
|
165
|
+
local lastRefill = nowMs
|
|
166
|
+
|
|
167
|
+
-- Try to consume
|
|
168
|
+
local consumed = 0
|
|
169
|
+
if tokens >= 1 then
|
|
170
|
+
tokens = tokens - 1
|
|
171
|
+
consumed = 1
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
-- Persist
|
|
175
|
+
bucket.tokens = tokens
|
|
176
|
+
bucket.lastRefill = lastRefill
|
|
177
|
+
bucket.userId = userId
|
|
178
|
+
redis.call('SETEX', KEYS[1], ttl, cjson.encode(bucket))
|
|
179
|
+
|
|
180
|
+
-- Return integers (tokens * 1000 to keep 3 decimal places)
|
|
181
|
+
return { math.floor(tokens * 1000), lastRefill, consumed }
|
|
182
|
+
`;
|
|
183
|
+
const result = await client.eval(luaScript, 1, key, String(limit), String(windowMs), String(nowMs), String(this.ttlSeconds), userId);
|
|
184
|
+
if (result === null || result === undefined) {
|
|
185
|
+
return null; // Key did not exist
|
|
186
|
+
}
|
|
187
|
+
const [tokensTimes1000, lastRefill, consumed] = result;
|
|
188
|
+
return {
|
|
189
|
+
bucket: {
|
|
190
|
+
tokens: tokensTimes1000 / 1000,
|
|
191
|
+
lastRefill,
|
|
192
|
+
userId,
|
|
193
|
+
},
|
|
194
|
+
consumed: consumed === 1,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
logger.warn("Redis atomicConsume failed, falling back to non-atomic:", error);
|
|
199
|
+
return null; // Fallback: caller will use get+set path
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async healthCheck() {
|
|
203
|
+
try {
|
|
204
|
+
const client = await this.getClient();
|
|
205
|
+
const pong = await client.ping();
|
|
206
|
+
return pong === "PONG";
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async cleanup() {
|
|
213
|
+
if (this.client) {
|
|
214
|
+
await this.client.quit();
|
|
215
|
+
this.client = null;
|
|
216
|
+
this.initPromise = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Token bucket rate limiter implementation
|
|
222
|
+
*
|
|
223
|
+
* Uses the token bucket algorithm which allows for burst traffic while
|
|
224
|
+
* maintaining an average rate limit. Tokens are continuously added to
|
|
225
|
+
* the bucket at a fixed rate, and each request consumes one token.
|
|
226
|
+
*/
|
|
227
|
+
export class UserRateLimiter {
|
|
228
|
+
storage;
|
|
229
|
+
config;
|
|
230
|
+
constructor(config, storage) {
|
|
231
|
+
this.config = {
|
|
232
|
+
message: "Rate limit exceeded. Please try again later.",
|
|
233
|
+
...config,
|
|
234
|
+
};
|
|
235
|
+
this.storage =
|
|
236
|
+
storage ||
|
|
237
|
+
new MemoryRateLimitStorage(Math.max(60000, config.windowMs), config.windowMs);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get the rate limit for a specific user based on their roles
|
|
241
|
+
*/
|
|
242
|
+
getLimitForUser(user) {
|
|
243
|
+
// Check user-specific limits first
|
|
244
|
+
if (this.config.userLimits && user.id in this.config.userLimits) {
|
|
245
|
+
return this.config.userLimits[user.id];
|
|
246
|
+
}
|
|
247
|
+
// Check role-based limits (use highest if user has multiple roles)
|
|
248
|
+
if (this.config.roleLimits) {
|
|
249
|
+
let maxLimit = this.config.maxRequests;
|
|
250
|
+
for (const role of user.roles) {
|
|
251
|
+
if (role in this.config.roleLimits) {
|
|
252
|
+
maxLimit = Math.max(maxLimit, this.config.roleLimits[role]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return maxLimit;
|
|
256
|
+
}
|
|
257
|
+
return this.config.maxRequests;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Check if a user should skip rate limiting (based on roles)
|
|
261
|
+
*/
|
|
262
|
+
shouldSkipRateLimit(user) {
|
|
263
|
+
if (!this.config.skipRoles || this.config.skipRoles.length === 0) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
return user.roles.some((role) => this.config.skipRoles?.includes(role) ?? false);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Consume a token from the user's bucket
|
|
270
|
+
* Returns the rate limit result
|
|
271
|
+
*
|
|
272
|
+
* When the storage backend supports `atomicConsume` (e.g. Redis with Lua),
|
|
273
|
+
* the entire refill-and-consume is executed as a single atomic operation,
|
|
274
|
+
* preventing race conditions where parallel requests both read the same
|
|
275
|
+
* token count and both succeed.
|
|
276
|
+
*/
|
|
277
|
+
async consume(user) {
|
|
278
|
+
// Skip rate limiting for exempt roles
|
|
279
|
+
if (this.shouldSkipRateLimit(user)) {
|
|
280
|
+
return {
|
|
281
|
+
allowed: true,
|
|
282
|
+
remaining: Infinity,
|
|
283
|
+
resetIn: 0,
|
|
284
|
+
limit: Infinity,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const userId = user.id;
|
|
288
|
+
const limit = this.getLimitForUser(user);
|
|
289
|
+
const now = Date.now();
|
|
290
|
+
// Try atomic consume first (Redis Lua script – race-condition safe)
|
|
291
|
+
if (this.storage.atomicConsume) {
|
|
292
|
+
const atomicResult = await this.storage.atomicConsume(userId, limit, this.config.windowMs, now);
|
|
293
|
+
if (atomicResult !== null) {
|
|
294
|
+
const { bucket, consumed } = atomicResult;
|
|
295
|
+
if (consumed) {
|
|
296
|
+
return {
|
|
297
|
+
allowed: true,
|
|
298
|
+
remaining: Math.floor(bucket.tokens),
|
|
299
|
+
resetIn: Math.ceil(((limit - bucket.tokens) / limit) * this.config.windowMs),
|
|
300
|
+
limit,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// Rate limited
|
|
304
|
+
const resetIn = Math.ceil(((1 - bucket.tokens) / limit) * this.config.windowMs);
|
|
305
|
+
return {
|
|
306
|
+
allowed: false,
|
|
307
|
+
remaining: 0,
|
|
308
|
+
resetIn,
|
|
309
|
+
limit,
|
|
310
|
+
error: this.config.message,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// atomicConsume returned null → bucket does not exist yet.
|
|
314
|
+
// Fall through to create it below.
|
|
315
|
+
}
|
|
316
|
+
// Fallback: non-atomic get+set (safe for single-threaded in-memory storage)
|
|
317
|
+
let bucket = await this.storage.getBucket(userId);
|
|
318
|
+
if (!bucket) {
|
|
319
|
+
// Create new bucket with full tokens
|
|
320
|
+
bucket = {
|
|
321
|
+
tokens: limit,
|
|
322
|
+
lastRefill: now,
|
|
323
|
+
userId,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// Calculate tokens to add based on time elapsed
|
|
327
|
+
const timePassed = now - bucket.lastRefill;
|
|
328
|
+
const tokensToAdd = (timePassed / this.config.windowMs) * limit;
|
|
329
|
+
bucket.tokens = Math.min(limit, bucket.tokens + tokensToAdd);
|
|
330
|
+
bucket.lastRefill = now;
|
|
331
|
+
// Try to consume a token
|
|
332
|
+
if (bucket.tokens >= 1) {
|
|
333
|
+
bucket.tokens -= 1;
|
|
334
|
+
await this.storage.setBucket(userId, bucket);
|
|
335
|
+
return {
|
|
336
|
+
allowed: true,
|
|
337
|
+
remaining: Math.floor(bucket.tokens),
|
|
338
|
+
resetIn: Math.ceil(((limit - bucket.tokens) / limit) * this.config.windowMs),
|
|
339
|
+
limit,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
// Rate limited
|
|
343
|
+
await this.storage.setBucket(userId, bucket);
|
|
344
|
+
const resetIn = Math.ceil(((1 - bucket.tokens) / limit) * this.config.windowMs);
|
|
345
|
+
return {
|
|
346
|
+
allowed: false,
|
|
347
|
+
remaining: 0,
|
|
348
|
+
resetIn,
|
|
349
|
+
limit,
|
|
350
|
+
error: this.config.message,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Get current rate limit status for a user without consuming a token
|
|
355
|
+
*/
|
|
356
|
+
async getStatus(user) {
|
|
357
|
+
if (this.shouldSkipRateLimit(user)) {
|
|
358
|
+
return {
|
|
359
|
+
allowed: true,
|
|
360
|
+
remaining: Infinity,
|
|
361
|
+
resetIn: 0,
|
|
362
|
+
limit: Infinity,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const limit = this.getLimitForUser(user);
|
|
366
|
+
const bucket = await this.storage.getBucket(user.id);
|
|
367
|
+
if (!bucket) {
|
|
368
|
+
return {
|
|
369
|
+
allowed: true,
|
|
370
|
+
remaining: limit,
|
|
371
|
+
resetIn: 0,
|
|
372
|
+
limit,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
// Calculate current tokens
|
|
376
|
+
const now = Date.now();
|
|
377
|
+
const timePassed = now - bucket.lastRefill;
|
|
378
|
+
const tokensToAdd = (timePassed / this.config.windowMs) * limit;
|
|
379
|
+
const currentTokens = Math.min(limit, bucket.tokens + tokensToAdd);
|
|
380
|
+
return {
|
|
381
|
+
allowed: currentTokens >= 1,
|
|
382
|
+
remaining: Math.floor(currentTokens),
|
|
383
|
+
resetIn: currentTokens >= 1
|
|
384
|
+
? 0
|
|
385
|
+
: Math.ceil(((1 - currentTokens) / limit) * this.config.windowMs),
|
|
386
|
+
limit,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Reset rate limit for a user (admin action)
|
|
391
|
+
*/
|
|
392
|
+
async resetUser(userId) {
|
|
393
|
+
await this.storage.deleteBucket(userId);
|
|
394
|
+
logger.debug(`Rate limit reset for user: ${maskUserId(userId)}`);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Check storage health
|
|
398
|
+
*/
|
|
399
|
+
async healthCheck() {
|
|
400
|
+
return this.storage.healthCheck();
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Cleanup resources
|
|
404
|
+
*/
|
|
405
|
+
async cleanup() {
|
|
406
|
+
await this.storage.cleanup();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Create rate limiting middleware for authenticated requests
|
|
411
|
+
*
|
|
412
|
+
* @param config - Rate limit configuration
|
|
413
|
+
* @param storage - Optional custom storage backend
|
|
414
|
+
* @returns Middleware function
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```typescript
|
|
418
|
+
* const rateLimitMiddleware = createRateLimitByUserMiddleware({
|
|
419
|
+
* maxRequests: 100,
|
|
420
|
+
* windowMs: 60000, // 1 minute
|
|
421
|
+
* roleLimits: {
|
|
422
|
+
* "premium": 500,
|
|
423
|
+
* "admin": 1000
|
|
424
|
+
* },
|
|
425
|
+
* skipRoles: ["super-admin"]
|
|
426
|
+
* });
|
|
427
|
+
*
|
|
428
|
+
* // Use in server
|
|
429
|
+
* app.use(async (request, context) => {
|
|
430
|
+
* const result = await rateLimitMiddleware(context);
|
|
431
|
+
* if (!result.proceed) {
|
|
432
|
+
* return result.response;
|
|
433
|
+
* }
|
|
434
|
+
* // Continue processing...
|
|
435
|
+
* });
|
|
436
|
+
* ```
|
|
437
|
+
*/
|
|
438
|
+
export function createRateLimitByUserMiddleware(config, storage) {
|
|
439
|
+
const limiter = new UserRateLimiter(config, storage);
|
|
440
|
+
return async (context) => {
|
|
441
|
+
const result = await limiter.consume(context.user);
|
|
442
|
+
if (!result.allowed) {
|
|
443
|
+
const response = createRateLimitResponse(result);
|
|
444
|
+
return {
|
|
445
|
+
proceed: false,
|
|
446
|
+
rateLimitResult: result,
|
|
447
|
+
response,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
proceed: true,
|
|
452
|
+
rateLimitResult: result,
|
|
453
|
+
};
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Create a combined auth and rate limit middleware
|
|
458
|
+
*
|
|
459
|
+
* @param authMiddleware - Authentication middleware function
|
|
460
|
+
* @param rateLimitConfig - Rate limit configuration
|
|
461
|
+
* @param storage - Optional custom storage backend
|
|
462
|
+
* @returns Combined middleware function
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* ```typescript
|
|
466
|
+
* const protectedRoute = createAuthenticatedRateLimitMiddleware(
|
|
467
|
+
* createAuthMiddleware({ provider: authProvider }),
|
|
468
|
+
* { maxRequests: 100, windowMs: 60000 }
|
|
469
|
+
* );
|
|
470
|
+
*
|
|
471
|
+
* // Use in routes
|
|
472
|
+
* app.post("/api/generate", async (request) => {
|
|
473
|
+
* const result = await protectedRoute(request);
|
|
474
|
+
* if (!result.proceed) {
|
|
475
|
+
* return result.response;
|
|
476
|
+
* }
|
|
477
|
+
* // Handle request with result.context
|
|
478
|
+
* });
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
export function createAuthenticatedRateLimitMiddleware(authMiddleware, rateLimitConfig, storage) {
|
|
482
|
+
const limiter = new UserRateLimiter(rateLimitConfig, storage);
|
|
483
|
+
return async (context) => {
|
|
484
|
+
// First, authenticate
|
|
485
|
+
const authResult = await authMiddleware(context);
|
|
486
|
+
if (!authResult.proceed || !authResult.context) {
|
|
487
|
+
return authResult;
|
|
488
|
+
}
|
|
489
|
+
// Then, check rate limit
|
|
490
|
+
const rateLimitResult = await limiter.consume(authResult.context.user);
|
|
491
|
+
if (!rateLimitResult.allowed) {
|
|
492
|
+
return {
|
|
493
|
+
proceed: false,
|
|
494
|
+
context: authResult.context,
|
|
495
|
+
rateLimitResult,
|
|
496
|
+
response: createRateLimitResponse(rateLimitResult),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
proceed: true,
|
|
501
|
+
context: authResult.context,
|
|
502
|
+
rateLimitResult,
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Create 429 Too Many Requests response
|
|
508
|
+
*/
|
|
509
|
+
function createRateLimitResponse(result) {
|
|
510
|
+
return new Response(JSON.stringify({
|
|
511
|
+
error: "Too Many Requests",
|
|
512
|
+
message: result.error || "Rate limit exceeded",
|
|
513
|
+
statusCode: 429,
|
|
514
|
+
retryAfter: Math.ceil(result.resetIn / 1000), // In seconds
|
|
515
|
+
limit: result.limit,
|
|
516
|
+
remaining: result.remaining,
|
|
517
|
+
}), {
|
|
518
|
+
status: 429,
|
|
519
|
+
headers: {
|
|
520
|
+
"Content-Type": "application/json",
|
|
521
|
+
"Retry-After": String(Math.ceil(result.resetIn / 1000)),
|
|
522
|
+
"X-RateLimit-Limit": String(result.limit),
|
|
523
|
+
"X-RateLimit-Remaining": String(result.remaining),
|
|
524
|
+
"X-RateLimit-Reset": String(Date.now() + result.resetIn),
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Create rate limit storage based on configuration
|
|
530
|
+
*
|
|
531
|
+
* @param config - Storage configuration
|
|
532
|
+
* @returns Appropriate storage backend
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```typescript
|
|
536
|
+
* // Memory storage (default)
|
|
537
|
+
* const storage = createRateLimitStorage({ type: "memory" });
|
|
538
|
+
*
|
|
539
|
+
* // Redis storage
|
|
540
|
+
* const storage = createRateLimitStorage({
|
|
541
|
+
* type: "redis",
|
|
542
|
+
* redis: {
|
|
543
|
+
* url: "redis://localhost:6379",
|
|
544
|
+
* prefix: "myapp:ratelimit:"
|
|
545
|
+
* }
|
|
546
|
+
* });
|
|
547
|
+
* ```
|
|
548
|
+
*/
|
|
549
|
+
export function createRateLimitStorage(config) {
|
|
550
|
+
if (config.type === "redis" && config.redis) {
|
|
551
|
+
return new RedisRateLimitStorage(config.redis);
|
|
552
|
+
}
|
|
553
|
+
return new MemoryRateLimitStorage(config.cleanupIntervalMs);
|
|
554
|
+
}
|