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

@memealerts/api

v1.3.0

Published

Типизированный SDK для MemeAlerts External API: OAuth, API v1, приложения платформы, виджеты и realtime-события

Readme

@memealerts/api

Типизированный SDK для MemeAlerts External API: OAuth, работа с профилем стримера, приём донатов и стикеров в реальном времени, приложения платформы, виджеты и вебхуки.

Пакет закрывает всё, что нужно стороннему приложению, и берёт на себя то, на чём чаще всего ломается ручная интеграция: одноразовые refresh token, подпись вебхуков по сырому телу и порядок подключения к Centrifugo.


Что это и для кого

Приложение на платформе MemeAlerts состоит из трёх частей, и у каждой свои требования к безопасности. Поэтому у пакета три точки входа.

| Импорт | Где выполняется | Что даёт | |--------|-----------------|----------| | @memealerts/api | Только бэкенд | OAuth, вызовы API v1, обмен токена запуска, проверка подписи вебхуков | | @memealerts/api/widget | Браузер, внутри iframe | Чтение launch_token, postMessage-контракт с MemeAlerts | | @memealerts/api/realtime | Бэкенд или браузер | Донаты, стикеры и события канала через Centrifugo |

Деление не косметическое. client_secret и webhookSecret существуют только на сервере, а серверная точка входа использует node:crypto и в браузерный бандл не собирается. Виджет секретов не имеет вообще: его код открыт любому зрителю на странице.

Бэкенд приложения                  Виджет в iframe
├── OAuth: authorize, code         ├── launch_token из URL ──┐
├── API v1                         └── postMessage: донат,   │
├── launch/exchange  ◄─────────────────  платёж приложению   │
└── проверка вебхуков  ◄── billingUrl                        │
        │                                                     │
        └── socket_connection_token ──► Centrifugo ◄──────────┘

Одну задачу пакет намеренно не решает: управление самими приложениями — создание, модерацию, тестировщиков, отчёты о доходе. Эти методы работают под сессией пользователя MemeAlerts, а не под токеном приложения, и делаются из личного кабинета.

Установка

npm install @memealerts/api

Для realtime дополнительно понадобится centrifuge — он объявлен peer-зависимостью, чтобы у тех, кому события не нужны, ничего лишнего не оказалось в бандле:

npm install centrifuge

Требования: Node.js 18+ (используется встроенный fetch). Пакет поставляется в ESM и CJS с типами, так что import и require работают одинаково.

import { MemeAlertsClient } from '@memealerts/api';
const { MemeAlertsClient } = require('@memealerts/api');

Быстрый старт

Перед началом зарегистрируйте приложение и получите client_id и client_secret. Полные примеры лежат в examples/express-backend.ts и examples/widget.ts.

import { MemeAlertsClient, SCOPES } from '@memealerts/api';

const client = new MemeAlertsClient({
    clientId: process.env.MA_CLIENT_ID!,
    clientSecret: process.env.MA_CLIENT_SECRET!,
});

// 1. Отправляем пользователя авторизовываться
const url = client.oauth.getAuthorizeUrl({
    redirectUri: 'https://my-app.com/callback',
    scopes: [SCOPES.USER_SHOW, SCOPES.DONATION_SUBSCRIBE],
    state: 'случайная строка',
});

// 2. В обработчике callback меняем код на токены
const tokens = await client.oauth.exchangeCode({ code, redirectUri: 'https://my-app.com/callback' });
await client.setTokens(tokens);

// 3. Работаем от имени стримера
const profile = await client.users.me();
console.log(profile.name);

Авторизация

MemeAlerts использует OAuth 2.0 с типом authorization_code.

1. Ссылка авторизации

const state = crypto.randomUUID();
req.session.oauthState = state;

const url = client.oauth.getAuthorizeUrl({
    redirectUri: 'https://my-app.com/callback',
    scopes: [SCOPES.USER_SHOW, SCOPES.DONATION_SUBSCRIBE],
    state,
});

redirectUri должен точно совпадать с одним из зарегистрированных в приложении, а scopes — входить в разрешённый ему список.

Доступные права:

