@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 ADDED
@@ -0,0 +1,5 @@
1
+ # @beignet/provider-rate-limit-upstash
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial Beignet release under the `@beignet` npm scope.
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
@@ -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
+ });