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

@andrey4emk/npm-app-back-b24

v3.8.2

Published

Bitrix24 OAuth helpers for Node.js projects

Readme

npm_appBackB24

Утилиты для работы с Bitrix24 OAuth на основе @bitrix24/b24jssdk.

Содержание

Установка

npm install @andrey4emk/npm-app-back-b24

Точки входа

| Импорт | Что отдаёт | Поднимает OAuth-модуль | | --------------------------------------------- | ----------------------------- | ---------------------- | | @andrey4emk/npm-app-back-b24 | всё | да | | @andrey4emk/npm-app-back-b24/logs | logs | нет | | @andrey4emk/npm-app-back-b24/fetchRetry | fetchRetry, fetchWithTimeout, isNetworkError, isPreConnectionError, isTimeoutError, isAbortError, maskUrl, FETCH_TIMEOUTS, DEFAULT_FETCH_TIMEOUT_MS | нет |

Корневой импорт — barrel: он тянет модуль OAuth, который при загрузке читает authB24.json и, если приложение авторизовано, запускает таймер проактивного обновления токена. Проектам, которые ходят в B24 по входящему вебхуку или берут из пакета только логгер и fetchRetry, удобнее импортировать по подпутям — тогда OAuth-модуль не загружается вообще.

// Проект без OAuth: ничего лишнего не поднимается
import { logs } from "@andrey4emk/npm-app-back-b24/logs";
import { fetchRetry, maskUrl } from "@andrey4emk/npm-app-back-b24/fetchRetry";

Корневой импорт при этом остаётся безопасным: если APP_B24_CLIENT_ID и APP_B24_CLIENT_SECRET не заданы, пакет считает, что OAuth в проекте не используется, молча выставляет $b24 = null и пишет об этом только в debug.

Строгость типов

Пакет публикуется исходниками на TypeScript и компилируется в программе потребителя — значит его код проверяется настройками потребителя, а не нашими. Поэтому сам пакет собирается с strict и noUncheckedIndexedAccess: на настройках Nuxt (где оба флага включены) штатный nuxt typecheck проходит по коду пакета без обходных скриптов и без skipLibCheck-заплаток.

Практическое следствие: индексация массива внутри пакета всегда сопровождается проверкой на undefined. Если в своём проекте эти флаги включены — ошибок из node_modules/@andrey4emk/npm-app-back-b24 быть не должно.

Nuxt: сборка в Nitro и импорт $b24

Пакет публикуется исходниками на TypeScript, а Nitro по умолчанию не транспилирует ничего из node_modules. Двух ловушек здесь достаточно, чтобы потерять полдня, — обе проверены на Nuxt 4.5.

Сборка. Одного nitro.externals.inline мало: Nuxt жёстко ставит esbuild.options.exclude = [/node_modules/], а defu при слиянии конфигов массивы склеивает, а не заменяет — переопределить это из nuxt.config нельзя. Rollup получает сырой TypeScript и падает на Expected '{', got 'interface'. Рабочая комбинация — inline плюс хук nitro:config, который присваивает exclude заново:

// nuxt.config.ts
export default defineNuxtConfig({
    nitro: { externals: { inline: ["@andrey4emk/npm-app-back-b24"] } },
    hooks: {
        "nitro:config"(nitroConfig) {
            // ЗАМЕНЯЕТ exclude, а не дополняет — иначе /node_modules/ из дефолта победит
            nitroConfig.esbuild.options.exclude = [/node_modules\/(?!@andrey4emk\/)/];
        },
    },
});

Импорт $b24. Это export let, а не const: reinitializeB24() переприсваивает биндинг. Захват значения в момент импорта (const b24 = $b24 на уровне модуля) навсегда оставит то, что было при загрузке, — как правило null, потому что токены к этому моменту ещё не прочитаны. Читать свойство нужно в момент вызова:

// server/utils/b24.ts
import { $b24 } from "@andrey4emk/npm-app-back-b24";

// Геттер, а не const: биндинг $b24 меняется после reinitializeB24()
export const getB24 = () => $b24;

// Использование
const b24 = getB24();
if (b24) {
    const res = await b24.callMethod("crm.deal.get", { id: 123 });
}

Использование

import {
    $b24,
    saveAuthB24Handler,
    refreshAndSaveTokens,
    reinitializeB24,
    stopProactiveRefresh,
    getResultData,
    callProtected,
    errorB24,
    event,
    Event,
    ChatApp,
    Smsgold,
    Email,
    Wappi,
    logs,
    fetchRetry,
    fetchWithTimeout,
    isNetworkError,
    isTimeoutError,
    maskUrl,
    FETCH_TIMEOUTS,
} from "@andrey4emk/npm-app-back-b24";

// $b24 — готовый экземпляр B24OAuth (или null, если токены не настроены)
if ($b24) {
    const result = await $b24.callMethod("crm.contact.list", { limit: 5 });
}

// HTTP-обработчик для сохранения токенов с фронта
app.post("/b24/auth", saveAuthB24Handler);

// Обновление токенов вручную
const refreshResult = await refreshAndSaveTokens();

// Создание служебной задачи при ошибке
await errorB24({
    title: "Ошибка синхронизации",
    description: "Детали ошибки здесь",
    ufCrmTask: ["D_5"],
});

// Работа с событиями (готовый экземпляр, или null если $b24 не инициализирован)
if (event) {
    const { error, data } = await event.get("ONCRMDYNAMICITEMUPDATE_149");
}

// ChatApp — создаёте экземпляр самостоятельно
const chatApp = new ChatApp(makeParam, authParam, typeParam);
await chatApp.sendMessageChatApp("whatsApp", {
    phone: "+1234567890",
    message: "Привет!",
});

// Smsgold — создаёте экземпляр самостоятельно
const smsClient = new Smsgold({ user: "login", pass: "password" });
await smsClient.sendSms({ phone: "+79990000000", message: "Привет!" });

// Email — создаёте экземпляр самостоятельно
const emailClient = new Email({ user: "[email protected]", pass: "app-password" });
await emailClient.send({ to: "[email protected]", subject: "Тема", text: "Текст" });