| Константа | Значение | Что открывает | |-----------|----------|---------------| | SCOPES.USER_SHOW | oauth-user-show | Профиль и socket_connection_token | | SCOPES.DONATION_SUBSCRIBE | oauth-donation-subscribe | Канал донатов | | SCOPES.STICKER_SUBSCRIBE | oauth-sticker-subscribe | Канал стикеров | | SCOPES.EVENTS_READ | oauth-events-read | Общий канал событий | | SCOPES.BONUS_GIVE | oauth-bonus-give | Выдача бонусов и список саппортеров |

2. Callback

state возвращается неизменным. Сверьте его до обмена кода — это защита от CSRF, и без неё чужой запрос может привязать к вашей сессии чужой аккаунт.

app.get('/callback', async (req, res) => {
    if (req.query.state !== req.session.oauthState) {
        return res.status(400).send('Некорректный state');
    }

    const tokens = await client.oauth.exchangeCode({
        code: String(req.query.code),
        redirectUri: 'https://my-app.com/callback',
    });

    await client.setTokens(tokens);
    res.redirect('/');
});

Код одноразовый и живёт 10 минут.

3. Хранение токенов

TokenPair содержит accessToken, refreshToken, expiresIn и посчитанный expiresAt. Клиент сам обновляет пару за 60 секунд до истечения и записывает новую в хранилище.

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

import { MemeAlertsClient, type TokenPair, type TokenStore } from '@memealerts/api';

class DatabaseTokenStore implements TokenStore {
    constructor(private readonly streamerId: string) {}

    async load(): Promise<TokenPair | null> {
        return await db.tokens.findOne({ streamerId: this.streamerId });
    }

    async save(tokens: TokenPair): Promise<void> {
        await db.tokens.upsert({ streamerId: this.streamerId }, tokens);
    }
}

const client = new MemeAlertsClient({
    clientId: process.env.MA_CLIENT_ID!,
    clientSecret: process.env.MA_CLIENT_SECRET!,
    tokenStore: new DatabaseTokenStore(streamerId),
});

Один клиент обслуживает одного стримера. Для нескольких заведите по клиенту на каждого — объект дешёвый, состояние всё равно живёт в хранилище.

Одноразовый refresh token

Каждое обновление гасит старый refresh token и выдаёт новый. Отсюда два следствия, которые SDK берёт на себя:

  • Новую пару обязательно сохранить. Клиент делает это сам через TokenStore — если вы вызываете oauth.refresh() напрямую, сохраните результат руками.
  • Параллельные обновления склеиваются. Два одновременных запроса с одним refresh token означали бы, что второй наверняка получит invalid_grant. OAuthClient держит обновления в полёте по значению токена и отдаёт всем ожидающим один результат.

invalid_grant против server_error

Обе ситуации выглядят как «не удалось обновить токен», но лечатся по-разному, поэтому у ошибки есть два готовых флага:

import { MemeAlertsOAuthError } from '@memealerts/api';

try {
    await client.users.me();
} catch (error) {
    if (error instanceof MemeAlertsOAuthError) {
        if (error.requiresReauth) {
            // Грант мёртв: токен уже использован, отозван или приложение удалено с канала.
            // Повторять бессмысленно — отправляйте стримера авторизовываться заново.
            await db.tokens.delete({ streamerId });
            return redirectToAuthorize();
        }

        if (error.retryable) {
            // Сбой на нашей стороне. Старый refresh token остался валидным — повторите позже.
            return scheduleRetry();
        }
    }

    throw error;
}

API v1

Все методы сами следят за сроком жизни access token.

Профиль

const profile = await client.users.me();

profile.id;                      // нужен для имён каналов Centrifugo
profile.socket_connection_token; // токен подключения, живёт недолго
profile.channel.unique_link;

channel.currency_name_declensions может быть null — у части каналов склонения не заполнены, проверяйте перед обращением. Требует scope oauth-user-show.

Выдача бонуса

await client.users.giveBonus({ userId: '633edfa2e6cb81985b72e7aa', value: 50 });

