@voltro/plugin-ratelimit 0.85.0 → 0.87.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
@@ -1,8 +1,12 @@
1
1
  import { ColumnBuilder } from '@voltro/database';
2
+ import { ConnectionRef } from '@voltro/kv/connection';
3
+ import { ConnectionRegistryShape } from '@voltro/kv/connection';
2
4
  import { DataStore } from '@voltro/database';
3
5
  import { Effect } from 'effect';
4
6
  import { FieldDefinitions } from '@voltro/database';
5
- import { RespClient } from '@voltro/kv/connection';
7
+ import { HotRateLimitState } from '@voltro/kv/hot';
8
+ import { HotRateLimitState as HotRateLimitState_2 } from '@voltro/kv/hot/contract';
9
+ import { HotStoreShape } from '@voltro/kv/hot';
6
10
  import { RpcKind } from '@voltro/protocol';
7
11
  import { Schema } from 'effect';
8
12
  import { Subject } from '@voltro/protocol';
@@ -19,20 +23,7 @@ declare interface BucketResult {
19
23
 
20
24
  /** Opaque per-key state. The store persists it verbatim; only this
21
25
  * module reads its shape. */
22
- export declare type BucketState = {
23
- readonly kind: 'fixed';
24
- readonly windowStart: number;
25
- readonly count: number;
26
- } | {
27
- readonly kind: 'sliding';
28
- readonly currStart: number;
29
- readonly currCount: number;
30
- readonly prevCount: number;
31
- } | {
32
- readonly kind: 'token';
33
- readonly tokens: number;
34
- readonly lastRefill: number;
35
- };
26
+ export declare type BucketState = HotRateLimitState_2;
36
27
 
37
28
  export declare const consumeBucket: (prev: BucketState | undefined, limit: ResolvedLimit, now: number) => BucketResult;
38
29
 
@@ -101,14 +92,15 @@ export declare interface RateLimitContext {
101
92
  /** Convenience: the subject's tenant (`null` only for unscoped anonymous). */
102
93
  readonly tenantId: string | null;
103
94
  readonly traceId: string;
104
- /** `'rest'` when the call came through the `publicApi` projection; absent
95
+ /** `'rest'` for a hand-authored REST route or `publicApi` projection; absent
105
96
  * on the rpc wire. */
106
97
  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`. */
98
+ /** The descriptor's own REST quota — `rateLimit` or `publicApi.rateLimit`,
99
+ * ahead of `rules` and `default`. */
109
100
  readonly publicApiRateLimit?: {
110
101
  readonly limit: number;
111
102
  readonly window: string;
103
+ readonly by?: 'subject' | 'tenant' | 'apiKey' | 'ip' | 'global';
112
104
  };
113
105
  /** The caller's address, resolved by the listener through the app's
114
106
  * `security.trustedProxies` — what `by: 'ip'` keys on. Absent for a call
@@ -179,17 +171,16 @@ export declare interface RateLimitPluginOptions {
179
171
  /**
180
172
  * Counter backend:
181
173
  * - `'memory'` (default) — single-node, zero-config.
182
- * - `'postgres'` — multi-node correct; opens its own pool from the
183
- * same `DB_*`/`PG_*` env the framework reads, creates
184
- * `_voltro_ratelimit`, mutates state under a row lock. Fails OPEN
185
- * on db errors (degrades to no-limit, never to 500s).
186
- * - `'redis'` — multi-node, fastest; connects ioredis from
187
- * `CACHE_REDIS_URL`/`REDIS_URL`, runs the whole decision in one
188
- * atomic Lua script. For a custom client/url use the
189
- * `redisStore({...})` factory from `@voltro/plugin-ratelimit/redis`.
174
+ * - `'postgres'` — counters in the app's declared schema, bound through
175
+ * `DataStore`; atomic transactions, fail-open on unavailable storage.
176
+ * - `'redis'` — atomic HotStore decisions over the app-owned registry.
177
+ * Use `redis.connection` to select a declared Redis connection or
178
+ * `redis.hotStore` to borrow a replacement port.
190
179
  * - a custom `RateLimitStore`.
191
180
  */
192
181
  readonly store?: 'memory' | 'postgres' | 'redis' | RateLimitStore;
182
+ /** Registry reference or replacement HotStore for the Redis counter backend. */
183
+ readonly redis?: RedisStoreOptions;
193
184
  /**
194
185
  * Master gate. `false` disables all limiting; a function lets you gate
195
186
  * per-call (e.g. exempt a tenant, or a maintenance bypass). Returning
@@ -262,7 +253,7 @@ export declare interface RateLimitStore {
262
253
  readonly size: () => number;
263
254
  /** Optional one-shot setup at plugin activate (open pools, create
264
255
  * tables). Receives the process env. */
265
- readonly activate?: (env: NodeJS.ProcessEnv) => Promise<void>;
256
+ readonly activate?: (env: NodeJS.ProcessEnv, connectionRegistry?: ConnectionRegistryShape) => Promise<void>;
266
257
  /** Optional teardown at plugin deactivate (close pools). */
267
258
  readonly deactivate?: () => Promise<void>;
268
259
  /** Optional bind of the app's `DataStore` (the plugin's `bindDataStore`).
@@ -277,37 +268,20 @@ export declare interface RateLimitStore {
277
268
  export declare const rateLimitTable: Table<"_voltro_ratelimit", FieldDefinitions<{
278
269
  readonly id: ColumnBuilder<number, "id", boolean>;
279
270
  readonly key: ColumnBuilder<string, "text", boolean>;
280
- readonly state: ColumnBuilder<BucketState | null, "json", boolean>;
271
+ readonly state: ColumnBuilder< HotRateLimitState | null, "json", boolean>;
281
272
  readonly touchedAt: ColumnBuilder<Date, "timestamp", true>;
282
273
  readonly touched: ColumnBuilder<unknown, "text", boolean>;
283
274
  }>, false, never>;
284
275
 
276
+ /** Redis limiter decisions go only through HotStore. The app owns every socket. */
285
277
  export declare const redisStore: (options?: RedisStoreOptions) => RateLimitStore;
286
278
 
287
279
  export declare interface RedisStoreOptions {
288
- /** Connection url. `resp` driver: `redis://…` / `rediss://…`. `http`
289
- * driver: the Upstash REST url. Falls back to `CACHE_REDIS_URL` /
290
- * `REDIS_URL` env at activate time. */
291
- readonly url?: string;
292
- /**
293
- * How to talk to the server — mirrors the cache plugin's axis:
294
- * - `'resp'` (default) — ioredis over TCP. Works for Redis, Valkey,
295
- * KeyDB, Dragonfly, and Upstash's TCP endpoint.
296
- * - `'http'` — `@upstash/redis` over REST, for serverless/edge where
297
- * TCP isn't available.
298
- * Resolved from `CACHE_REDIS_DRIVER` env when omitted. `'redis'` as a
299
- * store value is the umbrella for the whole RESP family — the server
300
- * brand is just the url; only the DRIVER is a real choice.
301
- */
302
- readonly driver?: 'resp' | 'http';
303
- /** Auth token — `http` (Upstash REST) driver only. Falls back to
304
- * `CACHE_REDIS_TOKEN` / `UPSTASH_REDIS_REST_TOKEN` env. */
305
- readonly token?: string;
306
- /** Bring your own already-connected RESP client (from
307
- * `@voltro/kv/connection`'s `connect`, or the shared registry). Takes
308
- * precedence over `url` / `driver`. */
309
- readonly client?: RespClient;
310
- /** Key namespace. Default `'voltro:rl:'`. */
280
+ /** Registry reference. Omit to use the app's default Redis connection. */
281
+ readonly connection?: ConnectionRef<'redis'>;
282
+ /** Borrow a replacement HotStore port. Its owner retains disposal. */
283
+ readonly hotStore?: HotStoreShape;
284
+ /** HotStore namespace. Default 'voltro:rl'; cannot contain braces. */
311
285
  readonly keyPrefix?: string;
312
286
  }
313
287
 
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 { newTraceId as h } from "@voltro/shared";
10
+ import { restError as g } from "@voltro/protocol/rest";
11
+ import { createLogger as _ } from "@voltro/logger";
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,143 +40,113 @@ 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 = _({ scope: "voltro:ratelimit" }), T = (t = {}) => {
44
+ if (t.redis !== void 0 && t.store !== "redis") throw Error("Rate limiter redis options require store: redis");
45
+ let a = t.rules ?? [], _ = t.store === void 0 || t.store === "memory" ? i() : t.store === "postgres" ? o() : t.store === "redis" ? c(t.redis) : t.store, T = r(t, _), E = (e) => {
46
+ let n = t.enabled;
47
+ return n === void 0 ? !0 : typeof n == "function" ? n(e) : n;
48
+ }, D = (e, n, r, i) => {
49
+ if (!t.onLimited) return;
193
50
  let a = {
194
51
  tag: e.tag,
195
52
  kind: e.kind,
196
- key: t,
53
+ key: n,
197
54
  tenantId: e.tenantId,
198
55
  limit: r.limit,
199
56
  windowMs: r.windowMs,
200
57
  retryAfterMs: i.retryAfterMs
201
58
  };
202
59
  try {
203
- n.onLimited(a);
60
+ t.onLimited(a);
204
61
  } 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)}`), {
62
+ }, O = (e, n) => {
63
+ let r = t.resolve;
64
+ return r ? m.suspend(() => {
65
+ let t = r(e, n);
66
+ 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)));
67
+ }) : m.succeed(u(n));
68
+ }, k = (n, r) => m.suspend(() => {
69
+ let i = x(r);
70
+ if (!E(i)) return T.bypass(), n;
71
+ let o = S(i) ?? d(a, t.default, i);
72
+ if (!o || o.effective.disabled) return T.bypass(), n;
73
+ let s = p(f(o.effective, i));
74
+ return O(i, s).pipe(m.flatMap((t) => {
75
+ if (t === "exempt") return T.bypass(), n;
76
+ let r = l(o, i);
77
+ return _.consume(r, t, Date.now()).pipe(m.catchDefect((e) => m.as(m.logWarning(`[ratelimit] store error — failing open for ${i.tag}: ${String(e)}`), {
221
78
  allowed: !0,
222
79
  failOpenReason: "store-error",
223
- remaining: n.limit,
80
+ remaining: t.limit,
224
81
  resetAtMs: Date.now(),
225
82
  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({
83
+ })), 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
84
  tag: i.tag,
228
- limit: n.limit,
229
- windowMs: n.windowMs,
85
+ limit: t.limit,
86
+ windowMs: t.windowMs,
230
87
  retryAfterMs: a.retryAfterMs,
231
88
  resetAtMs: a.resetAtMs
232
89
  })))));
233
90
  }));
234
- }), y = n.http ? async (e, r) => {
235
- let i = n.http, a = {
91
+ }), A = t.http ? async (e, r) => {
92
+ let i = t.http, a = {
236
93
  limit: i.limit,
237
- windowMs: t(i.window),
94
+ windowMs: n(i.window),
238
95
  algorithm: i.algorithm ?? "fixed-window",
239
96
  burst: i.limit
240
- }, o = r.remoteAddr ?? "unknown", l = Date.now(), u;
97
+ }, o = r.remoteAddr ?? "unknown", s = Date.now(), c;
241
98
  try {
242
- u = await s.runPromise(c.consume(`http:ip:${o}`, a, l));
99
+ c = await m.runPromise(_.consume(`http:ip:${o}`, a, s));
243
100
  } catch (t) {
244
- return f.record("http", {
101
+ return T.record("http", {
245
102
  allowed: !0,
246
103
  failOpenReason: "store-error",
247
104
  remaining: 0,
248
- resetAtMs: l,
105
+ resetAtMs: s,
249
106
  retryAfterMs: 0
250
- }), N.warn("http shield store failure — request ALLOWED without a limit check", { cause: String(t) }), e();
107
+ }), w.warn("http shield store failure — request ALLOWED without a limit check", { cause: String(t) }), e();
251
108
  }
252
- f.record("http", u);
253
- let d = String(Math.max(0, Math.ceil((u.resetAtMs - l) / 1e3)));
254
- return u.allowed ? e() : {
109
+ T.record("http", c);
110
+ let l = String(Math.max(0, Math.ceil((c.resetAtMs - s) / 1e3)));
111
+ return c.allowed ? e() : {
255
112
  status: 429,
256
113
  body: "Too Many Requests",
257
114
  headers: {
258
115
  "content-type": "text/plain; charset=utf-8",
259
- "retry-after": String(Math.max(1, Math.ceil(u.retryAfterMs / 1e3))),
116
+ "retry-after": String(Math.max(1, Math.ceil(c.retryAfterMs / 1e3))),
260
117
  "ratelimit-limit": String(a.limit),
261
- "ratelimit-remaining": String(Math.max(0, u.remaining)),
262
- "ratelimit-reset": d
118
+ "ratelimit-remaining": String(Math.max(0, c.remaining)),
119
+ "ratelimit-reset": l
263
120
  }
264
121
  };
265
- } : void 0, b = [
122
+ } : void 0, j = [
266
123
  "inspect:read",
267
124
  "rpc:intercept:mutation",
268
125
  "rpc:intercept:query",
269
126
  "rpc:intercept:action",
270
127
  "rpc:intercept:workflow",
271
- ...n.http ? ["http:intercept"] : [],
272
- ...n.store === "postgres" ? ["store:write"] : []
273
- ], x, S;
274
- return u({
275
- name: d({
128
+ "http:intercept",
129
+ ...t.store === "postgres" ? ["store:write"] : []
130
+ ], M, N;
131
+ return y({
132
+ name: b({
276
133
  base: "@voltro/plugin-ratelimit",
277
- instance: n.name
134
+ instance: t.name
278
135
  }),
279
136
  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,
137
+ permissions: j,
281
138
  inspectEndpoints: [{
282
139
  method: "GET",
283
140
  path: "/state",
284
141
  scope: "process",
285
142
  description: "Declared rules and process-local limiting evidence, without bucket identities.",
286
- handler: () => s.sync(() => ({
143
+ handler: () => m.sync(() => ({
287
144
  kind: "json",
288
- data: f.snapshot()
145
+ data: T.snapshot()
289
146
  }))
290
147
  }],
291
- declaredEnv: [
292
- {
293
- name: "CACHE_REDIS_URL",
294
- required: !1,
295
- secret: !0,
296
- description: "Redis connection URL for the redis store (credential-carrying). Falls back to REDIS_URL."
297
- },
298
- {
299
- name: "REDIS_URL",
300
- required: !1,
301
- secret: !0,
302
- description: "Fallback Redis connection URL for the redis store (credential-carrying)."
303
- },
304
- {
305
- name: "CACHE_REDIS_DRIVER",
306
- required: !1,
307
- secret: !1,
308
- description: "Redis driver selector: \"http\" for the Upstash REST client, otherwise RESP."
309
- },
310
- {
311
- name: "CACHE_REDIS_TOKEN",
312
- required: !1,
313
- secret: !0,
314
- description: "Upstash REST token for the http redis driver. Falls back to UPSTASH_REDIS_REST_TOKEN."
315
- },
316
- {
317
- name: "UPSTASH_REDIS_REST_TOKEN",
318
- required: !1,
319
- secret: !0,
320
- description: "Fallback Upstash REST token for the http redis driver."
321
- }
322
- ],
323
- ...y ? { onHttpRequest: y } : {},
148
+ declaredEnv: [],
149
+ ...A ? { onHttpRequest: A } : {},
324
150
  errorSchemas: [{
325
151
  schema: e,
326
152
  import: {
@@ -329,34 +155,47 @@ var f = (e) => typeof e == "function" ? "dynamic" : e, p = (e) => typeof e == "f
329
155
  },
330
156
  rest: {
331
157
  status: 429,
332
- headers: M
158
+ headers: C
333
159
  }
334
160
  }],
335
- interceptMutation: v,
336
- interceptQuery: v,
337
- interceptAction: v,
338
- interceptWorkflow: v,
339
- ...n.store === "postgres" ? { extendSchema: { tables: [i] } } : {},
161
+ restRateLimit: (t, n, r) => k(m.void, {
162
+ tag: `${r.method} ${r.path}`,
163
+ kind: "action",
164
+ input: void 0,
165
+ subject: n.subject,
166
+ traceId: h(),
167
+ surface: "rest",
168
+ publicApi: { rateLimit: t },
169
+ ...n.remoteAddr === void 0 ? {} : { remoteAddr: n.remoteAddr }
170
+ }).pipe(m.catch((t) => m.fail(t instanceof e ? g(429, {
171
+ error: "RateLimited",
172
+ message: "Rate limit exceeded"
173
+ }, C(t)) : t))),
174
+ interceptMutation: k,
175
+ interceptQuery: k,
176
+ interceptAction: k,
177
+ interceptWorkflow: k,
178
+ ...t.store === "postgres" ? { extendSchema: { tables: [s] } } : {},
340
179
  bindDataStore: (e) => {
341
- c.bind && (f.binding("binding"), c.bind(e).then(() => {
342
- S = void 0, f.binding("bound");
180
+ _.bind && (T.binding("binding"), _.bind(e).then(() => {
181
+ N = void 0, T.binding("bound");
343
182
  }).catch((e) => {
344
- f.binding("failed"), S = e;
183
+ T.binding("failed"), N = e;
345
184
  }));
346
185
  },
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", {
186
+ onActivate: (e) => m.gen(function* () {
187
+ _.activate && (yield* m.promise(() => _.activate(e.env, e.connectionRegistry))), M = setInterval(() => _.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) });
188
+ let n = t.store === void 0 || t.store === "memory" ? v(e.env) : null;
189
+ 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
190
  rules: a.length,
352
- hasDefault: n.default !== void 0,
353
- store: typeof n.store == "string" ? n.store : n.store === void 0 ? "memory" : "custom"
191
+ hasDefault: t.default !== void 0,
192
+ store: typeof t.store == "string" ? t.store : t.store === void 0 ? "memory" : "custom"
354
193
  });
355
194
  }),
356
- onDeactivate: () => s.gen(function* () {
357
- x && clearInterval(x), c.deactivate && (yield* s.promise(() => c.deactivate()));
195
+ onDeactivate: () => m.gen(function* () {
196
+ M && clearInterval(M), _.deactivate && (yield* m.promise(() => _.deactivate()));
358
197
  })
359
198
  });
360
199
  };
361
200
  //#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 };
201
+ 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 };