// Wappi — создаёте экземпляр самостоятельно
const wappi = new Wappi(maxAuth, telegaAuth, whatsAppAuth);
await wappi.sendMessageWappi("whatsApp", { phone: "+1234567890", message: "Привет!" });

// Логирование
logs.add("Сообщение", "info");

// fetch с повторными попытками при сетевых ошибках (бюджет попытки — 60с)
const response = await fetchRetry("https://api.example.com/data", { method: "GET" });

// Один запрос без повторов — для неидемпотентных операций
const sent = await fetchWithTimeout("https://api.example.com/send", { method: "POST" }, FETCH_TIMEOUTS.send);

API

Bitrix24 OAuth

Модуль работает с токенами через файл authB24.json (используется библиотека conf). Модель БД не требуется.

  • $b24 — готовый экземпляр B24Client (B24OAuth плюс методы фасада, см. ниже), создаётся автоматически при импорте на основе сохранённых токенов. Если данные авторизации отсутствуют — null.

    import { $b24 } from "@andrey4emk/npm-app-back-b24";
    
    if ($b24) {
        const result = await $b24.callMethod("crm.contact.list", { limit: 5 });
    }

    Особенности:

    • При обновлении токенов SDK автоматически сохраняет их в файл через callback setCallbackRefreshAuth.
    • Окружение (DEV/PROD) определяется переменной APP_ENV — используется как ключ секции в конфиге авторизации.
    • Признак того, что проект использует OAuth, — заданные APP_B24_CLIENT_ID и APP_B24_CLIENT_SECRET. Если их нет, $b24 молча становится null (уровень debug). Если они заданы, но токенов в authB24.json не хватает, пишется error — это уже настоящая проблема конфигурации.

    Методы callMethod, callListMethod, fetchListMethod, callBatch, callBatchByChunk — фасад пакета.

    SDK 2.x помечает эти пять методов к удалению в следующем major и на каждый вызов пишет предупреждение об устаревании. Пакет реализует их сам поверх actions.v2.*, поэтому вызовы у потребителей не меняются, а зависимости от удаляемого API у пакета больше нет. Сигнатуры и типы возврата прежние — правок в проектах не требуется.

    Поведение прежнее у четырёх методов из пяти. callListMethod отличается в двух местах: он бросает внятную ошибку с именем метода там, где SDK падал TypeError из своих недр (портал вернул «мягкую» ошибку, ответ не список, конверт нечитаем, next не растёт), и у него есть потолок в 1000 страниц — при упоре в него вызов тоже бросает, а не отдаёт молча обрезанный список. Если у вас есть try/catch вокруг постраничных выборок, текст ошибки изменится; если его нет — падение станет заметнее, но не появится там, где раньше всё работало.

    Прямой доступ к $b24.actions.* остаётся: это сырой SDK без retry и гейта идемпотентности. Если защита нужна, оборачивайте вызов в callProtected().

    Retry при сетевых ошибках:

    Экземпляр $b24 обёрнут Proxy, который автоматически повторяет запросы при сетевых ошибках. Повторные попытки применяются к методам callMethod, callListMethod, callBatch, а также к обновлению токена (refreshAuth). Метод fetchListMethod из ретраев исключён — это async-генератор, оборачивать его в async-функцию нельзя. callBatchByChunk пакет реализует, но не ретраит.

    callListMethod повторяется целиком: при сетевом сбое пагинация начинается с нулевой страницы заново. Постраничный retry был бы сменой семантики, поэтому поведение оставлено прежним.

    | Параметр | Значение | | -------------- | ----------------------------------------- | | Попыток | 5 | | Задержка | 500 мс (линейно растущая для refreshAuth) |

    Retry срабатывает только при сетевых проблемах (ECONNRESET, ETIMEDOUT, ERR_NETWORK и т.д.). HTTP-ошибки (400, 500) и ошибки бизнес-логики Bitrix24 не вызывают повторных попыток.

    Уровни логов. Промежуточные попытки пишутся на warn, окончательный отказ — на error: исчерпание всех попыток и отмена повтора гейтом. Так один упавший вызов даёт одно сообщение в чат B24 вместо пяти, но при этом не теряется совсем. Логировать провал обязан сам Proxy: errorB24(), Event и Smsgold ошибку только возвращают вызывающему коду и в лог не пишут.

    Создающие вызовы не повторяются Proxy-слоем. Если в вызове есть метод, создающий сущность или запускающий действие (*.add, *.create, *.start, *.send, *.uploadfile, *.import, *.register — в том числе среди команд callBatch), retry отменяется, а в лог идёт повтор отменён. Причина: транспортная ошибка не говорит, дошёл ли запрос до портала. NETWORK_ERROR приходит и когда соединение не состоялось, и когда оно оборвалось после того, как портал уже принял и выполнил запрос — а повтор во втором случае создаёт вторую задачу, второй файл, второй экземпляр бизнес-процесса. Идемпотентные записи (*.update, *.delete, *.set) повторяются как прежде: повторное применение даёт то же состояние.

    Команды callBatch разбираются в обеих формах — кортеж ["crm.deal.add", {...}] (основная в SDK 2.x) и объект { method, params }. Если форму команды разобрать не удалось, вызов считается создающим и не повторяется: для защиты от дубликатов безопаснее ошибиться в сторону осторожности.

    Важно: транспортные сбои SDK не повторяет вообще — пакет задаёт параметры ограничителя при создании клиента. Иначе гейт был бы дырявым: SDK не различает идемпотентные и создающие вызовы и повторил бы оборванный crm.deal.add до трёх раз внутри себя, куда наша защита не дотягивается. Измерено: без этих параметров обрыв соединения, 502 и 504 дают по три выполнения на портале, с ними — по одному.

    Повторы SDK при 429 и 503 сохранены: портал отвечает отказом, не выполнив запрос.

    Отсюда следствие для вашего кода: не вызывайте setRestrictionManagerParams() и не поднимайте initB24Helper() / B24HelperManager на $b24 — они перезаписывают настройки ограничителя целиком, не сливая, и молча вернут повторы SDK вместе с риском дубликатов.

    Обмен refresh-токена повторяется по более строгому правилу — только если соединение заведомо не состоялось (см. isPreConnectionError()). При таймауте или обрыве сервер мог уже провести ротацию; тогда локальный refresh_token мёртв независимо от наших действий, и повтор не помогает, а лишь маскирует проблему серией одинаковых отказов. Следующая попытка всё равно будет: проактивный таймер повторяет через 2 минуты при времени жизни токена около часа.

    У самого SDK есть собственный слой ретраев (maxRetries: 3), но на транспортных ошибках он выключен — там одна попытка, повторяет только Proxy. При 429, 503 и неизвестных 5xx SDK повторяет по-прежнему.

    Отдельно про ответы шлюза перед порталом. 502 повторяет наш слой: шлюз не смог получить ответ от upstream, запрос почти наверняка не выполнялся. 504 не повторяет никто — это «портал взял запрос и считает прямо сейчас»; повтор создающего вызова дал бы дубликат, а читающего — ничего, кроме нагрузки на портал в худший для него момент.

    Диагностика SDK. Начиная с 3.7.0 собственные сообщения SDK от уровня WARNING и выше попадают в лог пакета с префиксом SDK B24: вместе с контекстом (requestId, method, status, код ошибки) — например предупреждение fetchList про игнорируемый order или про остановку пагинации, когда idKey не совпал с полем ответа. Порог нужен: на уровне info SDK пишет две строки на каждый запрос. Ошибки SDK приходят как warn, а не error — это диагностика отдельной неуспешной попытки, а окончательный провал вызова один раз пишет retry-слой.

    Ответы со статусом 4xx уходят на debug: лимитер SDK пишет через свой error() любой 4xx кроме 408 и 429, а Bitrix24 отдаёт 400 на штатные «мягкие» ошибки вроде несуществующей сущности. Без этого проверка существования в цикле давала бы строку на каждой итерации. Разбирайте такие ответы через getResultData() — это нормальный путь, а не сбой.

    «Мягкие» ошибки Bitrix24:

    Часть ошибок (ERROR_ENTITY_NOT_FOUND, BITRIX_REST_V3_EXCEPTION_*) SDK не бросает исключением, а возвращает как AjaxResult с ошибкой: isSuccess === false, и тогда response.getData() вернёт undefined. Отдельный случай — успешный ответ, у которого сам result пустой (null/undefined). Чтобы не падать на undefined, разбирайте ответы через getResultData() — он покрывает обе ситуации и бросает понятную ошибку с именем метода.

    Проактивное обновление токена:

    Токен автоматически обновляется каждые 25 минут. Если обновление не удалось — повтор через 2 минуты (вместо ожидания следующих 25 минут), что предотвращает протухание токена.

  • saveAuthB24Handler(req, res) — HTTP-обработчик для сохранения набора токенов из req.body. После сохранения автоматически переинициализирует $b24 — перезапуск сервера не требуется.

    • Параметры:
      • req (Express Request) — объект запроса с полем body.
      • res (Express Response) — объект ответа.
    • Тело запроса (req.body):
      {
          "access_token": "string",
          "refresh_token": "string",
          "domain": "string",
          "expires_in": "number",
          "member_id": "string"
      }
    • Возвращает: HTTP-ответ с JSON { status, message } (код 201 при успехе, 400/500 при ошибках).
    app.post("/b24/auth", saveAuthB24Handler);
  • refreshAndSaveTokens() — обновляет токены из текущего экземпляра $b24 и сохраняет в файл.

    • Возвращает: промис с объектом { error: boolean, message: string }.
    const result = await refreshAndSaveTokens();
    if (!result.error) {
        console.log("Токены обновлены:", result.message);
    }
  • saveTokens(authData) — низкоуровневая функция для прямого сохранения токенов в файл. Перед записью валидирует данные — если access_token, refresh_token, domain или expires пустые/отсутствуют, операция отклоняется.

    • Параметры:
      • authData (AuthData) — объект с полями access_token, refresh_token, domain, expires_in, member_id, expires.
    • Возвращает: объект { error: boolean, message: string }.
  • reinitializeB24() — пересоздаёт экземпляр $b24 из актуального authB24.json без перезапуска сервера. Останавливает текущий проактивный таймер, создаёт новый B24OAuth, настраивает колбэк автосохранения, обновляет токены и запускает таймер заново.

    • Возвращает: промис с объектом { error: boolean, message: string }.
    import { reinitializeB24 } from "@andrey4emk/npm-app-back-b24";
    
    const result = await reinitializeB24();
    if (!result.error) {
        console.log("$b24 переинициализирован");
    }
  • stopProactiveRefresh() — останавливает проактивный таймер обновления токена. Используется при graceful shutdown.

    import { stopProactiveRefresh } from "@andrey4emk/npm-app-back-b24";
    
    process.on("SIGTERM", () => {
        stopProactiveRefresh();
        process.exit(0);
    });
  • getResultData(response, methodName) — достаёт result из ответа SDK. Если Bitrix24 вернул «мягкую» ошибку (AjaxResult с ошибкой вместо исключения) или пустой ответ, бросает Error с понятным текстом вместо падения на undefined.

    • Параметры:
      • response (AjaxResult) — результат $b24.callMethod().
      • methodName (string) — имя метода B24, попадёт в текст ошибки.
    • Возвращает: содержимое result. Тип задаётся дженериком.
    import { $b24, getResultData } from "@andrey4emk/npm-app-back-b24";
    
    const response = await $b24.callMethod("crm.deal.get", { id: 123 });
    const deal = getResultData(response, "crm.deal.get");
  • callProtected(run, methodName) — прогоняет произвольный вызов $b24.actions.* через тот же retry и гейт идемпотентности, что и методы фасада. Нужен, когда требуется API, которого у фасада нет: FilterV3, keyset-пагинация (callTail/fetchTail), aggregate.

    • Параметры:
      • run (() => Promise<T>) — сам вызов.
      • methodName (string | string[]) — имя REST-метода B24 (не метода SDK) либо список имён, если внутри batch: по ним гейт решает, создаёт ли вызов сущность.
    • Возвращает: промис с результатом run.
    import { $b24, callProtected } from "@andrey4emk/npm-app-back-b24";
    
    const response = await callProtected(
        () => $b24.actions.v3.call.make({ method: "crm.item.list", params }),
        "crm.item.list"
    );
    
    // Для batch перечисляйте методы команд, а не "batch"
    const batchResponse = await callProtected(
        () => $b24.actions.v3.batch.make({ calls }),
        ["crm.item.get", "crm.item.add"]
    );

    Гейт работает fail-closed. Если список имён пуст или хоть одно имя не похоже на REST-метод (легальные всегда с точкой — crm.deal.add, disk.folder.uploadfile), вызов считается создающим и не повторяется. Поэтому callProtected(() => batch.make({ calls }), "batch") защиту не обойдёт: имя batch не разбирается, и батч с crm.deal.add внутри не будет повторён пять раз при status: 0. Цена ошибки в эту сторону — лишняя необработанная сетевая ошибка; в обратную — дубликаты сущностей.

    Без обёртки вызов $b24.actions.* идёт мимо защиты: ни повторов при сетевых сбоях, ни блокировки повтора для создающих методов.

  • B24Client — экспортируемый тип: B24OAuth плюс пять методов, которые пакет реализует сам. Объявлен как interface B24Client extends B24OAuth, поэтому $b24 по-прежнему принимается везде, где ожидается B24OAuth — например в new Smsgold(auth, $b24).

    import type { B24Client } from "@andrey4emk/npm-app-back-b24";
    
    function sync(client: B24Client) { /* ... */ }

