All files / src/core/bindings RateLimitBinding.ts

50% Statements 6/12
0% Branches 0/2
33.33% Functions 1/3
54.54% Lines 6/11

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 9028x 28x                                                               28x                                   28x   28x   28x                                                                    
import { Binding } from '@/core/bindings/Binding'
import { lazySettings } from '@/utils/lazySettings'
import { RateLimitSettings } from '@/types/RateLimitSettings'
import type { CloudflareEnv } from '@/types/CloudflareEnv'
 
/**
 * Accessor for a rate limiting binding (`ratelimits` in `wrangler.json`).
 *
 * @remarks
 * Unlike every other binding here, the interesting configuration is not in
 * `src/config/` and cannot be: the rate and the window are fixed in
 * `wrangler.json` at deploy time, and the runtime API takes only the key to
 * count against. A Worker that needs two different rates declares two bindings
 * and constructs one of these per binding.
 *
 * The limiter is eventually consistent and scoped to a Cloudflare location, so
 * the effective global rate is higher than the configured one — by roughly the
 * number of locations seeing traffic. It is an abuse control, not an accounting
 * record: never use it for anything that must balance, like a quota a customer
 * is billed against.
 *
 * @example
 * ```ts
 * const limiter = new RateLimitBinding() // binding name from the configuration layer
 * const { success } = await limiter.limit(ctx.env, ip)
 * ```
 *
 * @class
 *
 * @author Bayu Dwiyan Satria
 * @version 1.0.0
 * @since 1.0.0
 */
export class RateLimitBinding<E extends CloudflareEnv = CloudflareEnv> extends Binding<RateLimit, E> {
  /**
   * This accessor’s settings, resolved on first read.
   */
  private readonly settings: () => RateLimitSettings
 
  /**
   * Whether an unbound limiter admits the request.
   */
  private readonly failOpen: boolean
 
  /**
   * Constructs a RateLimitBinding instance.
   *
   * @param settings Resolved settings. Defaults to `resolve('rateLimit')`, so
   *   callers normally construct this with no arguments at all.
   */
  constructor(settings?: RateLimitSettings) {
    const settle = lazySettings<RateLimitSettings>('rateLimit', settings)
 
    super(() => settle().binding)
 
    this.settings = settle
  }
 
  /**
   * Counts one request against a key and reports whether it is within the rate.
   *
   * @remarks
   * The key decides what is being limited, and choosing it is the whole design:
   * a client IP limits one caller, a conversation id limits one conversation, a
   * route name limits everyone at once. Cloudflare hashes it, so it may carry
   * an identifier — but it is still a key in a shared namespace, so prefix it
   * when one binding limits more than one thing.
   *
   * Calling this *is* the increment. There is no way to ask without counting,
   * which means a request must be checked exactly once.
   *
   * @param env The Worker environment.
   * @param key What to count against — an IP, a user id, a route name.
   * @returns `true` when the request is within the rate, `false` when it is
   *   over. Returns {@link RateLimitBinding.failOpen} when the binding is
   *   absent.
   */
  public async limit(env: E, key: string): Promise<boolean> {
    const limiter = this.tryResolve(env)
 
    if (limiter === null) {
      return this.settings().failOpen
    }
 
    const outcome = await limiter.limit({ key })
 
    return outcome.success
  }
}