@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 +6 -0
- package/README.md +49 -18
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +365 -9
- package/dist/index.d.ts +365 -9
- package/dist/index.mjs +1 -11
- package/dist/packem_shared/RateLimitError-9Y5OLxpl.mjs +1 -0
- package/dist/packem_shared/RateLimiter-Cbc67DNk.mjs +1 -0
- package/dist/packem_shared/availableAt-BSKYrouo.mjs +1 -0
- package/dist/packem_shared/createDbStore-ve7GsO2i.mjs +1 -0
- package/dist/packem_shared/dbRateLimit-BuvEF1x0.mjs +1 -0
- package/dist/packem_shared/rateLimit-CWZur6XE.mjs +1 -0
- package/dist/packem_shared/ratelimitPlugin-CFnDs9rj.mjs +1 -0
- package/dist/packem_shared/tokenBudget-BRlWn1im.mjs +1 -0
- package/package.json +10 -3
- package/dist/packem_shared/RateLimitError-YOUfRzXc.mjs +0 -18
- package/dist/packem_shared/RateLimiter-CeElVjB0.mjs +0 -122
- package/dist/packem_shared/availableAt-D5GyJtxT.mjs +0 -117
- package/dist/packem_shared/createDbStore-L1kD1g1n.mjs +0 -103
- package/dist/packem_shared/dbRateLimit-C4nMF5cN.mjs +0 -7
- package/dist/packem_shared/rateLimit-uQxVMZfh.mjs +0 -42
- package/dist/packem_shared/ratelimitPlugin-D5_-KV1T.mjs +0 -12
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,
|
|
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,
|
|
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 {
|
|
62
|
+
import { dbRateLimit } from "@lunora/ratelimit";
|
|
63
63
|
|
|
64
64
|
import { mutation } from "./_generated/server";
|
|
65
65
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
+
import { RateLimiter, RateLimitError, createDbStore } from "@lunora/ratelimit";
|
|
94
|
+
|
|
95
|
+
import { action } from "./_generated/server";
|
|
83
96
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
(`
|
|
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
|
|