Задачи ошибок

  • errorB24(dataTask) — создаёт служебную задачу в Bitrix24 при ошибках/событиях. Использует глобальный $b24.

    • Алгоритм:

      1. Сначала проверяет, сколько задач с таким TITLE уже создано (через tasks.task.list).
      2. Если их меньше чем maxTasks, создаёт новую задачу (tasks.task.add).
    • Параметры:

      • dataTask (ErrorTaskData) — объект с параметрами задачи (см. ниже).
    • Возвращает: промис с объектом:

      {
          error: false,
          message: "Задача создана в Битрикс24.",
          data: { /* API response */ }
      }

      или при превышении лимита:

      {
          error: false,
          message: "Превышено максимальное количество задач с таким названием (100). Новая задача не создана."
      }

      или при ошибке:

      {
          error: true,
          message: "Не удалось создать задачу в Битрикс24: <текст ошибки>"
      }

      или если $b24 не инициализирован:

      {
          error: true,
          message: "$b24 не инициализирован"
      }

    Параметры dataTask (ErrorTaskData):

    | Параметр | Тип | По умолчанию | Описание | | ---------------- | ---------------- | ------------------------- | ---------------------------------------------- | | title | string | обязательно | Заголовок задачи | | description | string | "" | Описание задачи | | createdBy | number | 138 | ID создателя | | responsibleId | number | 1 | ID ответственного | | deadline | ISO string | DateTime.now() + 1 день | Дедлайн в ISO формате | | groupId | number|null | null | ID группы (опционально) | | accomplices | number[] | [] | Массив ID соисполнителей | | maxTasks | number | 100 | Максимум существующих задач с таким названием | | ufCrmTask | string|string[] | "" | Значение для UF_CRM_TASK (массив или строка) | | entityTypeAbbr | string | "" | ~~Устаревший~~ Используйте ufCrmTask |

    await errorB24({
        title: "Ошибка синхронизации",
        description: "Не удалось подключиться к API",
        responsibleId: 42,
        deadline: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
        maxTasks: 50,
        ufCrmTask: ["D_5", "C_10"],
    });
  • checkB24Scope() — проверяет и логирует права (скоупы) приложения Bitrix24. Вызывается автоматически при старте, если $b24 инициализирован.

