@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/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'` when the call came through the `publicApi` projection; absent
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`, on a projected call — the
108
- * endpoint's rule for that surface, ahead of `rules` and `default`. */
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 { a as t, i as n, n as r, r as i, t as a } from "./postgresStore-D_oHSGXq.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
- //#region src/inspect.ts
9
- var f = (e) => typeof e == "function" ? "dynamic" : e, p = (e) => typeof e == "function" ? "dynamic" : e, m = (e, n) => ({
10
- id: n === null ? "default" : `r${n}`,
11
- match: "match" in e ? e.match instanceof RegExp ? String(e.match) : Array.isArray(e.match) ? e.match.join(", ") : e.match ?? "*" : "*",
12
- kinds: e.kind === void 0 ? [] : typeof e.kind == "string" ? [e.kind] : e.kind,
13
- limit: f(e.limit),
14
- windowMs: typeof e.window == "function" ? "dynamic" : t(e.window),
15
- algorithm: e.algorithm ?? "sliding-window",
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
- }), j = (e) => e.publicApiRateLimit === void 0 ? void 0 : {
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
- }, M = (e) => {
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
- }, 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) => {
189
- let t = n.enabled;
190
- return t === void 0 ? !0 : typeof t == "function" ? t(e) : t;
191
- }, m = (e, t, r, i) => {
192
- if (!n.onLimited) return;
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: t,
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
- n.onLimited(a);
59
+ t.onLimited(a);
204
60
  } catch {}
205
- }, h = (e, t) => {
206
- let r = n.resolve;
207
- return r ? s.suspend(() => {
208
- let n = r(e, t);
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.catchDefect((e) => s.as(s.logWarning(`[ratelimit] store error — failing open for ${i.tag}: ${String(e)}`), {
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: n.limit,
79
+ remaining: t.limit,
224
80
  resetAtMs: Date.now(),
225
81
  retryAfterMs: 0
226
- })), s.tap((e) => s.sync(() => f.record("rpc", e))), s.flatMap((a) => a.allowed ? t : s.andThen(s.sync(() => m(i, r, n, a)), s.fail(new e({
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: n.limit,
229
- windowMs: n.windowMs,
84
+ limit: t.limit,
85
+ windowMs: t.windowMs,
230
86
  retryAfterMs: a.retryAfterMs,
231
87
  resetAtMs: a.resetAtMs
232
88
  })))));
233
89
  }));
234
- }), y = n.http ? async (e, r) => {
235
- let i = n.http, a = {
90
+ }), A = t.http ? async (e, r) => {
91
+ let i = t.http, a = {
236
92
  limit: i.limit,
237
- windowMs: t(i.window),
93
+ windowMs: n(i.window),
238
94
  algorithm: i.algorithm ?? "fixed-window",
239
95
  burst: i.limit
240
- }, o = r.remoteAddr ?? "unknown", l = Date.now(), u;
96
+ }, o = r.remoteAddr ?? "unknown", s = Date.now(), c;
241
97
  try {
242
- u = await s.runPromise(c.consume(`http:ip:${o}`, a, l));
98
+ c = await m.runPromise(g.consume(`http:ip:${o}`, a, s));
243
99
  } catch (t) {
244
- return f.record("http", {
100
+ return T.record("http", {
245
101
  allowed: !0,
246
102
  failOpenReason: "store-error",
247
103
  remaining: 0,
248
- resetAtMs: l,
104
+ resetAtMs: s,
249
105
  retryAfterMs: 0
250
- }), N.warn("http shield store failure — request ALLOWED without a limit check", { cause: String(t) }), e();
106
+ }), w.warn("http shield store failure — request ALLOWED without a limit check", { cause: String(t) }), e();
251
107
  }
252
- f.record("http", u);
253
- let d = String(Math.max(0, Math.ceil((u.resetAtMs - l) / 1e3)));
254
- return u.allowed ? e() : {
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(u.retryAfterMs / 1e3))),
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, u.remaining)),
262
- "ratelimit-reset": d
117
+ "ratelimit-remaining": String(Math.max(0, c.remaining)),
118
+ "ratelimit-reset": l
263
119
  }
264
120
  };
265
- } : void 0, b = [
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
- ...n.http ? ["http:intercept"] : [],
272
- ...n.store === "postgres" ? ["store:write"] : []
273
- ], x, S;
274
- return u({
275
- name: d({
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: n.name
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: b,
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: () => s.sync(() => ({
142
+ handler: () => m.sync(() => ({
287
143
  kind: "json",
288
- data: f.snapshot()
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
- ...y ? { onHttpRequest: y } : {},
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: M
188
+ headers: C
333
189
  }
334
190
  }],
335
- interceptMutation: v,
336
- interceptQuery: v,
337
- interceptAction: v,
338
- interceptWorkflow: v,
339
- ...n.store === "postgres" ? { extendSchema: { tables: [i] } } : {},
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
- c.bind && (f.binding("binding"), c.bind(e).then(() => {
342
- S = void 0, f.binding("bound");
210
+ g.bind && (T.binding("binding"), g.bind(e).then(() => {
211
+ N = void 0, T.binding("bound");
343
212
  }).catch((e) => {
344
- f.binding("failed"), S = e;
213
+ T.binding("failed"), N = e;
345
214
  }));
346
215
  },
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;
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", {
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: n.default !== void 0,
353
- store: typeof n.store == "string" ? n.store : n.store === void 0 ? "memory" : "custom"
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: () => s.gen(function* () {
357
- x && clearInterval(x), c.deactivate && (yield* s.promise(() => c.deactivate()));
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, n as consumeBucket, _ as memoryStore, t as parseWindow, r as postgresStore, P as rateLimitPlugin, i as rateLimitTable, o as redisStore };
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 };
@@ -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 };
@@ -1,2 +1,48 @@
1
- import { n as e, r as t, t as n } from "./postgresStore-D_oHSGXq.js";
2
- export { n as RATELIMIT_TABLE, e as postgresStore, t as rateLimitTable };
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 };
@@ -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 };