@beignet/provider-rate-limit-upstash 0.0.2 → 0.0.4
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 +49 -0
- package/README.md +47 -18
- package/dist/index.d.ts +38 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +59 -33
- package/dist/index.js.map +1 -1
- package/package.json +37 -3
- package/src/index.ts +104 -43
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# @beignet/provider-rate-limit-upstash
|
|
2
2
|
|
|
3
|
+
## 0.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8bcb31f: Mark package READMEs with Beignet's experimental alpha status and 0.0.x stability expectations.
|
|
8
|
+
- d137044: Declare `@beignet/core` as a peer dependency with a lockstep version range in
|
|
9
|
+
every integration and provider package instead of a regular `"*"` dependency.
|
|
10
|
+
Installs now always resolve a single shared copy of core, so `instanceof`
|
|
11
|
+
checks such as `isContractError` and upload error identity keep working, and
|
|
12
|
+
mixed Beignet versions fail loudly at install time instead of at runtime.
|
|
13
|
+
|
|
14
|
+
If your package manager does not install peer dependencies automatically, add
|
|
15
|
+
`@beignet/core` to your app alongside these packages. `@beignet/nuqs` now also
|
|
16
|
+
declares `@beignet/react-query` as a peer dependency, and
|
|
17
|
+
`@beignet/provider-storage-s3` now expects you to install
|
|
18
|
+
`@aws-sdk/client-s3` and `@aws-sdk/s3-request-presigner` yourself, matching
|
|
19
|
+
how other providers treat their SDKs.
|
|
20
|
+
|
|
21
|
+
- 603478f: Align package documentation with the canonical route registry and AppContext conventions.
|
|
22
|
+
- 1a79090: Emit Node-compatible ESM: all relative imports in published packages now carry explicit .js extensions, fixing ERR_MODULE_NOT_FOUND when running the CLI or importing package dist files under plain Node.
|
|
23
|
+
- 44f1192: Move first-party provider diagnostics to package-owned `beignet.provider`
|
|
24
|
+
manifest metadata and have doctor read installed provider package manifests.
|
|
25
|
+
- 2aa77ca: Add static provider metadata and provider wiring diagnostics for generated apps.
|
|
26
|
+
- 2da5a05: Add an `UPSTASH_ALGORITHM` option that selects between `fixed-window` (the
|
|
27
|
+
default, unchanged behavior) and `sliding-window` rate limiting. Note that
|
|
28
|
+
switching algorithms changes how counters are keyed in Redis, so in-flight
|
|
29
|
+
windows effectively reset when the algorithm changes. The provider now also
|
|
30
|
+
caches one `Ratelimit` instance per `(limit, windowSec, algorithm)` combination
|
|
31
|
+
instead of constructing a new instance on every `hit()` call.
|
|
32
|
+
- 69b8c35: Rename the default rate limit key prefix from `ck:ratelimit` to `beignet:ratelimit`.
|
|
33
|
+
|
|
34
|
+
This changes the Redis key names the provider writes when `UPSTASH_PREFIX` is not set: after upgrading, in-flight rate limit windows stored under the old prefix are abandoned and counting restarts under the new prefix. Set `UPSTASH_PREFIX=ck:ratelimit` to keep the previous keys. `UPSTASH_PREFIX` is now also listed in the provider's env metadata.
|
|
35
|
+
|
|
36
|
+
- 89390fe: Move the raw Upstash Redis client escape hatch to a separate port. The
|
|
37
|
+
provider now contributes `{ rateLimit: RateLimitPort, upstash: { client } }`.
|
|
38
|
+
`UpstashRateLimitPort` and `CreateUpstashRateLimitPortOptions` are replaced by
|
|
39
|
+
the exported `UpstashRateLimitProviderPorts` interface; access the raw client
|
|
40
|
+
through `ctx.ports.upstash.client` instead of casting `ctx.ports.rateLimit`.
|
|
41
|
+
|
|
42
|
+
## 0.0.3
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- Updated dependencies [3160184]
|
|
47
|
+
- Updated dependencies [254ef6d]
|
|
48
|
+
- Updated dependencies [4cb1784]
|
|
49
|
+
- Updated dependencies [8bd9085]
|
|
50
|
+
- @beignet/core@0.0.3
|
|
51
|
+
|
|
3
52
|
## 0.0.2
|
|
4
53
|
|
|
5
54
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @beignet/provider-rate-limit-upstash
|
|
2
2
|
|
|
3
|
+
> [!CAUTION]
|
|
4
|
+
> Beignet is experimental alpha software. The `0.0.x` package line is for early
|
|
5
|
+
> evaluation, and APIs may change between releases while the framework settles.
|
|
6
|
+
|
|
3
7
|
Upstash-backed `RateLimitPort` provider for Beignet applications.
|
|
4
8
|
|
|
5
9
|
The provider installs `ctx.ports.rateLimit` using
|
|
@@ -11,12 +15,13 @@ The provider installs `ctx.ports.rateLimit` using
|
|
|
11
15
|
- Implements the standard `RateLimitPort` interface.
|
|
12
16
|
- Uses the Upstash Redis REST API, so it is serverless-friendly.
|
|
13
17
|
- Supports dynamic limits per request with a configurable key prefix.
|
|
18
|
+
- Supports fixed window and sliding window algorithms via `UPSTASH_ALGORITHM`.
|
|
14
19
|
- Emits devtools events for allowed, blocked, and failed hits.
|
|
15
20
|
|
|
16
21
|
## Install
|
|
17
22
|
|
|
18
23
|
```bash
|
|
19
|
-
bun add @beignet/provider-rate-limit-upstash @upstash/redis @upstash/ratelimit
|
|
24
|
+
bun add @beignet/provider-rate-limit-upstash @beignet/core @upstash/redis @upstash/ratelimit
|
|
20
25
|
```
|
|
21
26
|
|
|
22
27
|
## Configuration
|
|
@@ -27,7 +32,19 @@ Set these environment variables:
|
|
|
27
32
|
|----------|----------|-------------|---------|
|
|
28
33
|
| `UPSTASH_REDIS_REST_URL` | Yes | Your Upstash Redis REST URL | `https://us1-properly-ancient-12345.upstash.io` |
|
|
29
34
|
| `UPSTASH_REDIS_REST_TOKEN` | Yes | Your Upstash Redis REST token | `AXXXeyJpZCI6IjEy...` |
|
|
30
|
-
| `UPSTASH_PREFIX` | No | Key prefix for rate limit keys (default: `
|
|
35
|
+
| `UPSTASH_PREFIX` | No | Key prefix for rate limit keys (default: `beignet:ratelimit`) | `myapp:ratelimit` |
|
|
36
|
+
| `UPSTASH_ALGORITHM` | No | Rate limit algorithm, `fixed-window` or `sliding-window` (default: `fixed-window`) | `sliding-window` |
|
|
37
|
+
|
|
38
|
+
### Choosing an algorithm
|
|
39
|
+
|
|
40
|
+
- `fixed-window` (default) is the cheapest option: one counter per window. It
|
|
41
|
+
can allow short bursts at window boundaries, since a client can spend a full
|
|
42
|
+
limit at the end of one window and again at the start of the next.
|
|
43
|
+
- `sliding-window` smooths those boundary bursts by weighting the previous
|
|
44
|
+
window into the current one, at the cost of slightly more Redis work per hit.
|
|
45
|
+
|
|
46
|
+
Switching algorithms changes how counters are keyed in Redis, so in-flight
|
|
47
|
+
windows effectively reset when you change `UPSTASH_ALGORITHM`.
|
|
31
48
|
|
|
32
49
|
### Getting Upstash credentials
|
|
33
50
|
|
|
@@ -50,7 +67,7 @@ export const server = await createNextServer({
|
|
|
50
67
|
ports: appPorts,
|
|
51
68
|
providers: [upstashRateLimitProvider],
|
|
52
69
|
hooks: [createRateLimitHooks<AppContext>()],
|
|
53
|
-
|
|
70
|
+
context: ({ ports }) => ({ ports }),
|
|
54
71
|
routes,
|
|
55
72
|
});
|
|
56
73
|
```
|
|
@@ -62,7 +79,7 @@ policies, or use cases:
|
|
|
62
79
|
|
|
63
80
|
```ts
|
|
64
81
|
// Example app-specific policy that rate limits by IP address
|
|
65
|
-
async function checkIpRateLimit(ctx:
|
|
82
|
+
async function checkIpRateLimit(ctx: AppContext) {
|
|
66
83
|
const result = await ctx.ports.rateLimit.hit({
|
|
67
84
|
key: `ip:${ctx.ip}`,
|
|
68
85
|
limit: 100,
|
|
@@ -134,7 +151,7 @@ type RateLimitMetadata = {
|
|
|
134
151
|
};
|
|
135
152
|
};
|
|
136
153
|
|
|
137
|
-
async function rateLimitFromMeta(ctx:
|
|
154
|
+
async function rateLimitFromMeta(ctx: AppContext, meta?: RateLimitMetadata) {
|
|
138
155
|
if (!meta?.rateLimit) return;
|
|
139
156
|
|
|
140
157
|
const { max, windowSec, scope = "global" } = meta.rateLimit;
|
|
@@ -176,9 +193,12 @@ interface RateLimitResult {
|
|
|
176
193
|
|
|
177
194
|
## Implementation details
|
|
178
195
|
|
|
179
|
-
- **Algorithm**: Uses
|
|
196
|
+
- **Algorithm**: Uses `Ratelimit.fixedWindow()` by default, or
|
|
197
|
+
`Ratelimit.slidingWindow()` when `UPSTASH_ALGORITHM=sliding-window`
|
|
180
198
|
- **Backend**: Upstash Redis REST API (serverless-compatible)
|
|
181
|
-
- **Per-request configuration**:
|
|
199
|
+
- **Per-request configuration**: Caches one `Ratelimit` instance per
|
|
200
|
+
`(limit, windowSec, algorithm)` combination to support dynamic limits without
|
|
201
|
+
reconstructing limiters on every `hit()` call
|
|
182
202
|
- **Key prefix**: Configurable prefix to avoid key collisions
|
|
183
203
|
|
|
184
204
|
## Devtools
|
|
@@ -187,26 +207,35 @@ When `@beignet/devtools` is installed before this provider, rate limit
|
|
|
187
207
|
checks appear under the dashboard's Rate limits watcher.
|
|
188
208
|
|
|
189
209
|
The provider records `rateLimit.hit` events with the key, limit, window,
|
|
190
|
-
configured prefix, allowed/blocked result, remaining count, reset
|
|
191
|
-
retry-after value, and duration. Provider failures are recorded as
|
|
210
|
+
configured prefix, algorithm, allowed/blocked result, remaining count, reset
|
|
211
|
+
time, retry-after value, and duration. Provider failures are recorded as
|
|
192
212
|
`rateLimit.hit.failed`.
|
|
193
213
|
|
|
194
|
-
##
|
|
195
|
-
|
|
196
|
-
### Access the underlying Redis client
|
|
214
|
+
## Escape hatch
|
|
197
215
|
|
|
198
|
-
The provider
|
|
216
|
+
The provider contributes the standard `rateLimit` port plus `ctx.ports.upstash`
|
|
217
|
+
with the raw Upstash Redis client for operations the stable rate limit port
|
|
218
|
+
does not model:
|
|
199
219
|
|
|
200
220
|
```ts
|
|
201
|
-
|
|
221
|
+
// Access the Redis client for advanced operations
|
|
222
|
+
await ctx.ports.upstash.client.get("some:key");
|
|
223
|
+
await ctx.ports.upstash.client.set("some:key", "value");
|
|
224
|
+
```
|
|
202
225
|
|
|
203
|
-
|
|
226
|
+
To get proper type inference for the contributed ports, extend your ports type
|
|
227
|
+
with `UpstashRateLimitProviderPorts`:
|
|
204
228
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
229
|
+
```ts
|
|
230
|
+
import type { UpstashRateLimitProviderPorts } from "@beignet/provider-rate-limit-upstash";
|
|
231
|
+
|
|
232
|
+
type AppPorts = typeof basePorts & UpstashRateLimitProviderPorts;
|
|
233
|
+
// { rateLimit: RateLimitPort; upstash: { client: Redis } }
|
|
208
234
|
```
|
|
209
235
|
|
|
236
|
+
Use the stable `RateLimitPort` for normal application behavior. Use the raw
|
|
237
|
+
client only when the Upstash-specific operation is intentional.
|
|
238
|
+
|
|
210
239
|
## Testing
|
|
211
240
|
|
|
212
241
|
The provider includes comprehensive tests. Run them with:
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* Configuration:
|
|
7
7
|
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
8
8
|
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
9
|
-
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "
|
|
9
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "beignet:ratelimit")
|
|
10
|
+
* - UPSTASH_ALGORITHM: Optional rate limit algorithm, "fixed-window" or
|
|
11
|
+
* "sliding-window" (default: "fixed-window")
|
|
10
12
|
*
|
|
11
13
|
* @example
|
|
12
14
|
* ```ts
|
|
@@ -32,9 +34,15 @@
|
|
|
32
34
|
* ```
|
|
33
35
|
*/
|
|
34
36
|
import type { RateLimitPort } from "@beignet/core/ports";
|
|
35
|
-
import { type ProviderInstrumentationTarget } from "@beignet/core/providers";
|
|
36
37
|
import { Redis } from "@upstash/redis";
|
|
37
38
|
import { z } from "zod";
|
|
39
|
+
/**
|
|
40
|
+
* Rate limit algorithms supported by the Upstash provider.
|
|
41
|
+
*
|
|
42
|
+
* - "fixed-window": cheaper, but allows bursts at window boundaries.
|
|
43
|
+
* - "sliding-window": smoother enforcement at slightly more Redis work.
|
|
44
|
+
*/
|
|
45
|
+
export type UpstashRateLimitAlgorithm = "fixed-window" | "sliding-window";
|
|
38
46
|
/**
|
|
39
47
|
* Configuration schema for the Upstash Rate Limit provider.
|
|
40
48
|
* Validates environment variables with UPSTASH_ prefix.
|
|
@@ -43,30 +51,37 @@ declare const UpstashRateLimitConfigSchema: z.ZodObject<{
|
|
|
43
51
|
REDIS_REST_URL: z.ZodString;
|
|
44
52
|
REDIS_REST_TOKEN: z.ZodString;
|
|
45
53
|
PREFIX: z.ZodDefault<z.ZodString>;
|
|
54
|
+
ALGORITHM: z.ZodDefault<z.ZodEnum<{
|
|
55
|
+
"fixed-window": "fixed-window";
|
|
56
|
+
"sliding-window": "sliding-window";
|
|
57
|
+
}>>;
|
|
46
58
|
}, z.core.$strip>;
|
|
47
59
|
/**
|
|
48
60
|
* Inferred configuration type for Upstash Rate Limit provider.
|
|
49
61
|
*/
|
|
50
62
|
export type UpstashRateLimitConfig = z.infer<typeof UpstashRateLimitConfigSchema>;
|
|
51
63
|
/**
|
|
52
|
-
*
|
|
53
|
-
* The Upstash provider adds the underlying client for advanced operations.
|
|
64
|
+
* Escape hatch for apps that need the raw Upstash Redis client.
|
|
54
65
|
*/
|
|
55
|
-
export interface
|
|
66
|
+
export interface UpstashRateLimitEscapeHatch {
|
|
56
67
|
/**
|
|
57
|
-
*
|
|
58
|
-
* Use this for
|
|
68
|
+
* Raw Upstash Redis client.
|
|
69
|
+
* Use this for Upstash operations the stable rate limit port does not model.
|
|
59
70
|
*/
|
|
60
71
|
client: Redis;
|
|
61
72
|
}
|
|
62
73
|
/**
|
|
63
|
-
*
|
|
74
|
+
* Ports contributed by the Upstash rate limit provider.
|
|
64
75
|
*/
|
|
65
|
-
export interface
|
|
76
|
+
export interface UpstashRateLimitProviderPorts {
|
|
77
|
+
/**
|
|
78
|
+
* Beignet rate limit port backed by Upstash.
|
|
79
|
+
*/
|
|
80
|
+
rateLimit: RateLimitPort;
|
|
66
81
|
/**
|
|
67
|
-
*
|
|
82
|
+
* Raw Upstash Redis client escape hatch.
|
|
68
83
|
*/
|
|
69
|
-
|
|
84
|
+
upstash: UpstashRateLimitEscapeHatch;
|
|
70
85
|
}
|
|
71
86
|
/**
|
|
72
87
|
* Upstash rate limit provider that extends ports with rate limiting capabilities.
|
|
@@ -74,7 +89,9 @@ export interface CreateUpstashRateLimitPortOptions {
|
|
|
74
89
|
* Configuration via environment variables:
|
|
75
90
|
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
76
91
|
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
77
|
-
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "
|
|
92
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "beignet:ratelimit")
|
|
93
|
+
* - UPSTASH_ALGORITHM: Optional rate limit algorithm, "fixed-window" or
|
|
94
|
+
* "sliding-window" (default: "fixed-window")
|
|
78
95
|
*
|
|
79
96
|
* @example
|
|
80
97
|
* ```ts
|
|
@@ -89,8 +106,15 @@ export declare const upstashRateLimitProvider: import("@beignet/core/providers")
|
|
|
89
106
|
REDIS_REST_URL: z.ZodString;
|
|
90
107
|
REDIS_REST_TOKEN: z.ZodString;
|
|
91
108
|
PREFIX: z.ZodDefault<z.ZodString>;
|
|
109
|
+
ALGORITHM: z.ZodDefault<z.ZodEnum<{
|
|
110
|
+
"fixed-window": "fixed-window";
|
|
111
|
+
"sliding-window": "sliding-window";
|
|
112
|
+
}>>;
|
|
92
113
|
}, z.core.$strip>, {
|
|
93
|
-
rateLimit:
|
|
94
|
-
|
|
114
|
+
rateLimit: RateLimitPort;
|
|
115
|
+
upstash: {
|
|
116
|
+
client: Redis;
|
|
117
|
+
};
|
|
118
|
+
}, unknown, void>;
|
|
95
119
|
export {};
|
|
96
120
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,qBAAqB,CAAC;AAO1E,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AACH,MAAM,MAAM,yBAAyB,GAAG,cAAc,GAAG,gBAAgB,CAAC;AAE1E;;;GAGG;AACH,QAAA,MAAM,4BAA4B;;;;;;;;iBA4BhC,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAC1C,OAAO,4BAA4B,CACpC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,MAAM,EAAE,KAAK,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,6BAA6B;IAC5C;;OAEG;IACH,SAAS,EAAE,aAAa,CAAC;IACzB;;OAEG;IACH,OAAO,EAAE,2BAA2B,CAAC;CACtC;AA6ID;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;iBAiCnC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* Configuration:
|
|
7
7
|
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
8
8
|
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
9
|
-
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "
|
|
9
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "beignet:ratelimit")
|
|
10
|
+
* - UPSTASH_ALGORITHM: Optional rate limit algorithm, "fixed-window" or
|
|
11
|
+
* "sliding-window" (default: "fixed-window")
|
|
10
12
|
*
|
|
11
13
|
* @example
|
|
12
14
|
* ```ts
|
|
@@ -52,46 +54,59 @@ const UpstashRateLimitConfigSchema = z.object({
|
|
|
52
54
|
/**
|
|
53
55
|
* Optional prefix for keys used by this rate limiter.
|
|
54
56
|
* Helps avoid collisions if you reuse the same Redis for multiple things.
|
|
55
|
-
* Defaults to "
|
|
57
|
+
* Defaults to "beignet:ratelimit".
|
|
56
58
|
*/
|
|
57
|
-
PREFIX: z.string().default("
|
|
59
|
+
PREFIX: z.string().default("beignet:ratelimit"),
|
|
60
|
+
/**
|
|
61
|
+
* Optional rate limit algorithm.
|
|
62
|
+
* "fixed-window" is cheaper but bursty at window boundaries.
|
|
63
|
+
* "sliding-window" is smoother at slightly more Redis work.
|
|
64
|
+
* Switching algorithms changes how counters are keyed in Redis, so existing
|
|
65
|
+
* windows effectively reset when the algorithm changes.
|
|
66
|
+
* Defaults to "fixed-window".
|
|
67
|
+
*/
|
|
68
|
+
ALGORITHM: z.enum(["fixed-window", "sliding-window"]).default("fixed-window"),
|
|
58
69
|
});
|
|
59
70
|
function errorMessage(error) {
|
|
60
71
|
return error instanceof Error ? error.message : String(error);
|
|
61
72
|
}
|
|
62
73
|
/**
|
|
63
74
|
* Creates an Upstash-backed RateLimitPort implementation.
|
|
64
|
-
*
|
|
65
|
-
* @param config - Validated Upstash configuration
|
|
66
|
-
* @returns A RateLimitPort implementation using Upstash
|
|
67
75
|
*/
|
|
68
|
-
function createUpstashRateLimitPort(
|
|
76
|
+
function createUpstashRateLimitPort(options) {
|
|
69
77
|
const instrumentation = createProviderInstrumentation(options.instrumentation, {
|
|
70
78
|
providerName: "rate-limit-upstash",
|
|
71
79
|
watcher: "rateLimit",
|
|
72
80
|
});
|
|
73
|
-
const redis =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const redis = options.client;
|
|
82
|
+
const prefix = options.prefix;
|
|
83
|
+
const algorithm = options.algorithm;
|
|
84
|
+
// Upstash's Ratelimit fixes the limiter config at construction time, so we
|
|
85
|
+
// keep one instance per (limit, windowSec, algorithm). Rate limit configs
|
|
86
|
+
// come from contract metadata, so the cardinality is tiny and an unbounded
|
|
87
|
+
// Map is fine.
|
|
88
|
+
const limiters = new Map();
|
|
89
|
+
function limiterFor(limit, windowSec) {
|
|
90
|
+
const cacheKey = `${algorithm}:${limit}:${windowSec}`;
|
|
91
|
+
const cached = limiters.get(cacheKey);
|
|
92
|
+
if (cached) {
|
|
93
|
+
return cached;
|
|
94
|
+
}
|
|
95
|
+
const ratelimit = new Ratelimit({
|
|
96
|
+
redis,
|
|
97
|
+
limiter: algorithm === "sliding-window"
|
|
98
|
+
? Ratelimit.slidingWindow(limit, `${windowSec} s`)
|
|
99
|
+
: Ratelimit.fixedWindow(limit, `${windowSec} s`),
|
|
100
|
+
prefix,
|
|
101
|
+
});
|
|
102
|
+
limiters.set(cacheKey, ratelimit);
|
|
103
|
+
return ratelimit;
|
|
104
|
+
}
|
|
78
105
|
const port = {
|
|
79
106
|
async hit({ key, limit, windowSec }) {
|
|
80
107
|
const startedAt = Date.now();
|
|
81
108
|
try {
|
|
82
|
-
|
|
83
|
-
// This is necessary because Upstash's Ratelimit is configured with a specific
|
|
84
|
-
// limit/window at construction time, and we need dynamic limits per call.
|
|
85
|
-
// Performance note: Creating instances is lightweight as Upstash uses HTTP-based
|
|
86
|
-
// REST API calls, not persistent connections. The actual rate limiting logic
|
|
87
|
-
// happens server-side at Upstash.
|
|
88
|
-
const ratelimit = new Ratelimit({
|
|
89
|
-
redis,
|
|
90
|
-
// Use fixed window algorithm - simple and effective
|
|
91
|
-
// Alternative: Ratelimit.slidingWindow(limit, `${windowSec} s`)
|
|
92
|
-
limiter: Ratelimit.fixedWindow(limit, `${windowSec} s`),
|
|
93
|
-
prefix,
|
|
94
|
-
});
|
|
109
|
+
const ratelimit = limiterFor(limit, windowSec);
|
|
95
110
|
const result = await ratelimit.limit(key);
|
|
96
111
|
const allowed = result.success;
|
|
97
112
|
// result.remaining is the number of remaining requests in the window
|
|
@@ -115,6 +130,7 @@ function createUpstashRateLimitPort(config, options = {}) {
|
|
|
115
130
|
limit,
|
|
116
131
|
windowSec,
|
|
117
132
|
prefix,
|
|
133
|
+
algorithm,
|
|
118
134
|
allowed,
|
|
119
135
|
remaining,
|
|
120
136
|
resetAt: resetAt?.toISOString() ?? null,
|
|
@@ -134,6 +150,7 @@ function createUpstashRateLimitPort(config, options = {}) {
|
|
|
134
150
|
limit,
|
|
135
151
|
windowSec,
|
|
136
152
|
prefix,
|
|
153
|
+
algorithm,
|
|
137
154
|
durationMs: Date.now() - startedAt,
|
|
138
155
|
error: errorMessage(error),
|
|
139
156
|
},
|
|
@@ -141,7 +158,6 @@ function createUpstashRateLimitPort(config, options = {}) {
|
|
|
141
158
|
throw error;
|
|
142
159
|
}
|
|
143
160
|
},
|
|
144
|
-
client: redis,
|
|
145
161
|
};
|
|
146
162
|
return port;
|
|
147
163
|
}
|
|
@@ -151,7 +167,9 @@ function createUpstashRateLimitPort(config, options = {}) {
|
|
|
151
167
|
* Configuration via environment variables:
|
|
152
168
|
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
153
169
|
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
154
|
-
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "
|
|
170
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "beignet:ratelimit")
|
|
171
|
+
* - UPSTASH_ALGORITHM: Optional rate limit algorithm, "fixed-window" or
|
|
172
|
+
* "sliding-window" (default: "fixed-window")
|
|
155
173
|
*
|
|
156
174
|
* @example
|
|
157
175
|
* ```ts
|
|
@@ -173,14 +191,22 @@ export const upstashRateLimitProvider = createProvider({
|
|
|
173
191
|
throw new Error("[upstashRateLimitProvider] Missing config. " +
|
|
174
192
|
"Please set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN environment variables.");
|
|
175
193
|
}
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
194
|
+
const client = new Redis({
|
|
195
|
+
url: config.REDIS_REST_URL,
|
|
196
|
+
token: config.REDIS_REST_TOKEN,
|
|
179
197
|
});
|
|
180
|
-
const
|
|
181
|
-
|
|
198
|
+
const rateLimit = createUpstashRateLimitPort({
|
|
199
|
+
client,
|
|
200
|
+
prefix: config.PREFIX,
|
|
201
|
+
algorithm: config.ALGORITHM,
|
|
202
|
+
instrumentation: ports,
|
|
182
203
|
});
|
|
183
|
-
return {
|
|
204
|
+
return {
|
|
205
|
+
ports: {
|
|
206
|
+
rateLimit,
|
|
207
|
+
upstash: { client },
|
|
208
|
+
},
|
|
209
|
+
};
|
|
184
210
|
},
|
|
185
211
|
});
|
|
186
212
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beignet/provider-rate-limit-upstash",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Upstash-based rate limit provider for Beignet - adds rate limit port using Upstash Redis",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"node": ">=18.0.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
+
"@beignet/core": ">=0.0.3 <1.0.0",
|
|
56
57
|
"@upstash/ratelimit": "^2.0.0",
|
|
57
58
|
"@upstash/redis": "^1.0.0"
|
|
58
59
|
},
|
|
59
60
|
"dependencies": {
|
|
60
|
-
"zod": "^4.0.0"
|
|
61
|
-
"@beignet/core": "*"
|
|
61
|
+
"zod": "^4.0.0"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@beignet/devtools": "*",
|
|
@@ -67,5 +67,39 @@
|
|
|
67
67
|
"@upstash/ratelimit": "^2.0.7",
|
|
68
68
|
"@upstash/redis": "^1.35.7",
|
|
69
69
|
"typescript": "^5.3.0"
|
|
70
|
+
},
|
|
71
|
+
"beignet": {
|
|
72
|
+
"provider": {
|
|
73
|
+
"displayName": "Upstash rate-limit provider",
|
|
74
|
+
"ports": [
|
|
75
|
+
"rateLimit",
|
|
76
|
+
"upstash"
|
|
77
|
+
],
|
|
78
|
+
"appPorts": [
|
|
79
|
+
{
|
|
80
|
+
"name": "rateLimit",
|
|
81
|
+
"type": "RateLimitPort"
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"env": [
|
|
85
|
+
"UPSTASH_REDIS_REST_URL",
|
|
86
|
+
"UPSTASH_REDIS_REST_TOKEN",
|
|
87
|
+
"UPSTASH_PREFIX",
|
|
88
|
+
"UPSTASH_ALGORITHM"
|
|
89
|
+
],
|
|
90
|
+
"requiredEnv": [
|
|
91
|
+
"UPSTASH_REDIS_REST_URL",
|
|
92
|
+
"UPSTASH_REDIS_REST_TOKEN"
|
|
93
|
+
],
|
|
94
|
+
"watchers": [
|
|
95
|
+
"rateLimit"
|
|
96
|
+
],
|
|
97
|
+
"registration": {
|
|
98
|
+
"required": true,
|
|
99
|
+
"tokens": [
|
|
100
|
+
"upstashRateLimitProvider"
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
70
104
|
}
|
|
71
105
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* Configuration:
|
|
7
7
|
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
8
8
|
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
9
|
-
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "
|
|
9
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "beignet:ratelimit")
|
|
10
|
+
* - UPSTASH_ALGORITHM: Optional rate limit algorithm, "fixed-window" or
|
|
11
|
+
* "sliding-window" (default: "fixed-window")
|
|
10
12
|
*
|
|
11
13
|
* @example
|
|
12
14
|
* ```ts
|
|
@@ -42,6 +44,14 @@ import { Ratelimit } from "@upstash/ratelimit";
|
|
|
42
44
|
import { Redis } from "@upstash/redis";
|
|
43
45
|
import { z } from "zod";
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Rate limit algorithms supported by the Upstash provider.
|
|
49
|
+
*
|
|
50
|
+
* - "fixed-window": cheaper, but allows bursts at window boundaries.
|
|
51
|
+
* - "sliding-window": smoother enforcement at slightly more Redis work.
|
|
52
|
+
*/
|
|
53
|
+
export type UpstashRateLimitAlgorithm = "fixed-window" | "sliding-window";
|
|
54
|
+
|
|
45
55
|
/**
|
|
46
56
|
* Configuration schema for the Upstash Rate Limit provider.
|
|
47
57
|
* Validates environment variables with UPSTASH_ prefix.
|
|
@@ -61,9 +71,19 @@ const UpstashRateLimitConfigSchema = z.object({
|
|
|
61
71
|
/**
|
|
62
72
|
* Optional prefix for keys used by this rate limiter.
|
|
63
73
|
* Helps avoid collisions if you reuse the same Redis for multiple things.
|
|
64
|
-
* Defaults to "
|
|
74
|
+
* Defaults to "beignet:ratelimit".
|
|
65
75
|
*/
|
|
66
|
-
PREFIX: z.string().default("
|
|
76
|
+
PREFIX: z.string().default("beignet:ratelimit"),
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Optional rate limit algorithm.
|
|
80
|
+
* "fixed-window" is cheaper but bursty at window boundaries.
|
|
81
|
+
* "sliding-window" is smoother at slightly more Redis work.
|
|
82
|
+
* Switching algorithms changes how counters are keyed in Redis, so existing
|
|
83
|
+
* windows effectively reset when the algorithm changes.
|
|
84
|
+
* Defaults to "fixed-window".
|
|
85
|
+
*/
|
|
86
|
+
ALGORITHM: z.enum(["fixed-window", "sliding-window"]).default("fixed-window"),
|
|
67
87
|
});
|
|
68
88
|
|
|
69
89
|
/**
|
|
@@ -74,21 +94,46 @@ export type UpstashRateLimitConfig = z.infer<
|
|
|
74
94
|
>;
|
|
75
95
|
|
|
76
96
|
/**
|
|
77
|
-
*
|
|
78
|
-
* The Upstash provider adds the underlying client for advanced operations.
|
|
97
|
+
* Escape hatch for apps that need the raw Upstash Redis client.
|
|
79
98
|
*/
|
|
80
|
-
export interface
|
|
99
|
+
export interface UpstashRateLimitEscapeHatch {
|
|
81
100
|
/**
|
|
82
|
-
*
|
|
83
|
-
* Use this for
|
|
101
|
+
* Raw Upstash Redis client.
|
|
102
|
+
* Use this for Upstash operations the stable rate limit port does not model.
|
|
84
103
|
*/
|
|
85
104
|
client: Redis;
|
|
86
105
|
}
|
|
87
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Ports contributed by the Upstash rate limit provider.
|
|
109
|
+
*/
|
|
110
|
+
export interface UpstashRateLimitProviderPorts {
|
|
111
|
+
/**
|
|
112
|
+
* Beignet rate limit port backed by Upstash.
|
|
113
|
+
*/
|
|
114
|
+
rateLimit: RateLimitPort;
|
|
115
|
+
/**
|
|
116
|
+
* Raw Upstash Redis client escape hatch.
|
|
117
|
+
*/
|
|
118
|
+
upstash: UpstashRateLimitEscapeHatch;
|
|
119
|
+
}
|
|
120
|
+
|
|
88
121
|
/**
|
|
89
122
|
* Options for the internal Upstash rate-limit port factory.
|
|
90
123
|
*/
|
|
91
|
-
|
|
124
|
+
interface CreateUpstashRateLimitPortOptions {
|
|
125
|
+
/**
|
|
126
|
+
* Upstash Redis client used for rate limit counters.
|
|
127
|
+
*/
|
|
128
|
+
client: Redis;
|
|
129
|
+
/**
|
|
130
|
+
* Prefix for keys used by this rate limiter.
|
|
131
|
+
*/
|
|
132
|
+
prefix: string;
|
|
133
|
+
/**
|
|
134
|
+
* Rate limit algorithm used for every bucket served by this port.
|
|
135
|
+
*/
|
|
136
|
+
algorithm: UpstashRateLimitAlgorithm;
|
|
92
137
|
/**
|
|
93
138
|
* Optional provider instrumentation target.
|
|
94
139
|
*/
|
|
@@ -101,14 +146,10 @@ function errorMessage(error: unknown): string {
|
|
|
101
146
|
|
|
102
147
|
/**
|
|
103
148
|
* Creates an Upstash-backed RateLimitPort implementation.
|
|
104
|
-
*
|
|
105
|
-
* @param config - Validated Upstash configuration
|
|
106
|
-
* @returns A RateLimitPort implementation using Upstash
|
|
107
149
|
*/
|
|
108
150
|
function createUpstashRateLimitPort(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
): UpstashRateLimitPort {
|
|
151
|
+
options: CreateUpstashRateLimitPortOptions,
|
|
152
|
+
): RateLimitPort {
|
|
112
153
|
const instrumentation = createProviderInstrumentation(
|
|
113
154
|
options.instrumentation,
|
|
114
155
|
{
|
|
@@ -116,31 +157,41 @@ function createUpstashRateLimitPort(
|
|
|
116
157
|
watcher: "rateLimit",
|
|
117
158
|
},
|
|
118
159
|
);
|
|
119
|
-
const redis =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
160
|
+
const redis = options.client;
|
|
161
|
+
const prefix = options.prefix;
|
|
162
|
+
const algorithm = options.algorithm;
|
|
123
163
|
|
|
124
|
-
|
|
164
|
+
// Upstash's Ratelimit fixes the limiter config at construction time, so we
|
|
165
|
+
// keep one instance per (limit, windowSec, algorithm). Rate limit configs
|
|
166
|
+
// come from contract metadata, so the cardinality is tiny and an unbounded
|
|
167
|
+
// Map is fine.
|
|
168
|
+
const limiters = new Map<string, Ratelimit>();
|
|
125
169
|
|
|
126
|
-
|
|
170
|
+
function limiterFor(limit: number, windowSec: number): Ratelimit {
|
|
171
|
+
const cacheKey = `${algorithm}:${limit}:${windowSec}`;
|
|
172
|
+
const cached = limiters.get(cacheKey);
|
|
173
|
+
if (cached) {
|
|
174
|
+
return cached;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const ratelimit = new Ratelimit({
|
|
178
|
+
redis,
|
|
179
|
+
limiter:
|
|
180
|
+
algorithm === "sliding-window"
|
|
181
|
+
? Ratelimit.slidingWindow(limit, `${windowSec} s`)
|
|
182
|
+
: Ratelimit.fixedWindow(limit, `${windowSec} s`),
|
|
183
|
+
prefix,
|
|
184
|
+
});
|
|
185
|
+
limiters.set(cacheKey, ratelimit);
|
|
186
|
+
return ratelimit;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const port: RateLimitPort = {
|
|
127
190
|
async hit({ key, limit, windowSec }): Promise<RateLimitResult> {
|
|
128
191
|
const startedAt = Date.now();
|
|
129
192
|
|
|
130
193
|
try {
|
|
131
|
-
|
|
132
|
-
// This is necessary because Upstash's Ratelimit is configured with a specific
|
|
133
|
-
// limit/window at construction time, and we need dynamic limits per call.
|
|
134
|
-
// Performance note: Creating instances is lightweight as Upstash uses HTTP-based
|
|
135
|
-
// REST API calls, not persistent connections. The actual rate limiting logic
|
|
136
|
-
// happens server-side at Upstash.
|
|
137
|
-
const ratelimit = new Ratelimit({
|
|
138
|
-
redis,
|
|
139
|
-
// Use fixed window algorithm - simple and effective
|
|
140
|
-
// Alternative: Ratelimit.slidingWindow(limit, `${windowSec} s`)
|
|
141
|
-
limiter: Ratelimit.fixedWindow(limit, `${windowSec} s`),
|
|
142
|
-
prefix,
|
|
143
|
-
});
|
|
194
|
+
const ratelimit = limiterFor(limit, windowSec);
|
|
144
195
|
|
|
145
196
|
const result = await ratelimit.limit(key);
|
|
146
197
|
|
|
@@ -173,6 +224,7 @@ function createUpstashRateLimitPort(
|
|
|
173
224
|
limit,
|
|
174
225
|
windowSec,
|
|
175
226
|
prefix,
|
|
227
|
+
algorithm,
|
|
176
228
|
allowed,
|
|
177
229
|
remaining,
|
|
178
230
|
resetAt: resetAt?.toISOString() ?? null,
|
|
@@ -192,6 +244,7 @@ function createUpstashRateLimitPort(
|
|
|
192
244
|
limit,
|
|
193
245
|
windowSec,
|
|
194
246
|
prefix,
|
|
247
|
+
algorithm,
|
|
195
248
|
durationMs: Date.now() - startedAt,
|
|
196
249
|
error: errorMessage(error),
|
|
197
250
|
},
|
|
@@ -199,8 +252,6 @@ function createUpstashRateLimitPort(
|
|
|
199
252
|
throw error;
|
|
200
253
|
}
|
|
201
254
|
},
|
|
202
|
-
|
|
203
|
-
client: redis,
|
|
204
255
|
};
|
|
205
256
|
|
|
206
257
|
return port;
|
|
@@ -212,7 +263,9 @@ function createUpstashRateLimitPort(
|
|
|
212
263
|
* Configuration via environment variables:
|
|
213
264
|
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
214
265
|
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
215
|
-
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "
|
|
266
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "beignet:ratelimit")
|
|
267
|
+
* - UPSTASH_ALGORITHM: Optional rate limit algorithm, "fixed-window" or
|
|
268
|
+
* "sliding-window" (default: "fixed-window")
|
|
216
269
|
*
|
|
217
270
|
* @example
|
|
218
271
|
* ```ts
|
|
@@ -239,13 +292,21 @@ export const upstashRateLimitProvider = createProvider({
|
|
|
239
292
|
);
|
|
240
293
|
}
|
|
241
294
|
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
295
|
+
const client = new Redis({
|
|
296
|
+
url: config.REDIS_REST_URL,
|
|
297
|
+
token: config.REDIS_REST_TOKEN,
|
|
245
298
|
});
|
|
246
|
-
const
|
|
247
|
-
|
|
299
|
+
const rateLimit = createUpstashRateLimitPort({
|
|
300
|
+
client,
|
|
301
|
+
prefix: config.PREFIX,
|
|
302
|
+
algorithm: config.ALGORITHM,
|
|
303
|
+
instrumentation: ports,
|
|
248
304
|
});
|
|
249
|
-
return {
|
|
305
|
+
return {
|
|
306
|
+
ports: {
|
|
307
|
+
rateLimit,
|
|
308
|
+
upstash: { client },
|
|
309
|
+
} satisfies UpstashRateLimitProviderPorts,
|
|
310
|
+
};
|
|
250
311
|
},
|
|
251
312
|
});
|