События

  • event — готовый экземпляр класса Event (или null, если $b24 не инициализирован). Создаётся автоматически при импорте.

    import { event } from "@andrey4emk/npm-app-back-b24";
    
    if (event) {
        const result = await event.get("ONCRMDYNAMICITEMUPDATE_149");
    }
  • Event — класс для работы с офлайн-событиями Bitrix24. Можно создать свой экземпляр, передав B24OAuth.

    import { Event } from "@andrey4emk/npm-app-back-b24";
    
    const myEvent = new Event(b24Instance);

    Методы:

    • get(eventName) — получить офлайн-события по названию.

      • Параметры:

        • eventName (string) — название события (например, 'ONCRMDYNAMICITEMUPDATE_149' или 'ONIMCONNECTORMESSAGEADD').
      • Возвращает: промис с объектом { error, message, data }.

        Для событий сущностей (сделки, смарт-процессы):

        {
            error: false,
            message: "События получены",
            data: {
                processId: "12345",
                entitysId: ["101", "102", "103"],
                arrMessageIdAndEntityId: [
                    { messageId: "msg_1", entityId: "101" },
                    { messageId: "msg_2", entityId: "102" }
                ]
            }
        }

        Для событий коннектора (ONIMCONNECTORMESSAGEADD):

        {
            error: false,
            message: "События коннектора получены",
            data: {
                processId: "12345",
                message: [
                    {
                        messageId: "7cf5d29bee90cc37382a9fff07fc2a43", // MESSAGE_ID записи очереди — ключ для clear()
                        eventId: "4821",                             // ID записи очереди, информационное; в clear() не передавать
                        connectorId: "...",
                        lineId: "...",
                        chatId: "...",
                        text: "Текст сообщения",
                        file: null,
                        attachments: null,
                        im: null
                    }
                ]
            }
        }

        или при ошибке:

        {
            error: true,
            message: "Не удалось получить события. <текст ошибки>",
            data: null
        }
      • Особенности:

        • Автоматически отбрасывает и очищает события пользователя 138 — владельца OAuth-токенов приложений. Это защита от петли: всё, что приложения сами пишут в B24, приходит событием с user_id: '138', и без фильтра код обрабатывал бы собственные записи повторно. Оборотная сторона: ручные правки под этой же учёткой (администратор портала) тоже не возвращаются. Для полной выгрузки всех изменений класс не подходит — там читать event.offline.get напрямую.
        • Для коннекторных событий конвертирует файлы в формат с типами image, video, document.
        • Если событий нет, возвращает data с пустыми массивами.
        • Гранулярность очистки — запись очереди, а не сообщение. Одна запись коннектора (один MESSAGE_ID) может нести несколько сообщений, и они приходят отдельными элементами message с одинаковым messageId. Очистка по этому ключу удаляет запись целиком: если из двух сообщений одной записи ушло одно, clear() заберёт и неотправленное. Строить поэлементную очистку с точностью до сообщения на messageId нельзя. Для дедупликации отдельных сообщений он не годится по той же причине — у сообщений одной записи он общий, для этого есть im.id. Обратная сторона: запись, из которой не разобрано ни одного сообщения, в data.message не попадёт вовсе, и очистка по собранным messageId её не заберёт. Безопасный режим — clear(processId) целиком.
        • Типы ConnectorMessage, ConnectorEvents и EntityEvents экспортируются из пакета.
      const { error, data } = await event.get("ONCRMDYNAMICITEMUPDATE_149");
      if (!error && data.entitysId.length > 0) {
          console.log("Обработанные ID сущностей:", data.entitysId);
      }
    • clear(processId, messageId?) — очистить обработанные офлайн-события в Bitrix24.

      • Параметры:

        • processId (string) — ID процесса (получен из get()).
        • messageId (string|string[]) — опциональный MESSAGE_ID или массив MESSAGE_ID записей очереди для очистки. Без него очищается весь пакет processId. Пустой массив означает «очищать нечего»: вызов до API не доходит и возвращает error: false (раньше уезжало message_id: []). «Мягкая» ошибка портала, например ERROR_ENTITY_NOT_FOUND на протухшем processId, возвращается как error: true.
      • Возвращает: промис с объектом { error, message, data }.

      const { data } = await event.get("ONCRMDYNAMICITEMUPDATE_149");
      if (data.processId) {
          const messageIds = data.arrMessageIdAndEntityId.map((item) => item.messageId);
          await event.clear(data.processId, messageIds);
      }

