Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/ciscoheat/sveltekit-rate-limiter
A modular rate limiter for SvelteKit. Use in password resets, account registration, etc.
https://github.com/ciscoheat/sveltekit-rate-limiter
rate-limiting sveltekit throttle-requests
Last synced: 15 days ago
JSON representation
A modular rate limiter for SvelteKit. Use in password resets, account registration, etc.
- Host: GitHub
- URL: https://github.com/ciscoheat/sveltekit-rate-limiter
- Owner: ciscoheat
- License: mit
- Created: 2023-03-03T12:54:08.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2024-09-19T14:17:34.000Z (about 2 months ago)
- Last Synced: 2024-09-19T14:18:37.205Z (about 2 months ago)
- Topics: rate-limiting, sveltekit, throttle-requests
- Language: TypeScript
- Homepage:
- Size: 185 KB
- Stars: 200
- Watchers: 5
- Forks: 4
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# sveltekit-rate-limiter
A modular rate limiter for password resets, account registration, API call limiting, etc. Use in your `+page.server.ts`, `+server.ts` or `src/hooks.server.ts`.
Uses an in-memory cache ([@isaacs/ttlcache](https://www.npmjs.com/package/@isaacs/ttlcache)), but can be swapped for something else. Same for limiters, which are plugins. The [source file](https://github.com/ciscoheat/sveltekit-rate-limiter/blob/main/src/lib/server/index.ts#L24-L32) lists both interfaces.
## Installation
```
npm i -D sveltekit-rate-limiter
``````
pnpm i -D sveltekit-rate-limiter
```## How to use
```ts
import { error } from '@sveltejs/kit';
import { RateLimiter } from 'sveltekit-rate-limiter/server';const limiter = new RateLimiter({
// A rate is defined as [number, unit]
IP: [10, 'h'], // IP address limiter
IPUA: [5, 'm'], // IP + User Agent limiter
cookie: {
// Cookie limiter
name: 'limiterid', // Unique cookie name for this limiter
secret: 'SECRETKEY-SERVER-ONLY', // Use $env/static/private
rate: [2, 'm'],
preflight: true // Require preflight call (see load function)
}
});export const load = async (event) => {
/**
* Preflight prevents direct posting. If preflight option for the
* cookie limiter is true and this function isn't called before posting,
* request will be limited.
*
* Remember to await, so the cookie will be set before returning!
*/
await limiter.cookieLimiter?.preflight(event);
};export const actions = {
default: async (event) => {
// Every call to isLimited counts as a hit towards the rate limit for the event.
if (await limiter.isLimited(event)) throw error(429);
}
};
```## Call order for limiters
The limiters will be called in smallest unit and rate order, so in the example above:
```
cookie(2/min) → IPUA(5/min) → IP(10/hour)
```For four consecutive requests from the same source within one minute, the following will happen:
| Request | Cookie | IPUA | IP |
| ------- | --------- | ----- | ----- |
| 1 | Hit 1 | Hit 1 | Hit 1 |
| 2 | Hit 2 | Hit 2 | Hit 2 |
| 3 | **Limit** | - | - |
| 4 | **Limit** | - | - |If the cookie is deleted but the User-Agent stays the same, the counter keeps going for the other limiters:
| Request | Cookie | IPUA | IP |
| ------- | --------- | ----- | ----- |
| 5 | Hit 1 | Hit 3 | Hit 3 |
| 6 | Hit 2 | Hit 4 | Hit 4 |
| 7 | **Limit** | - | - |If deleted one more time, the User-Agent limiter will reach its limit:
| Request | Cookie | IPUA | IP |
| ------- | --------- | --------- | ----- |
| 8 | Hit 1 | Hit 5 | Hit 5 |
| 9 | Hit 2 | **Limit** | - |
| 10 | **Limit** | - | - |## Valid units
Valid units are, from smallest to largest:
```
'100ms' | '250ms' | '500ms'
's' | '2s' | '5s' | '10s' | '15s' | '30s' | '45s'
'm' | '2m' | '5m | '10m' | '15m' | '30m' | '45m'
'h' | '2h' | '6h' | '12h'
'd'
```## Multiple limits
You can specify the rates as an array, to handle multiple rates per limiter, like "Max 1 per second and 100 per hour": `[[1, 's'], [100, 'h']]`.
## Retry-After limiter
There is a version of the rate limiter that will return [Retry-After](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) information, the number of seconds before the request should be attempted again. This has been implemented in the `src/hooks.server.ts` file and instead of throwing an error code like other pages, we have to create a new response so that we can add the header.
```ts
import type { Handle } from '@sveltejs/kit';
import { RetryAfterRateLimiter } from 'sveltekit-rate-limiter/server';const limiter = new RetryAfterRateLimiter({
IP: [10, 'h'],
IPUA: [5, 'm']
});export const handle: Handle = async ({ event, resolve }) => {
const status = await limiter.check(event);
if (status.limited) {
let response = new Response(
`You are being rate limited. Please try after ${status.retryAfter} seconds.`,
{
status: 429,
headers: { 'Retry-After': status.retryAfter.toString() }
}
);
return response;
}
const response = await resolve(event);
return response;
};
```A custom store for the `RetryAfterRateLimiter` can also be used, in which the second argument to the constructor should be a [RateLimiterStore](https://github.com/ciscoheat/sveltekit-rate-limiter/blob/main/src/lib/server/index.ts#L24) that returns a unix timestamp describing when the request should be reattempted, based on the unit sent to it.
## Clearing the limits
Clearing all rate limits can be done by calling the `clear` method of the rate limiter object.
## Custom hash function
The default hash function is using `crypto.subtle` to generate a SHA-256 digest, but if isn't available in your environment, you can supply your own with the `hashFunction` option. Here's an example with the NodeJS `crypto` package:
```ts
import crypto from 'crypto';// (input: string) => MaybePromise
const hashFunction = (input: string) =>
crypto.createHash('sha256').update(input).digest('hex');
```## Creating a custom limiter
Implement the `RateLimiterPlugin` interface:
```ts
interface RateLimiterPlugin {
hash: (event: RequestEvent) => MaybePromise;
get rate(): Rate | Rate[];
}
```In `hash`, return one of the following:
- A `string` based on a [RequestEvent](https://kit.svelte.dev/docs/types#public-types-requestevent), which will be counted and checked against the rate.
- A `boolean`, to short-circuit the plugin chain and make the request fail (`false`) or succeed (`true`) no matter the current rate.
- Or `null`, to signify an indeterminate result and move to the next plugin in the chain, or fail the request if it's the last and no previous limiter have passed.### String hash rules
- **The string will be hashed later**, so you don't need to use a hash function.
- **The string cannot be empty**, in which case an exception will be thrown.### Example
Here's the source for the IP + User Agent limiter:
```ts
import type { RequestEvent } from '@sveltejs/kit';
import type { Rate, RateLimiterPlugin } from 'sveltekit-rate-limiter/server';class IPUserAgentRateLimiter implements RateLimiterPlugin {
readonly rate: Rate | Rate[];constructor(rate: Rate | Rate[]) {
this.rate = rate;
}async hash(event: RequestEvent) {
const ua = event.request.headers.get('user-agent');
if (!ua) return false;
return event.getClientAddress() + ua;
}
}
```Add your limiter to the `plugins` option to use it.
```ts
import { RateLimiter } from 'sveltekit-rate-limiter/server';const limiter = new RateLimiter({
plugins: [new CustomLimiter([5, 'm'])]
// The built-in limiters can be added as well.
});
```## Custom data for the limiter
You can specify a type parameter to `RateLimiter` that expands the `isLimited` method with an extra parameter. There you can add extra data that will be supplied to the custom limiters:
```ts
class AllowDomain implements RateLimiterPlugin {
// Shortest rate, so it will be executed first
readonly rate: Rate = [0, '100ms'];
readonly allowedDomain: string;constructor(allowedDomain: string) {
this.allowedDomain = allowedDomain;
}async hash(_: RequestEvent, extraData: { email: string }) {
// Return true to bypass the rest of the plugin chain
return extraData.email.endsWith(this.allowedDomain) ? true : null;
}
}
``````ts
const limiter = new RateLimiter<{ email: string }>({
plugins: [new AllowDomain('company-domain.com')],
IP: [10, 'm']
});export const actions = {
default: async (event) => {
if (await limiter.isLimited(event, { email: event.locals.user.email })) {
throw error(429);
}
}
};
```