value — целое от 1 до 1000. Получатель должен быть саппортером стримера, иначе запрос вернёт ошибку. Требует scope oauth-bonus-give.

Саппортеры

const { data, total } = await client.users.supporters({ skip: 0, limit: 50, query: 'ivan' });

limit по умолчанию 20, максимум 100. Требует scope oauth-bonus-give.

Realtime

События приходят через Centrifugo. Порядок подключения задан сервером: сначала WebSocket с токеном подключения, потом выданный Centrifugo client id, и только с ним выдаются токены подписок. MemeAlertsRealtime проходит эти шаги сам.

import { MemeAlertsClient } from '@memealerts/api';
import { MemeAlertsRealtime, tokenSourceFromClient } from '@memealerts/api/realtime';

const profile = await client.users.me();

const realtime = new MemeAlertsRealtime({
    userId: profile.id,
    tokens: tokenSourceFromClient(client),
});

realtime
    .onDonation(donation => {
        console.log(`${donation.username}: ${donation.amount} ${donation.currencyName}`);

        // Донат пришёл через виджет приложения — у обычных донатов поля нет
        if (donation.clientId) console.log('из приложения', donation.clientId);
    })
    .onSticker(sticker => console.log(sticker.stickerName, sticker.price))
    .onScopeDenied(channels => console.warn('нет доступа к каналам:', channels))
    .onError(error => console.error(error));

await realtime.connect();

Каналы собираются из userId; по умолчанию поднимаются все три. Ограничить набор можно опцией channels: ['donation'].

| Канал | Scope | Колбэк | |-------|-------|--------| | $alerts:donation_<id> | oauth-donation-subscribe | onDonation | | $alerts:sticker_<id> | oauth-sticker-subscribe | onSticker | | $alerts:events_<id> | oauth-events-read | onEvent, onCoinPurchase |

Общий канал шлёт разнородные события, размеченные полем type:

realtime.onEvent(event => {
    if (event.type === 'donation') console.log(event.data.amount);
    if (event.type === 'coin_purchase') console.log(event.data.price, event.data.priceCurrency);
});

Донат приходит и в свой канал, и в общий — если подписаны на оба, обработаете его дважды.

Если событий нет

Каналы, на которые у токена нет нужного scope, сервер отбрасывает молча: ответ приходит успешный, просто без них. Поэтому есть onScopeDenied — иначе отсутствие событий выглядело бы как ошибка в своём коде, а причина оставалась бы невидимой.

Подписки создаются с обновляемым токеном: источник в OBS живёт часами, и статического токена подписки на такой срок не хватило бы.

Realtime без client_secret

Если токены приезжают со своего бэкенда, а секрета в этом процессе нет, передайте свой источник токенов:

const realtime = new MemeAlertsRealtime({
    userId,
    tokens: {
        getConnectionToken: async () => await fetch('/api/ma/connection-token').then(r => r.text()),
        getSubscriptionTokens: async params => await fetch('/api/ma/subscribe', {
            method: 'POST',
            body: JSON.stringify(params),
        }).then(r => r.json()),
    },
});

Приложения платформы

Приложение открывается в одном из трёх контекстов, и в каждом получает свой набор данных:

| Контекст | Где открыт | Стример | Зритель | |----------|------------|---------|---------| | settings | Кабинет стримера | да | нет | | obs | Источник в OBS | да | нет | | viewer | Страница канала, у зрителя | да | да |

В любом контексте URL запуска содержит параметр launch_token:

https://my-app.com/viewer?launch_token=9f86d081...

Обмен токена запуска

import { readLaunchToken } from '@memealerts/api/widget';

// В браузере: достаём токен и отправляем на свой бэкенд
const launchToken = readLaunchToken();
await fetch('/api/session', { method: 'POST', body: JSON.stringify({ launchToken }) });
// На бэкенде: меняем на данные о запуске
const session = await client.launch.exchange(launchToken);

session.context;         // 'settings' | 'obs' | 'viewer'
session.streamer.id;     // владелец канала
session.viewer?.id;      // только в контексте viewer
session.scopes;
session.token;           // пара токенов, всегда выпущенная на стримера

