Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

sveltekit-rate-limiter

Package Overview
Dependencies
Maintainers
0
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

sveltekit-rate-limiter - npm Package Compare versions

Comparing version 0.5.2 to 0.6.0

dist/server/hashFunction.d.ts

2

dist/index.d.ts

@@ -1,1 +0,1 @@

export type { Rate, RateUnit } from './server/index.js';
export type { Rate, RateUnit } from './server/rate.js';

@@ -1,117 +0,8 @@

import type { Cookies, RequestEvent, MaybePromise } from '@sveltejs/kit';
export type RateUnit = 'ms' | '100ms' | '250ms' | '500ms' | 's' | '2s' | '5s' | '10s' | '15s' | '30s' | '45s' | 'm' | '2m' | '5m' | '10m' | '15m' | '30m' | '45m' | 'h' | '2h' | '6h' | '12h' | 'd';
export type Rate = [number, RateUnit];
export interface RateLimiterStore {
add: (hash: string, unit: RateUnit) => MaybePromise<number>;
clear: () => MaybePromise<void>;
}
export interface RateLimiterPlugin<Extra = never> {
hash: (event: RequestEvent, extraData: Extra) => MaybePromise<string | boolean | null>;
get rate(): Rate;
}
type CookieSerializeOptions = NonNullable<Parameters<Cookies['set']>[2]>;
type CookieRateLimiterOptions = {
name: string;
secret: string;
rate: Rate;
preflight: boolean;
serializeOptions?: CookieSerializeOptions;
hashFunction?: HashFunction;
};
declare class CookieRateLimiter implements RateLimiterPlugin {
readonly rate: Rate;
private readonly cookieOptions;
private readonly secret;
private readonly requirePreflight;
private readonly cookieId;
private readonly hashFunction;
constructor(options: CookieRateLimiterOptions);
hash(event: RequestEvent): Promise<string | false>;
preflight(event: RequestEvent): Promise<string>;
private userIdFromCookie;
}
type HashFunction = (input: string) => MaybePromise<string>;
export type RateLimiterOptions = Partial<{
plugins: RateLimiterPlugin[];
store: RateLimiterStore;
maxItems: number;
onLimited: (event: RequestEvent, reason: 'rate' | 'rejected') => MaybePromise<void | boolean>;
/**
* @deprecated Add the IP/IPUA/cookie rates to the main object, no need for "rates".
*/
rates: {
/**
* @deprecated Add the IP option to the main object, no need for "rates".
*/
IP?: Rate;
/**
* @deprecated Add the IPUA option to the main object, no need for "rates".
*/
IPUA?: Rate;
/**
* @deprecated Add cookie option to the main object, no need for "rates".
*/
cookie?: CookieRateLimiterOptions;
};
IP: Rate;
IPUA: Rate;
cookie: CookieRateLimiterOptions;
hashFunction: HashFunction;
}>;
export declare class RateLimiter<Extra = never> {
private readonly store;
private readonly plugins;
private readonly onLimited;
private readonly hashFunction;
readonly cookieLimiter: CookieRateLimiter | undefined;
static TTLTime(unit: RateUnit): number;
/**
* Check if a request event is rate limited.
* @param {RequestEvent} event
* @returns {Promise<boolean>} true if request is limited, false otherwise
*/
isLimited(event: [Extra] extends [never] ? RequestEvent : {
missing_extraData: Extra;
}): Promise<boolean>;
/**
* Check if a request event is rate limited, supplying extra data that will be available for plugins.
* @param {RequestEvent} event
* @returns {Promise<boolean>} true if request is limited, false otherwise
*/
isLimited(event: RequestEvent, extraData: Extra): Promise<boolean>;
/**
* Clear all rate limits.
*/
clear(): Promise<void>;
/**
* Check if a request event is rate limited.
* @param {RequestEvent} event
* @returns {Promise<boolean>} true if request is limited, false otherwise
*/
protected _isLimited(event: RequestEvent, extraData: Extra): Promise<{
limited: boolean;
hash: string | null;
unit: RateUnit;
}>;
constructor(options?: RateLimiterOptions);
}
export declare class RetryAfterRateLimiter<Extra = never> extends RateLimiter<Extra> {
private readonly retryAfter;
constructor(options?: RateLimiterOptions, retryAfterStore?: RateLimiterStore);
private static toSeconds;
private static unitToSeconds;
/**
* Clear all rate limits.
*/
clear(): Promise<void>;
/**
* Check if a request event is rate limited.
* @param {RequestEvent} event
* @returns {Promise<limited: boolean, retryAfter: number>} Rate limit status for the event.
*/
check(event: RequestEvent, extraData?: Extra): Promise<{
limited: boolean;
retryAfter: number;
}>;
}
export {};
export { RateLimiter } from './rateLimiter.js';
export { RetryAfterRateLimiter } from './retryAfterRateLimiter.js';
export { defaultHashFunction } from './hashFunction.js';
export { TTLTime } from './rate.js';
export type { RateLimiterPlugin } from './limiters/rateLimiterPlugin.js';
export type { RateLimiterStore } from './stores/index.js';
export type { HashFunction } from './hashFunction.js';
export type { Rate, RateUnit } from './rate.js';

