@tuturuuu/utils 0.12.1 → 0.15.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 +69 -0
- package/biome.json +1 -1
- package/package.json +10 -10
- package/src/__tests__/api-proxy-guard.test.ts +40 -17
- package/src/__tests__/app-url.test.ts +6 -0
- package/src/__tests__/task-transformers.test.ts +21 -0
- package/src/abuse-protection/__tests__/index-fail-open.test.ts +93 -0
- package/src/abuse-protection/__tests__/index.test.ts +76 -1
- package/src/abuse-protection/index.ts +136 -123
- package/src/api-proxy-guard.ts +15 -117
- package/src/calendar-settings-resolver.ts +200 -0
- package/src/internal-domains.ts +44 -0
- package/src/launchable-apps.test.ts +44 -0
- package/src/launchable-apps.ts +60 -47
- package/src/platform-release.test.ts +2 -2
- package/src/platform-release.ts +3 -1
- package/src/portless.ts +4 -0
- package/src/shared-cookie.test.ts +114 -0
- package/src/shared-cookie.ts +235 -0
- package/src/task/transformers.ts +2 -0
- package/src/topic-announcements.ts +1 -0
- package/src/user-helper.ts +6 -95
- package/src/workspace-user-link.test.ts +53 -0
- package/src/workspace-user-link.ts +122 -0
- package/src/yjs-helper.ts +2 -2
|
@@ -31,9 +31,6 @@ export * from './reputation';
|
|
|
31
31
|
export * from './types';
|
|
32
32
|
export * from './user-agent';
|
|
33
33
|
|
|
34
|
-
// In-memory fallback store
|
|
35
|
-
const memoryStore = new Map<string, { count: number; expiresAt: number }>();
|
|
36
|
-
|
|
37
34
|
const SHARED_IP_THROTTLE_ONLY_BLOCK_REASONS = new Set<AbuseEventType>([
|
|
38
35
|
'otp_send',
|
|
39
36
|
'otp_verify_failed',
|
|
@@ -50,6 +47,28 @@ export function isSharedIpThrottleOnlyBlockReason(reason: AbuseEventType) {
|
|
|
50
47
|
let redisClient: RedisClient | null = null;
|
|
51
48
|
let redisInitialized = false;
|
|
52
49
|
|
|
50
|
+
function markRedisUnavailable(message: string, ...details: unknown[]): void {
|
|
51
|
+
if (details.length > 0) {
|
|
52
|
+
console.warn(message, ...details);
|
|
53
|
+
} else {
|
|
54
|
+
console.warn(message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
redisClient = null;
|
|
58
|
+
redisInitialized = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function markRedisCommandFailed(message: string, ...details: unknown[]): void {
|
|
62
|
+
if (details.length > 0) {
|
|
63
|
+
console.warn(message, ...details);
|
|
64
|
+
} else {
|
|
65
|
+
console.warn(message);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
redisClient = null;
|
|
69
|
+
redisInitialized = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
53
72
|
function parsePositiveIntEnv(name: string, fallback: number): number {
|
|
54
73
|
const rawValue = process.env[name];
|
|
55
74
|
if (!rawValue) {
|
|
@@ -85,19 +104,22 @@ async function getRedisClient(): Promise<RedisClient | null> {
|
|
|
85
104
|
|
|
86
105
|
try {
|
|
87
106
|
if (!hasUpstashRestEnv()) {
|
|
88
|
-
|
|
89
|
-
'[Abuse Protection] Redis not configured -
|
|
107
|
+
markRedisUnavailable(
|
|
108
|
+
'[Abuse Protection] Redis not configured - failing open'
|
|
90
109
|
);
|
|
91
|
-
redisInitialized = true;
|
|
92
110
|
return null;
|
|
93
111
|
}
|
|
94
112
|
|
|
95
113
|
redisClient = await getUpstashRestRedisClient();
|
|
114
|
+
if (!redisClient) {
|
|
115
|
+
markRedisUnavailable(
|
|
116
|
+
'[Abuse Protection] Redis client unavailable - failing open'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
96
119
|
redisInitialized = true;
|
|
97
120
|
return redisClient;
|
|
98
121
|
} catch (error) {
|
|
99
|
-
|
|
100
|
-
redisInitialized = true;
|
|
122
|
+
markRedisUnavailable('[Abuse Protection] Redis unavailable:', error);
|
|
101
123
|
return null;
|
|
102
124
|
}
|
|
103
125
|
}
|
|
@@ -191,83 +213,70 @@ function createAbuseLogContext(
|
|
|
191
213
|
}
|
|
192
214
|
|
|
193
215
|
/**
|
|
194
|
-
* Increment a
|
|
216
|
+
* Increment a Redis counter with automatic expiration.
|
|
217
|
+
* Fails open when Redis is unavailable so abuse protection never becomes an
|
|
218
|
+
* app availability dependency.
|
|
195
219
|
*/
|
|
196
220
|
async function incrementCounter(
|
|
197
221
|
key: string,
|
|
198
222
|
windowMs: number
|
|
199
223
|
): Promise<{ count: number; ttl: number }> {
|
|
200
224
|
const redis = await getRedisClient();
|
|
225
|
+
const neutral = { count: 0, ttl: Math.ceil(windowMs / 1000) };
|
|
201
226
|
|
|
202
|
-
if (redis) {
|
|
203
|
-
|
|
204
|
-
const count = await redis.incr(key);
|
|
205
|
-
if (count === 1) {
|
|
206
|
-
await redis.expire(key, Math.ceil(windowMs / 1000));
|
|
207
|
-
}
|
|
208
|
-
const ttl = await redis.ttl(key);
|
|
209
|
-
return { count, ttl: ttl > 0 ? ttl : Math.ceil(windowMs / 1000) };
|
|
210
|
-
} catch (error) {
|
|
211
|
-
console.error('[Abuse Protection] Redis error:', error);
|
|
212
|
-
// Fall through to memory
|
|
213
|
-
}
|
|
227
|
+
if (!redis) {
|
|
228
|
+
return neutral;
|
|
214
229
|
}
|
|
215
230
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return { count:
|
|
231
|
+
try {
|
|
232
|
+
const count = await redis.incr(key);
|
|
233
|
+
if (count === 1) {
|
|
234
|
+
await redis.expire(key, Math.ceil(windowMs / 1000));
|
|
235
|
+
}
|
|
236
|
+
const ttl = await redis.ttl(key);
|
|
237
|
+
return { count, ttl: ttl > 0 ? ttl : Math.ceil(windowMs / 1000) };
|
|
238
|
+
} catch (error) {
|
|
239
|
+
markRedisCommandFailed('[Abuse Protection] Redis counter error:', error);
|
|
240
|
+
return neutral;
|
|
223
241
|
}
|
|
224
|
-
|
|
225
|
-
existing.count++;
|
|
226
|
-
return {
|
|
227
|
-
count: existing.count,
|
|
228
|
-
ttl: Math.ceil((existing.expiresAt - now) / 1000),
|
|
229
|
-
};
|
|
230
242
|
}
|
|
231
243
|
|
|
232
244
|
/**
|
|
233
|
-
* Get counter value from Redis
|
|
245
|
+
* Get a counter value from Redis.
|
|
234
246
|
*/
|
|
235
247
|
async function getCounter(key: string): Promise<number> {
|
|
236
248
|
const redis = await getRedisClient();
|
|
237
249
|
|
|
238
|
-
if (redis) {
|
|
239
|
-
|
|
240
|
-
const count = await redis.get<number>(key);
|
|
241
|
-
return count || 0;
|
|
242
|
-
} catch {
|
|
243
|
-
// Fall through to memory
|
|
244
|
-
}
|
|
250
|
+
if (!redis) {
|
|
251
|
+
return 0;
|
|
245
252
|
}
|
|
246
253
|
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
try {
|
|
255
|
+
const count = await redis.get<number>(key);
|
|
256
|
+
return count || 0;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
markRedisCommandFailed(
|
|
259
|
+
'[Abuse Protection] Redis counter read error:',
|
|
260
|
+
error
|
|
261
|
+
);
|
|
249
262
|
return 0;
|
|
250
263
|
}
|
|
251
|
-
return existing.count;
|
|
252
264
|
}
|
|
253
265
|
|
|
254
266
|
/**
|
|
255
|
-
* Delete keys
|
|
267
|
+
* Delete Redis keys when Redis is available.
|
|
256
268
|
*/
|
|
257
269
|
async function deleteKeys(...keys: string[]): Promise<void> {
|
|
258
270
|
const redis = await getRedisClient();
|
|
259
271
|
|
|
260
|
-
if (redis) {
|
|
261
|
-
|
|
262
|
-
await redis.del(...keys);
|
|
263
|
-
return;
|
|
264
|
-
} catch {
|
|
265
|
-
// Fall through to memory
|
|
266
|
-
}
|
|
272
|
+
if (!redis) {
|
|
273
|
+
return;
|
|
267
274
|
}
|
|
268
275
|
|
|
269
|
-
|
|
270
|
-
|
|
276
|
+
try {
|
|
277
|
+
await redis.del(...keys);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
markRedisCommandFailed('[Abuse Protection] Redis delete error:', error);
|
|
271
280
|
}
|
|
272
281
|
}
|
|
273
282
|
|
|
@@ -278,23 +287,17 @@ async function deleteKeysWithCount(...keys: string[]): Promise<number> {
|
|
|
278
287
|
|
|
279
288
|
const redis = await getRedisClient();
|
|
280
289
|
|
|
281
|
-
if (redis) {
|
|
282
|
-
|
|
283
|
-
const deleted = await redis.del(...keys);
|
|
284
|
-
return typeof deleted === 'number' ? deleted : 0;
|
|
285
|
-
} catch {
|
|
286
|
-
// Fall through to memory
|
|
287
|
-
}
|
|
290
|
+
if (!redis) {
|
|
291
|
+
return 0;
|
|
288
292
|
}
|
|
289
293
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
294
|
+
try {
|
|
295
|
+
const deleted = await redis.del(...keys);
|
|
296
|
+
return typeof deleted === 'number' ? deleted : 0;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
markRedisCommandFailed('[Abuse Protection] Redis delete error:', error);
|
|
299
|
+
return 0;
|
|
295
300
|
}
|
|
296
|
-
|
|
297
|
-
return deleted;
|
|
298
301
|
}
|
|
299
302
|
|
|
300
303
|
/**
|
|
@@ -324,30 +327,33 @@ export async function isIPBlocked(
|
|
|
324
327
|
): Promise<BlockInfo | null> {
|
|
325
328
|
const redis = await getRedisClient();
|
|
326
329
|
|
|
330
|
+
if (!redis) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
327
334
|
// Check Redis cache first
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
};
|
|
342
|
-
}
|
|
335
|
+
try {
|
|
336
|
+
const cached = await redis.get<string>(REDIS_KEYS.IP_BLOCKED(ipAddress));
|
|
337
|
+
if (cached) {
|
|
338
|
+
const blockInfo =
|
|
339
|
+
typeof cached === 'string' ? JSON.parse(cached) : cached;
|
|
340
|
+
if (new Date(blockInfo.expiresAt) > new Date()) {
|
|
341
|
+
return {
|
|
342
|
+
id: blockInfo.id,
|
|
343
|
+
blockLevel: blockInfo.level,
|
|
344
|
+
reason: blockInfo.reason,
|
|
345
|
+
expiresAt: new Date(blockInfo.expiresAt),
|
|
346
|
+
blockedAt: new Date(blockInfo.blockedAt),
|
|
347
|
+
};
|
|
343
348
|
}
|
|
344
|
-
} catch (error) {
|
|
345
|
-
console.error(
|
|
346
|
-
'[Abuse Protection] Redis cache error:',
|
|
347
|
-
error,
|
|
348
|
-
createAbuseLogContext(ipAddress, context)
|
|
349
|
-
);
|
|
350
349
|
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
markRedisCommandFailed(
|
|
352
|
+
'[Abuse Protection] Redis cache error:',
|
|
353
|
+
error,
|
|
354
|
+
createAbuseLogContext(ipAddress, context)
|
|
355
|
+
);
|
|
356
|
+
return null;
|
|
351
357
|
}
|
|
352
358
|
|
|
353
359
|
// Check database
|
|
@@ -376,21 +382,29 @@ export async function isIPBlocked(
|
|
|
376
382
|
};
|
|
377
383
|
|
|
378
384
|
// Cache in Redis
|
|
379
|
-
if (
|
|
385
|
+
if (blockInfo.expiresAt > new Date()) {
|
|
380
386
|
const ttl = Math.ceil(
|
|
381
387
|
(blockInfo.expiresAt.getTime() - Date.now()) / 1000
|
|
382
388
|
);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
389
|
+
try {
|
|
390
|
+
await redis.set(
|
|
391
|
+
REDIS_KEYS.IP_BLOCKED(ipAddress),
|
|
392
|
+
JSON.stringify({
|
|
393
|
+
id: blockInfo.id,
|
|
394
|
+
level: blockInfo.blockLevel,
|
|
395
|
+
reason: blockInfo.reason,
|
|
396
|
+
expiresAt: blockInfo.expiresAt.toISOString(),
|
|
397
|
+
blockedAt: blockInfo.blockedAt.toISOString(),
|
|
398
|
+
}),
|
|
399
|
+
{ ex: ttl }
|
|
400
|
+
);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
markRedisCommandFailed(
|
|
403
|
+
'[Abuse Protection] Redis cache set error:',
|
|
404
|
+
error
|
|
405
|
+
);
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
394
408
|
}
|
|
395
409
|
|
|
396
410
|
return blockInfo;
|
|
@@ -413,31 +427,25 @@ export async function blockIP(
|
|
|
413
427
|
}
|
|
414
428
|
|
|
415
429
|
try {
|
|
430
|
+
const redis = await getRedisClient();
|
|
431
|
+
if (!redis) return;
|
|
432
|
+
|
|
416
433
|
const sbAdmin = await getSupabaseAdmin();
|
|
417
434
|
if (!sbAdmin) return;
|
|
418
435
|
|
|
419
|
-
const redis = await getRedisClient();
|
|
420
|
-
|
|
421
436
|
// Get current block level
|
|
422
437
|
let currentLevel = 0;
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
currentLevel = (data as number) || 0;
|
|
435
|
-
}
|
|
436
|
-
} else {
|
|
437
|
-
const { data } = await sbAdmin.rpc('get_ip_block_level', {
|
|
438
|
-
p_ip_address: ipAddress,
|
|
439
|
-
});
|
|
440
|
-
currentLevel = (data as number) || 0;
|
|
438
|
+
try {
|
|
439
|
+
const level = await redis.get<number>(
|
|
440
|
+
REDIS_KEYS.IP_BLOCK_LEVEL(ipAddress)
|
|
441
|
+
);
|
|
442
|
+
currentLevel = level || 0;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
markRedisCommandFailed(
|
|
445
|
+
'[Abuse Protection] Redis block-level error:',
|
|
446
|
+
error
|
|
447
|
+
);
|
|
448
|
+
return;
|
|
441
449
|
}
|
|
442
450
|
|
|
443
451
|
// Calculate new block level (max 4)
|
|
@@ -470,7 +478,7 @@ export async function blockIP(
|
|
|
470
478
|
}
|
|
471
479
|
|
|
472
480
|
// Update Redis cache
|
|
473
|
-
|
|
481
|
+
try {
|
|
474
482
|
await Promise.all([
|
|
475
483
|
redis.set(
|
|
476
484
|
REDIS_KEYS.IP_BLOCKED(ipAddress),
|
|
@@ -487,6 +495,11 @@ export async function blockIP(
|
|
|
487
495
|
ex: WINDOW_MS.TWENTY_FOUR_HOURS / 1000,
|
|
488
496
|
}),
|
|
489
497
|
]);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
markRedisCommandFailed(
|
|
500
|
+
'[Abuse Protection] Redis block cache error:',
|
|
501
|
+
error
|
|
502
|
+
);
|
|
490
503
|
}
|
|
491
504
|
|
|
492
505
|
console.log(
|
package/src/api-proxy-guard.ts
CHANGED
|
@@ -112,13 +112,7 @@ type RateLimiter = {
|
|
|
112
112
|
limit: (identifier: string) => Promise<RateLimitResult>;
|
|
113
113
|
};
|
|
114
114
|
|
|
115
|
-
type LocalRateLimitState = {
|
|
116
|
-
count: number;
|
|
117
|
-
reset: number;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
115
|
const limiterCache = new Map<string, Limiters>();
|
|
121
|
-
const localLimiterState = new Map<string, LocalRateLimitState>();
|
|
122
116
|
const GENERIC_SUPABASE_AUTH_COOKIE_NAME_PATTERN =
|
|
123
117
|
/^sb-[a-z0-9-]+-auth-token(?:\.\d+)?$/i;
|
|
124
118
|
const UUID_PATH_SEGMENT =
|
|
@@ -697,87 +691,10 @@ function createRedisRateLimitBuckets(
|
|
|
697
691
|
}));
|
|
698
692
|
}
|
|
699
693
|
|
|
700
|
-
function rateLimitDurationMs(duration: RateLimitConfig['duration']) {
|
|
701
|
-
switch (duration) {
|
|
702
|
-
case '1 m':
|
|
703
|
-
return 60_000;
|
|
704
|
-
case '1 h':
|
|
705
|
-
return 60 * 60_000;
|
|
706
|
-
case '1 d':
|
|
707
|
-
return 24 * 60 * 60_000;
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function createLocalRateLimiter(
|
|
712
|
-
prefix: string,
|
|
713
|
-
config: RateLimitConfig
|
|
714
|
-
): RateLimiter {
|
|
715
|
-
const durationMs = rateLimitDurationMs(config.duration);
|
|
716
|
-
|
|
717
|
-
return {
|
|
718
|
-
async limit(identifier: string) {
|
|
719
|
-
const now = Date.now();
|
|
720
|
-
const key = `${prefix}:${identifier}`;
|
|
721
|
-
let state = localLimiterState.get(key);
|
|
722
|
-
|
|
723
|
-
if (!state || state.reset <= now) {
|
|
724
|
-
state = {
|
|
725
|
-
count: 0,
|
|
726
|
-
reset: now + durationMs,
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
if (state.count >= config.limit) {
|
|
731
|
-
localLimiterState.set(key, state);
|
|
732
|
-
return {
|
|
733
|
-
limit: config.limit,
|
|
734
|
-
remaining: 0,
|
|
735
|
-
reset: state.reset,
|
|
736
|
-
success: false,
|
|
737
|
-
};
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
state.count += 1;
|
|
741
|
-
localLimiterState.set(key, state);
|
|
742
|
-
|
|
743
|
-
return {
|
|
744
|
-
limit: config.limit,
|
|
745
|
-
remaining: Math.max(0, config.limit - state.count),
|
|
746
|
-
reset: state.reset,
|
|
747
|
-
success: true,
|
|
748
|
-
};
|
|
749
|
-
},
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function createLocalRateLimitBuckets(
|
|
754
|
-
prefixBase: string,
|
|
755
|
-
kind: 'get' | 'mutate',
|
|
756
|
-
configs: RateLimitConfig[]
|
|
757
|
-
): RateLimitBucket[] {
|
|
758
|
-
return configs.map((config) => ({
|
|
759
|
-
window: config.window,
|
|
760
|
-
limiter: createLocalRateLimiter(
|
|
761
|
-
`${prefixBase}:local:${kind}:${config.window}`,
|
|
762
|
-
config
|
|
763
|
-
),
|
|
764
|
-
}));
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function createLocalRateLimiters(
|
|
768
|
-
prefixBase: string,
|
|
769
|
-
profile: RateLimitProfile
|
|
770
|
-
): Limiters {
|
|
771
|
-
return {
|
|
772
|
-
get: createLocalRateLimitBuckets(prefixBase, 'get', profile.get),
|
|
773
|
-
mutate: createLocalRateLimitBuckets(prefixBase, 'mutate', profile.mutate),
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
|
|
777
694
|
async function getRateLimiters(
|
|
778
695
|
prefixBase: string,
|
|
779
696
|
profile: RateLimitProfile
|
|
780
|
-
): Promise<Limiters> {
|
|
697
|
+
): Promise<Limiters | null> {
|
|
781
698
|
const cacheKey = `${prefixBase}:${JSON.stringify(profile)}`;
|
|
782
699
|
const cached = limiterCache.get(cacheKey);
|
|
783
700
|
if (cached) {
|
|
@@ -786,9 +703,7 @@ async function getRateLimiters(
|
|
|
786
703
|
|
|
787
704
|
const redis = await getUpstashRatelimitRedisClient().catch(() => null);
|
|
788
705
|
if (!redis) {
|
|
789
|
-
|
|
790
|
-
limiterCache.set(cacheKey, localLimiters);
|
|
791
|
-
return localLimiters;
|
|
706
|
+
return null;
|
|
792
707
|
}
|
|
793
708
|
|
|
794
709
|
const limiters = {
|
|
@@ -805,16 +720,6 @@ async function getRateLimiters(
|
|
|
805
720
|
return limiters;
|
|
806
721
|
}
|
|
807
722
|
|
|
808
|
-
function replaceWithLocalRateLimiters(
|
|
809
|
-
cacheKey: string,
|
|
810
|
-
prefixBase: string,
|
|
811
|
-
profile: RateLimitProfile
|
|
812
|
-
): Limiters {
|
|
813
|
-
const localLimiters = createLocalRateLimiters(prefixBase, profile);
|
|
814
|
-
limiterCache.set(cacheKey, localLimiters);
|
|
815
|
-
return localLimiters;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
723
|
function getRoutePolicy(
|
|
819
724
|
req: NextRequest,
|
|
820
725
|
routePolicies: ProxyRoutePolicy[]
|
|
@@ -1242,7 +1147,6 @@ export function isTrustedProxyBypassRequest(
|
|
|
1242
1147
|
|
|
1243
1148
|
export function clearApiProxyGuardLimiterCache() {
|
|
1244
1149
|
limiterCache.clear();
|
|
1245
|
-
localLimiterState.clear();
|
|
1246
1150
|
}
|
|
1247
1151
|
|
|
1248
1152
|
async function requestBodyExceedsLimit(
|
|
@@ -1418,32 +1322,26 @@ export async function guardApiProxyRequest(
|
|
|
1418
1322
|
);
|
|
1419
1323
|
const limiterCacheKey = `${limiterPrefix}:${JSON.stringify(rateLimits)}`;
|
|
1420
1324
|
const limiters = await getRateLimiters(limiterPrefix, rateLimits);
|
|
1421
|
-
|
|
1325
|
+
const activeLimiters = limiters
|
|
1326
|
+
? isRead
|
|
1327
|
+
? limiters.get
|
|
1328
|
+
: limiters.mutate
|
|
1329
|
+
: [];
|
|
1422
1330
|
|
|
1423
1331
|
for (let index = 0; index < activeLimiters.length; index += 1) {
|
|
1424
1332
|
const { limiter, window } = activeLimiters[index]!;
|
|
1425
|
-
let result: RateLimitResult;
|
|
1333
|
+
let result: RateLimitResult | null | undefined;
|
|
1426
1334
|
|
|
1427
1335
|
try {
|
|
1428
1336
|
result = await limiter.limit(limiterId);
|
|
1429
1337
|
} catch {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
: fallbackLimiters.mutate;
|
|
1438
|
-
const fallbackBucket =
|
|
1439
|
-
activeLimiters[index] ??
|
|
1440
|
-
activeLimiters.find((bucket) => bucket.window === window);
|
|
1441
|
-
|
|
1442
|
-
if (!fallbackBucket) {
|
|
1443
|
-
continue;
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
result = await fallbackBucket.limiter.limit(limiterId);
|
|
1338
|
+
limiterCache.delete(limiterCacheKey);
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (!result) {
|
|
1343
|
+
limiterCache.delete(limiterCacheKey);
|
|
1344
|
+
break;
|
|
1447
1345
|
}
|
|
1448
1346
|
|
|
1449
1347
|
const { success, limit, remaining, reset } = result;
|