sveltekit-rate-limiter

repository·main·Indexed 43 days ago

https://github.com/ciscoheat/sveltekit-rate-limiter

A modular rate limiting library for SvelteKit applications version 0.8.0. It allows developers to protect server-side routes, actions, loads, and hooks using strategies such as IP, User-Agent, or signed cookies. The library includes built-in limiters like CloudflareIPRateLimiter and CookieRateLimiter, supports multiple rate thresholds per limiter, and provides a RetryAfterRateLimiter for returning Retry-After headers. Developers can extend functionality by implementing the RateLimiterPlugin interface.

Tokens
6K
Snippets
21
Records
34
Agent score
53%

What's inside sveltekit-rate-limiter

  1. Use the RateLimiter in SvelteKit

    main

    Import RateLimiter from sveltekit-rate-limiter/server to protect your +page.server.ts, +server.ts, or src/hooks.server.ts.

    You can define built-in limiters like IP, IPUA (IP + User Agent), or cookie. The cookie limiter requires a name and a secret. If preflight is set to true for a cookie limiter, you must call limiter.cookieLimiter?.preflight(event) in your load function to prevent direct POST requests from being automatically limited.

    To check and increment the limit, use limiter.isLimited(event) or limiter.check(event) (which provides more detail about the reason for limiting).

    import { error } from '@sveltejs/kit';
    import { RateLimiter } from 'sveltekit-rate-limiter/server';
    
    const limiter = new RateLimiter({
      IP: [10, 'h'],
      IPUA: [5, 'm'],
      cookie: {
        name: 'limiterid',
        secret: 'SECRETKEY-SERVER-ONLY',
        rate: [2, 'm'],
        preflight: true
      }
    });
    
    export const load = async (event) => {
      // Required if cookie preflight is true
      await limiter.cookieLimiter?.preflight(event);
    };
    
    export const actions = {
      default: async (event) => {
        if (await limiter.isLimited(event)) throw error(429);
      },
    
      debug: async (event) => {
        const status = await limiter.check(event);
        if (status.limited) {
          // status.reason can be 'IP' | 'IPUA' | 'cookie' | number
          console.log('Limited due to ' + status.reason);
          throw error(429);
        }
      }
    };
  2. Use RetryAfterRateLimiter to return Retry-After headers

    main

    If you need to inform clients how many seconds to wait before retrying, use RetryAfterRateLimiter. This is best implemented in src/hooks.server.ts using the handle hook. When a request is limited, you must manually construct a Response to include the Retry-After header using status.retryAfter.

    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;
    };
  3. Pass extra data to custom limiters

    main

    You can pass custom data to the isLimited method by providing a type parameter to the RateLimiter class. This data is then available as the second argument to the hash method in your custom plugins.

    class AllowDomain implements RateLimiterPlugin {
      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;
      }
    }
    
    // Usage
    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);
        }
      }
    };
  4. How CookieRateLimiter handles identity and preflight

    main

    The CookieRateLimiter uses a two-part cookie format: userId;secretHash.

    1. Identity Verification: When a request arrives, the limiter extracts the userId and re-calculates the hash using the provided secret and the hashFunction. If the calculated hash matches the secretHash in the cookie, the user is identified.
    2. Preflight Mechanism: If no valid cookie is found (either missing or tampered with), the limiter can perform a preflight action:
      • If preflight: true is set in options, the hash method returns false (or null depending on context), which typically results in the rate limiter not being able to identify a user.
      • If preflight: false (default), the limiter automatically generates a new nanoid user ID, signs it, and sets it in the response cookies via setPreflightCookie.
    3. Tamper Protection: If the hash verification fails, the limiter treats the cookie as invalid and triggers the preflight mechanism to issue a new, valid cookie.
  5. Define multiple rates for a single limiter

    main

    To handle multiple rate thresholds for a single limiter (e.g., 'Max 1 per second AND 100 per hour'), pass an array of rate arrays to the limiter configuration.

    // Example: Max 1 per second and 100 per hour
    const limiter = new RateLimiter({
      IP: [[1, 's'], [100, 'h']]
    });
  6. Create a custom RateLimiterPlugin

    main

    To create a custom limiter, implement the RateLimiterPlugin interface. This allows you to define custom logic for identifying users and setting their specific rates.

    Interface

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

    Hash Return Values

    • string: The value will be hashed and used as the key for rate tracking. The string cannot be empty.
    • boolean:
      • true: Short-circuits the chain and allows the request (bypasses other limiters).
      • false: Short-circuits the chain and fails the request immediately.
    • null: Moves to the next plugin in the chain.

    Implementation Example

    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;
      }
    }

    Registration

    Add your plugin to the plugins array in the RateLimiter constructor:

    const limiter = new RateLimiter({
      plugins: [new IPUserAgentRateLimiter([5, 'm'])]
    });
  7. Provide a custom hash function

    main

    If crypto.subtle is unavailable in your environment, you can provide a custom hashFunction in the RateLimiter options. The function signature is (input: string) => MaybePromise<string>.

    import crypto from 'crypto';
    
    // (input: string) => MaybePromise<string>
    const hashFunction = (input: string) =>
      crypto.createHash('sha256').update(input).digest('hex');
  8. Configure the CookieRateLimiter

    main

    The CookieRateLimiter is a plugin that tracks rate limits using a signed cookie. It identifies users by a unique ID stored in a cookie, which is validated against a secret using a hash function to prevent tampering.

    Configuration Options

    OptionTypeDescription
    namestringThe name of the cookie used to store the rate limit ID.
    secretstringA secret string used to sign the cookie hash.
    rateRate | Rate[]The rate limit configuration (e.g., number of requests per time unit).
    preflightbooleanIf true, the limiter will return null (effectively bypassing or failing the check) if no valid cookie is present, rather than automatically issuing a new one.
    serializeOptionsCookieSerializeOptionsStandard SvelteKit cookies.set options. Defaults to path: '/', httpOnly: true, maxAge: 604800 (7 days), and sameSite: 'strict'.
    hashFunctionHashFunctionA custom function to hash the secret and user ID. Defaults to defaultHashFunction.
    const limiter = new CookieRateLimiter({
      name: 'rate-limit-id',
      secret: 'your-super-secret-key',
      rate: { limit: 10, interval: 60 },
      preflight: true,
      serializeOptions: {
        maxAge: 60 * 60 * 24, // 1 day
        secure: true
      }
    });
  9. Configure the RateLimiter class

    main

    The RateLimiter class is the main entry point for rate limiting in SvelteKit. It is initialized with a RateLimiterOptions object.

    Key Configuration Options:

    • IP: A Rate or Rate[] defining limits based on the user's IP address. (Note: The rates.IP property is deprecated; use the top-level IP property instead).
    • IPUA: A Rate or Rate[] defining limits based on IP and User Agent. (Note: The rates.IPUA property is deprecated; use the top-level IPUA property instead).
    • cookie: A CookieRateLimiterOptions object for cookie-based rate limiting. (Note: The rates.cookie property is deprecated; use the top-level cookie property instead).
    • plugins: An array of RateLimiterPlugin objects for custom limiting logic.
    • store: A RateLimiterStore implementation to persist rate data. If not provided, a TTLStore is used.
    • maxItems: The maximum number of items to keep in the store (used if creating a new TTLStore).
    • onLimited: A callback function triggered when a request is rate limited or rejected. It receives the RequestEvent and a reason ('rate' or 'rejected'). Returning true from this callback can bypass the rate limit.
    • hashFunction: A function used to hash identifiers. Defaults to defaultHashFunction.
    const limiter = new RateLimiter({
      IP: '10/1m',
      IPUA: '5/10m',
      cookie: { /* CookieRateLimiterOptions */ },
      onLimited: (event, reason) => {
        console.log(`Limited due to ${reason}`);
        return false; // Do not bypass
      }
    });
  10. Configure valid rate units

    main

    Rates are defined as [number, unit]. The following units are supported, ordered from smallest to largest:

    • Milliseconds: '100ms', '250ms', '500ms'
    • Seconds: 's', '2s', '5s', '10s', '15s', '30s', '45s'
    • Minutes: 'm', '2m', '5m', '10m', '15m', '30m', '45m'
    • Hours: 'h', '2h', '6h', '12h'
    • Days: 'd'