@voltro/plugin-ratelimit 0.86.0 → 0.87.0
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 +5321 -0
- package/THIRD-PARTY-NOTICES.md +860 -221
- package/dist/index.d.ts +21 -48
- package/dist/index.js +14 -44
- package/dist/inspect.js +1 -1
- package/dist/postgresStore.d.ts +4 -19
- package/dist/redisStore.d.ts +10 -25
- package/dist/redisStore.js +36 -32
- package/dist/window.js +11 -109
- package/package.json +7 -6
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { ColumnBuilder } from '@voltro/database';
|
|
2
|
+
import { ConnectionRef } from '@voltro/kv/connection';
|
|
3
|
+
import { ConnectionRegistryShape } from '@voltro/kv/connection';
|
|
2
4
|
import { DataStore } from '@voltro/database';
|
|
3
5
|
import { Effect } from 'effect';
|
|
4
6
|
import { FieldDefinitions } from '@voltro/database';
|
|
5
|
-
import {
|
|
7
|
+
import { HotRateLimitState } from '@voltro/kv/hot';
|
|
8
|
+
import { HotRateLimitState as HotRateLimitState_2 } from '@voltro/kv/hot/contract';
|
|
9
|
+
import { HotStoreShape } from '@voltro/kv/hot';
|
|
6
10
|
import { RpcKind } from '@voltro/protocol';
|
|
7
11
|
import { Schema } from 'effect';
|
|
8
12
|
import { Subject } from '@voltro/protocol';
|
|
@@ -19,20 +23,7 @@ declare interface BucketResult {
|
|
|
19
23
|
|
|
20
24
|
/** Opaque per-key state. The store persists it verbatim; only this
|
|
21
25
|
* module reads its shape. */
|
|
22
|
-
export declare type BucketState =
|
|
23
|
-
readonly kind: 'fixed';
|
|
24
|
-
readonly windowStart: number;
|
|
25
|
-
readonly count: number;
|
|
26
|
-
} | {
|
|
27
|
-
readonly kind: 'sliding';
|
|
28
|
-
readonly currStart: number;
|
|
29
|
-
readonly currCount: number;
|
|
30
|
-
readonly prevCount: number;
|
|
31
|
-
} | {
|
|
32
|
-
readonly kind: 'token';
|
|
33
|
-
readonly tokens: number;
|
|
34
|
-
readonly lastRefill: number;
|
|
35
|
-
};
|
|
26
|
+
export declare type BucketState = HotRateLimitState_2;
|
|
36
27
|
|
|
37
28
|
export declare const consumeBucket: (prev: BucketState | undefined, limit: ResolvedLimit, now: number) => BucketResult;
|
|
38
29
|
|
|
@@ -180,17 +171,16 @@ export declare interface RateLimitPluginOptions {
|
|
|
180
171
|
/**
|
|
181
172
|
* Counter backend:
|
|
182
173
|
* - `'memory'` (default) — single-node, zero-config.
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* `CACHE_REDIS_URL`/`REDIS_URL`, runs the whole decision in one
|
|
189
|
-
* atomic Lua script. For a custom client/url use the
|
|
190
|
-
* `redisStore({...})` factory from `@voltro/plugin-ratelimit/redis`.
|
|
174
|
+
* - `'postgres'` — counters in the app's declared schema, bound through
|
|
175
|
+
* `DataStore`; atomic transactions, fail-open on unavailable storage.
|
|
176
|
+
* - `'redis'` — atomic HotStore decisions over the app-owned registry.
|
|
177
|
+
* Use `redis.connection` to select a declared Redis connection or
|
|
178
|
+
* `redis.hotStore` to borrow a replacement port.
|
|
191
179
|
* - a custom `RateLimitStore`.
|
|
192
180
|
*/
|
|
193
181
|
readonly store?: 'memory' | 'postgres' | 'redis' | RateLimitStore;
|
|
182
|
+
/** Registry reference or replacement HotStore for the Redis counter backend. */
|
|
183
|
+
readonly redis?: RedisStoreOptions;
|
|
194
184
|
/**
|
|
195
185
|
* Master gate. `false` disables all limiting; a function lets you gate
|
|
196
186
|
* per-call (e.g. exempt a tenant, or a maintenance bypass). Returning
|
|
@@ -263,7 +253,7 @@ export declare interface RateLimitStore {
|
|
|
263
253
|
readonly size: () => number;
|
|
264
254
|
/** Optional one-shot setup at plugin activate (open pools, create
|
|
265
255
|
* tables). Receives the process env. */
|
|
266
|
-
readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
|
|
256
|
+
readonly activate?: (env: NodeJS.ProcessEnv, connectionRegistry?: ConnectionRegistryShape) => Promise<void>;
|
|
267
257
|
/** Optional teardown at plugin deactivate (close pools). */
|
|
268
258
|
readonly deactivate?: () => Promise<void>;
|
|
269
259
|
/** Optional bind of the app's `DataStore` (the plugin's `bindDataStore`).
|
|
@@ -278,37 +268,20 @@ export declare interface RateLimitStore {
|
|
|
278
268
|
export declare const rateLimitTable: Table<"_voltro_ratelimit", FieldDefinitions<{
|
|
279
269
|
readonly id: ColumnBuilder<number, "id", boolean>;
|
|
280
270
|
readonly key: ColumnBuilder<string, "text", boolean>;
|
|
281
|
-
readonly state: ColumnBuilder<
|
|
271
|
+
readonly state: ColumnBuilder< HotRateLimitState | null, "json", boolean>;
|
|
282
272
|
readonly touchedAt: ColumnBuilder<Date, "timestamp", true>;
|
|
283
273
|
readonly touched: ColumnBuilder<unknown, "text", boolean>;
|
|
284
274
|
}>, false, never>;
|
|
285
275
|
|
|
276
|
+
/** Redis limiter decisions go only through HotStore. The app owns every socket. */
|
|
286
277
|
export declare const redisStore: (options?: RedisStoreOptions) => RateLimitStore;
|
|
287
278
|
|
|
288
279
|
export declare interface RedisStoreOptions {
|
|
289
|
-
/**
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
readonly
|
|
293
|
-
/**
|
|
294
|
-
* How to talk to the server — mirrors the cache plugin's axis:
|
|
295
|
-
* - `'resp'` (default) — ioredis over TCP. Works for Redis, Valkey,
|
|
296
|
-
* KeyDB, Dragonfly, and Upstash's TCP endpoint.
|
|
297
|
-
* - `'http'` — `@upstash/redis` over REST, for serverless/edge where
|
|
298
|
-
* TCP isn't available.
|
|
299
|
-
* Resolved from `CACHE_REDIS_DRIVER` env when omitted. `'redis'` as a
|
|
300
|
-
* store value is the umbrella for the whole RESP family — the server
|
|
301
|
-
* brand is just the url; only the DRIVER is a real choice.
|
|
302
|
-
*/
|
|
303
|
-
readonly driver?: 'resp' | 'http';
|
|
304
|
-
/** Auth token — `http` (Upstash REST) driver only. Falls back to
|
|
305
|
-
* `CACHE_REDIS_TOKEN` / `UPSTASH_REDIS_REST_TOKEN` env. */
|
|
306
|
-
readonly token?: string;
|
|
307
|
-
/** Bring your own already-connected RESP client (from
|
|
308
|
-
* `@voltro/kv/connection`'s `connect`, or the shared registry). Takes
|
|
309
|
-
* precedence over `url` / `driver`. */
|
|
310
|
-
readonly client?: RespClient;
|
|
311
|
-
/** Key namespace. Default `'voltro:rl:'`. */
|
|
280
|
+
/** Registry reference. Omit to use the app's default Redis connection. */
|
|
281
|
+
readonly connection?: ConnectionRef<'redis'>;
|
|
282
|
+
/** Borrow a replacement HotStore port. Its owner retains disposal. */
|
|
283
|
+
readonly hotStore?: HotStoreShape;
|
|
284
|
+
/** HotStore namespace. Default 'voltro:rl'; cannot contain braces. */
|
|
312
285
|
readonly keyPrefix?: string;
|
|
313
286
|
}
|
|
314
287
|
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,9 @@ import { RATELIMIT_TABLE as a, postgresStore as o, rateLimitTable as s } from ".
|
|
|
6
6
|
import { redisStore as c } from "./redisStore.js";
|
|
7
7
|
import { buildKey as l, limitFromOverride as u, matchRule as d, resolveLimit as f, toOverride as p } from "./resolve.js";
|
|
8
8
|
import { Effect as m } from "effect";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
9
|
+
import { newTraceId as h } from "@voltro/shared";
|
|
10
|
+
import { restError as g } from "@voltro/protocol/rest";
|
|
11
|
+
import { createLogger as _ } from "@voltro/logger";
|
|
12
12
|
import { replicaEvidence as v } from "@voltro/protocol/identity";
|
|
13
13
|
import { definePlugin as y, pluginInstanceName as b } from "@voltro/protocol";
|
|
14
14
|
//#region src/index.ts
|
|
@@ -40,8 +40,9 @@ var x = (e) => ({
|
|
|
40
40
|
"ratelimit-remaining": "0",
|
|
41
41
|
"ratelimit-reset": String(Math.max(0, Math.ceil((t.resetAtMs - n) / 1e3)))
|
|
42
42
|
};
|
|
43
|
-
}, w =
|
|
44
|
-
|
|
43
|
+
}, w = _({ scope: "voltro:ratelimit" }), T = (t = {}) => {
|
|
44
|
+
if (t.redis !== void 0 && t.store !== "redis") throw Error("Rate limiter redis options require store: redis");
|
|
45
|
+
let a = t.rules ?? [], _ = t.store === void 0 || t.store === "memory" ? i() : t.store === "postgres" ? o() : t.store === "redis" ? c(t.redis) : t.store, T = r(t, _), E = (e) => {
|
|
45
46
|
let n = t.enabled;
|
|
46
47
|
return n === void 0 ? !0 : typeof n == "function" ? n(e) : n;
|
|
47
48
|
}, D = (e, n, r, i) => {
|
|
@@ -73,7 +74,7 @@ var x = (e) => ({
|
|
|
73
74
|
return O(i, s).pipe(m.flatMap((t) => {
|
|
74
75
|
if (t === "exempt") return T.bypass(), n;
|
|
75
76
|
let r = l(o, i);
|
|
76
|
-
return
|
|
77
|
+
return _.consume(r, t, Date.now()).pipe(m.catchDefect((e) => m.as(m.logWarning(`[ratelimit] store error — failing open for ${i.tag}: ${String(e)}`), {
|
|
77
78
|
allowed: !0,
|
|
78
79
|
failOpenReason: "store-error",
|
|
79
80
|
remaining: t.limit,
|
|
@@ -95,7 +96,7 @@ var x = (e) => ({
|
|
|
95
96
|
burst: i.limit
|
|
96
97
|
}, o = r.remoteAddr ?? "unknown", s = Date.now(), c;
|
|
97
98
|
try {
|
|
98
|
-
c = await m.runPromise(
|
|
99
|
+
c = await m.runPromise(_.consume(`http:ip:${o}`, a, s));
|
|
99
100
|
} catch (t) {
|
|
100
101
|
return T.record("http", {
|
|
101
102
|
allowed: !0,
|
|
@@ -144,38 +145,7 @@ var x = (e) => ({
|
|
|
144
145
|
data: T.snapshot()
|
|
145
146
|
}))
|
|
146
147
|
}],
|
|
147
|
-
declaredEnv: [
|
|
148
|
-
{
|
|
149
|
-
name: "CACHE_REDIS_URL",
|
|
150
|
-
required: !1,
|
|
151
|
-
secret: !0,
|
|
152
|
-
description: "Redis connection URL for the redis store (credential-carrying). Falls back to REDIS_URL."
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
name: "REDIS_URL",
|
|
156
|
-
required: !1,
|
|
157
|
-
secret: !0,
|
|
158
|
-
description: "Fallback Redis connection URL for the redis store (credential-carrying)."
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
name: "CACHE_REDIS_DRIVER",
|
|
162
|
-
required: !1,
|
|
163
|
-
secret: !1,
|
|
164
|
-
description: "Redis driver selector: \"http\" for the Upstash REST client, otherwise RESP."
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
name: "CACHE_REDIS_TOKEN",
|
|
168
|
-
required: !1,
|
|
169
|
-
secret: !0,
|
|
170
|
-
description: "Upstash REST token for the http redis driver. Falls back to UPSTASH_REDIS_REST_TOKEN."
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
name: "UPSTASH_REDIS_REST_TOKEN",
|
|
174
|
-
required: !1,
|
|
175
|
-
secret: !0,
|
|
176
|
-
description: "Fallback Upstash REST token for the http redis driver."
|
|
177
|
-
}
|
|
178
|
-
],
|
|
148
|
+
declaredEnv: [],
|
|
179
149
|
...A ? { onHttpRequest: A } : {},
|
|
180
150
|
errorSchemas: [{
|
|
181
151
|
schema: e,
|
|
@@ -193,11 +163,11 @@ var x = (e) => ({
|
|
|
193
163
|
kind: "action",
|
|
194
164
|
input: void 0,
|
|
195
165
|
subject: n.subject,
|
|
196
|
-
traceId:
|
|
166
|
+
traceId: h(),
|
|
197
167
|
surface: "rest",
|
|
198
168
|
publicApi: { rateLimit: t },
|
|
199
169
|
...n.remoteAddr === void 0 ? {} : { remoteAddr: n.remoteAddr }
|
|
200
|
-
}).pipe(m.catch((t) => m.fail(t instanceof e ?
|
|
170
|
+
}).pipe(m.catch((t) => m.fail(t instanceof e ? g(429, {
|
|
201
171
|
error: "RateLimited",
|
|
202
172
|
message: "Rate limit exceeded"
|
|
203
173
|
}, C(t)) : t))),
|
|
@@ -207,14 +177,14 @@ var x = (e) => ({
|
|
|
207
177
|
interceptWorkflow: k,
|
|
208
178
|
...t.store === "postgres" ? { extendSchema: { tables: [s] } } : {},
|
|
209
179
|
bindDataStore: (e) => {
|
|
210
|
-
|
|
180
|
+
_.bind && (T.binding("binding"), _.bind(e).then(() => {
|
|
211
181
|
N = void 0, T.binding("bound");
|
|
212
182
|
}).catch((e) => {
|
|
213
183
|
T.binding("failed"), N = e;
|
|
214
184
|
}));
|
|
215
185
|
},
|
|
216
186
|
onActivate: (e) => m.gen(function* () {
|
|
217
|
-
|
|
187
|
+
_.activate && (yield* m.promise(() => _.activate(e.env, e.connectionRegistry))), M = setInterval(() => _.sweep(Date.now()), 6e4), M.unref?.(), N !== void 0 && e.logger.warn("rate-limit: SQL store bind failed — limiter runs fail-open", { error: N instanceof Error ? N.message : String(N) });
|
|
218
188
|
let n = t.store === void 0 || t.store === "memory" ? v(e.env) : null;
|
|
219
189
|
n !== null && e.logger.warn(`rate-limit: the counter store is PROCESS-LOCAL (the default), but ${n}, which means several replicas. Each pod counts on its own, so every limit is effectively multiplied by the number of replicas. Set \`store: 'redis'\` or \`store: 'postgres'\` to share the counters.`), e.logger.info("rate-limit active", {
|
|
220
190
|
rules: a.length,
|
|
@@ -223,7 +193,7 @@ var x = (e) => ({
|
|
|
223
193
|
});
|
|
224
194
|
}),
|
|
225
195
|
onDeactivate: () => m.gen(function* () {
|
|
226
|
-
M && clearInterval(M),
|
|
196
|
+
M && clearInterval(M), _.deactivate && (yield* m.promise(() => _.deactivate()));
|
|
227
197
|
})
|
|
228
198
|
});
|
|
229
199
|
};
|
package/dist/inspect.js
CHANGED
|
@@ -41,7 +41,7 @@ var t = (e) => typeof e == "function" ? "dynamic" : e, n = (e) => typeof e == "f
|
|
|
41
41
|
let e = n.size();
|
|
42
42
|
Number.isFinite(e) && e >= 0 && (i = e);
|
|
43
43
|
} catch {}
|
|
44
|
-
let c = typeof t.store == "string" ? t.store : t.store === void 0 ? "memory" : "custom";
|
|
44
|
+
let c = t.store === "redis" && t.redis?.hotStore !== void 0 ? "custom" : typeof t.store == "string" ? t.store : t.store === void 0 ? "memory" : "custom";
|
|
45
45
|
return {
|
|
46
46
|
capturedAt: Date.now(),
|
|
47
47
|
startedAt: a,
|
package/dist/postgresStore.d.ts
CHANGED
|
@@ -1,28 +1,13 @@
|
|
|
1
1
|
import { ColumnBuilder } from '@voltro/database';
|
|
2
|
+
import { ConnectionRegistryShape } from '@voltro/kv/connection';
|
|
2
3
|
import { DataStore } from '@voltro/database';
|
|
3
4
|
import { Effect } from 'effect';
|
|
4
5
|
import { FieldDefinitions } from '@voltro/database';
|
|
6
|
+
import { HotRateLimitState } from '@voltro/kv/hot';
|
|
5
7
|
import { Table } from '@voltro/database';
|
|
6
8
|
|
|
7
9
|
declare type Algorithm = 'fixed-window' | 'sliding-window' | 'token-bucket';
|
|
8
10
|
|
|
9
|
-
/** Opaque per-key state. The store persists it verbatim; only this
|
|
10
|
-
* module reads its shape. */
|
|
11
|
-
declare type BucketState = {
|
|
12
|
-
readonly kind: 'fixed';
|
|
13
|
-
readonly windowStart: number;
|
|
14
|
-
readonly count: number;
|
|
15
|
-
} | {
|
|
16
|
-
readonly kind: 'sliding';
|
|
17
|
-
readonly currStart: number;
|
|
18
|
-
readonly currCount: number;
|
|
19
|
-
readonly prevCount: number;
|
|
20
|
-
} | {
|
|
21
|
-
readonly kind: 'token';
|
|
22
|
-
readonly tokens: number;
|
|
23
|
-
readonly lastRefill: number;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
11
|
export declare const postgresStore: (options?: PostgresStoreOptions) => RateLimitStore;
|
|
27
12
|
|
|
28
13
|
export declare interface PostgresStoreOptions {
|
|
@@ -55,7 +40,7 @@ declare interface RateLimitStore {
|
|
|
55
40
|
readonly size: () => number;
|
|
56
41
|
/** Optional one-shot setup at plugin activate (open pools, create
|
|
57
42
|
* tables). Receives the process env. */
|
|
58
|
-
readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
|
|
43
|
+
readonly activate?: (env: NodeJS.ProcessEnv, connectionRegistry?: ConnectionRegistryShape) => Promise<void>;
|
|
59
44
|
/** Optional teardown at plugin deactivate (close pools). */
|
|
60
45
|
readonly deactivate?: () => Promise<void>;
|
|
61
46
|
/** Optional bind of the app's `DataStore` (the plugin's `bindDataStore`).
|
|
@@ -70,7 +55,7 @@ declare interface RateLimitStore {
|
|
|
70
55
|
export declare const rateLimitTable: Table<"_voltro_ratelimit", FieldDefinitions<{
|
|
71
56
|
readonly id: ColumnBuilder<number, "id", boolean>;
|
|
72
57
|
readonly key: ColumnBuilder<string, "text", boolean>;
|
|
73
|
-
readonly state: ColumnBuilder<
|
|
58
|
+
readonly state: ColumnBuilder< HotRateLimitState | null, "json", boolean>;
|
|
74
59
|
readonly touchedAt: ColumnBuilder<Date, "timestamp", true>;
|
|
75
60
|
readonly touched: ColumnBuilder<unknown, "text", boolean>;
|
|
76
61
|
}>, false, never>;
|
package/dist/redisStore.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { ConnectionRef } from '@voltro/kv/connection';
|
|
2
|
+
import { ConnectionRegistryShape } from '@voltro/kv/connection';
|
|
1
3
|
import { DataStore } from '@voltro/database';
|
|
2
4
|
import { Effect } from 'effect';
|
|
3
|
-
import {
|
|
5
|
+
import { HotStoreShape } from '@voltro/kv/hot';
|
|
4
6
|
|
|
5
7
|
declare type Algorithm = 'fixed-window' | 'sliding-window' | 'token-bucket';
|
|
6
8
|
|
|
@@ -27,7 +29,7 @@ declare interface RateLimitStore {
|
|
|
27
29
|
readonly size: () => number;
|
|
28
30
|
/** Optional one-shot setup at plugin activate (open pools, create
|
|
29
31
|
* tables). Receives the process env. */
|
|
30
|
-
readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
|
|
32
|
+
readonly activate?: (env: NodeJS.ProcessEnv, connectionRegistry?: ConnectionRegistryShape) => Promise<void>;
|
|
31
33
|
/** Optional teardown at plugin deactivate (close pools). */
|
|
32
34
|
readonly deactivate?: () => Promise<void>;
|
|
33
35
|
/** Optional bind of the app's `DataStore` (the plugin's `bindDataStore`).
|
|
@@ -37,32 +39,15 @@ declare interface RateLimitStore {
|
|
|
37
39
|
readonly bind?: (store: DataStore) => Promise<void>;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/** Redis limiter decisions go only through HotStore. The app owns every socket. */
|
|
40
43
|
export declare const redisStore: (options?: RedisStoreOptions) => RateLimitStore;
|
|
41
44
|
|
|
42
45
|
export declare interface RedisStoreOptions {
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
readonly
|
|
47
|
-
/**
|
|
48
|
-
* How to talk to the server — mirrors the cache plugin's axis:
|
|
49
|
-
* - `'resp'` (default) — ioredis over TCP. Works for Redis, Valkey,
|
|
50
|
-
* KeyDB, Dragonfly, and Upstash's TCP endpoint.
|
|
51
|
-
* - `'http'` — `@upstash/redis` over REST, for serverless/edge where
|
|
52
|
-
* TCP isn't available.
|
|
53
|
-
* Resolved from `CACHE_REDIS_DRIVER` env when omitted. `'redis'` as a
|
|
54
|
-
* store value is the umbrella for the whole RESP family — the server
|
|
55
|
-
* brand is just the url; only the DRIVER is a real choice.
|
|
56
|
-
*/
|
|
57
|
-
readonly driver?: 'resp' | 'http';
|
|
58
|
-
/** Auth token — `http` (Upstash REST) driver only. Falls back to
|
|
59
|
-
* `CACHE_REDIS_TOKEN` / `UPSTASH_REDIS_REST_TOKEN` env. */
|
|
60
|
-
readonly token?: string;
|
|
61
|
-
/** Bring your own already-connected RESP client (from
|
|
62
|
-
* `@voltro/kv/connection`'s `connect`, or the shared registry). Takes
|
|
63
|
-
* precedence over `url` / `driver`. */
|
|
64
|
-
readonly client?: RespClient;
|
|
65
|
-
/** Key namespace. Default `'voltro:rl:'`. */
|
|
46
|
+
/** Registry reference. Omit to use the app's default Redis connection. */
|
|
47
|
+
readonly connection?: ConnectionRef<'redis'>;
|
|
48
|
+
/** Borrow a replacement HotStore port. Its owner retains disposal. */
|
|
49
|
+
readonly hotStore?: HotStoreShape;
|
|
50
|
+
/** HotStore namespace. Default 'voltro:rl'; cannot contain braces. */
|
|
66
51
|
readonly keyPrefix?: string;
|
|
67
52
|
}
|
|
68
53
|
|
package/dist/redisStore.js
CHANGED
|
@@ -1,44 +1,48 @@
|
|
|
1
|
-
import { Effect as e } from "effect";
|
|
2
|
-
import {
|
|
1
|
+
import { Effect as e, Layer as t, ManagedRuntime as n } from "effect";
|
|
2
|
+
import { ConnectionRegistry as r } from "@voltro/kv/connection";
|
|
3
|
+
import { HotStore as i, layerRoutedHotRedis as a } from "@voltro/kv/hot";
|
|
3
4
|
//#region src/redisStore.ts
|
|
4
|
-
var
|
|
5
|
-
|
|
5
|
+
var o = (o = {}) => {
|
|
6
|
+
if (typeof o != "object" || !o || Array.isArray(o)) throw Error("Redis rate-limit options must be an object");
|
|
7
|
+
if (Object.keys(o).some((e) => ![
|
|
8
|
+
"connection",
|
|
9
|
+
"hotStore",
|
|
10
|
+
"keyPrefix"
|
|
11
|
+
].includes(e))) throw Error("Redis rate-limit options accept only connection, hotStore and keyPrefix; configure credentials in the app connection registry");
|
|
12
|
+
if (o.hotStore !== void 0 && (o.connection !== void 0 || o.keyPrefix !== void 0)) throw Error("An injected rate limiter HotStore owns its connection and namespace");
|
|
13
|
+
let s = o.hotStore, c, l, u, d = !1, f = (e, t, n) => ({
|
|
6
14
|
failOpenReason: n,
|
|
7
15
|
allowed: !0,
|
|
8
16
|
remaining: e.limit,
|
|
9
17
|
resetAtMs: t + e.windowMs,
|
|
10
18
|
retryAfterMs: 0
|
|
11
|
-
})
|
|
12
|
-
let i = l(n);
|
|
13
|
-
if (!i) throw Error("@voltro/plugin-ratelimit redisStore: no client and no url (set RATELIMIT_REDIS_URL / REDIS_URL or pass { url }).");
|
|
14
|
-
let s = r.driver ?? ((n.RATELIMIT_REDIS_DRIVER ?? n.CACHE_REDIS_DRIVER) === "http" ? "http" : "resp"), c = r.token ?? n.RATELIMIT_REDIS_TOKEN ?? n.CACHE_REDIS_TOKEN ?? n.UPSTASH_REDIS_REST_TOKEN, u = {
|
|
15
|
-
url: i,
|
|
16
|
-
driver: s,
|
|
17
|
-
...c === void 0 ? {} : { token: c }
|
|
18
|
-
};
|
|
19
|
-
a = await e.runPromise(t(u)), o = !0;
|
|
20
|
-
})().finally(() => {
|
|
21
|
-
c = void 0;
|
|
22
|
-
})), c), d = (t, r, o) => {
|
|
23
|
-
let c = a;
|
|
24
|
-
return c ? e.tryPromise(async () => {
|
|
25
|
-
let e = await c.eval(n, 1, `${i}${t}`, r.algorithm, r.limit, r.windowMs, r.burst, o);
|
|
26
|
-
return JSON.parse(String(e));
|
|
27
|
-
}).pipe(e.catch(() => e.succeed(s(r, o, "store-error")))) : e.succeed(s(r, o, "unbound"));
|
|
28
|
-
};
|
|
19
|
+
});
|
|
29
20
|
return {
|
|
30
|
-
consume: (t, n, r) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
})
|
|
21
|
+
consume: (t, n, r) => e.suspend(() => {
|
|
22
|
+
let i = s;
|
|
23
|
+
return i === void 0 || d ? e.succeed(f(n, r, "unbound")) : i.rateLimit(t, n, r).pipe(e.catch(() => e.succeed(f(n, r, "store-error"))));
|
|
24
|
+
}),
|
|
34
25
|
sweep: () => {},
|
|
35
26
|
size: () => -1,
|
|
36
|
-
activate: (e) => u(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
activate: (e, u) => d ? Promise.reject(/* @__PURE__ */ Error("Rate limiter HotStore scope is closed")) : s === void 0 ? l === void 0 ? u === void 0 ? Promise.reject(/* @__PURE__ */ Error("Redis rate limiter requires the app connection registry or an injected HotStore port")) : (l = (async () => {
|
|
28
|
+
let e = n.make(a({
|
|
29
|
+
consumer: "ratelimit",
|
|
30
|
+
namespace: o.keyPrefix ?? "voltro:rl",
|
|
31
|
+
...o.connection === void 0 ? {} : { connection: o.connection }
|
|
32
|
+
}).pipe(t.provide(t.succeed(r, u))));
|
|
33
|
+
c = e;
|
|
34
|
+
try {
|
|
35
|
+
s = await e.runPromise(i);
|
|
36
|
+
} catch {
|
|
37
|
+
throw await e.dispose(), c = void 0, Error("Redis rate limiter HotStore activation failed; check the registry reference and environment declarations");
|
|
38
|
+
}
|
|
39
|
+
})().finally(() => {
|
|
40
|
+
l = void 0;
|
|
41
|
+
}), l) : l : Promise.resolve(),
|
|
42
|
+
deactivate: () => u ??= (async () => {
|
|
43
|
+
d = !0, await l?.catch(() => void 0), s = void 0, await c?.dispose();
|
|
44
|
+
})()
|
|
41
45
|
};
|
|
42
46
|
};
|
|
43
47
|
//#endregion
|
|
44
|
-
export {
|
|
48
|
+
export { o as redisStore };
|
package/dist/window.js
CHANGED
|
@@ -1,117 +1,19 @@
|
|
|
1
|
+
import { consumeHotRateLimit as e } from "@voltro/kv/hot/contract";
|
|
1
2
|
//#region src/window.ts
|
|
2
|
-
var
|
|
3
|
+
var t = {
|
|
3
4
|
ms: 1,
|
|
4
5
|
s: 1e3,
|
|
5
6
|
m: 6e4,
|
|
6
7
|
h: 36e5,
|
|
7
8
|
d: 864e5
|
|
8
|
-
},
|
|
9
|
-
if (typeof
|
|
10
|
-
if (!Number.isFinite(
|
|
11
|
-
return
|
|
9
|
+
}, n = (e) => {
|
|
10
|
+
if (typeof e == "number") {
|
|
11
|
+
if (!Number.isFinite(e) || e <= 0) throw Error(`@voltro/plugin-ratelimit: window must be a positive number, got ${e}`);
|
|
12
|
+
return e;
|
|
12
13
|
}
|
|
13
|
-
let n = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/.exec(
|
|
14
|
-
if (!n) throw Error(`@voltro/plugin-ratelimit: invalid window "${
|
|
15
|
-
return Number(n[1]) *
|
|
16
|
-
},
|
|
17
|
-
switch (t.algorithm) {
|
|
18
|
-
case "fixed-window": return r(e, t, n);
|
|
19
|
-
case "sliding-window": return i(e, t, n);
|
|
20
|
-
case "token-bucket": return a(e, t, n);
|
|
21
|
-
}
|
|
22
|
-
}, r = (e, t, n) => {
|
|
23
|
-
let r = e?.kind === "fixed" ? e.windowStart : n, i = e?.kind === "fixed" ? e.count : 0;
|
|
24
|
-
n - r >= t.windowMs && (r = n, i = 0);
|
|
25
|
-
let a = r + t.windowMs;
|
|
26
|
-
return i >= t.limit ? {
|
|
27
|
-
state: {
|
|
28
|
-
kind: "fixed",
|
|
29
|
-
windowStart: r,
|
|
30
|
-
count: i
|
|
31
|
-
},
|
|
32
|
-
decision: {
|
|
33
|
-
allowed: !1,
|
|
34
|
-
remaining: 0,
|
|
35
|
-
resetAtMs: a,
|
|
36
|
-
retryAfterMs: a - n
|
|
37
|
-
}
|
|
38
|
-
} : (i += 1, {
|
|
39
|
-
state: {
|
|
40
|
-
kind: "fixed",
|
|
41
|
-
windowStart: r,
|
|
42
|
-
count: i
|
|
43
|
-
},
|
|
44
|
-
decision: {
|
|
45
|
-
allowed: !0,
|
|
46
|
-
remaining: t.limit - i,
|
|
47
|
-
resetAtMs: a,
|
|
48
|
-
retryAfterMs: 0
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
}, i = (e, t, n) => {
|
|
52
|
-
let r = t.windowMs, i = e?.kind === "sliding" ? e.currStart : n, a = e?.kind === "sliding" ? e.currCount : 0, o = e?.kind === "sliding" ? e.prevCount : 0, s = Math.floor((n - i) / r);
|
|
53
|
-
s >= 2 ? (o = 0, a = 0, i = n) : s === 1 && (o = a, a = 0, i += r);
|
|
54
|
-
let c = (r - (n - i)) / r, l = o * c + a, u = i + r;
|
|
55
|
-
return l >= t.limit ? {
|
|
56
|
-
state: {
|
|
57
|
-
kind: "sliding",
|
|
58
|
-
currStart: i,
|
|
59
|
-
currCount: a,
|
|
60
|
-
prevCount: o
|
|
61
|
-
},
|
|
62
|
-
decision: {
|
|
63
|
-
allowed: !1,
|
|
64
|
-
remaining: 0,
|
|
65
|
-
resetAtMs: u,
|
|
66
|
-
retryAfterMs: u - n
|
|
67
|
-
}
|
|
68
|
-
} : (a += 1, {
|
|
69
|
-
state: {
|
|
70
|
-
kind: "sliding",
|
|
71
|
-
currStart: i,
|
|
72
|
-
currCount: a,
|
|
73
|
-
prevCount: o
|
|
74
|
-
},
|
|
75
|
-
decision: {
|
|
76
|
-
allowed: !0,
|
|
77
|
-
remaining: Math.max(0, Math.floor(t.limit - l - 1)),
|
|
78
|
-
resetAtMs: u,
|
|
79
|
-
retryAfterMs: 0
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
}, a = (e, t, n) => {
|
|
83
|
-
let r = t.burst, i = t.limit / t.windowMs, a = e?.kind === "token" ? e.tokens : r, o = e?.kind === "token" ? e.lastRefill : n;
|
|
84
|
-
if (a = Math.min(r, a + (n - o) * i), a >= 1) {
|
|
85
|
-
--a;
|
|
86
|
-
let e = n + Math.ceil((r - a) / i);
|
|
87
|
-
return {
|
|
88
|
-
state: {
|
|
89
|
-
kind: "token",
|
|
90
|
-
tokens: a,
|
|
91
|
-
lastRefill: n
|
|
92
|
-
},
|
|
93
|
-
decision: {
|
|
94
|
-
allowed: !0,
|
|
95
|
-
remaining: Math.floor(a),
|
|
96
|
-
resetAtMs: e,
|
|
97
|
-
retryAfterMs: 0
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
let s = Math.ceil((1 - a) / i);
|
|
102
|
-
return {
|
|
103
|
-
state: {
|
|
104
|
-
kind: "token",
|
|
105
|
-
tokens: a,
|
|
106
|
-
lastRefill: n
|
|
107
|
-
},
|
|
108
|
-
decision: {
|
|
109
|
-
allowed: !1,
|
|
110
|
-
remaining: 0,
|
|
111
|
-
resetAtMs: n + s,
|
|
112
|
-
retryAfterMs: s
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
};
|
|
14
|
+
let n = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/.exec(e.trim());
|
|
15
|
+
if (!n) throw Error(`@voltro/plugin-ratelimit: invalid window "${e}" — use e.g. "1m", "10s", "500ms", "2h", "1d"`);
|
|
16
|
+
return Number(n[1]) * t[n[2]];
|
|
17
|
+
}, r = e;
|
|
116
18
|
//#endregion
|
|
117
|
-
export {
|
|
19
|
+
export { r as consumeBucket, n as parseWindow };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-ratelimit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.0",
|
|
4
4
|
"description": "Rate-limiting plugin — per-endpoint, per-subject and per-tenant request limits via the RPC interceptors. Highly configurable rules (fixed-window / sliding-window / token-bucket), composite keying, static per-tenant overrides and a dynamic resolve() hook. Stores: memory (default, single-node), postgres + redis (multi-node, atomic).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -48,11 +48,12 @@
|
|
|
48
48
|
"node": ">=24.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@voltro/
|
|
52
|
-
"@voltro/
|
|
53
|
-
"@voltro/
|
|
54
|
-
"@voltro/
|
|
55
|
-
"@voltro/
|
|
51
|
+
"@voltro/shared": "0.87.0",
|
|
52
|
+
"@voltro/database": "0.87.0",
|
|
53
|
+
"@voltro/kv": "0.87.0",
|
|
54
|
+
"@voltro/logger": "0.87.0",
|
|
55
|
+
"@voltro/protocol": "0.87.0",
|
|
56
|
+
"@voltro/runtime": "0.87.0"
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
59
|
"effect": "4.0.1"
|