Обмен возможен только на бэкенде. Он требует client_secret, а тот в браузере оказаться не должен ни при каких условиях.

Токены контекстов settings и viewer одноразовые и живут 5 минут, повторный обмен вернёт ошибку. Ссылка для obs стабильна и не протухает: источник в браузере OBS перезагружается когда угодно, и запросить свежий токен в этот момент некому.

Пара токенов всегда выпускается на стримера, даже в контексте viewer: приложение работает с API от имени того, кто его установил, а зритель приходит как данные, а не как субъект. Сохраните её в свой TokenStore и работайте обычным клиентом:

await client.setTokens(session.token);

Виджет

Точка входа @memealerts/api/widget собирается в браузерный бандл и не содержит ни секретов, ни серверных зависимостей.

Рукопожатие

import { MemeAlertsWidget } from '@memealerts/api/widget';

const widget = new MemeAlertsWidget();

try {
    const { context, streamerId } = await widget.ready();
    showDonateButton();
} catch {
    // Родитель не ответил: виджет открыт в OBS или по прямой ссылке.
    // Это не ошибка интеграции, но кнопки платежей показывать нельзя — отправлять сообщение некому.
    hideDonateButton();
}

Донат стримеру

const result = await widget.requestDonation({
    amount: 500,
    message: 'Го катку',
    isAnonymous: false,
});

if (result.status === 'paid') unlockFeature(result.donationId);

Виджет не запрашивает ссылку на оплату сам и не показывает платёжные системы. Он только называет сумму, а попап рисует MemeAlerts под сессией самого зрителя: токен из launch/exchange привязан к стримеру, и заплатить им за зрителя нельзя.

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

Статусы: paid, cancelled, failed. Отмена — обычный результат, а не исключение.

Платёж приложению

Зритель платит не стримеру, а самому приложению — за подписку, доступ к функции, внутреннюю валюту:

const result = await widget.requestPurchase({ amount: 300, payload: 'order-8842' });

payload необязателен, до 512 символов. MemeAlerts его не разбирает и возвращает как есть в вебхуке partner_payment.completed — это ваш способ связать платёж со своим заказом.

Выдавайте оплаченное по вебхуку, а не по результату postMessage: status: 'paid' говорит лишь о том, что зритель дошёл до конца в интерфейсе, и приходит в браузер, где его можно подделать.

Ссылка для OBS и режим стримера

В контексте settings виджет может сам открыть то, что стример видит кнопками в шапке:

await widget.openObsLink(); // попап со ссылкой для встраивания в OBS
await widget.openStreamerMode(); // приложение в режиме стримера в отдельном попапе

Ответа на эти команды нет: промис разрешается, как только сообщение ушло родителю. В других контекстах методы отклоняются ошибкой — MemeAlerts такие сообщения там игнорирует. Ссылка для OBS имеет смысл, только если у приложения задан obsUrl.

Один запрос за раз

В ответных сообщениях нет идентификатора запроса — сопоставить их можно только по типу. Поэтому SDK держит не больше одного незавершённого запроса каждого вида и отклоняет второй понятной ошибкой, вместо того чтобы разрешить не тот промис. Донат и платёж при этом не мешают друг другу.

Настройки

new MemeAlertsWidget({
    parentOrigin: 'https://memealerts.com', // с чьего origin принимать сообщения
    readyTimeoutMs: 5_000,
    resultTimeoutMs: 10 * 60 * 1000,        // зритель платит не быстро
});

Сообщения принимаются, только если пришли от окна, встроившего этот iframe, и с ожидаемого origin. Проверка встроена и отключить её нельзя, но на деве parentOrigin можно переставить на свой хост.

Когда виджет больше не нужен, вызовите widget.destroy(): он снимет слушатель и отклонит незавершённые запросы.

Вебхуки

Если в конфиге приложения заполнен billingUrl, на него приходит уведомление о каждом проведённом платеже и о каждом его откате.

Подпись считается по сырому телу

Это самая частая ошибка интеграции. express.json() разбирает тело и оставляет объект; обратная сборка через JSON.stringify даёт другие байты — те же данные, но иные пробелы и порядок ключей, — и подпись не сходится. Тело нужно сохранить до разбора JSON.

