@upstash/ratelimit 1.1.2 → 1.2.0-canary

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Upstash Rate Limit
2
2
 
3
- [![Tests](https://github.com/upstash/ratelimit/actions/workflows/tests.yaml/badge.svg)](https://github.com/upstash/ratelimit/actions/workflows/tests.yaml)
4
3
  [![npm (scoped)](https://img.shields.io/npm/v/@upstash/ratelimit)](https://www.npmjs.com/package/@upstash/ratelimit)
4
+ [![Tests](https://github.com/upstash/ratelimit/actions/workflows/tests.yaml/badge.svg)](https://github.com/upstash/ratelimit/actions/workflows/tests.yaml)
5
5
 
6
6
  > [!NOTE] > **This project is in GA Stage.**
7
7
  > The Upstash Professional Support fully covers this project. It receives regular updates, and bug fixes. The Upstash team is committed to maintaining and improving its functionality.
@@ -69,84 +69,13 @@ doExpensiveCalculation();
69
69
  return "Here you go!";
70
70
  ```
71
71
 
72
- For Cloudflare Workers and Fastly Compute@Edge, you can use the following imports:
73
-
74
- ```ts
75
- import { Redis } from "@upstash/redis/cloudflare"; // for cloudflare workers and pages
76
- import { Redis } from "@upstash/redis/fastly"; // for fastly compute@edge
77
- ```
72
+ For more information on getting started, you can refer to [our documentation](https://upstash.com/docs/oss/sdks/ts/ratelimit/gettingstarted).
78
73
 
79
74
  [Here's a complete nextjs example](https://github.com/upstash/ratelimit/tree/main/examples/nextjs)
80
75
 
81
- The `limit` method returns some more metadata that might be useful to you:
82
-
83
- ````ts
84
- export type RatelimitResponse = {
85
- /**
86
- * Whether the request may pass(true) or exceeded the limit(false)
87
- */
88
- success: boolean;
89
- /**
90
- * Maximum number of requests allowed within a window.
91
- */
92
- limit: number;
93
- /**
94
- * How many requests the user has left within the current window.
95
- */
96
- remaining: number;
97
- /**
98
- * Unix timestamp in milliseconds when the limits are reset.
99
- */
100
- reset: number;
76
+ ## Documentation
101
77
 
102
- /**
103
- * For the MultiRegion setup we do some synchronizing in the background, after returning the current limit.
104
- * Or when analytics is enabled, we send the analytics asynchronously after returning the limit.
105
- * In most case you can simply ignore this.
106
- *
107
- * On Vercel Edge or Cloudflare workers, you need to explicitly handle the pending Promise like this:
108
- *
109
- * ```ts
110
- * const { pending } = await ratelimit.limit("id")
111
- * context.waitUntil(pending)
112
- * ```
113
- *
114
- * See `waitUntil` documentation in
115
- * [Cloudflare](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/#contextwaituntil)
116
- * and [Vercel](https://vercel.com/docs/functions/edge-middleware/middleware-api#waituntil)
117
- * for more details.
118
- * ```
119
- */
120
- pending: Promise<unknown>;
121
- };
122
- ````
123
-
124
- ### Docs
125
-
126
- See [the documentation](https://upstash.com/docs/oss/sdks/ts/ratelimit/overview) for more information details.
127
-
128
-
129
- ### Using with CloudFlare Workers and Vercel Edge
130
-
131
- When we use CloudFlare Workers and Vercel Edge, we need to be careful about
132
- making sure that the rate limiting operations complete correctly before the runtime ends
133
- after returning the response.
134
-
135
- This is important in two cases where we do some operations in the backgroung asynchronously after `limit` is called:
136
-
137
- 1. Using MultiRegion: synchronize Redis instances in different regions
138
- 2. Enabling analytics: send analytics to Redis
139
-
140
- In these cases, we need to wait for these operations to finish before sending the response to the user. Otherwise, the runtime will end and we won't be able to complete our chores.
141
-
142
- In order to wait for these operations to finish, use the `pending` promise:
143
-
144
- ```ts
145
- const { pending } = await ratelimit.limit("id");
146
- context.waitUntil(pending);
147
- ```
148
-
149
- See `waitUntil` documentation in [Cloudflare](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/#contextwaituntil) and [Vercel](https://vercel.com/docs/functions/edge-middleware/middleware-api#waituntil) for more details.
78
+ See [the documentation](https://upstash.com/docs/oss/sdks/ts/ratelimit/overview) for more information details about this package.
150
79
 
151
80
  ## Contributing
152
81
 
@@ -157,6 +86,20 @@ the url and token.
157
86
 
158
87
  ### Running tests
159
88
 
89
+ To run the tests, you will need to set some environment variables. Here is a list of
90
+ variables to set:
91
+ - `UPSTASH_REDIS_REST_URL`
92
+ - `UPSTASH_REDIS_REST_TOKEN`
93
+ - `US1_UPSTASH_REDIS_REST_URL`
94
+ - `US1_UPSTASH_REDIS_REST_TOKEN`
95
+ - `APN_UPSTASH_REDIS_REST_URL`
96
+ - `APN_UPSTASH_REDIS_REST_TOKEN`
97
+ - `EU2_UPSTASH_REDIS_REST_URL`
98
+ - `EU2_UPSTASH_REDIS_REST_TOKEN`
99
+
100
+ You can create a single Upstash Redis and use its URL and token for all four above.
101
+
102
+ Once you set the environment variables, simply run:
160
103
  ```sh
161
- UPSTASH_REDIS_REST_URL=".." UPSTASH_REDIS_REST_TOKEN=".." pnpm test
104
+ pnpm test
162
105
  ```
package/dist/index.d.mts CHANGED
@@ -14,15 +14,23 @@ interface EphemeralCache {
14
14
  incr: (key: string) => number;
15
15
  pop: (key: string) => void;
16
16
  empty: () => void;
17
+ size: () => number;
17
18
  }
18
19
  type RegionContext = {
19
20
  redis: Redis;
20
21
  cache?: EphemeralCache;
22
+ scriptHashes: {
23
+ limitHash?: string;
24
+ getRemainingHash?: string;
25
+ resetHash?: string;
26
+ };
27
+ cacheScripts: boolean;
21
28
  };
22
29
  type MultiRegionContext = {
23
- redis: Redis[];
30
+ regionContexts: Omit<RegionContext[], "cache">;
24
31
  cache?: EphemeralCache;
25
32
  };
33
+ type RatelimitResponseType = "timeout" | "cacheBlock" | "denyList";
26
34
  type Context = RegionContext | MultiRegionContext;
27
35
  type RatelimitResponse = {
28
36
  /**
@@ -60,13 +68,37 @@ type RatelimitResponse = {
60
68
  * ```
61
69
  */
62
70
  pending: Promise<unknown>;
71
+ /**
72
+ * Reason behind the result in `success` field.
73
+ * - Is set to "timeout" when request times out
74
+ * - Is set to "cacheBlock" when an identifier is blocked through cache without calling redis because it was
75
+ * rate limited previously.
76
+ * - Is set to "denyList" when identifier or one of ip/user-agent/country parameters is in deny list. To enable
77
+ * deny list, see `enableProtection` parameter. To edit the deny list, see the Upstash Ratelimit Dashboard
78
+ * at https://console.upstash.com/ratelimit.
79
+ * - Is set to undefined if rate limit check had to use Redis. This happens in cases when `success` field in
80
+ * the response is true. It can also happen the first time sucecss is false.
81
+ */
82
+ reason?: RatelimitResponseType;
83
+ /**
84
+ * The value which was in the deny list if reason: "denyList"
85
+ */
86
+ deniedValue?: string;
63
87
  };
64
88
  type Algorithm<TContext> = () => {
65
89
  limit: (ctx: TContext, identifier: string, rate?: number, opts?: {
66
90
  cache?: EphemeralCache;
67
91
  }) => Promise<RatelimitResponse>;
68
92
  getRemaining: (ctx: TContext, identifier: string) => Promise<number>;
69
- resetTokens: (ctx: TContext, identifier: string) => void;
93
+ resetTokens: (ctx: TContext, identifier: string) => Promise<void>;
94
+ };
95
+ type IsDenied = 0 | 1;
96
+ type LimitOptions = {
97
+ geo?: Geo;
98
+ rate?: number;
99
+ ip?: string;
100
+ userAgent?: string;
101
+ country?: string;
70
102
  };
71
103
  /**
72
104
  * This is all we need from the redis sdk.
@@ -77,6 +109,9 @@ interface Redis {
77
109
  [key: string]: TValue;
78
110
  }) => Promise<number>;
79
111
  eval: <TArgs extends unknown[], TData = unknown>(...args: [script: string, keys: string[], args: TArgs]) => Promise<TData>;
112
+ evalsha: <TArgs extends unknown[], TData = unknown>(...args: [sha1: string, keys: string[], args: TArgs]) => Promise<TData>;
113
+ scriptLoad: (...args: [script: string]) => Promise<string>;
114
+ smismember: (key: string, members: string[]) => Promise<IsDenied[]>;
80
115
  }
81
116
 
82
117
  type Geo = {
@@ -85,10 +120,16 @@ type Geo = {
85
120
  region?: string;
86
121
  ip?: string;
87
122
  };
123
+ /**
124
+ * denotes the success field in the analytics submission.
125
+ * Set to true when ratelimit check passes. False when request is ratelimited.
126
+ * Set to "denied" when some request value is in deny list.
127
+ */
128
+ type EventSuccess = boolean | "denied";
88
129
  type Event = Geo & {
89
130
  identifier: string;
90
131
  time: number;
91
- success: boolean;
132
+ success: EventSuccess;
92
133
  };
93
134
  type AnalyticsConfig = {
94
135
  redis: Redis;
@@ -124,7 +165,11 @@ declare class Analytics {
124
165
  identifier: string;
125
166
  count: number;
126
167
  }[];
127
- blocked: {
168
+ ratelimited: {
169
+ identifier: string;
170
+ count: number;
171
+ }[];
172
+ denied: {
128
173
  identifier: string;
129
174
  count: number;
130
175
  }[];
@@ -184,6 +229,14 @@ type RatelimitConfig<TContext> = {
184
229
  * @default false
185
230
  */
186
231
  analytics?: boolean;
232
+ /**
233
+ * Enables deny list. If set to true, requests with identifier or ip/user-agent/countrie
234
+ * in the deny list will be rejected automatically. To edit the deny list, check out the
235
+ * ratelimit dashboard at https://console.upstash.com/ratelimit
236
+ *
237
+ * @default false
238
+ */
239
+ enableProtection?: boolean;
187
240
  };
188
241
  /**
189
242
  * Ratelimiter using serverless redis from https://upstash.com/
@@ -205,7 +258,9 @@ declare abstract class Ratelimit<TContext extends Context> {
205
258
  protected readonly ctx: TContext;
206
259
  protected readonly prefix: string;
207
260
  protected readonly timeout: number;
261
+ protected readonly primaryRedis: Redis;
208
262
  protected readonly analytics?: Analytics;
263
+ protected readonly enableProtection: boolean;
209
264
  constructor(config: RatelimitConfig<TContext>);
210
265
  /**
211
266
  * Determine if a request should pass or be rejected based on the identifier and previously chosen ratelimit.
@@ -243,10 +298,7 @@ declare abstract class Ratelimit<TContext extends Context> {
243
298
  * return "Yes"
244
299
  * ```
245
300
  */
246
- limit: (identifier: string, req?: {
247
- geo?: Geo;
248
- rate?: number;
249
- }) => Promise<RatelimitResponse>;
301
+ limit: (identifier: string, req?: LimitOptions) => Promise<RatelimitResponse>;
250
302
  /**
251
303
  * Block until the request may pass or timeout is reached.
252
304
  *
@@ -272,6 +324,46 @@ declare abstract class Ratelimit<TContext extends Context> {
272
324
  blockUntilReady: (identifier: string, timeout: number) => Promise<RatelimitResponse>;
273
325
  resetUsedTokens: (identifier: string) => Promise<void>;
274
326
  getRemaining: (identifier: string) => Promise<number>;
327
+ /**
328
+ * Checks if the identifier or the values in req are in the deny list cache.
329
+ * If so, returns the default denied response.
330
+ *
331
+ * Otherwise, calls redis to check the rate limit and deny list. Returns after
332
+ * resolving the result. Resolving is overriding the rate limit result if
333
+ * the some value is in deny list.
334
+ *
335
+ * @param identifier identifier to block
336
+ * @param req options with ip, user agent, country, rate and geo info
337
+ * @returns rate limit response
338
+ */
339
+ private getRatelimitResponse;
340
+ /**
341
+ * Creates an array with the original response promise and a timeout promise
342
+ * if this.timeout > 0.
343
+ *
344
+ * @param response Ratelimit response promise
345
+ * @returns array with the response and timeout promise. also includes the timeout id
346
+ */
347
+ private applyTimeout;
348
+ /**
349
+ * submits analytics if this.analytics is set
350
+ *
351
+ * @param ratelimitResponse final rate limit response
352
+ * @param identifier identifier to submit
353
+ * @param req limit options
354
+ * @returns rate limit response after updating the .pending field
355
+ */
356
+ private submitAnalytics;
357
+ private getKey;
358
+ /**
359
+ * returns a list of defined values from
360
+ * [identifier, req.ip, req.userAgent, req.country]
361
+ *
362
+ * @param identifier identifier
363
+ * @param req limit options
364
+ * @returns list of defined values
365
+ */
366
+ private getDefinedMembers;
275
367
  }
276
368
 
277
369
  type MultiRegionRatelimitConfig = {
@@ -324,6 +416,13 @@ type MultiRegionRatelimitConfig = {
324
416
  * @default false
325
417
  */
326
418
  analytics?: boolean;
419
+ /**
420
+ * If enabled, lua scripts will be sent to Redis with SCRIPT LOAD durint the first request.
421
+ * In the subsequent requests, hash of the script will be used to invoke it
422
+ *
423
+ * @default true
424
+ */
425
+ cacheScripts?: boolean;
327
426
  };
328
427
  /**
329
428
  * Ratelimiter using serverless redis from https://upstash.com/
@@ -451,6 +550,17 @@ type RegionRatelimitConfig = {
451
550
  * @default false
452
551
  */
453
552
  analytics?: boolean;
553
+ /**
554
+ * If enabled, lua scripts will be sent to Redis with SCRIPT LOAD durint the first request.
555
+ * In the subsequent requests, hash of the script will be used to invoke it
556
+ *
557
+ * @default true
558
+ */
559
+ cacheScripts?: boolean;
560
+ /**
561
+ * @default false
562
+ */
563
+ enableProtection?: boolean;
454
564
  };
455
565
  /**
456
566
  * Ratelimiter using serverless redis from https://upstash.com/
package/dist/index.d.ts CHANGED
@@ -14,15 +14,23 @@ interface EphemeralCache {
14
14
  incr: (key: string) => number;
15
15
  pop: (key: string) => void;
16
16
  empty: () => void;
17
+ size: () => number;
17
18
  }
18
19
  type RegionContext = {
19
20
  redis: Redis;
20
21
  cache?: EphemeralCache;
22
+ scriptHashes: {
23
+ limitHash?: string;
24
+ getRemainingHash?: string;
25
+ resetHash?: string;
26
+ };
27
+ cacheScripts: boolean;
21
28
  };
22
29
  type MultiRegionContext = {
23
- redis: Redis[];
30
+ regionContexts: Omit<RegionContext[], "cache">;
24
31
  cache?: EphemeralCache;
25
32
  };
33
+ type RatelimitResponseType = "timeout" | "cacheBlock" | "denyList";
26
34
  type Context = RegionContext | MultiRegionContext;
27
35
  type RatelimitResponse = {
28
36
  /**
@@ -60,13 +68,37 @@ type RatelimitResponse = {
60
68
  * ```
61
69
  */
62
70
  pending: Promise<unknown>;
71
+ /**
72
+ * Reason behind the result in `success` field.
73
+ * - Is set to "timeout" when request times out
74
+ * - Is set to "cacheBlock" when an identifier is blocked through cache without calling redis because it was
75
+ * rate limited previously.
76
+ * - Is set to "denyList" when identifier or one of ip/user-agent/country parameters is in deny list. To enable
77
+ * deny list, see `enableProtection` parameter. To edit the deny list, see the Upstash Ratelimit Dashboard
78
+ * at https://console.upstash.com/ratelimit.
79
+ * - Is set to undefined if rate limit check had to use Redis. This happens in cases when `success` field in
80
+ * the response is true. It can also happen the first time sucecss is false.
81
+ */
82
+ reason?: RatelimitResponseType;
83
+ /**
84
+ * The value which was in the deny list if reason: "denyList"
85
+ */
86
+ deniedValue?: string;
63
87
  };
64
88
  type Algorithm<TContext> = () => {
65
89
  limit: (ctx: TContext, identifier: string, rate?: number, opts?: {
66
90
  cache?: EphemeralCache;
67
91
  }) => Promise<RatelimitResponse>;
68
92
  getRemaining: (ctx: TContext, identifier: string) => Promise<number>;
69
- resetTokens: (ctx: TContext, identifier: string) => void;
93
+ resetTokens: (ctx: TContext, identifier: string) => Promise<void>;
94
+ };
95
+ type IsDenied = 0 | 1;
96
+ type LimitOptions = {
97
+ geo?: Geo;
98
+ rate?: number;
99
+ ip?: string;
100
+ userAgent?: string;
101
+ country?: string;
70
102
  };
71
103
  /**
72
104
  * This is all we need from the redis sdk.
@@ -77,6 +109,9 @@ interface Redis {
77
109
  [key: string]: TValue;
78
110
  }) => Promise<number>;
79
111
  eval: <TArgs extends unknown[], TData = unknown>(...args: [script: string, keys: string[], args: TArgs]) => Promise<TData>;
112
+ evalsha: <TArgs extends unknown[], TData = unknown>(...args: [sha1: string, keys: string[], args: TArgs]) => Promise<TData>;
113
+ scriptLoad: (...args: [script: string]) => Promise<string>;
114
+ smismember: (key: string, members: string[]) => Promise<IsDenied[]>;
80
115
  }
81
116
 
82
117
  type Geo = {
@@ -85,10 +120,16 @@ type Geo = {
85
120
  region?: string;
86
121
  ip?: string;
87
122
  };
123
+ /**
124
+ * denotes the success field in the analytics submission.
125
+ * Set to true when ratelimit check passes. False when request is ratelimited.
126
+ * Set to "denied" when some request value is in deny list.
127
+ */
128
+ type EventSuccess = boolean | "denied";
88
129
  type Event = Geo & {
89
130
  identifier: string;
90
131
  time: number;
91
- success: boolean;
132
+ success: EventSuccess;
92
133
  };
93
134
  type AnalyticsConfig = {
94
135
  redis: Redis;
@@ -124,7 +165,11 @@ declare class Analytics {
124
165
  identifier: string;
125
166
  count: number;
126
167
  }[];
127
- blocked: {
168
+ ratelimited: {
169
+ identifier: string;
170
+ count: number;
171
+ }[];
172
+ denied: {
128
173
  identifier: string;
129
174
  count: number;
130
175
  }[];
@@ -184,6 +229,14 @@ type RatelimitConfig<TContext> = {
184
229
  * @default false
185
230
  */
186
231
  analytics?: boolean;
232
+ /**
233
+ * Enables deny list. If set to true, requests with identifier or ip/user-agent/countrie
234
+ * in the deny list will be rejected automatically. To edit the deny list, check out the
235
+ * ratelimit dashboard at https://console.upstash.com/ratelimit
236
+ *
237
+ * @default false
238
+ */
239
+ enableProtection?: boolean;
187
240
  };
188
241
  /**
189
242
  * Ratelimiter using serverless redis from https://upstash.com/
@@ -205,7 +258,9 @@ declare abstract class Ratelimit<TContext extends Context> {
205
258
  protected readonly ctx: TContext;
206
259
  protected readonly prefix: string;
207
260
  protected readonly timeout: number;
261
+ protected readonly primaryRedis: Redis;
208
262
  protected readonly analytics?: Analytics;
263
+ protected readonly enableProtection: boolean;
209
264
  constructor(config: RatelimitConfig<TContext>);
210
265
  /**
211
266
  * Determine if a request should pass or be rejected based on the identifier and previously chosen ratelimit.
@@ -243,10 +298,7 @@ declare abstract class Ratelimit<TContext extends Context> {
243
298
  * return "Yes"
244
299
  * ```
245
300
  */
246
- limit: (identifier: string, req?: {
247
- geo?: Geo;
248
- rate?: number;
249
- }) => Promise<RatelimitResponse>;
301
+ limit: (identifier: string, req?: LimitOptions) => Promise<RatelimitResponse>;
250
302
  /**
251
303
  * Block until the request may pass or timeout is reached.
252
304
  *
@@ -272,6 +324,46 @@ declare abstract class Ratelimit<TContext extends Context> {
272
324
  blockUntilReady: (identifier: string, timeout: number) => Promise<RatelimitResponse>;
273
325
  resetUsedTokens: (identifier: string) => Promise<void>;
274
326
  getRemaining: (identifier: string) => Promise<number>;
327
+ /**
328
+ * Checks if the identifier or the values in req are in the deny list cache.
329
+ * If so, returns the default denied response.
330
+ *
331
+ * Otherwise, calls redis to check the rate limit and deny list. Returns after
332
+ * resolving the result. Resolving is overriding the rate limit result if
333
+ * the some value is in deny list.
334
+ *
335
+ * @param identifier identifier to block
336
+ * @param req options with ip, user agent, country, rate and geo info
337
+ * @returns rate limit response
338
+ */
339
+ private getRatelimitResponse;
340
+ /**
341
+ * Creates an array with the original response promise and a timeout promise
342
+ * if this.timeout > 0.
343
+ *
344
+ * @param response Ratelimit response promise
345
+ * @returns array with the response and timeout promise. also includes the timeout id
346
+ */
347
+ private applyTimeout;
348
+ /**
349
+ * submits analytics if this.analytics is set
350
+ *
351
+ * @param ratelimitResponse final rate limit response
352
+ * @param identifier identifier to submit
353
+ * @param req limit options
354
+ * @returns rate limit response after updating the .pending field
355
+ */
356
+ private submitAnalytics;
357
+ private getKey;
358
+ /**
359
+ * returns a list of defined values from
360
+ * [identifier, req.ip, req.userAgent, req.country]
361
+ *
362
+ * @param identifier identifier
363
+ * @param req limit options
364
+ * @returns list of defined values
365
+ */
366
+ private getDefinedMembers;
275
367
  }
276
368
 
277
369
  type MultiRegionRatelimitConfig = {
@@ -324,6 +416,13 @@ type MultiRegionRatelimitConfig = {
324
416
  * @default false
325
417
  */
326
418
  analytics?: boolean;
419
+ /**
420
+ * If enabled, lua scripts will be sent to Redis with SCRIPT LOAD durint the first request.
421
+ * In the subsequent requests, hash of the script will be used to invoke it
422
+ *
423
+ * @default true
424
+ */
425
+ cacheScripts?: boolean;
327
426
  };
328
427
  /**
329
428
  * Ratelimiter using serverless redis from https://upstash.com/
@@ -451,6 +550,17 @@ type RegionRatelimitConfig = {
451
550
  * @default false
452
551
  */
453
552
  analytics?: boolean;
553
+ /**
554
+ * If enabled, lua scripts will be sent to Redis with SCRIPT LOAD durint the first request.
555
+ * In the subsequent requests, hash of the script will be used to invoke it
556
+ *
557
+ * @default true
558
+ */
559
+ cacheScripts?: boolean;
560
+ /**
561
+ * @default false
562
+ */
563
+ enableProtection?: boolean;
454
564
  };
455
565
  /**
456
566
  * Ratelimiter using serverless redis from https://upstash.com/