@@ -1,323 +0,4 @@

import { nanoid } from 'nanoid';
import TTLCache from '@isaacs/ttlcache';
///// Plugins /////////////////////////////////////////////////////////////////
class IPRateLimiter {
rate;
constructor(rate) {
this.rate = rate;
}
async hash(event) {
return event.getClientAddress();
}
}
class IPUserAgentRateLimiter {
rate;
constructor(rate) {
this.rate = rate;
}
async hash(event) {
const ua = event.request.headers.get('user-agent');
if (!ua)
return false;
return event.getClientAddress() + ua;
}
}
class CookieRateLimiter {
rate;
cookieOptions;
secret;
requirePreflight;
cookieId;
hashFunction;
constructor(options) {
this.cookieId = options.name;
this.secret = options.secret;
this.rate = options.rate;
this.requirePreflight = options.preflight;
this.hashFunction = options.hashFunction ?? defaultHashFunction;
this.cookieOptions = {
path: '/',
httpOnly: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: 'strict',
...options.serializeOptions
};
}
async hash(event) {
const currentId = await this.userIdFromCookie(event.cookies.get(this.cookieId), event);
return currentId ? currentId : false;
}
async preflight(event) {
const data = event.cookies.get(this.cookieId);
if (data) {
const userId = await this.userIdFromCookie(data, event);
if (userId)
return userId;
}
const userId = nanoid();
event.cookies.set(this.cookieId, userId + ';' + (await this.hashFunction(this.secret + userId)), this.cookieOptions);
return userId;
}
async userIdFromCookie(cookie, event) {
const empty = () => {
return this.requirePreflight ? null : this.preflight(event);
};
if (!cookie)
return empty();
const [userId, secretHash] = cookie.split(';');
if (!userId || !secretHash)
return empty();
if ((await this.hashFunction(this.secret + userId)) != secretHash) {
return empty();
}
return userId;
}
}
let defaultHashFunction;
if (globalThis?.crypto?.subtle) {
defaultHashFunction = _subtleSha256;
}
async function _subtleSha256(str) {
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
return [...new Uint8Array(digest)]
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
}
export class RateLimiter {
store;
plugins;
onLimited;
hashFunction;
cookieLimiter;
static TTLTime(unit) {
switch (unit) {
case 's':
return 1000;
case 'm':
return 60000;
case 'h':
return 60 * 60000;
case '2s':
return 2000;
case '5s':
return 5000;
case '10s':
return 10000;
case '15s':
return 15000;
case '30s':
return 30000;
case '45s':
return 45000;
case '2m':
return 2 * 60000;
case '5m':
return 5 * 60000;
case '10m':
return 10 * 60000;
case '15m':
return 15 * 60000;
case '30m':
return 30 * 60000;
case '45m':
return 45 * 60000;
case '100ms':
return 100;
case '250ms':
return 250;
case '500ms':
return 500;
case '2h':
return 2 * 60 * 60000;
case '6h':
return 6 * 60 * 60000;
case '12h':
return 12 * 60 * 60000;
case 'd':
return 24 * 60 * 60000;
case 'ms':
return 1;
}
throw new Error('Invalid unit for TTLTime: ' + unit);
}
async isLimited(event, extraData) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (await this._isLimited(event, extraData))
.limited;
}
/**
* Clear all rate limits.
*/
async clear() {
return await this.store.clear();
}
/**
* Check if a request event is rate limited.
* @param {RequestEvent} event
* @returns {Promise<boolean>} true if request is limited, false otherwise
*/
async _isLimited(event, extraData) {
let limited = undefined;
for (const plugin of this.plugins) {
const rate = plugin.rate;
const id = await plugin.hash(event, extraData);
if (id === false) {
if (this.onLimited) {
const status = await this.onLimited(event, 'rejected');
if (status === true)
return { limited: false, hash: null, unit: rate[1] };
}
return { limited: true, hash: null, unit: rate[1] };
}
else if (id === null) {
if (limited === undefined)
limited = true;
continue;
}
else {
limited = false;
}
if (!id) {
throw new Error('Empty hash returned from rate limiter ' + plugin.constructor.name);
}
if (id === true) {
return { limited: false, hash: null, unit: rate[1] };
}
const hash = await this.hashFunction(id);
const currentRate = await this.store.add(hash, rate[1]);
if (currentRate > rate[0]) {
if (this.onLimited) {
const status = await this.onLimited(event, 'rate');
if (status === true)
return { limited: false, hash, unit: rate[1] };
}
return { limited: true, hash, unit: rate[1] };
}
}
return {
limited: limited ?? false,
hash: null,
unit: this.plugins[this.plugins.length - 1].rate[1]
};
}
constructor(options = {}) {
this.plugins = [...(options.plugins ?? [])];
this.onLimited = options.onLimited;
this.hashFunction = options.hashFunction ?? defaultHashFunction;
if (!this.hashFunction) {
throw new Error('No RateLimiter hash function found. Please set one with the hashFunction option.');
}
const IPRates = options.IP ?? options.rates?.IP;
if (IPRates)
this.plugins.push(new IPRateLimiter(IPRates));
const IPUARates = options.IPUA ?? options.rates?.IPUA;
if (IPUARates)
this.plugins.push(new IPUserAgentRateLimiter(IPUARates));
const cookieRates = options.cookie ?? options.rates?.cookie;
if (cookieRates) {
this.plugins.push((this.cookieLimiter = new CookieRateLimiter({
hashFunction: this.hashFunction,
...cookieRates
})));
}
if (!this.plugins.length) {
throw new Error('No plugins set for RateLimiter!');
}
// Sort plugins by rate, if early cancelling
this.plugins.sort((a, b) => {
const diff = RateLimiter.TTLTime(a.rate[1]) - RateLimiter.TTLTime(b.rate[1]);
return diff == 0 ? a.rate[0] - b.rate[0] : diff;
});
const maxTTL = this.plugins.reduce((acc, plugin) => {
const rate = plugin.rate[1];
if (rate == 'ms') {
console.warn('RateLimiter: The "ms" unit is not reliable due to OS timing issues.');
}
const time = RateLimiter.TTLTime(rate);
return Math.max(time, acc);
}, 0);
this.store = options.store ?? new TTLStore(maxTTL, options.maxItems);
}
}
export class RetryAfterRateLimiter extends RateLimiter {
retryAfter;
constructor(options = {}, retryAfterStore) {
super(options);
this.retryAfter = retryAfterStore ?? new RetryAfterStore();
}
static toSeconds(rateMs) {
return Math.max(0, Math.floor(rateMs / 1000));
}
static unitToSeconds(unit) {
return RetryAfterRateLimiter.toSeconds(RateLimiter.TTLTime(unit));
}
/**
* Clear all rate limits.
*/
async clear() {
await this.retryAfter.clear();
return await super.clear();
}
/**
* Check if a request event is rate limited.
* @param {RequestEvent} event
* @returns {Promise<limited: boolean, retryAfter: number>} Rate limit status for the event.
*/
async check(event, extraData) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await this._isLimited(event, extraData);
if (!result.limited)
return { limited: false, retryAfter: 0 };
if (result.hash === null) {
return {
limited: true,
retryAfter: RetryAfterRateLimiter.unitToSeconds(result.unit)
};
}
const retryAfter = RetryAfterRateLimiter.toSeconds((await this.retryAfter.add(result.hash, result.unit)) - Date.now());
return { limited: true, retryAfter };
}
}
///// Stores ///////////////////////////////////////////////////////////////////
class TTLStore {
cache;
constructor(maxTTL, maxItems = Infinity) {
this.cache = new TTLCache({
ttl: maxTTL,
max: maxItems,
noUpdateTTL: true
});
}
async clear() {
return this.cache.clear();
}
async add(hash, unit) {
const currentRate = this.cache.get(hash) ?? 0;
return this.set(hash, currentRate + 1, unit);
}
set(hash, rate, unit) {
this.cache.set(hash, rate, { ttl: RateLimiter.TTLTime(unit) });
return rate;
}
}
class RetryAfterStore {
cache;
constructor(maxItems = Infinity) {
this.cache = new TTLCache({
max: maxItems,
noUpdateTTL: true
});
}
async clear() {
return this.cache.clear();
}
async add(hash, unit) {
const currentRate = this.cache.get(hash);
if (currentRate)
return this.cache.get(hash) ?? 0;
const ttl = RateLimiter.TTLTime(unit);
const retryAfter = Date.now() + ttl;
this.cache.set(hash, retryAfter, { ttl });
return retryAfter;
}
}
export { RateLimiter } from './rateLimiter.js';
export { RetryAfterRateLimiter } from './retryAfterRateLimiter.js';
export { defaultHashFunction } from './hashFunction.js';
export { TTLTime } from './rate.js';
{
"name": "sveltekit-rate-limiter",
"version": "0.5.2",
"version": "0.6.0",
"author": "Andreas Söderlund <ciscoheat@gmail.com> (https://blog.encodeart.dev)",

@@ -27,2 +27,10 @@ "description": "A modular rate limiter for SvelteKit. Use in password resets, account registration, etc.",

"svelte": "./dist/server/index.js"
},
"./server/limiters": {
"types": "./dist/server/limiters/index.d.ts",
"svelte": "./dist/server/limiters/index.js"
},
"./server/stores": {
"types": "./dist/server/stores/index.d.ts",
"svelte": "./dist/server/stores/index.js"
}

@@ -29,0 +37,0 @@ },

@@ -100,2 +100,6 @@ # sveltekit-rate-limiter

## 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

@@ -156,3 +160,3 @@

hash: (event: RequestEvent) => MaybePromise<string | boolean | null>;
get rate(): Rate;
get rate(): Rate | Rate[];
}

@@ -181,5 +185,5 @@ ```

class IPUserAgentRateLimiter implements RateLimiterPlugin {
readonly rate: Rate;
readonly rate: Rate | Rate[];
constructor(rate: Rate) {
constructor(rate: Rate | Rate[]) {
this.rate = rate;

@@ -222,2 +226,3 @@ }

async hash(_: RequestEvent, extraData: { email: string }) {
// Return true to bypass the rest of the plugin chain
return extraData.email.endsWith(this.allowedDomain) ? true : null;

@@ -224,0 +229,0 @@ }

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc