@beignet/provider-rate-limit-upstash 0.0.3 → 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 CHANGED
@@ -1,5 +1,44 @@
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
+
3
42
  ## 0.0.3
4
43
 
5
44
  ### 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: `ck:ratelimit`) | `myapp:ratelimit` |
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
- createContext: ({ ports }) => ({ ports }),
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: AppCtx) {
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: AppCtx, meta?: RateLimitMetadata) {
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 fixed window rate limiting via `Ratelimit.fixedWindow()`
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**: Creates a new `Ratelimit` instance for each `hit()` call to support dynamic limits
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 time,
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
- ## Advanced usage
195
-
196
- ### Access the underlying Redis client
214
+ ## Escape hatch
197
215
 
198
- The provider extends the standard `RateLimitPort` with access to the underlying Upstash Redis client:
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
- import type { UpstashRateLimitPort } from "@beignet/provider-rate-limit-upstash";
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
- const rateLimit = ctx.ports.rateLimit as UpstashRateLimitPort;
226
+ To get proper type inference for the contributed ports, extend your ports type
227
+ with `UpstashRateLimitProviderPorts`:
204
228
 
205
- // Access the Redis client for advanced operations
206
- await rateLimit.client.get("some:key");
207
- await rateLimit.client.set("some:key", "value");
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: "ck:ratelimit")
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
- * Extended rate limit port interface that includes the Upstash Redis client.
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 UpstashRateLimitPort extends RateLimitPort {
66
+ export interface UpstashRateLimitEscapeHatch {
56
67
  /**
57
- * The underlying Upstash Redis client instance.
58
- * Use this for advanced operations not covered by the rate limit interface.
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
- * Options for the internal Upstash rate-limit port factory.
74
+ * Ports contributed by the Upstash rate limit provider.
64
75
  */
65
- export interface CreateUpstashRateLimitPortOptions {
76
+ export interface UpstashRateLimitProviderPorts {
77
+ /**
78
+ * Beignet rate limit port backed by Upstash.
79
+ */
80
+ rateLimit: RateLimitPort;
66
81
  /**
67
- * Optional provider instrumentation target.
82
+ * Raw Upstash Redis client escape hatch.
68
83
  */
69
- instrumentation?: ProviderInstrumentationTarget;
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: "ck:ratelimit")
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: UpstashRateLimitPort;
94
- }>;
114
+ rateLimit: RateLimitPort;
115
+ upstash: {
116
+ client: Redis;
117
+ };
118
+ }, unknown, void>;
95
119
  export {};
96
120
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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;;GAEG;AACH,MAAM,WAAW,iCAAiC;IAChD;;OAEG;IACH,eAAe,CAAC,EAAE,6BAA6B,CAAC;CACjD;AAiHD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,wBAAwB;;;;;;EAyBnC,CAAC"}
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: "ck:ratelimit")
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 "ck:ratelimit".
57
+ * Defaults to "beignet:ratelimit".
56
58
  */
57
- PREFIX: z.string().default("ck:ratelimit"),
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(config, options = {}) {
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 = new Redis({
74
- url: config.REDIS_REST_URL,
75
- token: config.REDIS_REST_TOKEN,
76
- });
77
- const prefix = config.PREFIX;
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
- // 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
- });
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: "ck:ratelimit")
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 instrumentation = createProviderInstrumentation(ports, {
177
- providerName: "rate-limit-upstash",
178
- watcher: "rateLimit",
194
+ const client = new Redis({
195
+ url: config.REDIS_REST_URL,
196
+ token: config.REDIS_REST_TOKEN,
179
197
  });
180
- const rateLimitPort = createUpstashRateLimitPort(config, {
181
- instrumentation,
198
+ const rateLimit = createUpstashRateLimitPort({
199
+ client,
200
+ prefix: config.PREFIX,
201
+ algorithm: config.ALGORITHM,
202
+ instrumentation: ports,
182
203
  });
183
- return { ports: { rateLimit: rateLimitPort } };
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":"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"}
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",
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: "ck:ratelimit")
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 "ck:ratelimit".
74
+ * Defaults to "beignet:ratelimit".
65
75
  */
66
- PREFIX: z.string().default("ck:ratelimit"),
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
- * Extended rate limit port interface that includes the Upstash Redis client.
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 UpstashRateLimitPort extends RateLimitPort {
99
+ export interface UpstashRateLimitEscapeHatch {
81
100
  /**
82
- * The underlying Upstash Redis client instance.
83
- * Use this for advanced operations not covered by the rate limit interface.
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
- export interface CreateUpstashRateLimitPortOptions {
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
- config: UpstashRateLimitConfig,
110
- options: CreateUpstashRateLimitPortOptions = {},
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 = new Redis({
120
- url: config.REDIS_REST_URL,
121
- token: config.REDIS_REST_TOKEN,
122
- });
160
+ const redis = options.client;
161
+ const prefix = options.prefix;
162
+ const algorithm = options.algorithm;
123
163
 
124
- const prefix = config.PREFIX;
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
- const port: UpstashRateLimitPort = {
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
- // Create a new Ratelimit instance for each call with the specified limit and window.
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: "ck:ratelimit")
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 instrumentation = createProviderInstrumentation(ports, {
243
- providerName: "rate-limit-upstash",
244
- watcher: "rateLimit",
295
+ const client = new Redis({
296
+ url: config.REDIS_REST_URL,
297
+ token: config.REDIS_REST_TOKEN,
245
298
  });
246
- const rateLimitPort = createUpstashRateLimitPort(config, {
247
- instrumentation,
299
+ const rateLimit = createUpstashRateLimitPort({
300
+ client,
301
+ prefix: config.PREFIX,
302
+ algorithm: config.ALGORITHM,
303
+ instrumentation: ports,
248
304
  });
249
- return { ports: { rateLimit: rateLimitPort } };
305
+ return {
306
+ ports: {
307
+ rateLimit,
308
+ upstash: { client },
309
+ } satisfies UpstashRateLimitProviderPorts,
310
+ };
250
311
  },
251
312
  });