@voltro/plugin-ratelimit 0.85.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 +666 -0
- package/THIRD-PARTY-NOTICES.md +3427 -26
- package/dist/index.d.ts +4 -3
- package/dist/index.js +101 -232
- package/dist/inspect.js +72 -0
- package/dist/postgresStore.js +48 -2
- package/dist/resolve.js +65 -0
- package/dist/store.js +29 -0
- package/dist/window.js +117 -0
- package/package.json +6 -5
- package/dist/postgresStore-D_oHSGXq.js +0 -160
package/dist/index.d.ts
CHANGED
|
@@ -101,14 +101,15 @@ export declare interface RateLimitContext {
|
|
|
101
101
|
/** Convenience: the subject's tenant (`null` only for unscoped anonymous). */
|
|
102
102
|
readonly tenantId: string | null;
|
|
103
103
|
readonly traceId: string;
|
|
104
|
-
/** `'rest'`
|
|
104
|
+
/** `'rest'` for a hand-authored REST route or `publicApi` projection; absent
|
|
105
105
|
* on the rpc wire. */
|
|
106
106
|
readonly surface?: 'rest';
|
|
107
|
-
/** The descriptor's own `publicApi.rateLimit`,
|
|
108
|
-
*
|
|
107
|
+
/** The descriptor's own REST quota — `rateLimit` or `publicApi.rateLimit`,
|
|
108
|
+
* ahead of `rules` and `default`. */
|
|
109
109
|
readonly publicApiRateLimit?: {
|
|
110
110
|
readonly limit: number;
|
|
111
111
|
readonly window: string;
|
|
112
|
+
readonly by?: 'subject' | 'tenant' | 'apiKey' | 'ip' | 'global';
|
|
112
113
|
};
|
|
113
114
|
/** The caller's address, resolved by the listener through the app's
|
|
114
115
|
* `security.trustedProxies` — what `by: 'ip'` keys on. Absent for a call
|
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 { restError as h } from "@voltro/protocol/rest";
|
|
10
|
+
import { createLogger as g } from "@voltro/logger";
|
|
11
|
+
import { newTraceId as _ } from "@voltro/runtime";
|
|
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,108 +40,108 @@ 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
|
-
let a =
|
|
189
|
-
let
|
|
190
|
-
return
|
|
191
|
-
},
|
|
192
|
-
if (!
|
|
43
|
+
}, w = g({ scope: "voltro:ratelimit" }), T = (t = {}) => {
|
|
44
|
+
let a = t.rules ?? [], g = t.store === void 0 || t.store === "memory" ? i() : t.store === "postgres" ? o() : t.store === "redis" ? c() : t.store, T = r(t, g), E = (e) => {
|
|
45
|
+
let n = t.enabled;
|
|
46
|
+
return n === void 0 ? !0 : typeof n == "function" ? n(e) : n;
|
|
47
|
+
}, D = (e, n, r, i) => {
|
|
48
|
+
if (!t.onLimited) return;
|
|
193
49
|
let a = {
|
|
194
50
|
tag: e.tag,
|
|
195
51
|
kind: e.kind,
|
|
196
|
-
key:
|
|
52
|
+
key: n,
|
|
197
53
|
tenantId: e.tenantId,
|
|
198
54
|
limit: r.limit,
|
|
199
55
|
windowMs: r.windowMs,
|
|
200
56
|
retryAfterMs: i.retryAfterMs
|
|
201
57
|
};
|
|
202
58
|
try {
|
|
203
|
-
|
|
59
|
+
t.onLimited(a);
|
|
204
60
|
} 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
|
|
61
|
+
}, O = (e, n) => {
|
|
62
|
+
let r = t.resolve;
|
|
63
|
+
return r ? m.suspend(() => {
|
|
64
|
+
let t = r(e, n);
|
|
65
|
+
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)));
|
|
66
|
+
}) : m.succeed(u(n));
|
|
67
|
+
}, k = (n, r) => m.suspend(() => {
|
|
68
|
+
let i = x(r);
|
|
69
|
+
if (!E(i)) return T.bypass(), n;
|
|
70
|
+
let o = S(i) ?? d(a, t.default, i);
|
|
71
|
+
if (!o || o.effective.disabled) return T.bypass(), n;
|
|
72
|
+
let s = p(f(o.effective, i));
|
|
73
|
+
return O(i, s).pipe(m.flatMap((t) => {
|
|
74
|
+
if (t === "exempt") return T.bypass(), n;
|
|
75
|
+
let r = l(o, i);
|
|
76
|
+
return g.consume(r, t, Date.now()).pipe(m.catchDefect((e) => m.as(m.logWarning(`[ratelimit] store error — failing open for ${i.tag}: ${String(e)}`), {
|
|
221
77
|
allowed: !0,
|
|
222
78
|
failOpenReason: "store-error",
|
|
223
|
-
remaining:
|
|
79
|
+
remaining: t.limit,
|
|
224
80
|
resetAtMs: Date.now(),
|
|
225
81
|
retryAfterMs: 0
|
|
226
|
-
})),
|
|
82
|
+
})), 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
83
|
tag: i.tag,
|
|
228
|
-
limit:
|
|
229
|
-
windowMs:
|
|
84
|
+
limit: t.limit,
|
|
85
|
+
windowMs: t.windowMs,
|
|
230
86
|
retryAfterMs: a.retryAfterMs,
|
|
231
87
|
resetAtMs: a.resetAtMs
|
|
232
88
|
})))));
|
|
233
89
|
}));
|
|
234
|
-
}),
|
|
235
|
-
let i =
|
|
90
|
+
}), A = t.http ? async (e, r) => {
|
|
91
|
+
let i = t.http, a = {
|
|
236
92
|
limit: i.limit,
|
|
237
|
-
windowMs:
|
|
93
|
+
windowMs: n(i.window),
|
|
238
94
|
algorithm: i.algorithm ?? "fixed-window",
|
|
239
95
|
burst: i.limit
|
|
240
|
-
}, o = r.remoteAddr ?? "unknown",
|
|
96
|
+
}, o = r.remoteAddr ?? "unknown", s = Date.now(), c;
|
|
241
97
|
try {
|
|
242
|
-
|
|
98
|
+
c = await m.runPromise(g.consume(`http:ip:${o}`, a, s));
|
|
243
99
|
} catch (t) {
|
|
244
|
-
return
|
|
100
|
+
return T.record("http", {
|
|
245
101
|
allowed: !0,
|
|
246
102
|
failOpenReason: "store-error",
|
|
247
103
|
remaining: 0,
|
|
248
|
-
resetAtMs:
|
|
104
|
+
resetAtMs: s,
|
|
249
105
|
retryAfterMs: 0
|
|
250
|
-
}),
|
|
106
|
+
}), w.warn("http shield store failure — request ALLOWED without a limit check", { cause: String(t) }), e();
|
|
251
107
|
}
|
|
252
|
-
|
|
253
|
-
let
|
|
254
|
-
return
|
|
108
|
+
T.record("http", c);
|
|
109
|
+
let l = String(Math.max(0, Math.ceil((c.resetAtMs - s) / 1e3)));
|
|
110
|
+
return c.allowed ? e() : {
|
|
255
111
|
status: 429,
|
|
256
112
|
body: "Too Many Requests",
|
|
257
113
|
headers: {
|
|
258
114
|
"content-type": "text/plain; charset=utf-8",
|
|
259
|
-
"retry-after": String(Math.max(1, Math.ceil(
|
|
115
|
+
"retry-after": String(Math.max(1, Math.ceil(c.retryAfterMs / 1e3))),
|
|
260
116
|
"ratelimit-limit": String(a.limit),
|
|
261
|
-
"ratelimit-remaining": String(Math.max(0,
|
|
262
|
-
"ratelimit-reset":
|
|
117
|
+
"ratelimit-remaining": String(Math.max(0, c.remaining)),
|
|
118
|
+
"ratelimit-reset": l
|
|
263
119
|
}
|
|
264
120
|
};
|
|
265
|
-
} : void 0,
|
|
121
|
+
} : void 0, j = [
|
|
266
122
|
"inspect:read",
|
|
267
123
|
"rpc:intercept:mutation",
|
|
268
124
|
"rpc:intercept:query",
|
|
269
125
|
"rpc:intercept:action",
|
|
270
126
|
"rpc:intercept:workflow",
|
|
271
|
-
|
|
272
|
-
...
|
|
273
|
-
],
|
|
274
|
-
return
|
|
275
|
-
name:
|
|
127
|
+
"http:intercept",
|
|
128
|
+
...t.store === "postgres" ? ["store:write"] : []
|
|
129
|
+
], M, N;
|
|
130
|
+
return y({
|
|
131
|
+
name: b({
|
|
276
132
|
base: "@voltro/plugin-ratelimit",
|
|
277
|
-
instance:
|
|
133
|
+
instance: t.name
|
|
278
134
|
}),
|
|
279
135
|
description: "Per-endpoint / per-subject / per-tenant rate limiting via the rpc interceptors. Configurable rules, three algorithms, static + dynamic per-tenant overrides.",
|
|
280
|
-
permissions:
|
|
136
|
+
permissions: j,
|
|
281
137
|
inspectEndpoints: [{
|
|
282
138
|
method: "GET",
|
|
283
139
|
path: "/state",
|
|
284
140
|
scope: "process",
|
|
285
141
|
description: "Declared rules and process-local limiting evidence, without bucket identities.",
|
|
286
|
-
handler: () =>
|
|
142
|
+
handler: () => m.sync(() => ({
|
|
287
143
|
kind: "json",
|
|
288
|
-
data:
|
|
144
|
+
data: T.snapshot()
|
|
289
145
|
}))
|
|
290
146
|
}],
|
|
291
147
|
declaredEnv: [
|
|
@@ -320,7 +176,7 @@ var f = (e) => typeof e == "function" ? "dynamic" : e, p = (e) => typeof e == "f
|
|
|
320
176
|
description: "Fallback Upstash REST token for the http redis driver."
|
|
321
177
|
}
|
|
322
178
|
],
|
|
323
|
-
...
|
|
179
|
+
...A ? { onHttpRequest: A } : {},
|
|
324
180
|
errorSchemas: [{
|
|
325
181
|
schema: e,
|
|
326
182
|
import: {
|
|
@@ -329,34 +185,47 @@ var f = (e) => typeof e == "function" ? "dynamic" : e, p = (e) => typeof e == "f
|
|
|
329
185
|
},
|
|
330
186
|
rest: {
|
|
331
187
|
status: 429,
|
|
332
|
-
headers:
|
|
188
|
+
headers: C
|
|
333
189
|
}
|
|
334
190
|
}],
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
191
|
+
restRateLimit: (t, n, r) => k(m.void, {
|
|
192
|
+
tag: `${r.method} ${r.path}`,
|
|
193
|
+
kind: "action",
|
|
194
|
+
input: void 0,
|
|
195
|
+
subject: n.subject,
|
|
196
|
+
traceId: _(),
|
|
197
|
+
surface: "rest",
|
|
198
|
+
publicApi: { rateLimit: t },
|
|
199
|
+
...n.remoteAddr === void 0 ? {} : { remoteAddr: n.remoteAddr }
|
|
200
|
+
}).pipe(m.catch((t) => m.fail(t instanceof e ? h(429, {
|
|
201
|
+
error: "RateLimited",
|
|
202
|
+
message: "Rate limit exceeded"
|
|
203
|
+
}, C(t)) : t))),
|
|
204
|
+
interceptMutation: k,
|
|
205
|
+
interceptQuery: k,
|
|
206
|
+
interceptAction: k,
|
|
207
|
+
interceptWorkflow: k,
|
|
208
|
+
...t.store === "postgres" ? { extendSchema: { tables: [s] } } : {},
|
|
340
209
|
bindDataStore: (e) => {
|
|
341
|
-
|
|
342
|
-
|
|
210
|
+
g.bind && (T.binding("binding"), g.bind(e).then(() => {
|
|
211
|
+
N = void 0, T.binding("bound");
|
|
343
212
|
}).catch((e) => {
|
|
344
|
-
|
|
213
|
+
T.binding("failed"), N = e;
|
|
345
214
|
}));
|
|
346
215
|
},
|
|
347
|
-
onActivate: (e) =>
|
|
348
|
-
|
|
349
|
-
let
|
|
350
|
-
|
|
216
|
+
onActivate: (e) => m.gen(function* () {
|
|
217
|
+
g.activate && (yield* m.promise(() => g.activate(e.env))), M = setInterval(() => g.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
|
+
let n = t.store === void 0 || t.store === "memory" ? v(e.env) : null;
|
|
219
|
+
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
220
|
rules: a.length,
|
|
352
|
-
hasDefault:
|
|
353
|
-
store: typeof
|
|
221
|
+
hasDefault: t.default !== void 0,
|
|
222
|
+
store: typeof t.store == "string" ? t.store : t.store === void 0 ? "memory" : "custom"
|
|
354
223
|
});
|
|
355
224
|
}),
|
|
356
|
-
onDeactivate: () =>
|
|
357
|
-
|
|
225
|
+
onDeactivate: () => m.gen(function* () {
|
|
226
|
+
M && clearInterval(M), g.deactivate && (yield* m.promise(() => g.deactivate()));
|
|
358
227
|
})
|
|
359
228
|
});
|
|
360
229
|
};
|
|
361
230
|
//#endregion
|
|
362
|
-
export { a as RATELIMIT_TABLE, e as RateLimited,
|
|
231
|
+
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 };
|
package/dist/inspect.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { parseWindow as e } from "./window.js";
|
|
2
|
+
//#region src/inspect.ts
|
|
3
|
+
var t = (e) => typeof e == "function" ? "dynamic" : e, n = (e) => typeof e == "function" ? "dynamic" : e, r = (r, i) => ({
|
|
4
|
+
id: i === null ? "default" : `r${i}`,
|
|
5
|
+
match: "match" in r ? r.match instanceof RegExp ? String(r.match) : Array.isArray(r.match) ? r.match.join(", ") : r.match ?? "*" : "*",
|
|
6
|
+
kinds: r.kind === void 0 ? [] : typeof r.kind == "string" ? [r.kind] : r.kind,
|
|
7
|
+
limit: t(r.limit),
|
|
8
|
+
windowMs: typeof r.window == "function" ? "dynamic" : e(r.window),
|
|
9
|
+
algorithm: r.algorithm ?? "sliding-window",
|
|
10
|
+
burst: r.burst === void 0 ? t(r.limit) : t(r.burst),
|
|
11
|
+
by: Array.isArray(r.by) ? r.by.map(n) : [n(r.by ?? "subject")],
|
|
12
|
+
scope: r.scope ?? "tag",
|
|
13
|
+
tenantOverrides: Object.keys(r.tenants ?? {}).length
|
|
14
|
+
}), i = () => ({
|
|
15
|
+
allowed: 0,
|
|
16
|
+
rejected: 0,
|
|
17
|
+
bypassed: 0,
|
|
18
|
+
unbound: 0,
|
|
19
|
+
storeErrors: 0,
|
|
20
|
+
lastRejectedAt: null,
|
|
21
|
+
lastStoreErrorAt: null
|
|
22
|
+
}), a = (t, n) => {
|
|
23
|
+
let a = Date.now(), o = {
|
|
24
|
+
rpc: i(),
|
|
25
|
+
http: i()
|
|
26
|
+
}, s = n.bind ? "waiting" : "not-required";
|
|
27
|
+
return {
|
|
28
|
+
binding: (e) => {
|
|
29
|
+
s = e;
|
|
30
|
+
},
|
|
31
|
+
bypass: () => {
|
|
32
|
+
o.rpc.bypassed++;
|
|
33
|
+
},
|
|
34
|
+
record: (e, t) => {
|
|
35
|
+
let n = o[e];
|
|
36
|
+
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());
|
|
37
|
+
},
|
|
38
|
+
snapshot: () => {
|
|
39
|
+
let i = null;
|
|
40
|
+
try {
|
|
41
|
+
let e = n.size();
|
|
42
|
+
Number.isFinite(e) && e >= 0 && (i = e);
|
|
43
|
+
} catch {}
|
|
44
|
+
let c = typeof t.store == "string" ? t.store : t.store === void 0 ? "memory" : "custom";
|
|
45
|
+
return {
|
|
46
|
+
capturedAt: Date.now(),
|
|
47
|
+
startedAt: a,
|
|
48
|
+
rpcGate: t.enabled === !1 ? "disabled" : typeof t.enabled == "function" ? "dynamic" : "enabled",
|
|
49
|
+
dynamicResolver: t.resolve !== void 0,
|
|
50
|
+
store: {
|
|
51
|
+
kind: c,
|
|
52
|
+
scope: c === "memory" ? "process" : c === "custom" ? "unknown" : "shared-store",
|
|
53
|
+
sqlBinding: s,
|
|
54
|
+
trackedBuckets: i
|
|
55
|
+
},
|
|
56
|
+
rules: (t.rules ?? []).map((e, t) => r(e, t)),
|
|
57
|
+
defaultRule: t.default ? r(t.default, null) : null,
|
|
58
|
+
http: t.http ? {
|
|
59
|
+
limit: t.http.limit,
|
|
60
|
+
windowMs: e(t.http.window),
|
|
61
|
+
algorithm: t.http.algorithm ?? "fixed-window"
|
|
62
|
+
} : null,
|
|
63
|
+
counters: {
|
|
64
|
+
rpc: { ...o.rpc },
|
|
65
|
+
http: { ...o.http }
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
export { a as makeRateLimitInspect };
|
package/dist/postgresStore.js
CHANGED
|
@@ -1,2 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { consumeBucket as e } from "./window.js";
|
|
2
|
+
import { Effect as t } from "effect";
|
|
3
|
+
import { dropped as n, eq as r, id as i, json as a, lt as o, queryFor as s, table as c, text as l, timestamp as u } from "@voltro/database";
|
|
4
|
+
//#region src/postgresStore.ts
|
|
5
|
+
var d = "_voltro_ratelimit", f = c(d, {
|
|
6
|
+
id: i({ scheme: "numeric" }),
|
|
7
|
+
key: l().maxLength(512),
|
|
8
|
+
state: a().nullable(),
|
|
9
|
+
touchedAt: u().default("now"),
|
|
10
|
+
touched: n()
|
|
11
|
+
}).unique("rateLimitKey", ["key"]).nonReactive(), p = (n = {}) => {
|
|
12
|
+
let i = n.staleAfterMs ?? 36e5, a = null, c = (e, t, n) => ({
|
|
13
|
+
failOpenReason: n,
|
|
14
|
+
allowed: !0,
|
|
15
|
+
remaining: e.limit,
|
|
16
|
+
resetAtMs: t + e.windowMs,
|
|
17
|
+
retryAfterMs: 0
|
|
18
|
+
}), l = async (t, n, i, a) => {
|
|
19
|
+
let o = new Date(a);
|
|
20
|
+
return await t.insertIgnore(d, {
|
|
21
|
+
key: n,
|
|
22
|
+
state: null,
|
|
23
|
+
touchedAt: o
|
|
24
|
+
}, { conflictColumns: ["key"] }), t.transactional(async (t) => {
|
|
25
|
+
let c = (await t.query(s(f).where(r("key", n)).forUpdate().descriptor))[0]?.state ?? void 0, { state: l, decision: u } = e(c ?? void 0, i, a);
|
|
26
|
+
return await t.updateMany(d, {
|
|
27
|
+
state: l,
|
|
28
|
+
touchedAt: o
|
|
29
|
+
}, { where: r("key", n) }), u;
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
consume: (e, n, r) => {
|
|
34
|
+
let i = a;
|
|
35
|
+
return i ? t.tryPromise(() => l(i, e, n, r)).pipe(t.catch(() => t.succeed(c(n, r, "store-error")))) : t.succeed(c(n, r, "unbound"));
|
|
36
|
+
},
|
|
37
|
+
sweep: (e) => {
|
|
38
|
+
let t = a;
|
|
39
|
+
t && t.deleteMany(d, { where: o("touchedAt", new Date(e - i)) }).catch(() => {});
|
|
40
|
+
},
|
|
41
|
+
size: () => -1,
|
|
42
|
+
bind: async (e) => {
|
|
43
|
+
a = e;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
48
|
+
export { d as RATELIMIT_TABLE, p as postgresStore, f as rateLimitTable };
|
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 };
|