ChatApp

  • ChatApp — класс для работы с мессенджерами через платформу ChatApp Online. Экземпляр нужно создать самостоятельно.

    import { ChatApp } from "@andrey4emk/npm-app-back-b24";
    
    const chatApp = new ChatApp(
        // makeParam — данные для создания токена
        { email: "your-email", pass: "your-password", appId: "your-app-id" },
        // authParam — текущие токены (можно пустой объект, заполнится после makeTokenChatApp)
        { accessToken: "", accessTokenEndTime: "", refreshToken: "", refreshTokenEndTime: "" },
        // typeParam — конфигурация мессенджеров и лицензий
        {
            whatsApp: { licenseId: "12345", messenger: [{ type: "grWhatsApp" }] },
            telegram: { licenseId: "67890", messenger: [{ type: "telegram" }] },
        }
    );

    Конструктор сразу разрешает тип мессенджера для whatsApp и telegram — берёт messenger[0].type и держит в полях, вместо того чтобы индексировать массив при каждой отправке. Если конфига нет или список messenger пуст, тип остаётся неразрешённым, и методы этого мессенджера возвращают { error: true, message: "ChatApp: не разрешён тип мессенджера whatsApp (нет конфига или пуст messenger)", data: null } — вместо запроса на URL с undefined в пути. Конструктор при этом не бросает: экземпляр обычно создаётся на уровне модуля, и бросок уронил бы загрузку всего модуля отправки вместе с соседними каналами.

    Таймаут отправки — статус неизвестен. Запросы идут с бюджетом времени (10 секунд на работу с токеном, 20 на проверку номера, 30 на отправку сообщения, 60 на отправку файла). Если ответа не дождались, отправка могла состояться — методы возвращают { error: true, message: "ChatApp: таймаут ... — статус неизвестен", data: null }, а не бросают исключение. Повторять такую отправку нельзя: клиент получит второе сообщение. Пакет дополнительно пишет строку уровня error (она уходит в чат B24) — решение о повторе принимает человек. Раньше зависание долетало до вызывающего кода исключением через пять минут; теперь оно возвращается объектом через тридцать секунд, поэтому запись в лог обязательна: без неё отказ стал бы тише, чем был.

    Отказ локальный: пустой whatsApp не мешает telegram. Но конфиг всё равно нужен для обоих мессенджеров, даже если проект пользуется одним — иначе второй канал молча перестанет работать, и узнать об этом можно будет только по res.error в логе.

    Методы:

    • makeTokenChatApp() — получить токены доступа для ChatApp API.

      • Возвращает: промис с объектом { error } или { error, message, data } при ошибке.
      • Особенности: Сохраняет полученные токены в свойствах экземпляра.
      const result = await chatApp.makeTokenChatApp();
      if (!result.error) {
          console.log("Токены успешно получены");
      }
    • refreshTokenChatApp() — обновить токены доступа.

      • Возвращает: промис с объектом { error } или { error, message, data } при ошибке.
      • Алгоритм: Пытается обновить токены через API. Если не удалось — автоматически вызывает makeTokenChatApp().
    • checkTokenChatApp() — проверить валидность текущего токена.

      • Возвращает: промис с объектом { error, message }.
      • Особенности: Если токен невалидный, автоматически вызывает refreshTokenChatApp().
    • getLicensesChatApp() — получить список лицензий для текущего аккаунта.

      • Возвращает: промис с объектом { error, message, data }, где data — массив лицензий.
      const result = await chatApp.getLicensesChatApp();
      if (!result.error) {
          console.log("Доступные лицензии:", result.data);
      }
    • sendMessageChatApp(messengerType, messageData) — отправить текстовое сообщение.

      • Параметры:
        • messengerType (string) — "whatsApp" или "telegram".
        • messageData (object) — { phone: string, message: string }.
      • Возвращает: промис с объектом { error, message, data, auth }. Если в процессе обновился токен, auth содержит новые данные авторизации.
      const result = await chatApp.sendMessageChatApp("whatsApp", {
          phone: "+1234567890",
          message: "Это тестовое сообщение",
      });
    • sendFileChatApp(messengerType, messageData) — отправить файл.

      • Параметры:
        • messengerType (string) — "whatsApp" или "telegram".
        • messageData (object) — { phone, message, fileUrl, fileName }.
    • phoneCheckChatApp(messengerType, phone) — проверить, существует ли номер в мессенджере.

      • Параметры:
        • messengerType (string) — "whatsApp" или "telegram".
        • phone (string) — номер телефона.
      • Возвращает: промис с объектом { error, message, data }, где data.check — результат проверки.
      • Особенности: если API ответил success: true, но без объекта data, метод возвращает { error: true, message: "Ответ ChatApp без поля data ...", data } с сырым ответом. Раньше в этом случае наружу летел TypeError — метод не обёрнут в try/catch. Подставлять check: undefined было бы хуже: вызывающий принял бы непонятный ответ за «телефон не найден».

