@voltro/plugin-ratelimit 0.32.0 → 0.34.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 +2006 -0
- package/dist/index.js +62 -55
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@ import { RateLimited as e } from "./errors.js";
|
|
|
2
2
|
import { n as t, t as n } from "./window-lyQ6LXhj.js";
|
|
3
3
|
import { redisStore as r } from "./redisStore.js";
|
|
4
4
|
import { Effect as i } from "effect";
|
|
5
|
-
import { definePlugin as a } from "@voltro/protocol";
|
|
5
|
+
import { definePlugin as a, pluginInstanceName as o } from "@voltro/protocol";
|
|
6
6
|
//#region src/store.ts
|
|
7
|
-
var
|
|
7
|
+
var s = (e = {}) => {
|
|
8
8
|
let t = e.maxKeys ?? 1e5, r = e.staleAfterMs ?? 36e5, a = /* @__PURE__ */ new Map(), o = (e) => {
|
|
9
9
|
if (a.size <= t) return;
|
|
10
10
|
let n = a.size - t, r = 0;
|
|
@@ -27,15 +27,15 @@ var o = (e = {}) => {
|
|
|
27
27
|
},
|
|
28
28
|
size: () => a.size
|
|
29
29
|
};
|
|
30
|
-
},
|
|
30
|
+
}, c = (e, t) => ({
|
|
31
31
|
allowed: !0,
|
|
32
32
|
remaining: e.limit,
|
|
33
33
|
resetAtMs: t + e.windowMs,
|
|
34
34
|
retryAfterMs: 0
|
|
35
|
-
}),
|
|
35
|
+
}), l = (e = {}) => {
|
|
36
36
|
let t = null;
|
|
37
37
|
return {
|
|
38
|
-
consume: (e, n, r) => t ? t.consume(e, n, r) : i.succeed(
|
|
38
|
+
consume: (e, n, r) => t ? t.consume(e, n, r) : i.succeed(c(n, r)),
|
|
39
39
|
sweep: (e) => t?.sweep(e),
|
|
40
40
|
size: () => t ? t.size() : -1,
|
|
41
41
|
bindSql: async (n) => {
|
|
@@ -43,7 +43,11 @@ var o = (e = {}) => {
|
|
|
43
43
|
await i.bindSql(n), t = i;
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
-
},
|
|
46
|
+
}, u = (e, t) => typeof e == "function" ? e(t) : e, d = /* @__PURE__ */ new WeakMap(), f = (e) => {
|
|
47
|
+
if (!e.global && !e.sticky) return e;
|
|
48
|
+
let t = d.get(e);
|
|
49
|
+
return t === void 0 && (t = new RegExp(e.source, e.flags.replace(/[gy]/g, "")), d.set(e, t)), t;
|
|
50
|
+
}, p = (e, t) => e === void 0 ? !0 : typeof e == "string" ? e === t : Array.isArray(e) ? e.includes(t) : f(e).test(t), m = (e, t) => e === void 0 ? !0 : Array.isArray(e) ? e.includes(t) : e === t, h = (e, t) => {
|
|
47
51
|
let n = t === null ? void 0 : e.tenants?.[t];
|
|
48
52
|
return {
|
|
49
53
|
limit: n?.limit ?? e.limit,
|
|
@@ -54,27 +58,27 @@ var o = (e = {}) => {
|
|
|
54
58
|
scope: e.scope ?? "tag",
|
|
55
59
|
disabled: n?.disabled ?? !1
|
|
56
60
|
};
|
|
57
|
-
},
|
|
61
|
+
}, g = (e, t, n) => {
|
|
58
62
|
for (let t = 0; t < e.length; t++) {
|
|
59
63
|
let r = e[t];
|
|
60
|
-
if (
|
|
61
|
-
effective:
|
|
64
|
+
if (p(r.match, n.tag) && m(r.kind, n.kind)) return {
|
|
65
|
+
effective: h(r, n.tenantId),
|
|
62
66
|
ruleId: `r${t}`
|
|
63
67
|
};
|
|
64
68
|
}
|
|
65
|
-
if (t &&
|
|
66
|
-
effective:
|
|
69
|
+
if (t && m(t.kind, n.kind)) return {
|
|
70
|
+
effective: h(t, n.tenantId),
|
|
67
71
|
ruleId: "default"
|
|
68
72
|
};
|
|
69
|
-
},
|
|
70
|
-
let r =
|
|
73
|
+
}, _ = (e, n) => {
|
|
74
|
+
let r = u(e.limit, n);
|
|
71
75
|
return {
|
|
72
76
|
limit: r,
|
|
73
|
-
windowMs: t(
|
|
77
|
+
windowMs: t(u(e.window, n)),
|
|
74
78
|
algorithm: e.algorithm ?? "sliding-window",
|
|
75
|
-
burst: e.burst === void 0 ? r :
|
|
79
|
+
burst: e.burst === void 0 ? r : u(e.burst, n)
|
|
76
80
|
};
|
|
77
|
-
},
|
|
81
|
+
}, v = (e) => {
|
|
78
82
|
let n = e.limit;
|
|
79
83
|
return {
|
|
80
84
|
limit: n,
|
|
@@ -82,12 +86,12 @@ var o = (e = {}) => {
|
|
|
82
86
|
algorithm: e.algorithm ?? "sliding-window",
|
|
83
87
|
burst: e.burst ?? n
|
|
84
88
|
};
|
|
85
|
-
},
|
|
89
|
+
}, y = (e) => ({
|
|
86
90
|
limit: e.limit,
|
|
87
91
|
window: e.windowMs,
|
|
88
92
|
algorithm: e.algorithm,
|
|
89
93
|
burst: e.burst
|
|
90
|
-
}),
|
|
94
|
+
}), b = (e, t) => {
|
|
91
95
|
if (typeof e == "function") return e(t);
|
|
92
96
|
switch (e) {
|
|
93
97
|
case "subject": return `s:${t.tenantId ?? "∅"}:${t.subject.id ?? "anon"}`;
|
|
@@ -95,20 +99,20 @@ var o = (e = {}) => {
|
|
|
95
99
|
case "apiKey": return t.subject.type === "apiKey" ? `k:${t.subject.id}` : `s:${t.tenantId ?? "∅"}:${t.subject.id ?? "anon"}`;
|
|
96
100
|
case "global": return "g";
|
|
97
101
|
}
|
|
98
|
-
},
|
|
102
|
+
}, x = (e, t) => {
|
|
99
103
|
let { effective: n, ruleId: r } = e, i = n.scope === "rule" ? `rule#${r}` : `tag:${t.tag}`, a = n.by;
|
|
100
|
-
return `${i}|${Array.isArray(a) ? a.map((e) =>
|
|
101
|
-
},
|
|
104
|
+
return `${i}|${Array.isArray(a) ? a.map((e) => b(e, t)).join("|") : b(a, t)}`;
|
|
105
|
+
}, S = (e) => ({
|
|
102
106
|
tag: e.tag,
|
|
103
107
|
kind: e.kind,
|
|
104
108
|
subject: e.subject,
|
|
105
109
|
tenantId: e.subject.tenantId ?? null,
|
|
106
110
|
traceId: e.traceId
|
|
107
|
-
}),
|
|
108
|
-
let
|
|
111
|
+
}), C = (n = {}) => {
|
|
112
|
+
let c = n.rules ?? [], u = n.store === void 0 || n.store === "memory" ? s() : n.store === "postgres" ? l() : n.store === "redis" ? r() : n.store, d = (e) => {
|
|
109
113
|
let t = n.enabled;
|
|
110
114
|
return t === void 0 ? !0 : typeof t == "function" ? t(e) : t;
|
|
111
|
-
},
|
|
115
|
+
}, f = (e, t, r, i) => {
|
|
112
116
|
if (!n.onLimited) return;
|
|
113
117
|
let a = {
|
|
114
118
|
tag: e.tag,
|
|
@@ -122,27 +126,27 @@ var o = (e = {}) => {
|
|
|
122
126
|
try {
|
|
123
127
|
n.onLimited(a);
|
|
124
128
|
} catch {}
|
|
125
|
-
},
|
|
129
|
+
}, p = (e, t) => {
|
|
126
130
|
let r = n.resolve;
|
|
127
131
|
return r ? i.suspend(() => {
|
|
128
132
|
let n = r(e, t);
|
|
129
|
-
return (i.isEffect(n) ? n : n instanceof Promise ? i.promise(() => n) : i.succeed(n)).pipe(i.map((e) => e === "exempt" ? "exempt" :
|
|
130
|
-
}) : i.succeed(
|
|
131
|
-
},
|
|
132
|
-
let a =
|
|
133
|
-
if (!
|
|
134
|
-
let o =
|
|
133
|
+
return (i.isEffect(n) ? n : n instanceof Promise ? i.promise(() => n) : i.succeed(n)).pipe(i.map((e) => e === "exempt" ? "exempt" : v(e === void 0 ? t : e)));
|
|
134
|
+
}) : i.succeed(v(t));
|
|
135
|
+
}, m = (t, r) => i.suspend(() => {
|
|
136
|
+
let a = S(r);
|
|
137
|
+
if (!d(a)) return t;
|
|
138
|
+
let o = g(c, n.default, a);
|
|
135
139
|
if (!o || o.effective.disabled) return t;
|
|
136
|
-
let
|
|
137
|
-
return
|
|
140
|
+
let s = y(_(o.effective, a));
|
|
141
|
+
return p(a, s).pipe(i.flatMap((n) => {
|
|
138
142
|
if (n === "exempt") return t;
|
|
139
|
-
let r =
|
|
140
|
-
return
|
|
143
|
+
let r = x(o, a);
|
|
144
|
+
return u.consume(r, n, Date.now()).pipe(i.catchAllDefect((e) => i.as(i.logWarning(`[ratelimit] store error — failing open for ${a.tag}: ${String(e)}`), {
|
|
141
145
|
allowed: !0,
|
|
142
146
|
remaining: n.limit,
|
|
143
147
|
resetAtMs: Date.now(),
|
|
144
148
|
retryAfterMs: 0
|
|
145
|
-
})), i.flatMap((o) => o.allowed ? t : i.zipRight(i.sync(() =>
|
|
149
|
+
})), i.flatMap((o) => o.allowed ? t : i.zipRight(i.sync(() => f(a, r, n, o)), i.fail(new e({
|
|
146
150
|
tag: a.tag,
|
|
147
151
|
limit: n.limit,
|
|
148
152
|
windowMs: n.windowMs,
|
|
@@ -150,34 +154,37 @@ var o = (e = {}) => {
|
|
|
150
154
|
resetAtMs: o.resetAtMs
|
|
151
155
|
})))));
|
|
152
156
|
}));
|
|
153
|
-
}),
|
|
157
|
+
}), h = n.http ? async (e, r) => {
|
|
154
158
|
let a = n.http, o = {
|
|
155
159
|
limit: a.limit,
|
|
156
160
|
windowMs: t(a.window),
|
|
157
161
|
algorithm: a.algorithm ?? "fixed-window",
|
|
158
162
|
burst: a.limit
|
|
159
|
-
}, s = r.remoteAddr ?? "unknown", c = Date.now(),
|
|
160
|
-
return
|
|
163
|
+
}, s = r.remoteAddr ?? "unknown", c = Date.now(), l = await i.runPromise(u.consume(`http:ip:${s}`, o, c)), d = String(Math.max(0, Math.ceil((l.resetAtMs - c) / 1e3)));
|
|
164
|
+
return l.allowed ? e() : {
|
|
161
165
|
status: 429,
|
|
162
166
|
body: "Too Many Requests",
|
|
163
167
|
headers: {
|
|
164
168
|
"content-type": "text/plain; charset=utf-8",
|
|
165
|
-
"retry-after": String(Math.max(1, Math.ceil(
|
|
169
|
+
"retry-after": String(Math.max(1, Math.ceil(l.retryAfterMs / 1e3))),
|
|
166
170
|
"ratelimit-limit": String(o.limit),
|
|
167
|
-
"ratelimit-remaining": String(Math.max(0,
|
|
171
|
+
"ratelimit-remaining": String(Math.max(0, l.remaining)),
|
|
168
172
|
"ratelimit-reset": d
|
|
169
173
|
}
|
|
170
174
|
};
|
|
171
|
-
} : void 0,
|
|
175
|
+
} : void 0, b = [
|
|
172
176
|
"rpc:intercept:mutation",
|
|
173
177
|
"rpc:intercept:query",
|
|
174
178
|
"rpc:intercept:action",
|
|
175
179
|
...n.http ? ["http:intercept"] : []
|
|
176
|
-
],
|
|
180
|
+
], C, w;
|
|
177
181
|
return a({
|
|
178
|
-
name:
|
|
182
|
+
name: o({
|
|
183
|
+
base: "@voltro/plugin-ratelimit",
|
|
184
|
+
instance: n.name
|
|
185
|
+
}),
|
|
179
186
|
description: "Per-endpoint / per-subject / per-tenant rate limiting via the rpc interceptors. Configurable rules, three algorithms, static + dynamic per-tenant overrides.",
|
|
180
|
-
permissions:
|
|
187
|
+
permissions: b,
|
|
181
188
|
declaredEnv: [
|
|
182
189
|
{
|
|
183
190
|
name: "CACHE_REDIS_URL",
|
|
@@ -210,7 +217,7 @@ var o = (e = {}) => {
|
|
|
210
217
|
description: "Fallback Upstash REST token for the http redis driver."
|
|
211
218
|
}
|
|
212
219
|
],
|
|
213
|
-
...
|
|
220
|
+
...h ? { onHttpRequest: h } : {},
|
|
214
221
|
errorSchemas: [{
|
|
215
222
|
schema: e,
|
|
216
223
|
import: {
|
|
@@ -218,26 +225,26 @@ var o = (e = {}) => {
|
|
|
218
225
|
name: "RateLimited"
|
|
219
226
|
}
|
|
220
227
|
}],
|
|
221
|
-
interceptMutation:
|
|
222
|
-
interceptQuery:
|
|
223
|
-
interceptAction:
|
|
228
|
+
interceptMutation: m,
|
|
229
|
+
interceptQuery: m,
|
|
230
|
+
interceptAction: m,
|
|
224
231
|
bindDataStore: (e, t) => {
|
|
225
232
|
let n = t?.sql;
|
|
226
|
-
n &&
|
|
227
|
-
|
|
233
|
+
n && u.bindSql && u.bindSql(n).catch((e) => {
|
|
234
|
+
w = e;
|
|
228
235
|
});
|
|
229
236
|
},
|
|
230
237
|
onActivate: (e) => i.gen(function* () {
|
|
231
|
-
|
|
232
|
-
rules:
|
|
238
|
+
u.activate && (yield* i.promise(() => u.activate(e.env))), C = setInterval(() => u.sweep(Date.now()), 6e4), C.unref?.(), w !== void 0 && e.logger.warn("rate-limit: SQL store bind failed — limiter runs fail-open", { error: w instanceof Error ? w.message : String(w) }), e.logger.info("rate-limit active", {
|
|
239
|
+
rules: c.length,
|
|
233
240
|
hasDefault: n.default !== void 0,
|
|
234
241
|
store: typeof n.store == "string" ? n.store : n.store === void 0 ? "memory" : "custom"
|
|
235
242
|
});
|
|
236
243
|
}),
|
|
237
244
|
onDeactivate: () => i.gen(function* () {
|
|
238
|
-
|
|
245
|
+
C && clearInterval(C), u.deactivate && (yield* i.promise(() => u.deactivate()));
|
|
239
246
|
})
|
|
240
247
|
});
|
|
241
248
|
};
|
|
242
249
|
//#endregion
|
|
243
|
-
export { e as RateLimited, n as consumeBucket,
|
|
250
|
+
export { e as RateLimited, n as consumeBucket, s as memoryStore, t as parseWindow, C as rateLimitPlugin, r as redisStore };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-ratelimit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"types": "./dist/errors.d.ts",
|
|
38
38
|
"import": "./dist/errors.js",
|
|
39
39
|
"default": "./dist/errors.js"
|
|
40
|
-
}
|
|
40
|
+
},
|
|
41
|
+
"./package.json": "./package.json"
|
|
41
42
|
},
|
|
42
43
|
"main": "./dist/index.js",
|
|
43
44
|
"module": "./dist/index.js",
|
|
@@ -47,8 +48,8 @@
|
|
|
47
48
|
"node": ">=24.0.0"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
|
-
"@voltro/kv": "0.
|
|
51
|
-
"@voltro/protocol": "0.
|
|
51
|
+
"@voltro/kv": "0.34.0",
|
|
52
|
+
"@voltro/protocol": "0.34.0"
|
|
52
53
|
},
|
|
53
54
|
"optionalDependencies": {
|
|
54
55
|
"@effect/sql": "^0.52.0"
|