@voltro/plugin-ratelimit 0.85.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 +5987 -0
- package/THIRD-PARTY-NOTICES.md +4298 -258
- package/dist/index.d.ts +25 -51
- package/dist/index.js +103 -264
- package/dist/inspect.js +72 -0
- package/dist/postgresStore.d.ts +4 -19
- package/dist/postgresStore.js +48 -2
- package/dist/redisStore.d.ts +10 -25
- package/dist/redisStore.js +36 -32
- package/dist/resolve.js +65 -0
- package/dist/store.js +29 -0
- package/dist/window.js +19 -0
- package/package.json +7 -5
- package/dist/postgresStore-D_oHSGXq.js +0 -160
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
|
|
|
@@ -101,14 +92,15 @@ export declare interface RateLimitContext {
|
|
|
101
92
|
/** Convenience: the subject's tenant (`null` only for unscoped anonymous). */
|
|
102
93
|
readonly tenantId: string | null;
|
|
103
94
|
readonly traceId: string;
|
|
104
|
-
/** `'rest'`
|
|
95
|
+
/** `'rest'` for a hand-authored REST route or `publicApi` projection; absent
|
|
105
96
|
* on the rpc wire. */
|
|
106
97
|
readonly surface?: 'rest';
|
|
107
|
-
/** The descriptor's own `publicApi.rateLimit`,
|
|
108
|
-
*
|
|
98
|
+
/** The descriptor's own REST quota — `rateLimit` or `publicApi.rateLimit`,
|
|
99
|
+
* ahead of `rules` and `default`. */
|
|
109
100
|
readonly publicApiRateLimit?: {
|
|
110
101
|
readonly limit: number;
|
|
111
102
|
readonly window: string;
|
|
103
|
+
readonly by?: 'subject' | 'tenant' | 'apiKey' | 'ip' | 'global';
|
|
112
104
|
};
|
|
113
105
|
/** The caller's address, resolved by the listener through the app's
|
|
114
106
|
* `security.trustedProxies` — what `by: 'ip'` keys on. Absent for a call
|
|
@@ -179,17 +171,16 @@ export declare interface RateLimitPluginOptions {
|
|
|
179
171
|
/**
|
|
180
172
|
* Counter backend:
|
|
181
173
|
* - `'memory'` (default) — single-node, zero-config.
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* `CACHE_REDIS_URL`/`REDIS_URL`, runs the whole decision in one
|
|
188
|
-
* atomic Lua script. For a custom client/url use the
|
|
189
|
-
* `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.
|
|
190
179
|
* - a custom `RateLimitStore`.
|
|
191
180
|
*/
|
|
192
181
|
readonly store?: 'memory' | 'postgres' | 'redis' | RateLimitStore;
|
|
182
|
+
/** Registry reference or replacement HotStore for the Redis counter backend. */
|
|
183
|
+
readonly redis?: RedisStoreOptions;
|
|
193
184
|
/**
|
|
194
185
|
* Master gate. `false` disables all limiting; a function lets you gate
|
|
195
186
|
* per-call (e.g. exempt a tenant, or a maintenance bypass). Returning
|
|
@@ -262,7 +253,7 @@ export declare interface RateLimitStore {
|
|
|
262
253
|
readonly size: () => number;
|
|
263
254
|
/** Optional one-shot setup at plugin activate (open pools, create
|
|
264
255
|
* tables). Receives the process env. */
|
|
265
|
-
readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
|
|
256
|
+
readonly activate?: (env: NodeJS.ProcessEnv, connectionRegistry?: ConnectionRegistryShape) => Promise<void>;
|
|
266
257
|
/** Optional teardown at plugin deactivate (close pools). */
|
|
267
258
|
readonly deactivate?: () => Promise<void>;
|
|
268
259
|
/** Optional bind of the app's `DataStore` (the plugin's `bindDataStore`).
|
|
@@ -277,37 +268,20 @@ export declare interface RateLimitStore {
|
|
|
277
268
|
export declare const rateLimitTable: Table<"_voltro_ratelimit", FieldDefinitions<{
|
|
278
269
|
readonly id: ColumnBuilder<number, "id", boolean>;
|
|
279
270
|
readonly key: ColumnBuilder<string, "text", boolean>;
|
|
280
|
-
readonly state: ColumnBuilder<
|
|
271
|
+
readonly state: ColumnBuilder< HotRateLimitState | null, "json", boolean>;
|
|
281
272
|
readonly touchedAt: ColumnBuilder<Date, "timestamp", true>;
|
|
282
273
|
readonly touched: ColumnBuilder<unknown, "text", boolean>;
|
|
283
274
|
}>, false, never>;
|
|
284
275
|
|
|
276
|
+
/** Redis limiter decisions go only through HotStore. The app owns every socket. */
|
|
285
277
|
export declare const redisStore: (options?: RedisStoreOptions) => RateLimitStore;
|
|
286
278
|
|
|
287
279
|
export declare interface RedisStoreOptions {
|
|
288
|
-
/**
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
readonly
|
|
292
|
-
/**
|
|
293
|
-
* How to talk to the server — mirrors the cache plugin's axis:
|
|
294
|
-
* - `'resp'` (default) — ioredis over TCP. Works for Redis, Valkey,
|
|
295
|
-
* KeyDB, Dragonfly, and Upstash's TCP endpoint.
|
|
296
|
-
* - `'http'` — `@upstash/redis` over REST, for serverless/edge where
|
|
297
|
-
* TCP isn't available.
|
|
298
|
-
* Resolved from `CACHE_REDIS_DRIVER` env when omitted. `'redis'` as a
|
|
299
|
-
* store value is the umbrella for the whole RESP family — the server
|
|
300
|
-
* brand is just the url; only the DRIVER is a real choice.
|
|
301
|
-
*/
|
|
302
|
-
readonly driver?: 'resp' | 'http';
|
|
303
|
-
/** Auth token — `http` (Upstash REST) driver only. Falls back to
|
|
304
|
-
* `CACHE_REDIS_TOKEN` / `UPSTASH_REDIS_REST_TOKEN` env. */
|
|
305
|
-
readonly token?: string;
|
|
306
|
-
/** Bring your own already-connected RESP client (from
|
|
307
|
-
* `@voltro/kv/connection`'s `connect`, or the shared registry). Takes
|
|
308
|
-
* precedence over `url` / `driver`. */
|
|
309
|
-
readonly client?: RespClient;
|
|
310
|
-
/** 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. */
|
|
311
285
|
readonly keyPrefix?: string;
|
|
312
286
|
}
|
|
313
287
|
|
package/dist/index.js
CHANGED
|
@@ -1,162 +1,18 @@
|
|
|
1
1
|
import { RateLimited as e } from "./errors.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
burst: e.burst === void 0 ? f(e.limit) : f(e.burst),
|
|
17
|
-
by: Array.isArray(e.by) ? e.by.map(p) : [p(e.by ?? "subject")],
|
|
18
|
-
scope: e.scope ?? "tag",
|
|
19
|
-
tenantOverrides: Object.keys(e.tenants ?? {}).length
|
|
20
|
-
}), h = () => ({
|
|
21
|
-
allowed: 0,
|
|
22
|
-
rejected: 0,
|
|
23
|
-
bypassed: 0,
|
|
24
|
-
unbound: 0,
|
|
25
|
-
storeErrors: 0,
|
|
26
|
-
lastRejectedAt: null,
|
|
27
|
-
lastStoreErrorAt: null
|
|
28
|
-
}), g = (e, n) => {
|
|
29
|
-
let r = Date.now(), i = {
|
|
30
|
-
rpc: h(),
|
|
31
|
-
http: h()
|
|
32
|
-
}, a = n.bind ? "waiting" : "not-required";
|
|
33
|
-
return {
|
|
34
|
-
binding: (e) => {
|
|
35
|
-
a = e;
|
|
36
|
-
},
|
|
37
|
-
bypass: () => {
|
|
38
|
-
i.rpc.bypassed++;
|
|
39
|
-
},
|
|
40
|
-
record: (e, t) => {
|
|
41
|
-
let n = i[e];
|
|
42
|
-
t.failOpenReason === "unbound" ? n.unbound++ : t.failOpenReason === "store-error" ? (n.storeErrors++, n.lastStoreErrorAt = Date.now()) : t.allowed ? n.allowed++ : (n.rejected++, n.lastRejectedAt = Date.now());
|
|
43
|
-
},
|
|
44
|
-
snapshot: () => {
|
|
45
|
-
let o = null;
|
|
46
|
-
try {
|
|
47
|
-
let e = n.size();
|
|
48
|
-
Number.isFinite(e) && e >= 0 && (o = e);
|
|
49
|
-
} catch {}
|
|
50
|
-
let s = typeof e.store == "string" ? e.store : e.store === void 0 ? "memory" : "custom";
|
|
51
|
-
return {
|
|
52
|
-
capturedAt: Date.now(),
|
|
53
|
-
startedAt: r,
|
|
54
|
-
rpcGate: e.enabled === !1 ? "disabled" : typeof e.enabled == "function" ? "dynamic" : "enabled",
|
|
55
|
-
dynamicResolver: e.resolve !== void 0,
|
|
56
|
-
store: {
|
|
57
|
-
kind: s,
|
|
58
|
-
scope: s === "memory" ? "process" : s === "custom" ? "unknown" : "shared-store",
|
|
59
|
-
sqlBinding: a,
|
|
60
|
-
trackedBuckets: o
|
|
61
|
-
},
|
|
62
|
-
rules: (e.rules ?? []).map((e, t) => m(e, t)),
|
|
63
|
-
defaultRule: e.default ? m(e.default, null) : null,
|
|
64
|
-
http: e.http ? {
|
|
65
|
-
limit: e.http.limit,
|
|
66
|
-
windowMs: t(e.http.window),
|
|
67
|
-
algorithm: e.http.algorithm ?? "fixed-window"
|
|
68
|
-
} : null,
|
|
69
|
-
counters: {
|
|
70
|
-
rpc: { ...i.rpc },
|
|
71
|
-
http: { ...i.http }
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
}, _ = (e = {}) => {
|
|
77
|
-
let t = e.maxKeys ?? 1e5, r = e.staleAfterMs ?? 36e5, i = /* @__PURE__ */ new Map(), a = (e) => {
|
|
78
|
-
if (i.size <= t) return;
|
|
79
|
-
let n = i.size - t, r = 0;
|
|
80
|
-
for (let t of i.keys()) {
|
|
81
|
-
if (r >= n) break;
|
|
82
|
-
t !== e && (i.delete(t), r += 1);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
return {
|
|
86
|
-
consume: (e, t, r) => s.sync(() => {
|
|
87
|
-
let o = i.get(e), { state: s, decision: c } = n(o?.state, t, r);
|
|
88
|
-
return i.set(e, {
|
|
89
|
-
state: s,
|
|
90
|
-
touched: r
|
|
91
|
-
}), a(e), c;
|
|
92
|
-
}),
|
|
93
|
-
sweep: (e) => {
|
|
94
|
-
let t = e - r;
|
|
95
|
-
for (let [e, n] of i) n.touched < t && i.delete(e);
|
|
96
|
-
},
|
|
97
|
-
size: () => i.size
|
|
98
|
-
};
|
|
99
|
-
}, v = (e, t) => typeof e == "function" ? e(t) : e, y = /* @__PURE__ */ new WeakMap(), b = (e) => {
|
|
100
|
-
if (!e.global && !e.sticky) return e;
|
|
101
|
-
let t = y.get(e);
|
|
102
|
-
return t === void 0 && (t = new RegExp(e.source, e.flags.replace(/[gy]/g, "")), y.set(e, t)), t;
|
|
103
|
-
}, x = (e, t) => e === void 0 ? !0 : typeof e == "string" ? e === t : Array.isArray(e) ? e.includes(t) : b(e).test(t), S = (e, t) => e === void 0 ? !0 : Array.isArray(e) ? e.includes(t) : e === t, C = (e, t) => {
|
|
104
|
-
let n = t === null ? void 0 : e.tenants?.[t];
|
|
105
|
-
return {
|
|
106
|
-
limit: n?.limit ?? e.limit,
|
|
107
|
-
window: n?.window ?? e.window,
|
|
108
|
-
algorithm: n?.algorithm ?? e.algorithm,
|
|
109
|
-
burst: n?.burst ?? e.burst,
|
|
110
|
-
by: e.by ?? "subject",
|
|
111
|
-
scope: e.scope ?? "tag",
|
|
112
|
-
disabled: n?.disabled ?? !1
|
|
113
|
-
};
|
|
114
|
-
}, w = (e, t, n) => {
|
|
115
|
-
for (let t = 0; t < e.length; t++) {
|
|
116
|
-
let r = e[t];
|
|
117
|
-
if (x(r.match, n.tag) && S(r.kind, n.kind)) return {
|
|
118
|
-
effective: C(r, n.tenantId),
|
|
119
|
-
ruleId: `r${t}`
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
if (t && S(t.kind, n.kind)) return {
|
|
123
|
-
effective: C(t, n.tenantId),
|
|
124
|
-
ruleId: "default"
|
|
125
|
-
};
|
|
126
|
-
}, T = (e, n) => {
|
|
127
|
-
let r = v(e.limit, n);
|
|
128
|
-
return {
|
|
129
|
-
limit: r,
|
|
130
|
-
windowMs: t(v(e.window, n)),
|
|
131
|
-
algorithm: e.algorithm ?? "sliding-window",
|
|
132
|
-
burst: e.burst === void 0 ? r : v(e.burst, n)
|
|
133
|
-
};
|
|
134
|
-
}, E = (e) => {
|
|
135
|
-
let n = e.limit;
|
|
136
|
-
return {
|
|
137
|
-
limit: n,
|
|
138
|
-
windowMs: t(e.window),
|
|
139
|
-
algorithm: e.algorithm ?? "sliding-window",
|
|
140
|
-
burst: e.burst ?? n
|
|
141
|
-
};
|
|
142
|
-
}, D = (e) => ({
|
|
143
|
-
limit: e.limit,
|
|
144
|
-
window: e.windowMs,
|
|
145
|
-
algorithm: e.algorithm,
|
|
146
|
-
burst: e.burst
|
|
147
|
-
}), O = (e, t) => {
|
|
148
|
-
if (typeof e == "function") return e(t);
|
|
149
|
-
switch (e) {
|
|
150
|
-
case "subject": return `s:${t.tenantId ?? "∅"}:${t.subject.id ?? "anon"}`;
|
|
151
|
-
case "tenant": return `t:${t.tenantId ?? "∅"}`;
|
|
152
|
-
case "apiKey": return t.subject.type === "apiKey" ? `k:${t.subject.id}` : `s:${t.tenantId ?? "∅"}:${t.subject.id ?? "anon"}`;
|
|
153
|
-
case "global": return "g";
|
|
154
|
-
case "ip": return `i:${t.remoteAddr ?? "unknown"}`;
|
|
155
|
-
}
|
|
156
|
-
}, k = (e, t) => {
|
|
157
|
-
let { effective: n, ruleId: r } = e, i = n.scope === "rule" ? `rule#${r}` : `tag:${t.tag}`, a = n.by;
|
|
158
|
-
return `${i}|${Array.isArray(a) ? a.map((e) => O(e, t)).join("|") : O(a, t)}`;
|
|
159
|
-
}, A = (e) => ({
|
|
2
|
+
import { consumeBucket as t, parseWindow as n } from "./window.js";
|
|
3
|
+
import { makeRateLimitInspect as r } from "./inspect.js";
|
|
4
|
+
import { memoryStore as i } from "./store.js";
|
|
5
|
+
import { RATELIMIT_TABLE as a, postgresStore as o, rateLimitTable as s } from "./postgresStore.js";
|
|
6
|
+
import { redisStore as c } from "./redisStore.js";
|
|
7
|
+
import { buildKey as l, limitFromOverride as u, matchRule as d, resolveLimit as f, toOverride as p } from "./resolve.js";
|
|
8
|
+
import { Effect as m } from "effect";
|
|
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
|
+
import { replicaEvidence as v } from "@voltro/protocol/identity";
|
|
13
|
+
import { definePlugin as y, pluginInstanceName as b } from "@voltro/protocol";
|
|
14
|
+
//#region src/index.ts
|
|
15
|
+
var x = (e) => ({
|
|
160
16
|
tag: e.tag,
|
|
161
17
|
kind: e.kind,
|
|
162
18
|
subject: e.subject,
|
|
@@ -165,18 +21,18 @@ var f = (e) => typeof e == "function" ? "dynamic" : e, p = (e) => typeof e == "f
|
|
|
165
21
|
...e.surface === void 0 ? {} : { surface: e.surface },
|
|
166
22
|
...e.remoteAddr === void 0 ? {} : { remoteAddr: e.remoteAddr },
|
|
167
23
|
...e.surface === "rest" && e.publicApi?.rateLimit !== void 0 ? { publicApiRateLimit: e.publicApi.rateLimit } : {}
|
|
168
|
-
}),
|
|
24
|
+
}), S = (e) => e.publicApiRateLimit === void 0 ? void 0 : {
|
|
169
25
|
ruleId: "publicApi",
|
|
170
26
|
effective: {
|
|
171
27
|
limit: e.publicApiRateLimit.limit,
|
|
172
28
|
window: e.publicApiRateLimit.window,
|
|
173
29
|
algorithm: void 0,
|
|
174
30
|
burst: void 0,
|
|
175
|
-
by: "subject",
|
|
31
|
+
by: e.publicApiRateLimit.by ?? "subject",
|
|
176
32
|
scope: "tag",
|
|
177
33
|
disabled: !1
|
|
178
34
|
}
|
|
179
|
-
},
|
|
35
|
+
}, C = (e) => {
|
|
180
36
|
let t = e, n = Date.now();
|
|
181
37
|
return {
|
|
182
38
|
"retry-after": String(Math.max(1, Math.ceil(t.retryAfterMs / 1e3))),
|
|
@@ -184,143 +40,113 @@ var f = (e) => typeof e == "function" ? "dynamic" : e, p = (e) => typeof e == "f
|
|
|
184
40
|
"ratelimit-remaining": "0",
|
|
185
41
|
"ratelimit-reset": String(Math.max(0, Math.ceil((t.resetAtMs - n) / 1e3)))
|
|
186
42
|
};
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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) => {
|
|
46
|
+
let n = t.enabled;
|
|
47
|
+
return n === void 0 ? !0 : typeof n == "function" ? n(e) : n;
|
|
48
|
+
}, D = (e, n, r, i) => {
|
|
49
|
+
if (!t.onLimited) return;
|
|
193
50
|
let a = {
|
|
194
51
|
tag: e.tag,
|
|
195
52
|
kind: e.kind,
|
|
196
|
-
key:
|
|
53
|
+
key: n,
|
|
197
54
|
tenantId: e.tenantId,
|
|
198
55
|
limit: r.limit,
|
|
199
56
|
windowMs: r.windowMs,
|
|
200
57
|
retryAfterMs: i.retryAfterMs
|
|
201
58
|
};
|
|
202
59
|
try {
|
|
203
|
-
|
|
60
|
+
t.onLimited(a);
|
|
204
61
|
} catch {}
|
|
205
|
-
},
|
|
206
|
-
let r =
|
|
207
|
-
return r ?
|
|
208
|
-
let
|
|
209
|
-
return (
|
|
210
|
-
}) :
|
|
211
|
-
},
|
|
212
|
-
let i =
|
|
213
|
-
if (!
|
|
214
|
-
let o =
|
|
215
|
-
if (!o || o.effective.disabled) return
|
|
216
|
-
let
|
|
217
|
-
return
|
|
218
|
-
if (
|
|
219
|
-
let r =
|
|
220
|
-
return
|
|
62
|
+
}, O = (e, n) => {
|
|
63
|
+
let r = t.resolve;
|
|
64
|
+
return r ? m.suspend(() => {
|
|
65
|
+
let t = r(e, n);
|
|
66
|
+
return (m.isEffect(t) ? t : t instanceof Promise ? m.promise(() => t) : m.succeed(t)).pipe(m.map((e) => e === "exempt" ? "exempt" : u(e === void 0 ? n : e)));
|
|
67
|
+
}) : m.succeed(u(n));
|
|
68
|
+
}, k = (n, r) => m.suspend(() => {
|
|
69
|
+
let i = x(r);
|
|
70
|
+
if (!E(i)) return T.bypass(), n;
|
|
71
|
+
let o = S(i) ?? d(a, t.default, i);
|
|
72
|
+
if (!o || o.effective.disabled) return T.bypass(), n;
|
|
73
|
+
let s = p(f(o.effective, i));
|
|
74
|
+
return O(i, s).pipe(m.flatMap((t) => {
|
|
75
|
+
if (t === "exempt") return T.bypass(), n;
|
|
76
|
+
let r = l(o, i);
|
|
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)}`), {
|
|
221
78
|
allowed: !0,
|
|
222
79
|
failOpenReason: "store-error",
|
|
223
|
-
remaining:
|
|
80
|
+
remaining: t.limit,
|
|
224
81
|
resetAtMs: Date.now(),
|
|
225
82
|
retryAfterMs: 0
|
|
226
|
-
})),
|
|
83
|
+
})), m.tap((e) => m.sync(() => T.record("rpc", e))), m.flatMap((a) => a.allowed ? n : m.andThen(m.sync(() => D(i, r, t, a)), m.fail(new e({
|
|
227
84
|
tag: i.tag,
|
|
228
|
-
limit:
|
|
229
|
-
windowMs:
|
|
85
|
+
limit: t.limit,
|
|
86
|
+
windowMs: t.windowMs,
|
|
230
87
|
retryAfterMs: a.retryAfterMs,
|
|
231
88
|
resetAtMs: a.resetAtMs
|
|
232
89
|
})))));
|
|
233
90
|
}));
|
|
234
|
-
}),
|
|
235
|
-
let i =
|
|
91
|
+
}), A = t.http ? async (e, r) => {
|
|
92
|
+
let i = t.http, a = {
|
|
236
93
|
limit: i.limit,
|
|
237
|
-
windowMs:
|
|
94
|
+
windowMs: n(i.window),
|
|
238
95
|
algorithm: i.algorithm ?? "fixed-window",
|
|
239
96
|
burst: i.limit
|
|
240
|
-
}, o = r.remoteAddr ?? "unknown",
|
|
97
|
+
}, o = r.remoteAddr ?? "unknown", s = Date.now(), c;
|
|
241
98
|
try {
|
|
242
|
-
|
|
99
|
+
c = await m.runPromise(_.consume(`http:ip:${o}`, a, s));
|
|
243
100
|
} catch (t) {
|
|
244
|
-
return
|
|
101
|
+
return T.record("http", {
|
|
245
102
|
allowed: !0,
|
|
246
103
|
failOpenReason: "store-error",
|
|
247
104
|
remaining: 0,
|
|
248
|
-
resetAtMs:
|
|
105
|
+
resetAtMs: s,
|
|
249
106
|
retryAfterMs: 0
|
|
250
|
-
}),
|
|
107
|
+
}), w.warn("http shield store failure — request ALLOWED without a limit check", { cause: String(t) }), e();
|
|
251
108
|
}
|
|
252
|
-
|
|
253
|
-
let
|
|
254
|
-
return
|
|
109
|
+
T.record("http", c);
|
|
110
|
+
let l = String(Math.max(0, Math.ceil((c.resetAtMs - s) / 1e3)));
|
|
111
|
+
return c.allowed ? e() : {
|
|
255
112
|
status: 429,
|
|
256
113
|
body: "Too Many Requests",
|
|
257
114
|
headers: {
|
|
258
115
|
"content-type": "text/plain; charset=utf-8",
|
|
259
|
-
"retry-after": String(Math.max(1, Math.ceil(
|
|
116
|
+
"retry-after": String(Math.max(1, Math.ceil(c.retryAfterMs / 1e3))),
|
|
260
117
|
"ratelimit-limit": String(a.limit),
|
|
261
|
-
"ratelimit-remaining": String(Math.max(0,
|
|
262
|
-
"ratelimit-reset":
|
|
118
|
+
"ratelimit-remaining": String(Math.max(0, c.remaining)),
|
|
119
|
+
"ratelimit-reset": l
|
|
263
120
|
}
|
|
264
121
|
};
|
|
265
|
-
} : void 0,
|
|
122
|
+
} : void 0, j = [
|
|
266
123
|
"inspect:read",
|
|
267
124
|
"rpc:intercept:mutation",
|
|
268
125
|
"rpc:intercept:query",
|
|
269
126
|
"rpc:intercept:action",
|
|
270
127
|
"rpc:intercept:workflow",
|
|
271
|
-
|
|
272
|
-
...
|
|
273
|
-
],
|
|
274
|
-
return
|
|
275
|
-
name:
|
|
128
|
+
"http:intercept",
|
|
129
|
+
...t.store === "postgres" ? ["store:write"] : []
|
|
130
|
+
], M, N;
|
|
131
|
+
return y({
|
|
132
|
+
name: b({
|
|
276
133
|
base: "@voltro/plugin-ratelimit",
|
|
277
|
-
instance:
|
|
134
|
+
instance: t.name
|
|
278
135
|
}),
|
|
279
136
|
description: "Per-endpoint / per-subject / per-tenant rate limiting via the rpc interceptors. Configurable rules, three algorithms, static + dynamic per-tenant overrides.",
|
|
280
|
-
permissions:
|
|
137
|
+
permissions: j,
|
|
281
138
|
inspectEndpoints: [{
|
|
282
139
|
method: "GET",
|
|
283
140
|
path: "/state",
|
|
284
141
|
scope: "process",
|
|
285
142
|
description: "Declared rules and process-local limiting evidence, without bucket identities.",
|
|
286
|
-
handler: () =>
|
|
143
|
+
handler: () => m.sync(() => ({
|
|
287
144
|
kind: "json",
|
|
288
|
-
data:
|
|
145
|
+
data: T.snapshot()
|
|
289
146
|
}))
|
|
290
147
|
}],
|
|
291
|
-
declaredEnv: [
|
|
292
|
-
|
|
293
|
-
name: "CACHE_REDIS_URL",
|
|
294
|
-
required: !1,
|
|
295
|
-
secret: !0,
|
|
296
|
-
description: "Redis connection URL for the redis store (credential-carrying). Falls back to REDIS_URL."
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
name: "REDIS_URL",
|
|
300
|
-
required: !1,
|
|
301
|
-
secret: !0,
|
|
302
|
-
description: "Fallback Redis connection URL for the redis store (credential-carrying)."
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
name: "CACHE_REDIS_DRIVER",
|
|
306
|
-
required: !1,
|
|
307
|
-
secret: !1,
|
|
308
|
-
description: "Redis driver selector: \"http\" for the Upstash REST client, otherwise RESP."
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
name: "CACHE_REDIS_TOKEN",
|
|
312
|
-
required: !1,
|
|
313
|
-
secret: !0,
|
|
314
|
-
description: "Upstash REST token for the http redis driver. Falls back to UPSTASH_REDIS_REST_TOKEN."
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
name: "UPSTASH_REDIS_REST_TOKEN",
|
|
318
|
-
required: !1,
|
|
319
|
-
secret: !0,
|
|
320
|
-
description: "Fallback Upstash REST token for the http redis driver."
|
|
321
|
-
}
|
|
322
|
-
],
|
|
323
|
-
...y ? { onHttpRequest: y } : {},
|
|
148
|
+
declaredEnv: [],
|
|
149
|
+
...A ? { onHttpRequest: A } : {},
|
|
324
150
|
errorSchemas: [{
|
|
325
151
|
schema: e,
|
|
326
152
|
import: {
|
|
@@ -329,34 +155,47 @@ var f = (e) => typeof e == "function" ? "dynamic" : e, p = (e) => typeof e == "f
|
|
|
329
155
|
},
|
|
330
156
|
rest: {
|
|
331
157
|
status: 429,
|
|
332
|
-
headers:
|
|
158
|
+
headers: C
|
|
333
159
|
}
|
|
334
160
|
}],
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
161
|
+
restRateLimit: (t, n, r) => k(m.void, {
|
|
162
|
+
tag: `${r.method} ${r.path}`,
|
|
163
|
+
kind: "action",
|
|
164
|
+
input: void 0,
|
|
165
|
+
subject: n.subject,
|
|
166
|
+
traceId: h(),
|
|
167
|
+
surface: "rest",
|
|
168
|
+
publicApi: { rateLimit: t },
|
|
169
|
+
...n.remoteAddr === void 0 ? {} : { remoteAddr: n.remoteAddr }
|
|
170
|
+
}).pipe(m.catch((t) => m.fail(t instanceof e ? g(429, {
|
|
171
|
+
error: "RateLimited",
|
|
172
|
+
message: "Rate limit exceeded"
|
|
173
|
+
}, C(t)) : t))),
|
|
174
|
+
interceptMutation: k,
|
|
175
|
+
interceptQuery: k,
|
|
176
|
+
interceptAction: k,
|
|
177
|
+
interceptWorkflow: k,
|
|
178
|
+
...t.store === "postgres" ? { extendSchema: { tables: [s] } } : {},
|
|
340
179
|
bindDataStore: (e) => {
|
|
341
|
-
|
|
342
|
-
|
|
180
|
+
_.bind && (T.binding("binding"), _.bind(e).then(() => {
|
|
181
|
+
N = void 0, T.binding("bound");
|
|
343
182
|
}).catch((e) => {
|
|
344
|
-
|
|
183
|
+
T.binding("failed"), N = e;
|
|
345
184
|
}));
|
|
346
185
|
},
|
|
347
|
-
onActivate: (e) =>
|
|
348
|
-
|
|
349
|
-
let
|
|
350
|
-
|
|
186
|
+
onActivate: (e) => m.gen(function* () {
|
|
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) });
|
|
188
|
+
let n = t.store === void 0 || t.store === "memory" ? v(e.env) : null;
|
|
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", {
|
|
351
190
|
rules: a.length,
|
|
352
|
-
hasDefault:
|
|
353
|
-
store: typeof
|
|
191
|
+
hasDefault: t.default !== void 0,
|
|
192
|
+
store: typeof t.store == "string" ? t.store : t.store === void 0 ? "memory" : "custom"
|
|
354
193
|
});
|
|
355
194
|
}),
|
|
356
|
-
onDeactivate: () =>
|
|
357
|
-
|
|
195
|
+
onDeactivate: () => m.gen(function* () {
|
|
196
|
+
M && clearInterval(M), _.deactivate && (yield* m.promise(() => _.deactivate()));
|
|
358
197
|
})
|
|
359
198
|
});
|
|
360
199
|
};
|
|
361
200
|
//#endregion
|
|
362
|
-
export { a as RATELIMIT_TABLE, e as RateLimited,
|
|
201
|
+
export { a as RATELIMIT_TABLE, e as RateLimited, t as consumeBucket, i as memoryStore, n as parseWindow, o as postgresStore, T as rateLimitPlugin, s as rateLimitTable, c as redisStore };
|