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

@smounters/kit

v2.22.1

Published

Batteries for Imperium services: structured logging, Redis module with a distributed lock, SSRF-safe fetch, decimal money, request context, env schemas

Downloads

4,503

Readme

@smounters/kit

Батарейки к @smounters/core: структурный журнал, Redis с распределённым локом, защита от SSRF, деньги на decimal, сквозной id запроса, схемы для env.

npm i @smounters/kit @smounters/core

Каждая часть — отдельный подпуть, а тяжёлые зависимости объявлены необязательными peer-зависимостями: ставится только то, чем пользуетесь. Взяли kit/moneyioredis и fastify не нужны.

| подпуть | что внутри | нужны | |---|---|---| | kit/log | транспорты для configureLogger() + автономный логгер: одна строка JSON вне local, цветная строка локально | @smounters/core | | kit/redis | RedisService: подключение, pub/sub, кеш «чтение сквозь», распределённый лок | @smounters/core, ioredis | | kit/http | реальный IP клиента, сквозной id запроса, точные байты тела, лимит частоты | fastify | | kit/net | isPrivateIp, assertPublicUrl, safeFetch | — | | kit/config | zod-препроцессоры для разбора env | zod | | kit/money | decimal-арифметика, масштабы, округление, проверка сбалансированности проводки | — | | kit/rpc | ProtoValidateInterceptor — правила buf.validate из контракта, enforced транспортом | @smounters/core, @connectrpc/connect, @bufbuild/* | | kit/util | ulid, redactSecrets | — |

Что здесь НЕ лежит и почему

Механизм — здесь, политика — в приложении. Это правило видно в подписях: лимитер частоты не знает ваших адресов, он получает bucketFor(req); RedisService не знает, откуда взялась строка подключения, она приходит значением-провайдером; журнал не знает имени сервиса, оно передаётся.

Поэтому сюда сознательно не попали: словарь ваших HTTP-заголовков (это словарь продукта, не инфраструктура), план счетов и любые доменные ключи Redis, матрицы прав.

Журнал

import { createTransports, createLogger } from "@smounters/kit/log";

app.configureLogger({ transports: createTransports({ service: "api" }) });
const logger = createLogger({ service: "api" }); // для кода вне DI: старт процесса, скрипты

Вне local каждая запись — один объект JSON в строку, поэтому сборщик логов достаёт поля без разбора регулярками. Структурный первый аргумент раскладывается в поля верхнего уровня:

this.logger.info({ type: "payment", event: "credited", amount, currency });

Если в приложении зарегистрирован requestContextHook, к КАЖДОЙ строке сам подмешивается reqId — вызывающий код о нём не знает.

Redis

Пакет не решает, откуда берётся конфигурация: подключение приходит значением-провайдером.

import { REDIS_CONNECTION, RedisService } from "@smounters/kit/redis";

@Module({
  providers: [{ provide: REDIS_CONNECTION, useValue: { url: appConfig.REDIS_URL } }, RedisService],
  exports: [RedisService],
  global: true,
})
export class RedisModule {}

withLock(key, ttlSec, fn) — для периодических задач при нескольких репликах: работа делается один раз на кластер, а не по разу на процесс. Освобождение — сравнение-и-удаление по случайному токену, поэтому лок, истёкший на середине и перехваченный другим процессом, не будет снят предыдущим владельцем. ttlSec обязан превышать худшее время работы fn.

Сквозной id запроса

import { clientIpNormalizeHook, requestContextHook } from "@smounters/kit/http";

app.addHook("onRequest", clientIpNormalizeHook());              // ПЕРВЫМ
app.addHook("onRequest", requestContextHook({ headerName: "X-Request-Id" }));

Порядок обязателен: нормализация IP должна быть первой (на req.ip опираются журнал и лимитер), а контекст — до всего остального, потому что он продолжает цепочку внутри AsyncLocalStorage и его видят только зарегистрированные ПОЗЖЕ хуки.

Зачем вообще: связать «пришёл вебхук → поставлена задача → воркер сделал внешний вызов» иначе нечем, кроме метки времени, — процессы разные, а логи в одном потоке. Протаскивать id параметром через все слои значило бы править сигнатуру каждой функции ради поля, которое нужно только журналу.

Деньги

Ни одна функция не принимает и не возвращает JS-число: double не представляет 0.1 точно, а книга, которая не может представить свои же суммы, перестаёт сходиться в первый же день. Суммы ходят десятичными строками.

import { isBalanced, quantizeToDecimals, ROUND_UP, toMoneyAmount } from "@smounters/kit/money";

toMoneyAmount("10.00001");            // null — точнее, чем хранит колонка: отклонить, а не округлить
isBalanced(lines);                    // ≥2 строки, без нулевых, сумма знаковых РОВНО 0
quantizeToDecimals(due, 6, ROUND_UP); // сумма, достижимая у токена с 6 знаками

quantizeToDecimals нужен, когда сумма должна быть достижима на другой стороне: токен с шестью знаками не переведёт значение с восемью, и «ожидаемая» сумма, скруглённая до масштаба книги, окажется неоплатной точно — а строгое сравнение назовёт это недоплатой, которой плательщик не мог избежать.

Защита от SSRF

import { safeFetch, SsrfBlockedError } from "@smounters/kit/net";

assertPublicUrl в одиночку проверяет только ПЕРВЫЙ адрес, а fetch по умолчанию идёт по перенаправлениям — публичный хост мог ответить 302 на приватный адрес, и проверка обходилась. safeFetch идёт по перенаправлениям вручную и проверяет каждый шаг. SsrfBlockedError отделён от сетевых ошибок намеренно: это ПОСТОЯННАЯ ошибка настройки, её нельзя перевыкладывать в очередь на повтор.

Известный предел: гонку DNS-rebinding между проверкой и подключением так не закрыть — для этого нужна фиксация адреса на момент соединения. Отсекаются реальные случаи: ошибка настройки и попытка достать адрес метаданных облака.