@voltro/plugin-ratelimit 0.82.0 → 0.83.1

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/dist/index.js CHANGED
@@ -1,23 +1,23 @@
1
1
  import { RateLimited as e } from "./errors.js";
2
- import { n as t, t as n } from "./window-lyQ6LXhj.js";
3
- import { redisStore as r } from "./redisStore.js";
4
- import { Effect as i } from "effect";
5
- import { createLogger as a } from "@voltro/logger";
6
- import { replicaEvidence as o } from "@voltro/protocol/identity";
7
- import { definePlugin as s, pluginInstanceName as c } from "@voltro/protocol";
2
+ import { a as t, i as n, n as r, r as i, t as a } from "./postgresStore-CDP61pxx.js";
3
+ import { redisStore as o } from "./redisStore.js";
4
+ import { Effect as s } from "effect";
5
+ import { createLogger as c } from "@voltro/logger";
6
+ import { replicaEvidence as l } from "@voltro/protocol/identity";
7
+ import { definePlugin as u, pluginInstanceName as d } from "@voltro/protocol";
8
8
  //#region src/inspect.ts
9
- var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "function" ? "dynamic" : e, d = (e, n) => ({
9
+ var f = (e) => typeof e == "function" ? "dynamic" : e, p = (e) => typeof e == "function" ? "dynamic" : e, m = (e, n) => ({
10
10
  id: n === null ? "default" : `r${n}`,
11
11
  match: "match" in e ? e.match instanceof RegExp ? String(e.match) : Array.isArray(e.match) ? e.match.join(", ") : e.match ?? "*" : "*",
12
12
  kinds: e.kind === void 0 ? [] : typeof e.kind == "string" ? [e.kind] : e.kind,
13
- limit: l(e.limit),
13
+ limit: f(e.limit),
14
14
  windowMs: typeof e.window == "function" ? "dynamic" : t(e.window),
15
15
  algorithm: e.algorithm ?? "sliding-window",
16
- burst: e.burst === void 0 ? l(e.limit) : l(e.burst),
17
- by: Array.isArray(e.by) ? e.by.map(u) : [u(e.by ?? "subject")],
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
18
  scope: e.scope ?? "tag",
19
19
  tenantOverrides: Object.keys(e.tenants ?? {}).length
20
- }), f = () => ({
20
+ }), h = () => ({
21
21
  allowed: 0,
22
22
  rejected: 0,
23
23
  bypassed: 0,
@@ -25,11 +25,11 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
25
25
  storeErrors: 0,
26
26
  lastRejectedAt: null,
27
27
  lastStoreErrorAt: null
28
- }), p = (e, n) => {
28
+ }), g = (e, n) => {
29
29
  let r = Date.now(), i = {
30
- rpc: f(),
31
- http: f()
32
- }, a = n.bindSql ? "waiting" : "not-required";
30
+ rpc: h(),
31
+ http: h()
32
+ }, a = n.bind ? "waiting" : "not-required";
33
33
  return {
34
34
  binding: (e) => {
35
35
  a = e;
@@ -59,8 +59,8 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
59
59
  sqlBinding: a,
60
60
  trackedBuckets: o
61
61
  },
62
- rules: (e.rules ?? []).map((e, t) => d(e, t)),
63
- defaultRule: e.default ? d(e.default, null) : null,
62
+ rules: (e.rules ?? []).map((e, t) => m(e, t)),
63
+ defaultRule: e.default ? m(e.default, null) : null,
64
64
  http: e.http ? {
65
65
  limit: e.http.limit,
66
66
  windowMs: t(e.http.window),
@@ -73,51 +73,34 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
73
73
  };
74
74
  }
75
75
  };
76
- }, m = (e = {}) => {
77
- let t = e.maxKeys ?? 1e5, r = e.staleAfterMs ?? 36e5, a = /* @__PURE__ */ new Map(), o = (e) => {
78
- if (a.size <= t) return;
79
- let n = a.size - t, r = 0;
80
- for (let t of a.keys()) {
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
81
  if (r >= n) break;
82
- t !== e && (a.delete(t), r += 1);
82
+ t !== e && (i.delete(t), r += 1);
83
83
  }
84
84
  };
85
85
  return {
86
- consume: (e, t, r) => i.sync(() => {
87
- let i = a.get(e), { state: s, decision: c } = n(i?.state, t, r);
88
- return a.set(e, {
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
89
  state: s,
90
90
  touched: r
91
- }), o(e), c;
91
+ }), a(e), c;
92
92
  }),
