@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не совпал с полем ответа. Порог нужен: на уровнеinfoSDK пишет две строки на каждый запрос. Ошибки 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 минут), что предотвращает протухание токена.
- При обновлении токенов SDK автоматически сохраняет их в файл через callback
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.Алгоритм:
- Сначала проверяет, сколько задач с таким
TITLEуже создано (черезtasks.task.list). - Если их меньше чем
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экспортируются из пакета.
- Автоматически отбрасывает и очищает события пользователя 138 — владельца OAuth-токенов приложений. Это защита от петли: всё, что приложения сами пишут в B24, приходит событием с
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 через 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 минут молчания сокета блокируют вызывающую очередь.
- SMTP-сервер:
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.check—true/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.jsonisPreConnectionError(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— именованные бюджеты, которыми пакет пользуется сам:quick10 000 (токены),api20 000 (короткое чтение),send30 000 (отправка),transfer60 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
