@beignet/provider-rate-limit-upstash 0.0.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/CHANGELOG.md +5 -0
- package/README.md +220 -0
- package/dist/index.d.ts +90 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +186 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
- package/src/index.ts +245 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# @beignet/provider-rate-limit-upstash
|
|
2
|
+
|
|
3
|
+
Upstash-backed `RateLimitPort` provider for Beignet applications.
|
|
4
|
+
|
|
5
|
+
The provider installs `ctx.ports.rateLimit` using
|
|
6
|
+
[Upstash Redis](https://upstash.com/) and
|
|
7
|
+
[@upstash/ratelimit](https://github.com/upstash/ratelimit).
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Implements the standard `RateLimitPort` interface.
|
|
12
|
+
- Uses the Upstash Redis REST API, so it is serverless-friendly.
|
|
13
|
+
- Supports dynamic limits per request with a configurable key prefix.
|
|
14
|
+
- Emits devtools events for allowed, blocked, and failed hits.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add @beignet/provider-rate-limit-upstash @upstash/redis @upstash/ratelimit
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
Set these environment variables:
|
|
25
|
+
|
|
26
|
+
| Variable | Required | Description | Example |
|
|
27
|
+
|----------|----------|-------------|---------|
|
|
28
|
+
| `UPSTASH_REDIS_REST_URL` | Yes | Your Upstash Redis REST URL | `https://us1-properly-ancient-12345.upstash.io` |
|
|
29
|
+
| `UPSTASH_REDIS_REST_TOKEN` | Yes | Your Upstash Redis REST token | `AXXXeyJpZCI6IjEy...` |
|
|
30
|
+
| `UPSTASH_PREFIX` | No | Key prefix for rate limit keys (default: `ck:ratelimit`) | `myapp:ratelimit` |
|
|
31
|
+
|
|
32
|
+
### Getting Upstash credentials
|
|
33
|
+
|
|
34
|
+
1. Sign up at [Upstash](https://upstash.com/)
|
|
35
|
+
2. Create a new Redis database
|
|
36
|
+
3. Navigate to the database details page
|
|
37
|
+
4. Copy the REST URL and REST token from the "REST API" section
|
|
38
|
+
|
|
39
|
+
## Setup
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { createNextServer } from "@beignet/next";
|
|
43
|
+
import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";
|
|
44
|
+
import { createRateLimitHooks } from "@beignet/core/server";
|
|
45
|
+
import type { AppContext } from "@/app-context";
|
|
46
|
+
import { appPorts } from "@/infra/app-ports";
|
|
47
|
+
import { routes } from "@/server/routes";
|
|
48
|
+
|
|
49
|
+
export const server = await createNextServer({
|
|
50
|
+
ports: appPorts,
|
|
51
|
+
providers: [upstashRateLimitProvider],
|
|
52
|
+
hooks: [createRateLimitHooks<AppContext>()],
|
|
53
|
+
createContext: ({ ports }) => ({ ports }),
|
|
54
|
+
routes,
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Direct use
|
|
59
|
+
|
|
60
|
+
Once the provider is registered, you can use the rate limit port in hooks,
|
|
61
|
+
policies, or use cases:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// Example app-specific policy that rate limits by IP address
|
|
65
|
+
async function checkIpRateLimit(ctx: AppCtx) {
|
|
66
|
+
const result = await ctx.ports.rateLimit.hit({
|
|
67
|
+
key: `ip:${ctx.ip}`,
|
|
68
|
+
limit: 100,
|
|
69
|
+
windowSec: 60, // 100 requests per 60 seconds
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!result.allowed) {
|
|
73
|
+
return {
|
|
74
|
+
status: 429,
|
|
75
|
+
headers: {
|
|
76
|
+
"X-RateLimit-Limit": "100",
|
|
77
|
+
"X-RateLimit-Remaining": String(result.remaining ?? 0),
|
|
78
|
+
"X-RateLimit-Reset": result.resetAt?.toISOString() ?? "",
|
|
79
|
+
"Retry-After": String(result.retryAfterSeconds ?? 0),
|
|
80
|
+
},
|
|
81
|
+
body: {
|
|
82
|
+
code: "TOO_MANY_REQUESTS",
|
|
83
|
+
message: "Rate limit exceeded. Please try again later.",
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Request is allowed
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Different rate limits for different endpoints
|
|
94
|
+
|
|
95
|
+
You can apply different rate limits for different operations:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// Strict rate limit for auth endpoints
|
|
99
|
+
const loginResult = await ctx.ports.rateLimit.hit({
|
|
100
|
+
key: `login:${ctx.ip}`,
|
|
101
|
+
limit: 5,
|
|
102
|
+
windowSec: 300, // 5 attempts per 5 minutes
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// More relaxed rate limit for API endpoints
|
|
106
|
+
const apiResult = await ctx.ports.rateLimit.hit({
|
|
107
|
+
key: `api:user:${userId}`,
|
|
108
|
+
limit: 1000,
|
|
109
|
+
windowSec: 3600, // 1000 requests per hour
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Using with contract metadata
|
|
114
|
+
|
|
115
|
+
You can define rate limit metadata on your contracts:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const getTodos = api.get("/todos")
|
|
119
|
+
.meta({
|
|
120
|
+
rateLimit: { max: 60, windowSec: 60, scope: "user" },
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The built-in `createRateLimitHooks(...)` helper reads this metadata and applies
|
|
125
|
+
the limit through `ctx.ports.rateLimit`. If your app needs custom behavior, keep
|
|
126
|
+
the same metadata shape and call the port directly:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
type RateLimitMetadata = {
|
|
130
|
+
rateLimit?: {
|
|
131
|
+
max: number;
|
|
132
|
+
windowSec: number;
|
|
133
|
+
scope?: "global" | "ip" | "user";
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
async function rateLimitFromMeta(ctx: AppCtx, meta?: RateLimitMetadata) {
|
|
138
|
+
if (!meta?.rateLimit) return;
|
|
139
|
+
|
|
140
|
+
const { max, windowSec, scope = "global" } = meta.rateLimit;
|
|
141
|
+
const actorId =
|
|
142
|
+
ctx.actor?.type === "user" && ctx.actor.id ? ctx.actor.id : undefined;
|
|
143
|
+
const result = await ctx.ports.rateLimit.hit({
|
|
144
|
+
key:
|
|
145
|
+
scope === "user"
|
|
146
|
+
? `user:${actorId ?? "anonymous"}`
|
|
147
|
+
: `${scope}:${ctx.ip ?? "global"}`,
|
|
148
|
+
limit: max,
|
|
149
|
+
windowSec,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!result.allowed) {
|
|
153
|
+
return {
|
|
154
|
+
status: 429,
|
|
155
|
+
body: {
|
|
156
|
+
code: "TOO_MANY_REQUESTS",
|
|
157
|
+
message: "Too many requests",
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Rate limit result
|
|
165
|
+
|
|
166
|
+
The `hit` method returns a `RateLimitResult` with:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
interface RateLimitResult {
|
|
170
|
+
allowed: boolean; // true if the hit is within the limit
|
|
171
|
+
remaining: number | null; // requests remaining in the window
|
|
172
|
+
resetAt: Date | null; // when the window resets
|
|
173
|
+
retryAfterSeconds: number | null; // retry delay when the hit is rejected
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Implementation details
|
|
178
|
+
|
|
179
|
+
- **Algorithm**: Uses fixed window rate limiting via `Ratelimit.fixedWindow()`
|
|
180
|
+
- **Backend**: Upstash Redis REST API (serverless-compatible)
|
|
181
|
+
- **Per-request configuration**: Creates a new `Ratelimit` instance for each `hit()` call to support dynamic limits
|
|
182
|
+
- **Key prefix**: Configurable prefix to avoid key collisions
|
|
183
|
+
|
|
184
|
+
## Devtools
|
|
185
|
+
|
|
186
|
+
When `@beignet/devtools` is installed before this provider, rate limit
|
|
187
|
+
checks appear under the dashboard's Rate limits watcher.
|
|
188
|
+
|
|
189
|
+
The provider records `rateLimit.hit` events with the key, limit, window,
|
|
190
|
+
configured prefix, allowed/blocked result, remaining count, reset time,
|
|
191
|
+
retry-after value, and duration. Provider failures are recorded as
|
|
192
|
+
`rateLimit.hit.failed`.
|
|
193
|
+
|
|
194
|
+
## Advanced usage
|
|
195
|
+
|
|
196
|
+
### Access the underlying Redis client
|
|
197
|
+
|
|
198
|
+
The provider extends the standard `RateLimitPort` with access to the underlying Upstash Redis client:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import type { UpstashRateLimitPort } from "@beignet/provider-rate-limit-upstash";
|
|
202
|
+
|
|
203
|
+
const rateLimit = ctx.ports.rateLimit as UpstashRateLimitPort;
|
|
204
|
+
|
|
205
|
+
// Access the Redis client for advanced operations
|
|
206
|
+
await rateLimit.client.get("some:key");
|
|
207
|
+
await rateLimit.client.set("some:key", "value");
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Testing
|
|
211
|
+
|
|
212
|
+
The provider includes comprehensive tests. Run them with:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
bun test
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-rate-limit-upstash
|
|
3
|
+
*
|
|
4
|
+
* Upstash-based rate limit provider that extends ports with rate limiting capabilities.
|
|
5
|
+
*
|
|
6
|
+
* Configuration:
|
|
7
|
+
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
8
|
+
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
9
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "ck:ratelimit")
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { createNextServer } from "@beignet/next";
|
|
14
|
+
* import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";
|
|
15
|
+
*
|
|
16
|
+
* const server = await createNextServer({
|
|
17
|
+
* ports: basePorts,
|
|
18
|
+
* providers: [upstashRateLimitProvider],
|
|
19
|
+
* // ...
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // In your middleware/policies:
|
|
23
|
+
* const result = await ctx.ports.rateLimit.hit({
|
|
24
|
+
* key: `ip:${ctx.ip}`,
|
|
25
|
+
* limit: 100,
|
|
26
|
+
* windowSec: 60,
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* if (!result.allowed) {
|
|
30
|
+
* return { status: 429, body: { message: "Too Many Requests" } };
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import type { RateLimitPort } from "@beignet/core/ports";
|
|
35
|
+
import { type ProviderInstrumentationTarget } from "@beignet/core/providers";
|
|
36
|
+
import { Redis } from "@upstash/redis";
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
/**
|
|
39
|
+
* Configuration schema for the Upstash Rate Limit provider.
|
|
40
|
+
* Validates environment variables with UPSTASH_ prefix.
|
|
41
|
+
*/
|
|
42
|
+
declare const UpstashRateLimitConfigSchema: z.ZodObject<{
|
|
43
|
+
REDIS_REST_URL: z.ZodString;
|
|
44
|
+
REDIS_REST_TOKEN: z.ZodString;
|
|
45
|
+
PREFIX: z.ZodDefault<z.ZodString>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
/**
|
|
48
|
+
* Inferred configuration type for Upstash Rate Limit provider.
|
|
49
|
+
*/
|
|
50
|
+
export type UpstashRateLimitConfig = z.infer<typeof UpstashRateLimitConfigSchema>;
|
|
51
|
+
/**
|
|
52
|
+
* Extended rate limit port interface that includes the Upstash Redis client.
|
|
53
|
+
* The Upstash provider adds the underlying client for advanced operations.
|
|
54
|
+
*/
|
|
55
|
+
export interface UpstashRateLimitPort extends RateLimitPort {
|
|
56
|
+
/**
|
|
57
|
+
* The underlying Upstash Redis client instance.
|
|
58
|
+
* Use this for advanced operations not covered by the rate limit interface.
|
|
59
|
+
*/
|
|
60
|
+
client: Redis;
|
|
61
|
+
}
|
|
62
|
+
export interface CreateUpstashRateLimitPortOptions {
|
|
63
|
+
instrumentation?: ProviderInstrumentationTarget;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Upstash rate limit provider that extends ports with rate limiting capabilities.
|
|
67
|
+
*
|
|
68
|
+
* Configuration via environment variables:
|
|
69
|
+
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
70
|
+
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
71
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "ck:ratelimit")
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const server = await createNextServer({
|
|
76
|
+
* ports: basePorts,
|
|
77
|
+
* providers: [upstashRateLimitProvider],
|
|
78
|
+
* // ...
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare const upstashRateLimitProvider: import("@beignet/core/providers").ServiceProvider<unknown, z.ZodObject<{
|
|
83
|
+
REDIS_REST_URL: z.ZodString;
|
|
84
|
+
REDIS_REST_TOKEN: z.ZodString;
|
|
85
|
+
PREFIX: z.ZodDefault<z.ZodString>;
|
|
86
|
+
}, z.core.$strip>, {
|
|
87
|
+
rateLimit: UpstashRateLimitPort;
|
|
88
|
+
}>;
|
|
89
|
+
export {};
|
|
90
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAGL,KAAK,6BAA6B,EACnC,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,QAAA,MAAM,4BAA4B;;;;iBAkBhC,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAC1C,OAAO,4BAA4B,CACpC,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IACzD;;;OAGG;IACH,MAAM,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,CAAC,EAAE,6BAA6B,CAAC;CACjD;AAiHD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,wBAAwB;;;;;;EAyBnC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-rate-limit-upstash
|
|
3
|
+
*
|
|
4
|
+
* Upstash-based rate limit provider that extends ports with rate limiting capabilities.
|
|
5
|
+
*
|
|
6
|
+
* Configuration:
|
|
7
|
+
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
8
|
+
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
9
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "ck:ratelimit")
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { createNextServer } from "@beignet/next";
|
|
14
|
+
* import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";
|
|
15
|
+
*
|
|
16
|
+
* const server = await createNextServer({
|
|
17
|
+
* ports: basePorts,
|
|
18
|
+
* providers: [upstashRateLimitProvider],
|
|
19
|
+
* // ...
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // In your middleware/policies:
|
|
23
|
+
* const result = await ctx.ports.rateLimit.hit({
|
|
24
|
+
* key: `ip:${ctx.ip}`,
|
|
25
|
+
* limit: 100,
|
|
26
|
+
* windowSec: 60,
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* if (!result.allowed) {
|
|
30
|
+
* return { status: 429, body: { message: "Too Many Requests" } };
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import { createProvider, createProviderInstrumentation, } from "@beignet/core/providers";
|
|
35
|
+
import { Ratelimit } from "@upstash/ratelimit";
|
|
36
|
+
import { Redis } from "@upstash/redis";
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
/**
|
|
39
|
+
* Configuration schema for the Upstash Rate Limit provider.
|
|
40
|
+
* Validates environment variables with UPSTASH_ prefix.
|
|
41
|
+
*/
|
|
42
|
+
const UpstashRateLimitConfigSchema = z.object({
|
|
43
|
+
/**
|
|
44
|
+
* Upstash Redis REST URL.
|
|
45
|
+
* Example: "https://us1-properly-ancient-12345.upstash.io"
|
|
46
|
+
*/
|
|
47
|
+
REDIS_REST_URL: z.string().url(),
|
|
48
|
+
/**
|
|
49
|
+
* Upstash Redis REST token.
|
|
50
|
+
*/
|
|
51
|
+
REDIS_REST_TOKEN: z.string(),
|
|
52
|
+
/**
|
|
53
|
+
* Optional prefix for keys used by this rate limiter.
|
|
54
|
+
* Helps avoid collisions if you reuse the same Redis for multiple things.
|
|
55
|
+
* Defaults to "ck:ratelimit".
|
|
56
|
+
*/
|
|
57
|
+
PREFIX: z.string().default("ck:ratelimit"),
|
|
58
|
+
});
|
|
59
|
+
function errorMessage(error) {
|
|
60
|
+
return error instanceof Error ? error.message : String(error);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Creates an Upstash-backed RateLimitPort implementation.
|
|
64
|
+
*
|
|
65
|
+
* @param config - Validated Upstash configuration
|
|
66
|
+
* @returns A RateLimitPort implementation using Upstash
|
|
67
|
+
*/
|
|
68
|
+
function createUpstashRateLimitPort(config, options = {}) {
|
|
69
|
+
const instrumentation = createProviderInstrumentation(options.instrumentation, {
|
|
70
|
+
providerName: "rate-limit-upstash",
|
|
71
|
+
watcher: "rateLimit",
|
|
72
|
+
});
|
|
73
|
+
const redis = new Redis({
|
|
74
|
+
url: config.REDIS_REST_URL,
|
|
75
|
+
token: config.REDIS_REST_TOKEN,
|
|
76
|
+
});
|
|
77
|
+
const prefix = config.PREFIX;
|
|
78
|
+
const port = {
|
|
79
|
+
async hit({ key, limit, windowSec }) {
|
|
80
|
+
const startedAt = Date.now();
|
|
81
|
+
try {
|
|
82
|
+
// Create a new Ratelimit instance for each call with the specified limit and window.
|
|
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
|
+
});
|
|
95
|
+
const result = await ratelimit.limit(key);
|
|
96
|
+
const allowed = result.success;
|
|
97
|
+
// result.remaining is the number of remaining requests in the window
|
|
98
|
+
const remaining = typeof result.remaining === "number" ? result.remaining : null;
|
|
99
|
+
// Upstash returns a JavaScript timestamp in milliseconds.
|
|
100
|
+
const resetAt = typeof result.reset === "number" ? new Date(result.reset) : null;
|
|
101
|
+
const rateLimitResult = {
|
|
102
|
+
allowed,
|
|
103
|
+
remaining,
|
|
104
|
+
resetAt,
|
|
105
|
+
retryAfterSeconds: allowed || resetAt == null
|
|
106
|
+
? null
|
|
107
|
+
: Math.max(0, Math.ceil((resetAt.getTime() - Date.now()) / 1000)),
|
|
108
|
+
};
|
|
109
|
+
instrumentation.custom({
|
|
110
|
+
name: "rateLimit.hit",
|
|
111
|
+
label: "Rate limit hit",
|
|
112
|
+
summary: allowed ? "Request allowed" : "Request blocked",
|
|
113
|
+
details: {
|
|
114
|
+
key,
|
|
115
|
+
limit,
|
|
116
|
+
windowSec,
|
|
117
|
+
prefix,
|
|
118
|
+
allowed,
|
|
119
|
+
remaining,
|
|
120
|
+
resetAt: resetAt?.toISOString() ?? null,
|
|
121
|
+
retryAfterSeconds: rateLimitResult.retryAfterSeconds,
|
|
122
|
+
durationMs: Date.now() - startedAt,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
return rateLimitResult;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
instrumentation.custom({
|
|
129
|
+
name: "rateLimit.hit.failed",
|
|
130
|
+
label: "Rate limit failed",
|
|
131
|
+
summary: "Rate limit hit failed",
|
|
132
|
+
details: {
|
|
133
|
+
key,
|
|
134
|
+
limit,
|
|
135
|
+
windowSec,
|
|
136
|
+
prefix,
|
|
137
|
+
durationMs: Date.now() - startedAt,
|
|
138
|
+
error: errorMessage(error),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
client: redis,
|
|
145
|
+
};
|
|
146
|
+
return port;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Upstash rate limit provider that extends ports with rate limiting capabilities.
|
|
150
|
+
*
|
|
151
|
+
* Configuration via environment variables:
|
|
152
|
+
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
153
|
+
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
154
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "ck:ratelimit")
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* const server = await createNextServer({
|
|
159
|
+
* ports: basePorts,
|
|
160
|
+
* providers: [upstashRateLimitProvider],
|
|
161
|
+
* // ...
|
|
162
|
+
* });
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export const upstashRateLimitProvider = createProvider({
|
|
166
|
+
name: "rate-limit-upstash",
|
|
167
|
+
config: {
|
|
168
|
+
schema: UpstashRateLimitConfigSchema,
|
|
169
|
+
envPrefix: "UPSTASH_",
|
|
170
|
+
},
|
|
171
|
+
async setup({ ports, config }) {
|
|
172
|
+
if (!config) {
|
|
173
|
+
throw new Error("[upstashRateLimitProvider] Missing config. " +
|
|
174
|
+
"Please set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN environment variables.");
|
|
175
|
+
}
|
|
176
|
+
const instrumentation = createProviderInstrumentation(ports, {
|
|
177
|
+
providerName: "rate-limit-upstash",
|
|
178
|
+
watcher: "rateLimit",
|
|
179
|
+
});
|
|
180
|
+
const rateLimitPort = createUpstashRateLimitPort(config, {
|
|
181
|
+
instrumentation,
|
|
182
|
+
});
|
|
183
|
+
return { ports: { rateLimit: rateLimitPort } };
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beignet/provider-rate-limit-upstash",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Upstash-based rate limit provider for Beignet - adds rate limit port using Upstash Redis",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src",
|
|
17
|
+
"!src/**/*.test.ts",
|
|
18
|
+
"!src/**/*.test.tsx",
|
|
19
|
+
"!src/**/*.test-d.ts",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"dev": "tsc --watch",
|
|
26
|
+
"clean": "rm -rf dist coverage .turbo",
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"test:coverage": "bun test --coverage",
|
|
29
|
+
"lint": "biome check ."
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"beignet",
|
|
33
|
+
"upstash",
|
|
34
|
+
"rate-limit",
|
|
35
|
+
"provider",
|
|
36
|
+
"redis",
|
|
37
|
+
"ports"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/taylorbryant/beignet.git",
|
|
43
|
+
"directory": "packages/provider-rate-limit-upstash"
|
|
44
|
+
},
|
|
45
|
+
"author": "Taylor Bryant",
|
|
46
|
+
"homepage": "https://github.com/taylorbryant/beignet#readme",
|
|
47
|
+
"bugs": "https://github.com/taylorbryant/beignet/issues",
|
|
48
|
+
"sideEffects": false,
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=18.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@upstash/ratelimit": "^2.0.0",
|
|
57
|
+
"@upstash/redis": "^1.0.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"zod": "^4.0.0",
|
|
61
|
+
"@beignet/core": "*"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@beignet/devtools": "*",
|
|
65
|
+
"@types/bun": "^1.3.13",
|
|
66
|
+
"@types/node": "^20.10.0",
|
|
67
|
+
"@upstash/ratelimit": "^2.0.7",
|
|
68
|
+
"@upstash/redis": "^1.35.7",
|
|
69
|
+
"typescript": "^5.3.0"
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/provider-rate-limit-upstash
|
|
3
|
+
*
|
|
4
|
+
* Upstash-based rate limit provider that extends ports with rate limiting capabilities.
|
|
5
|
+
*
|
|
6
|
+
* Configuration:
|
|
7
|
+
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
8
|
+
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
9
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "ck:ratelimit")
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { createNextServer } from "@beignet/next";
|
|
14
|
+
* import { upstashRateLimitProvider } from "@beignet/provider-rate-limit-upstash";
|
|
15
|
+
*
|
|
16
|
+
* const server = await createNextServer({
|
|
17
|
+
* ports: basePorts,
|
|
18
|
+
* providers: [upstashRateLimitProvider],
|
|
19
|
+
* // ...
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // In your middleware/policies:
|
|
23
|
+
* const result = await ctx.ports.rateLimit.hit({
|
|
24
|
+
* key: `ip:${ctx.ip}`,
|
|
25
|
+
* limit: 100,
|
|
26
|
+
* windowSec: 60,
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* if (!result.allowed) {
|
|
30
|
+
* return { status: 429, body: { message: "Too Many Requests" } };
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { RateLimitPort, RateLimitResult } from "@beignet/core/ports";
|
|
36
|
+
import {
|
|
37
|
+
createProvider,
|
|
38
|
+
createProviderInstrumentation,
|
|
39
|
+
type ProviderInstrumentationTarget,
|
|
40
|
+
} from "@beignet/core/providers";
|
|
41
|
+
import { Ratelimit } from "@upstash/ratelimit";
|
|
42
|
+
import { Redis } from "@upstash/redis";
|
|
43
|
+
import { z } from "zod";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Configuration schema for the Upstash Rate Limit provider.
|
|
47
|
+
* Validates environment variables with UPSTASH_ prefix.
|
|
48
|
+
*/
|
|
49
|
+
const UpstashRateLimitConfigSchema = z.object({
|
|
50
|
+
/**
|
|
51
|
+
* Upstash Redis REST URL.
|
|
52
|
+
* Example: "https://us1-properly-ancient-12345.upstash.io"
|
|
53
|
+
*/
|
|
54
|
+
REDIS_REST_URL: z.string().url(),
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Upstash Redis REST token.
|
|
58
|
+
*/
|
|
59
|
+
REDIS_REST_TOKEN: z.string(),
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional prefix for keys used by this rate limiter.
|
|
63
|
+
* Helps avoid collisions if you reuse the same Redis for multiple things.
|
|
64
|
+
* Defaults to "ck:ratelimit".
|
|
65
|
+
*/
|
|
66
|
+
PREFIX: z.string().default("ck:ratelimit"),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Inferred configuration type for Upstash Rate Limit provider.
|
|
71
|
+
*/
|
|
72
|
+
export type UpstashRateLimitConfig = z.infer<
|
|
73
|
+
typeof UpstashRateLimitConfigSchema
|
|
74
|
+
>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extended rate limit port interface that includes the Upstash Redis client.
|
|
78
|
+
* The Upstash provider adds the underlying client for advanced operations.
|
|
79
|
+
*/
|
|
80
|
+
export interface UpstashRateLimitPort extends RateLimitPort {
|
|
81
|
+
/**
|
|
82
|
+
* The underlying Upstash Redis client instance.
|
|
83
|
+
* Use this for advanced operations not covered by the rate limit interface.
|
|
84
|
+
*/
|
|
85
|
+
client: Redis;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface CreateUpstashRateLimitPortOptions {
|
|
89
|
+
instrumentation?: ProviderInstrumentationTarget;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function errorMessage(error: unknown): string {
|
|
93
|
+
return error instanceof Error ? error.message : String(error);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates an Upstash-backed RateLimitPort implementation.
|
|
98
|
+
*
|
|
99
|
+
* @param config - Validated Upstash configuration
|
|
100
|
+
* @returns A RateLimitPort implementation using Upstash
|
|
101
|
+
*/
|
|
102
|
+
function createUpstashRateLimitPort(
|
|
103
|
+
config: UpstashRateLimitConfig,
|
|
104
|
+
options: CreateUpstashRateLimitPortOptions = {},
|
|
105
|
+
): UpstashRateLimitPort {
|
|
106
|
+
const instrumentation = createProviderInstrumentation(
|
|
107
|
+
options.instrumentation,
|
|
108
|
+
{
|
|
109
|
+
providerName: "rate-limit-upstash",
|
|
110
|
+
watcher: "rateLimit",
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
const redis = new Redis({
|
|
114
|
+
url: config.REDIS_REST_URL,
|
|
115
|
+
token: config.REDIS_REST_TOKEN,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const prefix = config.PREFIX;
|
|
119
|
+
|
|
120
|
+
const port: UpstashRateLimitPort = {
|
|
121
|
+
async hit({ key, limit, windowSec }): Promise<RateLimitResult> {
|
|
122
|
+
const startedAt = Date.now();
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Create a new Ratelimit instance for each call with the specified limit and window.
|
|
126
|
+
// This is necessary because Upstash's Ratelimit is configured with a specific
|
|
127
|
+
// limit/window at construction time, and we need dynamic limits per call.
|
|
128
|
+
// Performance note: Creating instances is lightweight as Upstash uses HTTP-based
|
|
129
|
+
// REST API calls, not persistent connections. The actual rate limiting logic
|
|
130
|
+
// happens server-side at Upstash.
|
|
131
|
+
const ratelimit = new Ratelimit({
|
|
132
|
+
redis,
|
|
133
|
+
// Use fixed window algorithm - simple and effective
|
|
134
|
+
// Alternative: Ratelimit.slidingWindow(limit, `${windowSec} s`)
|
|
135
|
+
limiter: Ratelimit.fixedWindow(limit, `${windowSec} s`),
|
|
136
|
+
prefix,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const result = await ratelimit.limit(key);
|
|
140
|
+
|
|
141
|
+
const allowed = result.success;
|
|
142
|
+
|
|
143
|
+
// result.remaining is the number of remaining requests in the window
|
|
144
|
+
const remaining =
|
|
145
|
+
typeof result.remaining === "number" ? result.remaining : null;
|
|
146
|
+
|
|
147
|
+
// Upstash returns a JavaScript timestamp in milliseconds.
|
|
148
|
+
const resetAt =
|
|
149
|
+
typeof result.reset === "number" ? new Date(result.reset) : null;
|
|
150
|
+
|
|
151
|
+
const rateLimitResult = {
|
|
152
|
+
allowed,
|
|
153
|
+
remaining,
|
|
154
|
+
resetAt,
|
|
155
|
+
retryAfterSeconds:
|
|
156
|
+
allowed || resetAt == null
|
|
157
|
+
? null
|
|
158
|
+
: Math.max(0, Math.ceil((resetAt.getTime() - Date.now()) / 1000)),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
instrumentation.custom({
|
|
162
|
+
name: "rateLimit.hit",
|
|
163
|
+
label: "Rate limit hit",
|
|
164
|
+
summary: allowed ? "Request allowed" : "Request blocked",
|
|
165
|
+
details: {
|
|
166
|
+
key,
|
|
167
|
+
limit,
|
|
168
|
+
windowSec,
|
|
169
|
+
prefix,
|
|
170
|
+
allowed,
|
|
171
|
+
remaining,
|
|
172
|
+
resetAt: resetAt?.toISOString() ?? null,
|
|
173
|
+
retryAfterSeconds: rateLimitResult.retryAfterSeconds,
|
|
174
|
+
durationMs: Date.now() - startedAt,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return rateLimitResult;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
instrumentation.custom({
|
|
181
|
+
name: "rateLimit.hit.failed",
|
|
182
|
+
label: "Rate limit failed",
|
|
183
|
+
summary: "Rate limit hit failed",
|
|
184
|
+
details: {
|
|
185
|
+
key,
|
|
186
|
+
limit,
|
|
187
|
+
windowSec,
|
|
188
|
+
prefix,
|
|
189
|
+
durationMs: Date.now() - startedAt,
|
|
190
|
+
error: errorMessage(error),
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
client: redis,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return port;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Upstash rate limit provider that extends ports with rate limiting capabilities.
|
|
205
|
+
*
|
|
206
|
+
* Configuration via environment variables:
|
|
207
|
+
* - UPSTASH_REDIS_REST_URL: Upstash Redis REST URL (required)
|
|
208
|
+
* - UPSTASH_REDIS_REST_TOKEN: Upstash Redis REST token (required)
|
|
209
|
+
* - UPSTASH_PREFIX: Optional prefix for rate limit keys (default: "ck:ratelimit")
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* const server = await createNextServer({
|
|
214
|
+
* ports: basePorts,
|
|
215
|
+
* providers: [upstashRateLimitProvider],
|
|
216
|
+
* // ...
|
|
217
|
+
* });
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
export const upstashRateLimitProvider = createProvider({
|
|
221
|
+
name: "rate-limit-upstash",
|
|
222
|
+
|
|
223
|
+
config: {
|
|
224
|
+
schema: UpstashRateLimitConfigSchema,
|
|
225
|
+
envPrefix: "UPSTASH_",
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
async setup({ ports, config }) {
|
|
229
|
+
if (!config) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
"[upstashRateLimitProvider] Missing config. " +
|
|
232
|
+
"Please set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN environment variables.",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const instrumentation = createProviderInstrumentation(ports, {
|
|
237
|
+
providerName: "rate-limit-upstash",
|
|
238
|
+
watcher: "rateLimit",
|
|
239
|
+
});
|
|
240
|
+
const rateLimitPort = createUpstashRateLimitPort(config, {
|
|
241
|
+
instrumentation,
|
|
242
|
+
});
|
|
243
|
+
return { ports: { rateLimit: rateLimitPort } };
|
|
244
|
+
},
|
|
245
|
+
});
|