import express from 'express';
import { handleWebhook, WebhookVerificationError } from '@memealerts/api';

// Именно raw, и именно на этом маршруте — до глобального express.json()
app.post('/webhooks/memealerts', express.raw({ type: 'application/json' }), async (req, res) => {
    try {
        await handleWebhook({
            secret: process.env.MA_WEBHOOK_SECRET!,
            headers: req.headers,
            rawBody: req.body,
            onPaymentCompleted: async (event, ctx) => {
                await grantReward(event.orderId, event.commission.amount, ctx.deliveryId);
            },
            onPartnerPaymentCompleted: async event => {
                await activateSubscription(event.orderId, event.payload);
            },
            onPaymentRefunded: async event => await revokeReward(event.orderId),
            onPartnerPaymentRefunded: async event => await revokeSubscription(event.orderId),
        });

        res.sendStatus(200);
    } catch (error) {
        if (error instanceof WebhookVerificationError) return res.sendStatus(401);
        throw error;
    }
});

Подпись проверяется по webhookSecret приложения — не по client_secret, тот хранится у нас только хешем и подписать им нечего.

События

| Событие | Когда приходит | Ключевое поле | |---------|----------------|---------------| | payment.completed | Донат стримеру через виджет проведён | commission.amount — ваша доля | | partner_payment.completed | Прямой платёж приложению проведён | amount — ваша доля, payload — ваша строка | | payment.refunded | Донат откатили возвратом или чарджбэком | amount — сколько снимается | | partner_payment.refunded | Прямой платёж откатили | amount, reason |

WebhookEvent — размеченное объединение по полю event, так что TypeScript сам сузит тип внутри if:

import { parseWebhookEvent } from '@memealerts/api';

const event = parseWebhookEvent(rawBody);

if (event.event === 'payment.completed') {
    console.log(event.commission.amount, event.commission.rate);
}

Доля с доната режется из того, что остаётся стримеру, — после комиссии платёжной системы и комиссии платформы. У прямого платежа стример не получает ничего: его канал только контекст, в котором зритель запустил виджет.

Идемпотентность и повторы

X-MA-Delivery уникален для доставки и не меняется между попытками — используйте его как ключ дедупликации, он приходит в ctx.deliveryId.

Доставка считается успешной при любом коде 2xx. Любой другой ответ, таймаут в 10 секунд или сетевая ошибка приводят к повтору с нарастающей паузой: 1 минута, 5 минут, 30 минут, 2 часа, 6 часов. После шестой неудачной попытки доставка помечается FAILED и больше не повторяется.

Поэтому отвечайте 200 как можно раньше, а тяжёлую работу уводите в очередь: медленный обработчик выглядит для нас как таймаут и приводит к повторной доставке.

Если ваш биллинг пролежал дольше всех повторов, оповещение о любом платеже можно отправить заново из раздела «Платежи» в кабинете разработчика. Это новая доставка с новым X-MA-Delivery, поэтому дедуплицируйте выдачу ещё и по orderId: тело события то же самое, а у откаченного платежа повтор приходит событием *.refunded.

Возвраты и чарджбэки

Проведённый платёж может быть отменён: зритель попросил возврат или оспорил списание в банке. Тогда приходит парное событие, и начисление снимается — сумма вычитается из ближайшей месячной выплаты. Отвяжите по orderId то, что вы выдали за этот платёж. Событие приходит один раз на начисление; отмена ошибочного чарджбэка отдельным событием не сопровождается.

Проверка подписи отдельно

Если диспетчер handleWebhook не подходит, проверяйте подпись сами:

import { verifyWebhookSignature } from '@memealerts/api';

const ok = verifyWebhookSignature({
    secret: process.env.MA_WEBHOOK_SECRET!,
    signature: req.headers['x-ma-signature'] as string,
    timestamp: req.headers['x-ma-timestamp'] as string,
    rawBody: req.body,
});