Smsgold

  • Smsgold — класс для отправки SMS через сервис SMSGold. Экземпляр нужно создать самостоятельно.

    import { Smsgold } from "@andrey4emk/npm-app-back-b24";
    
    const smsClient = new Smsgold(
        { user: "smsgold_login", pass: "smsgold_password" },
        b24Instance, // опционально — экземпляр B24OAuth для загрузки файлов на диск
        123456 // опционально — ID папки на диске B24 для вложений, по умолчанию 2792881
    );

    Третий параметр — папка, куда кладётся файл из fileUrl перед отправкой SMS. Дефолт 2792881 — папка «contract_company» портала repacheb.bitrix24.ru; на другом портале такого ID нет, и его нужно передать своим. Параметр нужен только при отправке файлов.

    Методы:

    • sendSms(messageData) — отправить SMS.

      • Параметры:
        • messageData (object):
          • phone (string) — номер телефона.
          • message (string) — текст сообщения.
          • fileUrl (string) — опционально, ссылка на файл (будет загружен на диск Bitrix24 и публичная ссылка добавлена в SMS).
          • fileName (string) — опционально, имя файла.
      • Возвращает: промис с объектом { error, message, result }, а при таймауте — ещё и unknownDelivery: true.
      const result = await smsClient.sendSms({
          phone: "+79990000000",
          message: "Готово!",
      });
      if (!result.error) {
          console.log(result.message);
      }
    • Таймаут и неизвестный статус доставки. Отправка идёт без повторов с бюджетом 30 секунд. Если SMSGold не ответил, запрос уже ушёл — сообщение могло быть доставлено, и повторять отправку нельзя. Такой результат отличается от обычной ошибки: unknownDelivery: true и текст «SMSGold не ответил за 30с — статус отправки неизвестен». Дополнительно пакет пишет строку уровня error (она уходит в чат B24), потому что решение о повторе должен принимать человек, а не код. Это единственное место, где Smsgold пишет в лог сам.

      const result = await smsClient.sendSms({ phone, message });
      if (result.unknownDelivery) {
          // Не отправлять повторно: возможен дубль у клиента. Ставим задачу на ручную проверку
      } else if (result.error) {
          // Обычная ошибка — отправка не состоялась, повтор безопасен
      }
    • Скачивание файла по fileUrl идёт через fetchRetry с 3 попытками и бюджетом 60 секунд на попытку (раньше было 5 попыток без таймаута): пять попыток по минуте задерживали бы саму отправку SMS до пяти минут.

Email

  • Email — класс для отправки email через SMTP Яндекса. Экземпляр нужно создать самостоятельно.

    import { Email } from "@andrey4emk/npm-app-back-b24";
    
    const emailClient = new Email({
        user: "[email protected]",
        pass: "your-app-password",
    });

    Методы:

    • send(dataMail) — отправить письмо.

      • Параметры:
        • dataMail (object):
          • to (string) — адрес получателя.
          • subject (string) — тема письма.
          • text (string) — текстовая версия.
          • html (string) — опционально, HTML-версия.
          • fileUrl (string) — опционально, ссылка на файл (будет скачан и прикреплён).
          • fileName (string) — опционально, имя файла для вложения.
      • Возвращает: промис с объектом { error, info } при успехе или { error, message } при ошибке.
      const result = await emailClient.send({
          to: "[email protected]",
          subject: "Тема письма",
          text: "Текст письма",
          html: "<b>HTML текст</b>",
      });
    • Особенности:

      • SMTP-сервер: smtp.yandex.ru:465 (SMTPS).
      • Копия письма автоматически отправляется на адрес отправителя.
      • Транспорт — nodemailer ^9 (диапазон, а не точная версия: иначе патч безопасности нельзя было бы применить на стороне потребителя).
      • Вложения передаются готовым Buffer — файл по fileUrl пакет скачивает сам. Поля path и href намеренно не поддерживаются: nodemailer не должен ходить по URL. Скачивание идёт через fetchWithTimeout с бюджетом 60 секунд.
      • Таймауты SMTP-транспорта переопределены: соединение 15 с, приветствие 10 с, сокет 120 с. Дефолты nodemailer (2 мин / 30 с / 10 мин) слишком щедры — письмо на Яндекс уходит за секунды, а 10 минут молчания сокета блокируют вызывающую очередь.

