@lunora/ratelimit 1.0.0-alpha.1 → 1.0.0-alpha.100

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/LICENSE.md CHANGED
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
103
103
  under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
104
  CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
105
  specific language governing permissions and limitations under the License.
106
+
107
+ <!-- DEPENDENCIES -->
108
+ <!-- /DEPENDENCIES -->
109
+
110
+ <!-- TYPE_DEPENDENCIES -->
111
+ <!-- /TYPE_DEPENDENCIES -->
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  </a>
8
8
 
9
- <h3 align="center">Rate limiting: token-bucket / fixed-window / sliding-window algorithms, deny list, sharding, pluggable stores, and procedure middleware</h3>
9
+ <h3 align="center">Rate limiting: token-bucket / fixed-window / sliding-window algorithms, deny list, pluggable stores, and procedure middleware</h3>
10
10
 
11
11
  <!-- END_PACKAGE_OG_IMAGE_PLACEHOLDER -->
12
12
 
@@ -34,7 +34,7 @@
34
34
 
35
35
  ---
36
36
 
37
- Rate limiting for Lunora: token-bucket, fixed-window, and sliding-window algorithms, a deny list, optional sharding for hot limits, pluggable stores (in-memory, SQL, or the Lunora ORM), and procedure middleware that rides the `.use()` chain.
37
+ Rate limiting for Lunora: token-bucket, fixed-window, and sliding-window algorithms, a deny list, pluggable stores (in-memory, SQL, or the Lunora ORM), and procedure middleware that rides the `.use()` chain.
38
38
 
39
39
  Part of the [Lunora](https://github.com/anolilab/lunora) framework — a type-safe, real-time backend on Cloudflare Workers + Durable Objects with a Vite-first DX.
40
40
 
@@ -59,37 +59,68 @@ to a procedure's `.use(...)` chain. The procedure builders (`mutation`, …) com
59
59
  from your app's generated `_generated/server` module, not from a package.
60
60
 
61
61
  ```ts
62
- import { RateLimiter, rateLimit } from "@lunora/ratelimit";
62
+ import { dbRateLimit } from "@lunora/ratelimit";
63
63
 
64
64
  import { mutation } from "./_generated/server";
65
65
 
66
- const limiter = new RateLimiter({
67
- config: {
68
- login: { kind: "fixed window", period: 60_000, rate: 5 },
69
- send: { kind: "token bucket", period: 1_000, rate: 10 },
70
- },
71
- });
72
-
66
+ const config = {
67
+ login: { kind: "fixed window", period: 60_000, rate: 5 },
68
+ send: { kind: "token bucket", period: 1_000, rate: 10 },
69
+ };
70
+
71
+ // `dbRateLimit` builds the limiter per call, backed by a Lunora table via
72
+ // `ctx.db`, so the count is durable. A `RateLimiter` built with no explicit
73
+ // `store` falls back to `createMemoryStore` (a per-instance in-memory Map) —
74
+ // fine for a limit that only ever needs to hold within one Durable Object
75
+ // instance, but a limiter like `login` needs a durable store or it stops
76
+ // enforcing the configured rate the moment the DO instance is sharded,
77
+ // replicated, or recreated.
73
78
  // As procedure middleware — throws a structural LunoraError (429/403) on rejection.
74
- export const send = mutation.use(rateLimit(limiter, "send", { key: (ctx) => ctx.auth.userId })).mutation(async ({ ctx }) => {
79
+ // `key` must resolve to a string — a resolver returning `undefined` throws
80
+ // rather than silently sharing one bucket across every keyless caller.
81
+ export const send = mutation.use(dbRateLimit(config, "send", { key: (ctx) => ctx.auth.userId ?? "anonymous" })).mutation(async ({ ctx }) => {
75
82
  // …
76
83
  });
77
84
  ```
78
85
 
79
- Or call the limiter directly:
86
+ Or call the limiter directly, inside a mutation/action so `ctx.db` is available.
87
+ A login limiter belongs in an **action**: a mutation's `ctx.db` writes ride its
88
+ storage transaction, so a handler that throws (a wrong password) rolls the
89
+ consumed unit back and every failed attempt is free. An action's writes commit
90
+ on their own, so the charge stays whether or not the handler throws.
80
91
 
81
92
  ```ts
82
- const status = await limiter.limit("send", { key: userId });
93
+ import { RateLimiter, RateLimitError, createDbStore } from "@lunora/ratelimit";
94
+
95
+ import { action } from "./_generated/server";
83
96
 
84
- if (!status.ok) {
85
- // status.retryAfter is milliseconds until the request would succeed.
86
- // status.reason is "rate" or "deny".
87
- }
97
+ export const login = action.action(async ({ ctx, args }) => {
98
+ const limiter = new RateLimiter({ config, store: createDbStore({ db: ctx.db }) });
99
+ const status = await limiter.limit("login", { key: args.email });
100
+
101
+ if (!status.ok) {
102
+ // Stop here — `limit` returns a failing status by default rather than
103
+ // throwing, so falling through authenticates the rejected attempt.
104
+ // status.retryAfter is milliseconds until the request would succeed.
105
+ // status.reason is "rate" or "deny".
106
+ throw new RateLimitError(status);
107
+ }
108
+
109
+ await authenticate(args);
110
+ });
88
111
  ```
89
112
 
90
113
  For durable per-DO state inside a procedure, back the limiter with a Lunora
91
114
  table via `dbRateLimit(config, name, options)`, or supply a `store`
92
- (`createMemoryStore` / `createSqlStore` / `createDbStore`).
115
+ (`createSqlStore` / `createDbStore`). `createMemoryStore` stays the default —
116
+ correct inside a single Durable Object — but constructing a `RateLimiter` with
117
+ no explicit `store` now warns once so that choice is visible instead of
118
+ silent.
119
+
120
+ A key that is an IPv6 address (for example `ctx.ip`) is bucketed by its /64
121
+ prefix, and an IPv4-mapped address by the IPv4 address it wraps. An IPv6 client
122
+ usually controls a whole /64, so a per-address bucket would let it pick a fresh
123
+ address for every request. A deny-list IPv6 entry bans its /64.
93
124
 
94
125
  > This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs)**.
95
126