Сравнение идёт за постоянное время, а запросы со слишком старым X-MA-Timestamp отклоняются: допуск по умолчанию пять минут, меняется опцией toleranceMs. Есть и вариант с исключением — assertWebhookSignature, из его сообщения видно, что именно не сошлось.

Обработка ошибок

| Класс | Когда | Полезные поля | |-------|-------|---------------| | MemeAlertsApiError | REST-запрос вернул не 2xx | status, endpoint, body | | MemeAlertsOAuthError | Ошибка /oauth/token | code, requiresReauth, retryable | | WebhookVerificationError | Подпись не сошлась или тело битое | message | | MemeAlertsWidgetError | Проблема postMessage-обмена | message | | MemeAlertsError | Базовый класс всех перечисленных | message |

import { MemeAlertsApiError, MemeAlertsOAuthError } from '@memealerts/api';

try {
    await client.users.giveBonus({ userId, value: 50 });
} catch (error) {
    if (error instanceof MemeAlertsOAuthError && error.requiresReauth) {
        return redirectToAuthorize();
    }

    if (error instanceof MemeAlertsApiError) {
        if (error.status === 403) return; // нет scope oauth-bonus-give
        if (error.status === 429) return scheduleRetry();
    }

    throw error;
}

Справочник методов

@memealerts/api

| Метод SDK | Эндпоинт | |-----------|----------| | oauth.getAuthorizeUrl(params) | GET /oauth/authorize | | oauth.exchangeCode(params) | POST /oauth/token (authorization_code) | | oauth.refresh(refreshToken) | POST /oauth/token (refresh_token) | | users.me() | GET /api/v1/user/oauth | | users.giveBonus(params) | POST /api/v1/user/give-bonus | | users.supporters(query) | GET /api/v1/user/supporters | | centrifugo.subscriptionTokens(params) | POST /api/v1/centrifuge/subscribe | | launch.exchange(launchToken) | POST /api/v1/launch/exchange | | client.setTokens(tokens) | — | | client.getAccessToken() | — (обновит токен при необходимости) | | verifyWebhookSignature(params) | — | | assertWebhookSignature(params) | — | | parseWebhookEvent(rawBody) | — | | handleWebhook(params) | — |

@memealerts/api/widget

| Метод SDK | Сообщение postMessage | |-----------|-----------------------| | readLaunchToken(search?) | — (параметр launch_token в URL) | | widget.ready() | ждёт ma:ready | | widget.requestDonation(params) | ma:donate:request → ma:donate:result | | widget.requestPurchase(params) | ma:purchase:request → ma:purchase:result | | widget.openObsLink() | ma:obs-link:open, только settings, без ответа | | widget.openStreamerMode() | ma:streamer-mode:open, только settings, без ответа | | widget.destroy() | — |

@memealerts/api/realtime

| Метод SDK | Что делает | |-----------|------------| | realtime.connect() | WebSocket, client id, токены подписок, подписка на каналы | | realtime.disconnect() | Снимает подписки и закрывает соединение | | realtime.onDonation(cb) | $alerts:donation_<id> | | realtime.onSticker(cb) | $alerts:sticker_<id> | | realtime.onEvent(cb) | $alerts:events_<id>, размечен по type | | realtime.onCoinPurchase(cb) | Покупки валюты канала из общего канала | | realtime.onScopeDenied(cb) | Каналы, токены на которые не выданы | | realtime.onConnected(cb) / onDisconnected(cb) / onError(cb) | Состояние соединения | | tokenSourceFromClient(client) | Источник токенов поверх MemeAlertsClient |

Константы: SCOPES, WEBHOOK_EVENTS, WEBHOOK_HEADERS, WIDGET_MESSAGES, DEFAULT_BASE_URL, DEFAULT_WS_URL, билдеры каналов donationChannel, stickerChannel, eventsChannel.

Разработка

npm install
npm run build       # tsup: ESM, CJS и .d.ts на три точки входа
npm test            # vitest
npm run type-check
npm run lint

Публикация:

npm version <patch|minor|major>
npm publish --access public

prepublishOnly прогоняет проверку типов, линт, тесты и сборку, так что сломанная версия в реестр не уедет.

Лицензия

MIT