@voltro/plugin-ratelimit 0.84.0 → 0.86.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 +1215 -0
- package/THIRD-PARTY-NOTICES.md +3357 -12
- package/dist/errors.d.ts +8 -9
- package/dist/index.d.ts +12 -12
- package/dist/index.js +101 -232
- package/dist/inspect.js +72 -0
- package/dist/postgresStore.js +48 -2
- package/dist/redisStore.js +1 -1
- package/dist/resolve.js +65 -0
- package/dist/store.js +29 -0
- package/dist/window.js +117 -0
- package/package.json +7 -9
- package/dist/postgresStore-CDP61pxx.js +0 -160
package/dist/resolve.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { parseWindow as e } from "./window.js";
|
|
2
|
+
//#region src/resolve.ts
|
|
3
|
+
var t = (e, t) => typeof e == "function" ? e(t) : e, n = /* @__PURE__ */ new WeakMap(), r = (e) => {
|
|
4
|
+
if (!e.global && !e.sticky) return e;
|
|
5
|
+
let t = n.get(e);
|
|
6
|
+
return t === void 0 && (t = new RegExp(e.source, e.flags.replace(/[gy]/g, "")), n.set(e, t)), t;
|
|
7
|
+
}, i = (e, t) => e === void 0 ? !0 : typeof e == "string" ? e === t : Array.isArray(e) ? e.includes(t) : r(e).test(t), a = (e, t) => e === void 0 ? !0 : Array.isArray(e) ? e.includes(t) : e === t, o = (e, t) => {
|
|
8
|
+
let n = t === null ? void 0 : e.tenants?.[t];
|
|
9
|
+
return {
|
|
10
|
+
limit: n?.limit ?? e.limit,
|
|
11
|
+
window: n?.window ?? e.window,
|
|
12
|
+
algorithm: n?.algorithm ?? e.algorithm,
|
|
13
|
+
burst: n?.burst ?? e.burst,
|
|
14
|
+
by: e.by ?? "subject",
|
|
15
|
+
scope: e.scope ?? "tag",
|
|
16
|
+
disabled: n?.disabled ?? !1
|
|
17
|
+
};
|
|
18
|
+
}, s = (e, t, n) => {
|
|
19
|
+
for (let t = 0; t < e.length; t++) {
|
|
20
|
+
let r = e[t];
|
|
21
|
+
if (i(r.match, n.tag) && a(r.kind, n.kind)) return {
|
|
22
|
+
effective: o(r, n.tenantId),
|
|
23
|
+
ruleId: `r${t}`
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (t && a(t.kind, n.kind)) return {
|
|
27
|
+
effective: o(t, n.tenantId),
|
|
28
|
+
ruleId: "default"
|
|
29
|
+
};
|
|
30
|
+
}, c = (n, r) => {
|
|
31
|
+
let i = t(n.limit, r);
|
|
32
|
+
return {
|
|
33
|
+
limit: i,
|
|
34
|
+
windowMs: e(t(n.window, r)),
|
|
35
|
+
algorithm: n.algorithm ?? "sliding-window",
|
|
36
|
+
burst: n.burst === void 0 ? i : t(n.burst, r)
|
|
37
|
+
};
|
|
38
|
+
}, l = (t) => {
|
|
39
|
+
let n = t.limit;
|
|
40
|
+
return {
|
|
41
|
+
limit: n,
|
|
42
|
+
windowMs: e(t.window),
|
|
43
|
+
algorithm: t.algorithm ?? "sliding-window",
|
|
44
|
+
burst: t.burst ?? n
|
|
45
|
+
};
|
|
46
|
+
}, u = (e) => ({
|
|
47
|
+
limit: e.limit,
|
|
48
|
+
window: e.windowMs,
|
|
49
|
+
algorithm: e.algorithm,
|
|
50
|
+
burst: e.burst
|
|
51
|
+
}), d = (e, t) => {
|
|
52
|
+
if (typeof e == "function") return e(t);
|
|
53
|
+
switch (e) {
|
|
54
|
+
case "subject": return `s:${t.tenantId ?? "∅"}:${t.subject.id ?? "anon"}`;
|
|
55
|
+
case "tenant": return `t:${t.tenantId ?? "∅"}`;
|
|
56
|
+
case "apiKey": return t.subject.type === "apiKey" ? `k:${t.subject.id}` : `s:${t.tenantId ?? "∅"}:${t.subject.id ?? "anon"}`;
|
|
57
|
+
case "global": return "g";
|
|
58
|
+
case "ip": return `i:${t.remoteAddr ?? "unknown"}`;
|
|
59
|
+
}
|
|
60
|
+
}, f = (e, t) => {
|
|
61
|
+
let { effective: n, ruleId: r } = e, i = n.scope === "rule" ? `rule#${r}` : `tag:${t.tag}`, a = n.by;
|
|
62
|
+
return `${i}|${Array.isArray(a) ? a.map((e) => d(e, t)).join("|") : d(a, t)}`;
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
export { f as buildKey, l as limitFromOverride, s as matchRule, c as resolveLimit, u as toOverride };
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { consumeBucket as e } from "./window.js";
|
|
2
|
+
import { Effect as t } from "effect";
|
|
3
|
+
//#region src/store.ts
|
|
4
|
+
var n = (n = {}) => {
|
|
5
|
+
let r = n.maxKeys ?? 1e5, i = n.staleAfterMs ?? 36e5, a = /* @__PURE__ */ new Map(), o = (e) => {
|
|
6
|
+
if (a.size <= r) return;
|
|
7
|
+
let t = a.size - r, n = 0;
|
|
8
|
+
for (let r of a.keys()) {
|
|
9
|
+
if (n >= t) break;
|
|
10
|
+
r !== e && (a.delete(r), n += 1);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
consume: (n, r, i) => t.sync(() => {
|
|
15
|
+
let t = a.get(n), { state: s, decision: c } = e(t?.state, r, i);
|
|
16
|
+
return a.set(n, {
|
|
17
|
+
state: s,
|
|
18
|
+
touched: i
|
|
19
|
+
}), o(n), c;
|
|
20
|
+
}),
|
|
21
|
+
sweep: (e) => {
|
|
22
|
+
let t = e - i;
|
|
23
|
+
for (let [e, n] of a) n.touched < t && a.delete(e);
|
|
24
|
+
},
|
|
25
|
+
size: () => a.size
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { n as memoryStore };
|
package/dist/window.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//#region src/window.ts
|
|
2
|
+
var e = {
|
|
3
|
+
ms: 1,
|
|
4
|
+
s: 1e3,
|
|
5
|
+
m: 6e4,
|
|
6
|
+
h: 36e5,
|
|
7
|
+
d: 864e5
|
|
8
|
+
}, t = (t) => {
|
|
9
|
+
if (typeof t == "number") {
|
|
10
|
+
if (!Number.isFinite(t) || t <= 0) throw Error(`@voltro/plugin-ratelimit: window must be a positive number, got ${t}`);
|
|
11
|
+
return t;
|
|
12
|
+
}
|
|
13
|
+
let n = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/.exec(t.trim());
|
|
14
|
+
if (!n) throw Error(`@voltro/plugin-ratelimit: invalid window "${t}" — use e.g. "1m", "10s", "500ms", "2h", "1d"`);
|
|
15
|
+
return Number(n[1]) * e[n[2]];
|
|
16
|
+
}, n = (e, t, n) => {
|
|
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
|
+
};
|
|
116
|
+
//#endregion
|
|
117
|
+
export { n as consumeBucket, t as parseWindow };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-ratelimit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.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,16 +48,14 @@
|
|
|
48
48
|
"node": ">=24.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@voltro/database": "0.
|
|
52
|
-
"@voltro/kv": "0.
|
|
53
|
-
"@voltro/logger": "0.
|
|
54
|
-
"@voltro/protocol": "0.
|
|
55
|
-
|
|
56
|
-
"optionalDependencies": {
|
|
57
|
-
"@effect/sql": "^0.52.1"
|
|
51
|
+
"@voltro/database": "0.86.0",
|
|
52
|
+
"@voltro/kv": "0.86.0",
|
|
53
|
+
"@voltro/logger": "0.86.0",
|
|
54
|
+
"@voltro/protocol": "0.86.0",
|
|
55
|
+
"@voltro/runtime": "0.86.0"
|
|
58
56
|
},
|
|
59
57
|
"peerDependencies": {
|
|
60
|
-
"effect": "
|
|
58
|
+
"effect": "4.0.1"
|
|
61
59
|
},
|
|
62
60
|
"publishConfig": {
|
|
63
61
|
"access": "public"
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { Effect as e } from "effect";
|
|
2
|
-
import { dropped as t, eq as n, id as r, json as i, lt as a, queryFor as o, table as s, text as c, timestamp as l } from "@voltro/database";
|
|
3
|
-
//#region src/window.ts
|
|
4
|
-
var u = {
|
|
5
|
-
ms: 1,
|
|
6
|
-
s: 1e3,
|
|
7
|
-
m: 6e4,
|
|
8
|
-
h: 36e5,
|
|
9
|
-
d: 864e5
|
|
10
|
-
}, d = (e) => {
|
|
11
|
-
if (typeof e == "number") {
|
|
12
|
-
if (!Number.isFinite(e) || e <= 0) throw Error(`@voltro/plugin-ratelimit: window must be a positive number, got ${e}`);
|
|
13
|
-
return e;
|
|
14
|
-
}
|
|
15
|
-
let t = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/.exec(e.trim());
|
|
16
|
-
if (!t) throw Error(`@voltro/plugin-ratelimit: invalid window "${e}" — use e.g. "1m", "10s", "500ms", "2h", "1d"`);
|
|
17
|
-
return Number(t[1]) * u[t[2]];
|
|
18
|
-
}, f = (e, t, n) => {
|
|
19
|
-
switch (t.algorithm) {
|
|
20
|
-
case "fixed-window": return p(e, t, n);
|
|
21
|
-
case "sliding-window": return m(e, t, n);
|
|
22
|
-
case "token-bucket": return h(e, t, n);
|
|
23
|
-
}
|
|
24
|
-
}, p = (e, t, n) => {
|
|
25
|
-
let r = e?.kind === "fixed" ? e.windowStart : n, i = e?.kind === "fixed" ? e.count : 0;
|
|
26
|
-
n - r >= t.windowMs && (r = n, i = 0);
|
|
27
|
-
let a = r + t.windowMs;
|
|
28
|
-
return i >= t.limit ? {
|
|
29
|
-
state: {
|
|
30
|
-
kind: "fixed",
|
|
31
|
-
windowStart: r,
|
|
32
|
-
count: i
|
|
33
|
-
},
|
|
34
|
-
decision: {
|
|
35
|
-
allowed: !1,
|
|
36
|
-
remaining: 0,
|
|
37
|
-
resetAtMs: a,
|
|
38
|
-
retryAfterMs: a - n
|
|
39
|
-
}
|
|
40
|
-
} : (i += 1, {
|
|
41
|
-
state: {
|
|
42
|
-
kind: "fixed",
|
|
43
|
-
windowStart: r,
|
|
44
|
-
count: i
|
|
45
|
-
},
|
|
46
|
-
decision: {
|
|
47
|
-
allowed: !0,
|
|
48
|
-
remaining: t.limit - i,
|
|
49
|
-
resetAtMs: a,
|
|
50
|
-
retryAfterMs: 0
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
}, m = (e, t, n) => {
|
|
54
|
-
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);
|
|
55
|
-
s >= 2 ? (o = 0, a = 0, i = n) : s === 1 && (o = a, a = 0, i += r);
|
|
56
|
-
let c = (r - (n - i)) / r, l = o * c + a, u = i + r;
|
|
57
|
-
return l >= t.limit ? {
|
|
58
|
-
state: {
|
|
59
|
-
kind: "sliding",
|
|
60
|
-
currStart: i,
|
|
61
|
-
currCount: a,
|
|
62
|
-
prevCount: o
|
|
63
|
-
},
|
|
64
|
-
decision: {
|
|
65
|
-
allowed: !1,
|
|
66
|
-
remaining: 0,
|
|
67
|
-
resetAtMs: u,
|
|
68
|
-
retryAfterMs: u - n
|
|
69
|
-
}
|
|
70
|
-
} : (a += 1, {
|
|
71
|
-
state: {
|
|
72
|
-
kind: "sliding",
|
|
73
|
-
currStart: i,
|
|
74
|
-
currCount: a,
|
|
75
|
-
prevCount: o
|
|
76
|
-
},
|
|
77
|
-
decision: {
|
|
78
|
-
allowed: !0,
|
|
79
|
-
remaining: Math.max(0, Math.floor(t.limit - l - 1)),
|
|
80
|
-
resetAtMs: u,
|
|
81
|
-
retryAfterMs: 0
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
}, h = (e, t, n) => {
|
|
85
|
-
let r = t.burst, i = t.limit / t.windowMs, a = e?.kind === "token" ? e.tokens : r, o = e?.kind === "token" ? e.lastRefill : n;
|
|
86
|
-
if (a = Math.min(r, a + (n - o) * i), a >= 1) {
|
|
87
|
-
--a;
|
|
88
|
-
let e = n + Math.ceil((r - a) / i);
|
|
89
|
-
return {
|
|
90
|
-
state: {
|
|
91
|
-
kind: "token",
|
|
92
|
-
tokens: a,
|
|
93
|
-
lastRefill: n
|
|
94
|
-
},
|
|
95
|
-
decision: {
|
|
96
|
-
allowed: !0,
|
|
97
|
-
remaining: Math.floor(a),
|
|
98
|
-
resetAtMs: e,
|
|
99
|
-
retryAfterMs: 0
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
let s = Math.ceil((1 - a) / i);
|
|
104
|
-
return {
|
|
105
|
-
state: {
|
|
106
|
-
kind: "token",
|
|
107
|
-
tokens: a,
|
|
108
|
-
lastRefill: n
|
|
109
|
-
},
|
|
110
|
-
decision: {
|
|
111
|
-
allowed: !1,
|
|
112
|
-
remaining: 0,
|
|
113
|
-
resetAtMs: n + s,
|
|
114
|
-
retryAfterMs: s
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
}, g = "_voltro_ratelimit", _ = s(g, {
|
|
118
|
-
id: r({ scheme: "numeric" }),
|
|
119
|
-
key: c().maxLength(512),
|
|
120
|
-
state: i().nullable(),
|
|
121
|
-
touchedAt: l().default("now"),
|
|
122
|
-
touched: t()
|
|
123
|
-
}).unique("rateLimitKey", ["key"]).nonReactive(), v = (t = {}) => {
|
|
124
|
-
let r = t.staleAfterMs ?? 36e5, i = null, s = (e, t, n) => ({
|
|
125
|
-
failOpenReason: n,
|
|
126
|
-
allowed: !0,
|
|
127
|
-
remaining: e.limit,
|
|
128
|
-
resetAtMs: t + e.windowMs,
|
|
129
|
-
retryAfterMs: 0
|
|
130
|
-
}), c = async (e, t, r, i) => {
|
|
131
|
-
let a = new Date(i);
|
|
132
|
-
return await e.insertIgnore(g, {
|
|
133
|
-
key: t,
|
|
134
|
-
state: null,
|
|
135
|
-
touchedAt: a
|
|
136
|
-
}, { conflictColumns: ["key"] }), e.transactional(async (e) => {
|
|
137
|
-
let { state: s, decision: c } = f((await e.query(o(_).where(n("key", t)).forUpdate().descriptor))[0]?.state ?? void 0 ?? void 0, r, i);
|
|
138
|
-
return await e.updateMany(g, {
|
|
139
|
-
state: s,
|
|
140
|
-
touchedAt: a
|
|
141
|
-
}, { where: n("key", t) }), c;
|
|
142
|
-
});
|
|
143
|
-
};
|
|
144
|
-
return {
|
|
145
|
-
consume: (t, n, r) => {
|
|
146
|
-
let a = i;
|
|
147
|
-
return a ? e.tryPromise(() => c(a, t, n, r)).pipe(e.catchAll(() => e.succeed(s(n, r, "store-error")))) : e.succeed(s(n, r, "unbound"));
|
|
148
|
-
},
|
|
149
|
-
sweep: (e) => {
|
|
150
|
-
let t = i;
|
|
151
|
-
t && t.deleteMany(g, { where: a("touchedAt", new Date(e - r)) }).catch(() => {});
|
|
152
|
-
},
|
|
153
|
-
size: () => -1,
|
|
154
|
-
bind: async (e) => {
|
|
155
|
-
i = e;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
};
|
|
159
|
-
//#endregion
|
|
160
|
-
export { d as a, f as i, v as n, _ as r, g as t };
|