@upstash/ratelimit 1.2.0 → 1.2.1

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/dist/index.mjs CHANGED
@@ -1,3 +1,9 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
1
7
  // src/analytics.ts
2
8
  import { Analytics as CoreAnalytics } from "@upstash/core-analytics";
3
9
  var Analytics = class {
@@ -266,7 +272,107 @@ var resetScript = `
266
272
  until cursor == "0"
267
273
  `;
268
274
 
269
- // src/deny-list.ts
275
+ // src/types.ts
276
+ var DenyListExtension = "denyList";
277
+ var IpDenyListKey = "ipDenyList";
278
+ var IpDenyListStatusKey = "ipDenyListStatus";
279
+
280
+ // src/deny-list/scripts.ts
281
+ var checkDenyListScript = `
282
+ -- Checks if values provideed in ARGV are present in the deny lists.
283
+ -- This is done using the allDenyListsKey below.
284
+
285
+ -- Additionally, checks the status of the ip deny list using the
286
+ -- ipDenyListStatusKey below. Here are the possible states of the
287
+ -- ipDenyListStatusKey key:
288
+ -- * status == -1: set to "disabled" with no TTL
289
+ -- * status == -2: not set, meaning that is was set before but expired
290
+ -- * status > 0: set to "valid", with a TTL
291
+ --
292
+ -- In the case of status == -2, we set the status to "pending" with
293
+ -- 30 second ttl. During this time, the process which got status == -2
294
+ -- will update the ip deny list.
295
+
296
+ local allDenyListsKey = KEYS[1]
297
+ local ipDenyListStatusKey = KEYS[2]
298
+
299
+ local results = redis.call('SMISMEMBER', allDenyListsKey, unpack(ARGV))
300
+ local status = redis.call('TTL', ipDenyListStatusKey)
301
+ if status == -2 then
302
+ redis.call('SETEX', ipDenyListStatusKey, 30, "pending")
303
+ end
304
+
305
+ return { results, status }
306
+ `;
307
+
308
+ // src/deny-list/ip-deny-list.ts
309
+ var ip_deny_list_exports = {};
310
+ __export(ip_deny_list_exports, {
311
+ ThresholdError: () => ThresholdError,
312
+ disableIpDenyList: () => disableIpDenyList,
313
+ updateIpDenyList: () => updateIpDenyList
314
+ });
315
+
316
+ // src/deny-list/time.ts
317
+ var MILLISECONDS_IN_HOUR = 60 * 60 * 1e3;
318
+ var MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR;
319
+ var MILLISECONDS_TO_2AM = 2 * MILLISECONDS_IN_HOUR;
320
+ var getIpListTTL = (time) => {
321
+ const now = time || Date.now();
322
+ const timeSinceLast2AM = (now - MILLISECONDS_TO_2AM) % MILLISECONDS_IN_DAY;
323
+ return MILLISECONDS_IN_DAY - timeSinceLast2AM;
324
+ };
325
+
326
+ // src/deny-list/ip-deny-list.ts
327
+ var baseUrl = "https://raw.githubusercontent.com/stamparm/ipsum/master/levels";
328
+ var ThresholdError = class extends Error {
329
+ constructor(threshold) {
330
+ super(`Allowed threshold values are from 1 to 8, 1 and 8 included. Received: ${threshold}`);
331
+ this.name = "ThresholdError";
332
+ }
333
+ };
334
+ var getIpDenyList = async (threshold) => {
335
+ if (typeof threshold !== "number" || threshold < 1 || threshold > 8) {
336
+ throw new ThresholdError(threshold);
337
+ }
338
+ try {
339
+ const response = await fetch(`${baseUrl}/${threshold}.txt`);
340
+ if (!response.ok) {
341
+ throw new Error(`Error fetching data: ${response.statusText}`);
342
+ }
343
+ const data = await response.text();
344
+ const lines = data.split("\n");
345
+ return lines.filter((value) => value.length > 0);
346
+ } catch (error) {
347
+ throw new Error(`Failed to fetch ip deny list: ${error}`);
348
+ }
349
+ };
350
+ var updateIpDenyList = async (redis, prefix, threshold, ttl) => {
351
+ const allIps = await getIpDenyList(threshold);
352
+ const allDenyLists = [prefix, DenyListExtension, "all"].join(":");
353
+ const ipDenyList = [prefix, DenyListExtension, IpDenyListKey].join(":");
354
+ const statusKey = [prefix, IpDenyListStatusKey].join(":");
355
+ const transaction = redis.multi();
356
+ transaction.sdiffstore(allDenyLists, allDenyLists, ipDenyList);
357
+ transaction.del(ipDenyList);
358
+ transaction.sadd(ipDenyList, ...allIps);
359
+ transaction.sdiffstore(ipDenyList, ipDenyList, allDenyLists);
360
+ transaction.sunionstore(allDenyLists, allDenyLists, ipDenyList);
361
+ transaction.set(statusKey, "valid", { px: ttl ?? getIpListTTL() });
362
+ return await transaction.exec();
363
+ };
364
+ var disableIpDenyList = async (redis, prefix) => {
365
+ const allDenyListsKey = [prefix, DenyListExtension, "all"].join(":");
366
+ const ipDenyListKey = [prefix, DenyListExtension, IpDenyListKey].join(":");
367
+ const statusKey = [prefix, IpDenyListStatusKey].join(":");
368
+ const transaction = redis.multi();
369
+ transaction.sdiffstore(allDenyListsKey, allDenyListsKey, ipDenyListKey);
370
+ transaction.del(ipDenyListKey);
371
+ transaction.set(statusKey, "disabled");
372
+ return await transaction.exec();
373
+ };
374
+
375
+ // src/deny-list/deny-list.ts
270
376
  var denyListCache = new Cache(/* @__PURE__ */ new Map());
271
377
  var checkDenyListCache = (members) => {
272
378
  return members.find(
@@ -279,25 +385,39 @@ var blockMember = (member) => {
279
385
  denyListCache.blockUntil(member, Date.now() + 6e4);
280
386
  };
281
387
  var checkDenyList = async (redis, prefix, members) => {
282
- const deniedMembers = await redis.smismember(
283
- [prefix, "denyList", "all"].join(":"),
388
+ const [deniedValues, ipDenyListStatus] = await redis.eval(
389
+ checkDenyListScript,
390
+ [
391
+ [prefix, DenyListExtension, "all"].join(":"),
392
+ [prefix, IpDenyListStatusKey].join(":")
393
+ ],
284
394
  members
285
395
  );
286
- let deniedMember = void 0;
287
- deniedMembers.map((memberDenied, index) => {
396
+ let deniedValue = void 0;
397
+ deniedValues.map((memberDenied, index) => {
288
398
  if (memberDenied) {
289
399
  blockMember(members[index]);
290
- deniedMember = members[index];
400
+ deniedValue = members[index];
291
401
  }
292
402
  });
293
- return deniedMember;
403
+ return {
404
+ deniedValue,
405
+ invalidIpDenyList: ipDenyListStatus === -2
406
+ };
294
407
  };
295
- var resolveResponses = ([ratelimitResponse, denyListResponse]) => {
296
- if (denyListResponse) {
408
+ var resolveLimitPayload = (redis, prefix, [ratelimitResponse, denyListResponse], threshold) => {
409
+ if (denyListResponse.deniedValue) {
297
410
  ratelimitResponse.success = false;
298
411
  ratelimitResponse.remaining = 0;
299
412
  ratelimitResponse.reason = "denyList";
300
- ratelimitResponse.deniedValue = denyListResponse;
413
+ ratelimitResponse.deniedValue = denyListResponse.deniedValue;
414
+ }
415
+ if (denyListResponse.invalidIpDenyList) {
416
+ const updatePromise = updateIpDenyList(redis, prefix, threshold);
417
+ ratelimitResponse.pending = Promise.all([
418
+ ratelimitResponse.pending,
419
+ updatePromise
420
+ ]);
301
421
  }
302
422
  return ratelimitResponse;
303
423
  };
@@ -322,12 +442,14 @@ var Ratelimit = class {
322
442
  primaryRedis;
323
443
  analytics;
324
444
  enableProtection;
445
+ denyListThreshold;
325
446
  constructor(config) {
326
447
  this.ctx = config.ctx;
327
448
  this.limiter = config.limiter;
328
449
  this.timeout = config.timeout ?? 5e3;
329
450
  this.prefix = config.prefix ?? "@upstash/ratelimit";
330
451
  this.enableProtection = config.enableProtection ?? false;
452
+ this.denyListThreshold = config.denyListThreshold ?? 6;
331
453
  this.primaryRedis = "redis" in this.ctx ? this.ctx.redis : this.ctx.regionContexts[0].redis;
332
454
  this.analytics = config.analytics ? new Analytics({
333
455
  redis: this.primaryRedis,
@@ -457,21 +579,17 @@ var Ratelimit = class {
457
579
  getRatelimitResponse = async (identifier, req) => {
458
580
  const key = this.getKey(identifier);
459
581
  const definedMembers = this.getDefinedMembers(identifier, req);
460
- const deniedMember = checkDenyListCache(definedMembers);
582
+ const deniedValue = checkDenyListCache(definedMembers);
461
583
  let result;
462
- if (deniedMember) {
463
- result = [defaultDeniedResponse(deniedMember), deniedMember];
584
+ if (deniedValue) {
585
+ result = [defaultDeniedResponse(deniedValue), { deniedValue, invalidIpDenyList: false }];
464
586
  } else {
465
587
  result = await Promise.all([
466
588
  this.limiter().limit(this.ctx, key, req?.rate),
467
- checkDenyList(
468
- this.primaryRedis,
469
- this.prefix,
470
- definedMembers
471
- )
589
+ this.enableProtection ? checkDenyList(this.primaryRedis, this.prefix, definedMembers) : { deniedValue: void 0, invalidIpDenyList: false }
472
590
  ]);
473
591
  }
474
- return resolveResponses(result);
592
+ return resolveLimitPayload(this.primaryRedis, this.prefix, result, this.denyListThreshold);
475
593
  };
476
594
  /**
477
595
  * Creates an array with the original response promise and a timeout promise
@@ -1081,7 +1199,8 @@ var RegionRatelimit = class extends Ratelimit {
1081
1199
  cacheScripts: config.cacheScripts ?? true
1082
1200
  },
1083
1201
  ephemeralCache: config.ephemeralCache,
1084
- enableProtection: config.enableProtection
1202
+ enableProtection: config.enableProtection,
1203
+ denyListThreshold: config.denyListThreshold
1085
1204
  });
1086
1205
  }
1087
1206
  /**
@@ -1447,6 +1566,7 @@ var RegionRatelimit = class extends Ratelimit {
1447
1566
  };
1448
1567
  export {
1449
1568
  Analytics,
1569
+ ip_deny_list_exports as IpDenyList,
1450
1570
  MultiRegionRatelimit,
1451
1571
  RegionRatelimit as Ratelimit
1452
1572
  };