Wappi

  • Wappi — класс для отправки сообщений через Wappi API (WhatsApp, Telegram, Max). Экземпляр нужно создать самостоятельно.

    Таймаут отправки — статус неизвестен. Как и у ChatApp: бюджет 20 секунд на проверку номера, 30 на сообщение, 60 на файл. При таймауте отправки метод возвращает { error: true, message: "Wappi: таймаут ... — статус неизвестен", data: null } и пишет строку уровня error в чат B24 — сообщение могло уйти, повторять нельзя. Отдельно про Telegram: phoneCheckWappi при таймауте проверки контакта не создаёт контакт, а возвращает ошибку. Иначе не дождавшись ответа мы бы завели контакт на номер, который мог уже быть в адресной книге, и объявили номер проверенным без проверки.

    import { Wappi } from "@andrey4emk/npm-app-back-b24";
    
    const wappi = new Wappi(
        { token: "max-token", profile_id: "max-profile" },
        { token: "telegram-token", profile_id: "telegram-profile" },
        { token: "whatsapp-token", profile_id: "whatsapp-profile" }
    );

    Методы:

    • sendMessageWappi(messengerType, messageData) — отправить текстовое сообщение.

      • Параметры:
        • messengerType (string) — "whatsApp", "telegram" или "max".
        • messageData (object):
          • phone (string) — номер телефона.
          • message (string) — текст сообщения.
          • sendOpenLine (boolean) — опционально, отображать ли в открытой линии Битрикс24 (по умолчанию true).
      • Возвращает: промис с объектом { error, message, data }.
      const result = await wappi.sendMessageWappi("whatsApp", {
          phone: "+1234567890",
          message: "Привет из Wappi!",
      });
    • sendFileWappi(messengerType, messageData) — отправить файл.

      • Параметры:
        • messengerType (string) — "whatsApp", "telegram" или "max".
        • messageData (object):
          • phone (string) — номер телефона.
          • message (string) — подпись к файлу.
          • fileUrl (string) — ссылка на файл.
          • fileName (string) — имя файла.
          • sendOpenLine (boolean) — опционально.
      • Особенности: Для WhatsApp и Telegram файл конвертируется в base64. Для Max отправляется по URL.
    • phoneCheckWappi(messengerType, phone) — проверить, существует ли номер в мессенджере.

      • Параметры:
        • messengerType (string) — "whatsApp", "telegram" или "max".
        • phone (string) — номер телефона.
      • Возвращает: промис с объектом { error, message, data }, где data.checktrue/false.
      • Особенности: Для Telegram сначала проверяет контакт, при отсутствии — пытается создать.

Логирование

  • logs — экземпляр класса LogsAPI для цветного логирования в консоль.

    import { logs } from "@andrey4emk/npm-app-back-b24";
    
    logs.add("Информационное сообщение", "info");
    logs.add("Отладочное сообщение", "debug");
    logs.add("Ошибка!", "error");
    logs.add({ key: "value" }); // объект будет JSON.stringify
    logs.add("С доп. данными", "info", { detail: 123 }); // jsonData выведется через console.dir

    Уровни логирования:

    | Уровень | Цвет | По умолчанию | | ------- | ------- | ------------ | | debug | Серый | Выключен | | info | Зелёный | Включён | | error | Красный | Включён |

    • Конфигурация хранится в файле log.json (через conf).
    • Каждый уровень можно включить/выключить, изменить цвет.

fetchRetry и таймауты

  • fetchRetry(url, options?, retries?, delay?, timeoutMs?) — обёртка над fetch с повторными попытками при сетевых ошибках. HTTP-ошибки (4xx, 5xx) не вызывают повторных попыток — повторяются только сетевые сбои (TypeError, ECONNRESET, ETIMEDOUT и т.д.) и собственный таймаут запроса.

  • maskUrl(url) — маскирует секреты в адресе перед записью в лог. URL входящего вебхука B24 содержит секрет прямо в пути (/rest/1/<секрет>/method.json), а токен авторизации может приходить в query-параметрах (auth, access_token, refresh_token, token). Хост и имя метода сохраняются, тело секрета заменяется на ***. Используется внутри fetchRetry; применяйте в своём коде везде, где логируете адреса запросов к B24.

    import { maskUrl } from "@andrey4emk/npm-app-back-b24";
    
    logs.add(`Запрос не прошёл — ${maskUrl(url)}`, "warn");
    // https://portal.bitrix24.ru/rest/1/***/crm.deal.get.json
  • isPreConnectionError(error) — строгая проверка: соединение не состоялось, значит запрос точно не был выполнен сервером (ENOTFOUND, ECONNREFUSED, EAI_AGAIN, ENETUNREACH; код ищется и во вложенных originalError / cause). Отличие от isNetworkError() в том, что таймаут и обрыв соединения тоже сетевые, но при них ответ мог потеряться уже после обработки запроса. Используйте эту проверку там, где повтор небезопасен сам по себе: создание сущностей, отправка сообщений, обмен токена.

    import { isPreConnectionError } from "@andrey4emk/npm-app-back-b24";
    
    try {
        await sendSms(phone, text);
    } catch (error) {
        // Повторяем только если сообщение заведомо не ушло
        if (isPreConnectionError(error)) await sendSms(phone, text);
        else throw error;
    }
  • isNetworkError(error) — проверяет, является ли ошибка сетевой (стоит повторить запрос). Используется внутри fetchRetry и retry-обёртки $b24. Можно использовать в своём коде для аналогичных проверок.

    import { isNetworkError } from "@andrey4emk/npm-app-back-b24";
    
    try {
        await someRequest();
    } catch (error) {
        if (isNetworkError(error)) {
            // сетевая ошибка — можно повторить
        }
    }

    Параметры:

    | Параметр | Тип | По умолчанию | Описание | | --------- | ---------------------------- | --------------- | ----------------------------------------------- | | url | string | URL | Request | обязательно | Адрес запроса | | options | RequestInit | undefined | Параметры fetch (метод, заголовки, body и т.д.) | | retries | number | 5 | Количество попыток | | delay | number | 500 | Задержка между попытками (мс) | | timeoutMs | number | 60000 (или FETCH_TIMEOUT_MS) | Бюджет на одну попытку; 0 — без таймаута |

    • Возвращает: Promise<Response> — ответ от fetch.

    • Обрабатываемые сетевые ошибки: ECONNRESET, ECONNREFUSED, ETIMEDOUT, ENOTFOUND, ENETUNREACH, EAI_AGAIN, UND_ERR_CONNECT_TIMEOUT, UND_ERR_SOCKET, TypeError, а также ошибки с сообщениями fetch failed, network, socket.

    • Логирование: при каждой неудачной попытке записывает сообщение через logs.add() с уровнем warn. Адрес запроса перед записью пропускается через maskUrl(), поэтому секрет вебхука в лог не попадает.

    import { fetchRetry } from "@andrey4emk/npm-app-back-b24";
    
    // Простой GET-запрос с дефолтными параметрами (5 попыток, 500мс)
    const response = await fetchRetry("https://api.example.com/data");
    const json = await response.json();
    
    // POST-запрос с кастомными параметрами
    const res = await fetchRetry(
        "https://api.example.com/webhook",
        {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ key: "value" }),
        },
        3,   // 3 попытки
        1000 // 1 секунда между попытками
    );

Таймауты

