npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@application-kit/rate-limit

v0.3.0

Published

Частотная защита для NestJS: счётчик попыток с окном (память и Valkey с атомарным Lua), бюджет попыток и gate с Retry-After

Readme

@application-kit/rate-limit

Частотная защита для NestJS: счётчик попыток с окном, бюджет попыток и gate маршрута с заголовком Retry-After. Доменных понятий в пакете нет: ключи, пороги, оси и ошибки отказа объявляет приложение.

Установка

yarn add @application-kit/rate-limit

Peer-зависимости: @libs-for-dev/nestjs-config-module, @nestjs/common, @nestjs/core, class-transformer, class-validator, nestjs-pino, oxide.ts, rxjs. Клиент Valkey (iovalkey) — зависимость пакета.

Подключение

import { RateLimitModule } from '@application-kit/rate-limit'

RateLimitModule.forRootAsync({
  // Исполнитель команд оси — решения и исхода: обычно — шина команд приложения.
  execute: {
    inject: [CommandBus],
    useFactory: (commandBus: CommandBus) => async (command: object) => commandBus.execute(command),
  },
  imports: [CqrsModule],
})

Счётчик без опции counter выбирает окружение:

| Переменная | Умолчание | Смысл | |---|---|---| | VALKEY_HOST | '' | адрес хранилища; пусто — счётчик в памяти процесса и одно предупреждение в лог | | VALKEY_PORT | 6379 | порт | | VALKEY_CLUSTER | true | кластерный протокол (адрес — стартовый узел); false — одиночный сервер | | VALKEY_USERNAME | default | пользователь ACL | | VALKEY_PASSWORD | '' | пароль |

Клиент ленивый (lazyConnect): соединение открывает первая попытка, а не сборка графа. Окружение читается и валидируется в forRootAsync, а не на импорте пакета; неверное значение роняет старт с именем переменной. Свой счётчик — фабрикой в опции counter ({ useFactory: () => new FakeAttemptCounter() } из @application-kit/rate-limit/testing в спеках): тогда VALKEY_* не читается.

Модуль экспортирует токены счётчика и исполнителя (RATE_LIMIT_COUNTER, RATE_LIMIT_EXECUTE), ThrottleBudget и ThrottleGate: модуль ручки обязан их видеть — guard и интерцептор оси создаются там. Логгер отказов (RATE_LIMIT_LOGGER, RateLimitLogger) модуль связывает сам — нужен глобальный LoggerModule из nestjs-pino.

Счётчик

  • increment(key, windowSeconds) — фиксирует попытку. Окно открывает первая попытка, попытки внутри окна его не продлевают: иначе блокировку нельзя переждать.
  • read(key) — состояние окна или None.
  • reset(key) — сброс окна, например после успешного входа.

ValkeyAttemptCounter меняет счёт и срок одним Lua-скриптом: попытки разных реплик складываются, а рестарт процесса блокировку не снимает. InMemoryAttemptCounter — для спек и запуска без хранилища: порог в нём действует на процесс.

Бюджет

  • spend({ key, limit, refusal }) — попытка стоит окна независимо от исхода (ручки с одинаковым ответом всегда).
  • readFailures({ key, maxFailures, refusal }) — бюджет только читается; тратит его зарегистрированный исход попытки, то есть increment у потребителя.

Отказ пишется в лог одной записью { retryAfterSeconds, securityEvent } с сообщением refusal.message — без ключа и идентификаторов.

Ось

Ось — данные приложения, объявленные defineThrottleAxis и поставленные на ручку декоратором @Throttle(axis):

// Бюджет тратит каждая попытка: ответ ручки одинаков при любом исходе.
const PASSWORD_RESET = defineThrottleAxis<ClientContext>({
  createError: () => new TooManyPasswordResetRequestsError(),
  key: ({ clientId }) => `passwordReset:rate:${clientId}`,
  limit: PasswordResetProtectionConfig,
  readContext: readClientContext,
  refusal: { message: 'password reset request refused', securityEvent: 'passwordReset.refused' },
})

// Решение — команда приложения, исход попытки — вторая команда после ответа ручки.
const SIGN_IN = defineThrottleAxis<SignInContext, CheckSignInThrottleResult>({
  createCommand: ({ clientId, subject }) => new CheckSignInThrottleCommand({ clientId, subject }),
  createError: () => new TooManySignInAttemptsError(),
  isAllowed: (decision) => decision.reason === null,
  outcome: {
    createCommand: ({ subject }, succeeded) => new RegisterSignInOutcomeCommand({ subject, succeeded }),
    isAttemptSpent: isSignInAttemptSpent,
  },
  readContext: readSignInContext,
})

@Throttle(SIGN_IN)
public async signIn(...) {}
  • ThrottleGuard до тела ручки спрашивает ThrottleGate: у оси с limit решение — ThrottleBudget.spend по порогам из провайдера приложения, у оси с createCommand — команда через execute. Отказ пишет retry-after в ответ и бросает ошибку оси.
  • ThrottleOutcomeInterceptor стоит только у оси с outcome: успех регистрируется всегда, ошибка — когда isAttemptSpent считает её потраченной попыткой; ошибка ручки уходит дальше без изменений.

Спеки потребителя

import { FakeAttemptCounter } from '@application-kit/rate-limit/testing'

Фейк копит счёт и вызовы (increments, resets, seed, countOf) и отдаёт фиксированный остаток окна.