@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.
@@ -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.84.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.84.0",
52
- "@voltro/kv": "0.84.0",
53
- "@voltro/logger": "0.84.0",
54
- "@voltro/protocol": "0.84.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": "^3.22.2"
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 };