93
93
  sweep: (e) => {
94
94
  let t = e - r;
95
- for (let [e, n] of a) n.touched < t && a.delete(e);
95
+ for (let [e, n] of i) n.touched < t && i.delete(e);
96
96
  },
97
- size: () => a.size
97
+ size: () => i.size
98
98
  };
99
- }, h = (e, t) => ({
100
- allowed: !0,
101
- failOpenReason: "unbound",
102
- remaining: e.limit,
103
- resetAtMs: t + e.windowMs,
104
- retryAfterMs: 0
105
- }), g = (e = {}) => {
106
- let t = null;
107
- return {
108
- consume: (e, n, r) => t ? t.consume(e, n, r) : i.succeed(h(n, r)),
109
- sweep: (e) => t?.sweep(e),
110
- size: () => t ? t.size() : -1,
111
- bindSql: async (n) => {
112
- let { postgresStore: r } = await import("./postgresStore.js"), i = r(e);
113
- await i.bindSql(n), t = i;
114
- }
115
- };
116
- }, _ = (e, t) => typeof e == "function" ? e(t) : e, v = /* @__PURE__ */ new WeakMap(), y = (e) => {
99
+ }, v = (e, t) => typeof e == "function" ? e(t) : e, y = /* @__PURE__ */ new WeakMap(), b = (e) => {
117
100
  if (!e.global && !e.sticky) return e;
118
- let t = v.get(e);
119
- return t === void 0 && (t = new RegExp(e.source, e.flags.replace(/[gy]/g, "")), v.set(e, t)), t;
120
- }, b = (e, t) => e === void 0 ? !0 : typeof e == "string" ? e === t : Array.isArray(e) ? e.includes(t) : y(e).test(t), x = (e, t) => e === void 0 ? !0 : Array.isArray(e) ? e.includes(t) : e === t, S = (e, t) => {
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) => {
121
104
  let n = t === null ? void 0 : e.tenants?.[t];
122
105
  return {
123
106
  limit: n?.limit ?? e.limit,
@@ -128,27 +111,27 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
128
111
  scope: e.scope ?? "tag",
129
112
  disabled: n?.disabled ?? !1
130
113
  };
131
- }, C = (e, t, n) => {
114
+ }, w = (e, t, n) => {
132
115
  for (let t = 0; t < e.length; t++) {
133
116
  let r = e[t];
134
- if (b(r.match, n.tag) && x(r.kind, n.kind)) return {
135
- effective: S(r, n.tenantId),
117
+ if (x(r.match, n.tag) && S(r.kind, n.kind)) return {
118
+ effective: C(r, n.tenantId),
136
119
  ruleId: `r${t}`
137
120
  };
138
121
  }
139
- if (t && x(t.kind, n.kind)) return {
140
- effective: S(t, n.tenantId),
122
+ if (t && S(t.kind, n.kind)) return {
123
+ effective: C(t, n.tenantId),
141
124
  ruleId: "default"
142
125
  };
143
- }, w = (e, n) => {
144
- let r = _(e.limit, n);
126
+ }, T = (e, n) => {
127
+ let r = v(e.limit, n);
145
128
  return {
146
129
  limit: r,
147
- windowMs: t(_(e.window, n)),
130
+ windowMs: t(v(e.window, n)),
148
131
  algorithm: e.algorithm ?? "sliding-window",
149
- burst: e.burst === void 0 ? r : _(e.burst, n)
132
+ burst: e.burst === void 0 ? r : v(e.burst, n)
150
133
  };
151
- }, T = (e) => {
134
+ }, E = (e) => {
152
135
  let n = e.limit;
153
136
  return {
154
137
  limit: n,
@@ -156,12 +139,12 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
156
139
  algorithm: e.algorithm ?? "sliding-window",
157
140
  burst: e.burst ?? n
158
141
  };
159
- }, E = (e) => ({
142
+ }, D = (e) => ({
160
143
  limit: e.limit,
161
144
  window: e.windowMs,
162
145
  algorithm: e.algorithm,
163
146
  burst: e.burst
164
- }), D = (e, t) => {
147
+ }), O = (e, t) => {
165
148
  if (typeof e == "function") return e(t);
166
149
  switch (e) {
167
150
  case "subject": return `s:${t.tenantId ?? "∅"}:${t.subject.id ?? "anon"}`;
@@ -170,10 +153,10 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
170
153
  case "global": return "g";
171
154
  case "ip": return `i:${t.remoteAddr ?? "unknown"}`;
172
155
  }
173
- }, O = (e, t) => {
156
+ }, k = (e, t) => {
174
157
  let { effective: n, ruleId: r } = e, i = n.scope === "rule" ? `rule#${r}` : `tag:${t.tag}`, a = n.by;
175
- return `${i}|${Array.isArray(a) ? a.map((e) => D(e, t)).join("|") : D(a, t)}`;
176
- }, k = (e) => ({
158
+ return `${i}|${Array.isArray(a) ? a.map((e) => O(e, t)).join("|") : O(a, t)}`;
159
+ }, A = (e) => ({
177
160
  tag: e.tag,
178
161
  kind: e.kind,
179
162
  subject: e.subject,
@@ -182,7 +165,7 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
182
165
  ...e.surface === void 0 ? {} : { surface: e.surface },
183
166
  ...e.remoteAddr === void 0 ? {} : { remoteAddr: e.remoteAddr },
184
167
  ...e.surface === "rest" && e.publicApi?.rateLimit !== void 0 ? { publicApiRateLimit: e.publicApi.rateLimit } : {}
185
- }), A = (e) => e.publicApiRateLimit === void 0 ? void 0 : {
168
+ }), j = (e) => e.publicApiRateLimit === void 0 ? void 0 : {
186
169
  ruleId: "publicApi",
187
170
  effective: {
188
171
  limit: e.publicApiRateLimit.limit,
@@ -193,7 +176,7 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
193
176
  scope: "tag",
194
177
  disabled: !1
195
178
  }
196
- }, j = (e) => {
179
+ }, M = (e) => {
197
180
  let t = e, n = Date.now();
198
181
  return {
199
182
  "retry-after": String(Math.max(1, Math.ceil(t.retryAfterMs / 1e3))),
@@ -201,11 +184,11 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
201
184
  "ratelimit-remaining": "0",
202
185
  "ratelimit-reset": String(Math.max(0, Math.ceil((t.resetAtMs - n) / 1e3)))
203
186
  };
204
- }, M = a({ scope: "voltro:ratelimit" }), N = (n = {}) => {
205
- let a = n.rules ?? [], l = n.store === void 0 || n.store === "memory" ? m() : n.store === "postgres" ? g() : n.store === "redis" ? r() : n.store, u = p(n, l), d = (e) => {
187
+ }, N = c({ scope: "voltro:ratelimit" }), P = (n = {}) => {
188
+ let a = n.rules ?? [], c = n.store === void 0 || n.store === "memory" ? _() : n.store === "postgres" ? r() : n.store === "redis" ? o() : n.store, f = g(n, c), p = (e) => {
206
189
  let t = n.enabled;
207
190
  return t === void 0 ? !0 : typeof t == "function" ? t(e) : t;
208
- }, f = (e, t, r, i) => {
191
+ }, m = (e, t, r, i) => {
209
192
  if (!n.onLimited) return;
210
193
  let a = {
211
194
  tag: e.tag,
@@ -221,87 +204,88 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
221
204
  } catch {}
222
205
  }, h = (e, t) => {
223
206
  let r = n.resolve;
224
- return r ? i.suspend(() => {
207
+ return r ? s.suspend(() => {
225
208
  let n = r(e, t);
226
- return (i.isEffect(n) ? n : n instanceof Promise ? i.promise(() => n) : i.succeed(n)).pipe(i.map((e) => e === "exempt" ? "exempt" : T(e === void 0 ? t : e)));
227
- }) : i.succeed(T(t));
228
- }, _ = (t, r) => i.suspend(() => {
229
- let o = k(r);
230
- if (!d(o)) return u.bypass(), t;
231
- let s = A(o) ?? C(a, n.default, o);
232
- if (!s || s.effective.disabled) return u.bypass(), t;
233
- let c = E(w(s.effective, o));
234
- return h(o, c).pipe(i.flatMap((n) => {
235
- if (n === "exempt") return u.bypass(), t;
236
- let r = O(s, o);
237
- return l.consume(r, n, Date.now()).pipe(i.catchAllDefect((e) => i.as(i.logWarning(`[ratelimit] store error — failing open for ${o.tag}: ${String(e)}`), {
209
+ return (s.isEffect(n) ? n : n instanceof Promise ? s.promise(() => n) : s.succeed(n)).pipe(s.map((e) => e === "exempt" ? "exempt" : E(e === void 0 ? t : e)));
210
+ }) : s.succeed(E(t));
211
+ }, v = (t, r) => s.suspend(() => {
212
+ let i = A(r);
213
+ if (!p(i)) return f.bypass(), t;
214
+ let o = j(i) ?? w(a, n.default, i);
215
+ if (!o || o.effective.disabled) return f.bypass(), t;
216
+ let l = D(T(o.effective, i));
217
+ return h(i, l).pipe(s.flatMap((n) => {
218
+ if (n === "exempt") return f.bypass(), t;
219
+ let r = k(o, i);
220
+ return c.consume(r, n, Date.now()).pipe(s.catchAllDefect((e) => s.as(s.logWarning(`[ratelimit] store error — failing open for ${i.tag}: ${String(e)}`), {
238
221
  allowed: !0,
239
222
  failOpenReason: "store-error",
240
223
  remaining: n.limit,
241
224
  resetAtMs: Date.now(),
242
225
  retryAfterMs: 0
243
- })), i.tap((e) => i.sync(() => u.record("rpc", e))), i.flatMap((a) => a.allowed ? t : i.zipRight(i.sync(() => f(o, r, n, a)), i.fail(new e({
244
- tag: o.tag,
226
+ })), s.tap((e) => s.sync(() => f.record("rpc", e))), s.flatMap((a) => a.allowed ? t : s.zipRight(s.sync(() => m(i, r, n, a)), s.fail(new e({
227
+ tag: i.tag,
245
228
  limit: n.limit,
246
229
  windowMs: n.windowMs,
247
230
  retryAfterMs: a.retryAfterMs,
248
231
  resetAtMs: a.resetAtMs
249
232
  })))));
250
233
  }));
251
- }), v = n.http ? async (e, r) => {
252
- let a = n.http, o = {
253
- limit: a.limit,
254
- windowMs: t(a.window),
255
- algorithm: a.algorithm ?? "fixed-window",
256
- burst: a.limit
257
- }, s = r.remoteAddr ?? "unknown", c = Date.now(), d;
234
+ }), y = n.http ? async (e, r) => {
235
+ let i = n.http, a = {
236
+ limit: i.limit,
237
+ windowMs: t(i.window),
238
+ algorithm: i.algorithm ?? "fixed-window",
239
+ burst: i.limit
240
+ }, o = r.remoteAddr ?? "unknown", l = Date.now(), u;
258
241
  try {
259
- d = await i.runPromise(l.consume(`http:ip:${s}`, o, c));
242
+ u = await s.runPromise(c.consume(`http:ip:${o}`, a, l));
260
243
  } catch (t) {
261
- return u.record("http", {
244
+ return f.record("http", {
262
245
  allowed: !0,
263
246
  failOpenReason: "store-error",
264
247
  remaining: 0,
265
- resetAtMs: c,
248
+ resetAtMs: l,
266
249
  retryAfterMs: 0
267
- }), M.warn("http shield store failure — request ALLOWED without a limit check", { cause: String(t) }), e();
250
+ }), N.warn("http shield store failure — request ALLOWED without a limit check", { cause: String(t) }), e();
268
251
  }
269
- u.record("http", d);
270
- let f = String(Math.max(0, Math.ceil((d.resetAtMs - c) / 1e3)));
271
- return d.allowed ? e() : {
252
+ f.record("http", u);
253
+ let d = String(Math.max(0, Math.ceil((u.resetAtMs - l) / 1e3)));
254
+ return u.allowed ? e() : {
272
255
  status: 429,
273
256
  body: "Too Many Requests",
274
257
  headers: {
275
258
  "content-type": "text/plain; charset=utf-8",
276
- "retry-after": String(Math.max(1, Math.ceil(d.retryAfterMs / 1e3))),
277
- "ratelimit-limit": String(o.limit),
278
- "ratelimit-remaining": String(Math.max(0, d.remaining)),
279
- "ratelimit-reset": f
259
+ "retry-after": String(Math.max(1, Math.ceil(u.retryAfterMs / 1e3))),
260
+ "ratelimit-limit": String(a.limit),
261
+ "ratelimit-remaining": String(Math.max(0, u.remaining)),
262
+ "ratelimit-reset": d
280
263
  }
281
264
  };
282
- } : void 0, y = [
265
+ } : void 0, b = [
283
266
  "inspect:read",
284
267
  "rpc:intercept:mutation",
285
268
  "rpc:intercept:query",
286
269
  "rpc:intercept:action",
287
270
  "rpc:intercept:workflow",
288
- ...n.http ? ["http:intercept"] : []
289
- ], b, x;
290
- return s({
291
- name: c({
271
+ ...n.http ? ["http:intercept"] : [],
272
+ ...n.store === "postgres" ? ["store:write"] : []
273
+ ], x, S;
274
+ return u({
275
+ name: d({
292
276
  base: "@voltro/plugin-ratelimit",
293
277
  instance: n.name
294
278
  }),
295
279
  description: "Per-endpoint / per-subject / per-tenant rate limiting via the rpc interceptors. Configurable rules, three algorithms, static + dynamic per-tenant overrides.",
296
- permissions: y,
280
+ permissions: b,
297
281
  inspectEndpoints: [{
298
282
  method: "GET",
299
283
  path: "/state",
300
284
  scope: "process",
301
285
  description: "Declared rules and process-local limiting evidence, without bucket identities.",
302
- handler: () => i.sync(() => ({
286
+ handler: () => s.sync(() => ({
303
287
  kind: "json",
304
- data: u.snapshot()
288
+ data: f.snapshot()
305
289
  }))
306
290
  }],
307
291
  declaredEnv: [
@@ -336,7 +320,7 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
336
320
  description: "Fallback Upstash REST token for the http redis driver."
337
321
  }
338
322
  ],
339
- ...v ? { onHttpRequest: v } : {},
323
+ ...y ? { onHttpRequest: y } : {},
340
324
  errorSchemas: [{
341
325
  schema: e,
342
326
  import: {
@@ -345,34 +329,34 @@ var l = (e) => typeof e == "function" ? "dynamic" : e, u = (e) => typeof e == "f
345
329
  },
346
330
  rest: {
347
331
  status: 429,
348
- headers: j
332
+ headers: M
349
333
  }
350
334
  }],
351
- interceptMutation: _,
352
- interceptQuery: _,
353
- interceptAction: _,
354
- interceptWorkflow: _,
355
- bindDataStore: (e, t) => {
356
- let n = t?.sql;
357
- n && l.bindSql && (u.binding("binding"), l.bindSql(n).then(() => {
358
- x = void 0, u.binding("bound");
335
+ interceptMutation: v,
336
+ interceptQuery: v,
337
+ interceptAction: v,
338
+ interceptWorkflow: v,
339
+ ...n.store === "postgres" ? { extendSchema: { tables: [i] } } : {},
340
+ bindDataStore: (e) => {
341
+ c.bind && (f.binding("binding"), c.bind(e).then(() => {
342
+ S = void 0, f.binding("bound");
359
343
  }).catch((e) => {
360
- u.binding("failed"), x = e;
344
+ f.binding("failed"), S = e;
361
345
  }));
362
346
  },
363
- onActivate: (e) => i.gen(function* () {
364
- l.activate && (yield* i.promise(() => l.activate(e.env))), b = setInterval(() => l.sweep(Date.now()), 6e4), b.unref?.(), x !== void 0 && e.logger.warn("rate-limit: SQL store bind failed — limiter runs fail-open", { error: x instanceof Error ? x.message : String(x) });
365
- let t = n.store === void 0 || n.store === "memory" ? o(e.env) : null;
347
+ onActivate: (e) => s.gen(function* () {
348
+ c.activate && (yield* s.promise(() => c.activate(e.env))), x = setInterval(() => c.sweep(Date.now()), 6e4), x.unref?.(), S !== void 0 && e.logger.warn("rate-limit: SQL store bind failed — limiter runs fail-open", { error: S instanceof Error ? S.message : String(S) });
349
+ let t = n.store === void 0 || n.store === "memory" ? l(e.env) : null;
366
350
  t !== null && e.logger.warn(`rate-limit: the counter store is PROCESS-LOCAL (the default), but ${t}, 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", {
367
351
  rules: a.length,
368
352
  hasDefault: n.default !== void 0,
369
353
  store: typeof n.store == "string" ? n.store : n.store === void 0 ? "memory" : "custom"
370
354
  });
371
355
  }),
372
- onDeactivate: () => i.gen(function* () {
373
- b && clearInterval(b), l.deactivate && (yield* i.promise(() => l.deactivate()));
356
+ onDeactivate: () => s.gen(function* () {
357
+ x && clearInterval(x), c.deactivate && (yield* s.promise(() => c.deactivate()));
374
358
  })
375
359
  });
376
360
  };
377
361
  //#endregion
378
- export { e as RateLimited, n as consumeBucket, m as memoryStore, t as parseWindow, N as rateLimitPlugin, r as redisStore };
362
+ export { a as RATELIMIT_TABLE, e as RateLimited, n as consumeBucket, _ as memoryStore, t as parseWindow, r as postgresStore, P as rateLimitPlugin, i as rateLimitTable, o as redisStore };
@@ -0,0 +1,160 @@
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 };
@@ -1,17 +1,37 @@
1
+ import { ColumnBuilder } from '@voltro/database';
2
+ import { DataStore } from '@voltro/database';
1
3
  import { Effect } from 'effect';
2
- import { SqlClient } from '@effect/sql';
4
+ import { FieldDefinitions } from '@voltro/database';
5
+ import { Table } from '@voltro/database';
3
6
 
4
7
  declare type Algorithm = 'fixed-window' | 'sliding-window' | 'token-bucket';
5
8
 
9
+ /** Opaque per-key state. The store persists it verbatim; only this
10
+ * module reads its shape. */
11
+ declare type BucketState = {
12
+ readonly kind: 'fixed';
13
+ readonly windowStart: number;
14
+ readonly count: number;
15
+ } | {
16
+ readonly kind: 'sliding';
17
+ readonly currStart: number;
18
+ readonly currCount: number;
19
+ readonly prevCount: number;
20
+ } | {
21
+ readonly kind: 'token';
22
+ readonly tokens: number;
23
+ readonly lastRefill: number;
24
+ };
25
+
6
26
  export declare const postgresStore: (options?: PostgresStoreOptions) => RateLimitStore;
7
27
 
8
28
  export declare interface PostgresStoreOptions {
9
- /** Table name. Default `_voltro_ratelimit`. */
10
- readonly table?: string;
11
29
  /** Drop rows untouched for longer than this (ms) on sweep. Default 1h. */
12
30
  readonly staleAfterMs?: number;
13
31
  }
14
32
 
33
+ export declare const RATELIMIT_TABLE = "_voltro_ratelimit";
34
+
15
35
  declare interface RateLimitDecision {
16
36
  readonly allowed: boolean;
17
37
  /** The request passed without a quota check because the store was unavailable.
@@ -38,13 +58,23 @@ declare interface RateLimitStore {
38
58
  readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
39
59
  /** Optional teardown at plugin deactivate (close pools). */
40
60
  readonly deactivate?: () => Promise<void>;
41
- /** Optional bind of the framework's already-open `SqlClient` (from the
42
- * plugin's `bindDataStore(store, ctx)` → `ctx.sql`). A SQL-backed store
43
- * runs through THIS client instead of standing up its own pool; creates
44
- * its bookkeeping table here. Not present on memory / redis stores. */
45
- readonly bindSql?: (sql: SqlClient.SqlClient) => Promise<void>;
61
+ /** Optional bind of the app's `DataStore` (the plugin's `bindDataStore`).
62
+ * A store kept in the app's database reads and writes through it — its
63
+ * table is declared through `extendSchema`, never created here. Not
64
+ * present on memory / redis stores. */
65
+ readonly bind?: (store: DataStore) => Promise<void>;
46
66
  }
47
67
 
68
+ /** One row per rate-limit key. `id` + a unique `key` rather than `key` as the
69
+ * primary key: every dialect store addresses a row by `id`. */
70
+ export declare const rateLimitTable: Table<"_voltro_ratelimit", FieldDefinitions<{
71
+ readonly id: ColumnBuilder<number, "id", boolean>;
72
+ readonly key: ColumnBuilder<string, "text", boolean>;
73
+ readonly state: ColumnBuilder<BucketState | null, "json", boolean>;
74
+ readonly touchedAt: ColumnBuilder<Date, "timestamp", true>;
75
+ readonly touched: ColumnBuilder<unknown, "text", boolean>;
76
+ }>, false, never>;
77
+
48
78
  /** The fully-resolved limit the store consumes against. All dynamic /
49
79
  * per-tenant resolution has already collapsed into these scalars. */
50
80
  declare interface ResolvedLimit {