С версии 3.8.0 каждый запрос пакета имеет бюджет времени: зависшее соединение больше не держится до таймаутов undici (5 минут на попытку, а при медленной «капле» данных — бесконечно).

  • Бюджет считается на попытку, а не на весь вызов. Каждой попытке fetchRetry выдаётся свой AbortSignal.timeout(timeoutMs). Бюджет покрывает и чтение тела ответа: сигнал живёт до конца запроса, поэтому res.text() / res.arrayBuffer() могут упасть с TimeoutError уже после возврата из fetchRetry. Для крупных файлов передавайте больший timeoutMs или 0.

  • Худший случай по времениretries × (timeoutMs + delay). При дефолтах это 5 × (60 000 + 500) ≈ 5 минут.

  • Свой options.signal отключает наш дефолт. Если вызывающий передал сигнал именно в options, а timeoutMs не задан, пакет своего таймаута не добавляет — чужой осознанный бюджет не обрезается. Явно переданный timeoutMs применяется всегда, тогда побеждает тот сигнал, который сработал раньше. Отмена по чужому сигналу никогда не повторяется; различаем её по объекту сигнала, а не по типу ошибки: причиной отмены может быть любой объект, включая TypeError.

  • Сигнал у объекта Request на дефолт не влияет. Он участвует в отмене — сигналы склеиваются через AbortSignal.any, иначе fetch(request, init) затёр бы его, — но признаком «вызывающий сам управляет временем» не считается: у любого Request сигнал есть всегда, даже когда его никто не задавал. Учитывать его значило бы молча снять таймаут со всех вызовов такой формы. Нужен свой бюджет при вызове с Request — передавайте timeoutMs пятым аргументом.

  • Таймаут при чтении тела не повторяется и не логируется. Бюджет действует до конца запроса, поэтому res.text() / res.arrayBuffer() после возврата из fetchRetry могут упасть с TimeoutError уже у вас. Функция об этом не знает: повторов там нет, записи в лог тоже. Читаете большое тело — задавайте timeoutMs с запасом на скачивание.

  • Переменная FETCH_TIMEOUT_MS меняет дефолт без правки кода (читается один раз при загрузке модуля), FETCH_TIMEOUT_MS=0 выключает таймаут. Это аварийный рычаг на случай, когда дефолт обрывает живой долгий запрос — отчёт Seatable, Google Sheets, МойСклад. Действует только на вызовы без явного timeoutMs: внутренние вызовы пакета (ChatApp, Wappi, SMS, скачивание файлов) передают свои значения из FETCH_TIMEOUTS и переменную игнорируют. Значение должно быть целым от 0 до 2 147 483 647 — непригодное отбрасывается с записью в лог, а не выключает защиту молча.

  • Ограничение при вызове с объектом Request: тело такого запроса читается первой попыткой, и повтор упадёт с body used already. Существовало и до таймаутов; для повторяемых запросов передавайте строку URL и options.

  • fetchWithTimeout(url, options?, timeoutMs?) — один запрос с бюджетом времени, без повторов и без логирования. Для неидемпотентных операций: отправка сообщения, создание сущности — там, где повтор мог бы продублировать действие, но защита от зависшего соединения нужна. Правила про чужой сигнал те же, что у fetchRetry. Внутри пакета через неё идут все отправки ChatApp, Wappi и SMSGold.

    import { fetchWithTimeout, FETCH_TIMEOUTS } from "@andrey4emk/npm-app-back-b24/fetchRetry";
    
    const res = await fetchWithTimeout(
        "https://api.example.com/orders",
        { method: "POST", body: JSON.stringify(order) },
        FETCH_TIMEOUTS.send // 30 000 мс
    );
  • FETCH_TIMEOUTS — именованные бюджеты, которыми пакет пользуется сам: quick 10 000 (токены), api 20 000 (короткое чтение), send 30 000 (отправка), transfer 60 000 (файлы). DEFAULT_FETCH_TIMEOUT_MS — действующий дефолт fetchRetry (60 000 либо значение FETCH_TIMEOUT_MS).

  • isTimeoutError(error) / isAbortError(error) — различают «не дождались» и «отменили». Ошибка таймаута не оборачивается в свою: наружу идёт исходный DOMException с name: "TimeoutError" (отмена без причины — "AbortError"), проверки читают именно name.

    import { fetchRetry, isTimeoutError, isAbortError } from "@andrey4emk/npm-app-back-b24/fetchRetry";
    
    try {
        await fetchRetry(url, { signal: ac.signal }, 3, 500, 20_000);
    } catch (error) {
        if (isTimeoutError(error)) logs.add("не дождались ответа", "warn");
        else if (isAbortError(error)) logs.add("запрос отменили", "debug");
        else throw error;
    }

Переменные окружения

| Переменная | Назначение | | --------------------------- | ------------------------------------------------------------- | | APP_B24_DOMEN | Домен Bitrix24, для которого хранится запись авторизации в БД | | APP_B24_CLIENT_ID | Client ID приложения Bitrix24 | | APP_B24_CLIENT_SECRET | Client Secret приложения Bitrix24 | | APP_ENV | Определяет окружение, используется как ключ секции авторизации | | APP_NAME | Название приложения (используется в описании задач) | | CONFIG_DIR | Путь к директории с конфигами (по умолчанию ../config) | | FETCH_TIMEOUT_MS | Бюджет одной попытки fetchRetry/fetchWithTimeout, мс (по умолчанию 60000; 0 — без таймаута) | | CHATAPP_EMAIL | Email аккаунта ChatApp | | CHATAPP_PASS | Пароль аккаунта ChatApp | | CHATAPP_APP_ID | ID приложения в ChatApp |

Пример .env:

APP_B24_DOMEN=mycompany.bitrix24.ru
APP_ENV=DEV
APP_B24_CLIENT_ID=xxx
APP_B24_CLIENT_SECRET=yyy
APP_NAME=MyApp
CONFIG_DIR=../config
FETCH_TIMEOUT_MS=60000

[email protected]
CHATAPP_PASS=your-password
CHATAPP_APP_ID=your-app-id

Скрипты

  • npm run test — запуск тестов (заглушка).
  • npm run pack:dry — предпросмотр содержимого пакета перед публикацией.

Лицензия

MIT © 2025 andrey4emk