@andrey4emk/npm-app-back-b24 3.3.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,6 +26,24 @@
26
26
  npm install @andrey4emk/npm-app-back-b24
27
27
  ```
28
28
 
29
+ ### Точки входа
30
+
31
+ | Импорт | Что отдаёт | Поднимает OAuth-модуль |
32
+ | --------------------------------------------- | ----------------------------- | ---------------------- |
33
+ | `@andrey4emk/npm-app-back-b24` | всё | да |
34
+ | `@andrey4emk/npm-app-back-b24/logs` | `logs` | нет |
35
+ | `@andrey4emk/npm-app-back-b24/fetchRetry` | `fetchRetry`, `isNetworkError`, `maskUrl` | нет |
36
+
37
+ Корневой импорт — barrel: он тянет модуль OAuth, который при загрузке читает `authB24.json` и, если приложение авторизовано, запускает таймер проактивного обновления токена. Проектам, которые ходят в B24 по **входящему вебхуку** или берут из пакета только логгер и `fetchRetry`, удобнее импортировать по подпутям — тогда OAuth-модуль не загружается вообще.
38
+
39
+ ```js
40
+ // Проект без OAuth: ничего лишнего не поднимается
41
+ import { logs } from "@andrey4emk/npm-app-back-b24/logs";
42
+ import { fetchRetry, maskUrl } from "@andrey4emk/npm-app-back-b24/fetchRetry";
43
+ ```
44
+
45
+ Корневой импорт при этом остаётся безопасным: если `APP_B24_CLIENT_ID` и `APP_B24_CLIENT_SECRET` не заданы, пакет считает, что OAuth в проекте не используется, молча выставляет `$b24 = null` и пишет об этом только в `debug`.
46
+
29
47
  ## Использование
30
48
 
31
49
  ```js
@@ -35,6 +53,7 @@ import {
35
53
  refreshAndSaveTokens,
36
54
  reinitializeB24,
37
55
  stopProactiveRefresh,
56
+ getResultData,
38
57
  errorB24,
39
58
  event,
40
59
  Event,
@@ -45,6 +64,7 @@ import {
45
64
  logs,
46
65
  fetchRetry,
47
66
  isNetworkError,
67
+ maskUrl,
48
68
  } from "@andrey4emk/npm-app-back-b24";
49
69
 
50
70
  // $b24 — готовый экземпляр B24OAuth (или null, если токены не настроены)
@@ -115,17 +135,24 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
115
135
  **Особенности:**
116
136
  - При обновлении токенов SDK автоматически сохраняет их в файл через callback `setCallbackRefreshAuth`.
117
137
  - Окружение (`DEV`/`PROD`) определяется переменной `APP_ENV` — используется как ключ секции в конфиге авторизации.
138
+ - Признак того, что проект использует OAuth, — заданные `APP_B24_CLIENT_ID` и `APP_B24_CLIENT_SECRET`. Если их нет, `$b24` молча становится `null` (уровень `debug`). Если они заданы, но токенов в `authB24.json` не хватает, пишется `error` — это уже настоящая проблема конфигурации.
118
139
 
119
140
  **Retry при сетевых ошибках:**
120
141
 
121
- Экземпляр `$b24` обёрнут Proxy, который автоматически повторяет запросы при сетевых ошибках. Повторные попытки применяются к методам `callMethod`, `callListMethod`, `callBatch` и `fetchListMethod`, а также к обновлению токена (`refreshAuth`).
142
+ Экземпляр `$b24` обёрнут Proxy, который автоматически повторяет запросы при сетевых ошибках. Повторные попытки применяются к методам `callMethod`, `callListMethod`, `callBatch`, а также к обновлению токена (`refreshAuth`). Метод `fetchListMethod` из ретраев исключён — это async-генератор, оборачивать его в async-функцию нельзя; постраничные запросы SDK ретраит сам.
122
143
 
123
144
  | Параметр | Значение |
124
145
  | -------------- | ----------------------------------------- |
125
146
  | Попыток | 5 |
126
- | Задержка | 500 мс (экспоненциально для refreshAuth) |
147
+ | Задержка | 500 мс (линейно растущая для refreshAuth) |
148
+
149
+ Retry срабатывает только при сетевых проблемах (`ECONNRESET`, `ETIMEDOUT`, `ERR_NETWORK` и т.д.). HTTP-ошибки (400, 500) и ошибки бизнес-логики Bitrix24 **не** вызывают повторных попыток. Каждая неудачная попытка логируется через `logs.add()` с уровнем `error` — в сообщение попадает код ошибки SDK.
150
+
151
+ У самого SDK есть собственный слой ретраев (`maxRetries: 3`), но он работает только при rate limit (HTTP 429, 503) и при распознанных `NETWORK_ERROR` / `REQUEST_TIMEOUT`. Замаскированные транспортные сбои приходят с кодом `JSSDK_UNKNOWN_ERROR`, который входит в `BUILT_IN_HARD_ERROR_CODES` — на таких ошибках SDK делает одну попытку, и повторяет только Proxy.
127
152
 
128
- Retry срабатывает только при сетевых проблемах (`ECONNRESET`, `ETIMEDOUT`, `ERR_NETWORK` и т.д.). HTTP-ошибки (400, 500) и ошибки бизнес-логики Bitrix24 **не** вызывают повторных попыток. Каждая неудачная попытка логируется через `logs.add()` с уровнем `error`.
153
+ **«Мягкие» ошибки Bitrix24:**
154
+
155
+ Часть ошибок (`ERROR_ENTITY_NOT_FOUND`, `BITRIX_REST_V3_EXCEPTION_*`) SDK не бросает исключением, а возвращает как `AjaxResult` с ошибкой: `isSuccess === false`, и тогда `response.getData()` вернёт `undefined`. Отдельный случай — успешный ответ, у которого сам `result` пустой (`null`/`undefined`). Чтобы не падать на `undefined`, разбирайте ответы через `getResultData()` — он покрывает обе ситуации и бросает понятную ошибку с именем метода.
129
156
 
130
157
  **Проактивное обновление токена:**
131
158
 
@@ -193,6 +220,20 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
193
220
  });
194
221
  ```
195
222
 
223
+ - **`getResultData(response, methodName)`** — достаёт `result` из ответа SDK. Если Bitrix24 вернул «мягкую» ошибку (`AjaxResult` с ошибкой вместо исключения) или пустой ответ, бросает `Error` с понятным текстом вместо падения на `undefined`.
224
+
225
+ - **Параметры:**
226
+ - `response` (AjaxResult) — результат `$b24.callMethod()`.
227
+ - `methodName` (string) — имя метода B24, попадёт в текст ошибки.
228
+ - **Возвращает:** содержимое `result`. Тип задаётся дженериком.
229
+
230
+ ```js
231
+ import { $b24, getResultData } from "@andrey4emk/npm-app-back-b24";
232
+
233
+ const response = await $b24.callMethod("crm.deal.get", { id: 123 });
234
+ const deal = getResultData(response, "crm.deal.get");
235
+ ```
236
+
196
237
  ### Задачи ошибок
197
238
 
198
239
  - **`errorB24(dataTask)`** — создаёт служебную задачу в Bitrix24 при ошибках/событиях. Использует глобальный `$b24`.
@@ -612,6 +653,15 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
612
653
 
613
654
  - **`fetchRetry(url, options?, retries?, delay?)`** — обёртка над `fetch` с повторными попытками при сетевых ошибках. HTTP-ошибки (4xx, 5xx) **не** вызывают повторных попыток — повторяются только сетевые сбои (TypeError, ECONNRESET, ETIMEDOUT и т.д.).
614
655
 
656
+ - **`maskUrl(url)`** — маскирует секреты в адресе перед записью в лог. URL входящего вебхука B24 содержит секрет прямо в пути (`/rest/1/<секрет>/method.json`), а токен авторизации может приходить в query-параметрах (`auth`, `access_token`, `refresh_token`, `token`). Хост и имя метода сохраняются, тело секрета заменяется на `***`. Используется внутри `fetchRetry`; применяйте в своём коде везде, где логируете адреса запросов к B24.
657
+
658
+ ```js
659
+ import { maskUrl } from "@andrey4emk/npm-app-back-b24";
660
+
661
+ logs.add(`Запрос не прошёл — ${maskUrl(url)}`, "warn");
662
+ // https://portal.bitrix24.ru/rest/1/***/crm.deal.get.json
663
+ ```
664
+
615
665
  - **`isNetworkError(error)`** — проверяет, является ли ошибка сетевой (стоит повторить запрос). Используется внутри `fetchRetry` и retry-обёртки `$b24`. Можно использовать в своём коде для аналогичных проверок.
616
666
 
617
667
  ```js
@@ -640,7 +690,7 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
640
690
  - **Обрабатываемые сетевые ошибки:**
641
691
  `ECONNRESET`, `ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND`, `ENETUNREACH`, `EAI_AGAIN`, `UND_ERR_CONNECT_TIMEOUT`, `UND_ERR_SOCKET`, `TypeError`, а также ошибки с сообщениями `fetch failed`, `network`, `socket`.
642
692
 
643
- - **Логирование:** при каждой неудачной попытке записывает сообщение через `logs.add()` с уровнем `error`.
693
+ - **Логирование:** при каждой неудачной попытке записывает сообщение через `logs.add()` с уровнем `warn`. Адрес запроса перед записью пропускается через `maskUrl()`, поэтому секрет вебхука в лог не попадает.
644
694
 
645
695
  ```js
646
696
  import { fetchRetry } from "@andrey4emk/npm-app-back-b24";
package/bitrix24/b24.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { B24OAuth, AjaxError } from "@bitrix24/b24jssdk";
2
- import type { B24OAuthParams, B24OAuthSecret, AuthData } from "@bitrix24/b24jssdk";
1
+ import { B24OAuth, AjaxError, SdkError, Logger } from "@bitrix24/b24jssdk";
2
+ import type { B24OAuthParams, B24OAuthSecret, AuthData, AjaxResult } from "@bitrix24/b24jssdk";
3
3
  import { logs } from "../logs/logs.ts";
4
4
  import { isNetworkError } from "../utils/fetchRetry.ts";
5
5
  import Conf from "conf";
@@ -34,12 +34,15 @@ const RETRY_COUNT = 5;
34
34
  /** Задержка между попытками (мс) */
35
35
  const RETRY_DELAY_MS = 500;
36
36
 
37
- /** Методы B24OAuth, оборачиваемые retry-логикой */
38
- const RETRYABLE_METHODS = new Set(["callMethod", "callListMethod", "callBatch", "fetchListMethod"]);
37
+ /** Методы B24OAuth, оборачиваемые retry-логикой (fetchListMethod — async-генератор, его оборачивать нельзя) */
38
+ const RETRYABLE_METHODS = new Set(["callMethod", "callListMethod", "callBatch"]);
39
39
 
40
40
  /** Коды AxiosError, означающие проблему с сетью */
41
41
  const AXIOS_NETWORK_CODES = new Set(["ERR_NETWORK", "ECONNABORTED"]);
42
42
 
43
+ /** Коды ошибок SDK (AjaxError / RefreshTokenError), означающие транспортный сбой */
44
+ const SDK_NETWORK_CODES = new Set(["NETWORK_ERROR", "REQUEST_TIMEOUT", "ERR_NETWORK", "ECONNABORTED"]);
45
+
43
46
  const confAuthB24 = new Conf({
44
47
  cwd: path.resolve(CONFIG_DIR),
45
48
  configName: "authB24",
@@ -55,14 +58,40 @@ function cleanDomain(domain: string): string {
55
58
  /** Задержка на указанное количество миллисекунд */
56
59
  const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
57
60
 
61
+ /**
62
+ * Достаёт result из ответа SDK.
63
+ *
64
+ * SDK при «мягких» ошибках (ERROR_ENTITY_NOT_FOUND, BITRIX_REST_V3_EXCEPTION_*)
65
+ * не бросает исключение, а возвращает AjaxResult с ошибкой.
66
+ * Функция превращает такой ответ в понятную ошибку вместо падения на undefined.
67
+ * Также отсекает случай, когда запрос успешен, но result пустой (null/undefined).
68
+ *
69
+ * @param response — результат $b24.callMethod()
70
+ * @param methodName — имя метода B24, попадёт в текст ошибки
71
+ */
72
+ export function getResultData<T = any>(response: AjaxResult, methodName: string): T {
73
+ if (!response.isSuccess) {
74
+ const messages = response.getErrorMessages().join("; ") || "неизвестная ошибка";
75
+ throw new Error(`${methodName}: Bitrix24 вернул ошибку — ${messages}`);
76
+ }
77
+
78
+ const data = response.getData();
79
+ if (data?.result == null) {
80
+ throw new Error(`${methodName}: Bitrix24 вернул пустой ответ`);
81
+ }
82
+
83
+ return data.result as T;
84
+ }
85
+
58
86
  // ==================== Создание экземпляра B24OAuth ====================
59
87
 
60
88
  function createB24Instance(): B24OAuth | null {
61
- const store = confAuthB24.store as Record<string, AuthData>;
62
- const authConfig = store[APP_ENV];
63
-
64
- if (!authConfig?.domain || !authConfig?.access_token || !authConfig?.refresh_token) {
65
- logs.add( конфиге authB24 не хватает данных для авторизации. Сохрани токены через клиент и перезагрузи докер", "error");
89
+ // Проект вообще не использует OAuth: ходит в B24 по входящему вебхуку либо берёт
90
+ // из пакета только logs/fetchRetry. Отсутствие авторизации для него — норма, а не
91
+ // ошибка, поэтому уровень debug: иначе ложная строка попадает в мониторинг.
92
+ // Признак «OAuth задуман» наличие credentials приложения в .env.
93
+ if (!CLIENT_ID && !CLIENT_SECRET) {
94
+ logs.add("OAuth Bitrix24 не настроен (APP_B24_CLIENT_ID и APP_B24_CLIENT_SECRET не заданы), $b24 = null", "debug");
66
95
  return null;
67
96
  }
68
97
 
@@ -71,6 +100,14 @@ function createB24Instance(): B24OAuth | null {
71
100
  return null;
72
101
  }
73
102
 
103
+ const store = confAuthB24.store as Record<string, AuthData>;
104
+ const authConfig = store[APP_ENV];
105
+
106
+ if (!authConfig?.domain || !authConfig?.access_token || !authConfig?.refresh_token) {
107
+ logs.add("В конфиге authB24 не хватает данных для авторизации. Сохрани токены через клиент и перезагрузи докер", "error");
108
+ return null;
109
+ }
110
+
74
111
  const domain = cleanDomain(authConfig.domain);
75
112
 
76
113
  const authParams: B24OAuthParams = {
@@ -91,7 +128,14 @@ function createB24Instance(): B24OAuth | null {
91
128
 
92
129
  const secret: B24OAuthSecret = { clientId: CLIENT_ID, clientSecret: CLIENT_SECRET };
93
130
 
94
- return new B24OAuth(authParams, secret);
131
+ const b24 = new B24OAuth(authParams, secret);
132
+
133
+ // Логгер без хендлеров: SDK 2.x на каждый вызов callMethod/callBatch пишет
134
+ // предупреждение об устаревании напрямую в console.warn, если логгер — NullLogger.
135
+ // Любой другой логгер получает запись через свой интерфейс, поэтому консоль остаётся чистой.
136
+ b24.setLogger(Logger.create("npm-app-back-b24"));
137
+
138
+ return b24;
95
139
  }
96
140
 
97
141
  // ==================== Работа с токенами ====================
@@ -188,8 +232,9 @@ async function refreshAuthWithMutex(): Promise<AuthData> {
188
232
  lastError = error;
189
233
  if (!isB24NetworkError(error) || attempt === RETRY_COUNT) throw error;
190
234
  const msg = error instanceof Error ? error.message : String(error);
235
+ const code = error instanceof SdkError ? ` [${error.code}]` : "";
191
236
  const delayMs = RETRY_DELAY_MS * attempt;
192
- logs.add(`refreshAuth: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${delayMs}мс`, "error");
237
+ logs.add(`refreshAuth${code}: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${delayMs}мс`, "error");
193
238
  await delay(delayMs);
194
239
  }
195
240
  }
@@ -261,15 +306,23 @@ export function stopProactiveRefresh(): void {
261
306
 
262
307
  /**
263
308
  * Проверяет, является ли ошибка из SDK сетевой.
264
- * AjaxError хранит AxiosError в поле originalError.
309
+ *
310
+ * SDK 2.x маскирует транспортные сбои: в логирующей ветке падает собственный
311
+ * TypeError, и наружу приходит AjaxError с кодом JSSDK_UNKNOWN_ERROR и status 0.
312
+ * Поэтому проверяем и код, и originalError, и status.
313
+ * RefreshTokenError приходит как SdkError с кодом вроде ENOTFOUND и без originalError.
265
314
  */
266
315
  function isB24NetworkError(error: unknown): boolean {
267
316
  if (error instanceof AjaxError) {
268
- // SDK уже сам retry'ил и исчерпал все попытки — не повторять
317
+ // Практически недостижимо (SDK бросает реальную ошибку раньше),
318
+ // но если код всё же пришёл — SDK уже исчерпал свои попытки
269
319
  if (error.code === "JSSDK_CALL_ALL_ATTEMPTS_EXHAUSTED") return false;
320
+ }
270
321
 
271
- const { originalError, status } = error;
322
+ if (error instanceof SdkError) {
323
+ const { code, originalError, status } = error;
272
324
 
325
+ if (SDK_NETWORK_CODES.has(code)) return true;
273
326
  if (originalError && isNetworkError(originalError)) return true;
274
327
  if (AXIOS_NETWORK_CODES.has((originalError as any)?.code)) return true;
275
328
  if (status === 0) return true;
@@ -294,7 +347,8 @@ function withRetry<T extends (...args: any[]) => Promise<any>>(fn: T, context: a
294
347
  }
295
348
 
296
349
  const msg = error instanceof Error ? error.message : String(error);
297
- logs.add(`$b24.${methodName}: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${RETRY_DELAY_MS}мс`, "error");
350
+ const code = error instanceof SdkError ? ` [${error.code}]` : "";
351
+ logs.add(`$b24.${methodName}${code}: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${RETRY_DELAY_MS}мс`, "error");
298
352
 
299
353
  await delay(RETRY_DELAY_MS);
300
354
  }
@@ -306,32 +360,36 @@ function withRetry<T extends (...args: any[]) => Promise<any>>(fn: T, context: a
306
360
  }
307
361
 
308
362
  /**
309
- * Proxy-обёртка вокруг B24OAuth: автоматически оборачивает
310
- * callMethod, callListMethod, callBatch и fetchListMethod retry-логикой.
311
- * Обёрнутые методы кешируются обёртка создаётся один раз на метод.
363
+ * Proxy-обёртка вокруг B24OAuth.
364
+ *
365
+ * Методы из RETRYABLE_METHODS оборачиваются retry-логикой, все остальные
366
+ * привязываются к оригинальному объекту через bind: класс B24OAuth использует
367
+ * приватные поля (#authOAuthManager), и при вызове метода с this === Proxy
368
+ * движок бросает "Cannot read private member".
369
+ *
370
+ * Обёртки кешируются — по одной на метод, чтобы не ломать сравнение по ссылке.
312
371
  */
313
372
  function wrapB24WithRetry(b24: B24OAuth): B24OAuth {
314
373
  const methodCache = new Map<string, Function>();
315
374
 
316
375
  return new Proxy(b24, {
317
- get(target, prop, receiver) {
318
- // Передаём target вместо receiver this внутри геттеров должен указывать
319
- // на оригинальный target, иначе приватные поля (#authOAuthManager и др.)
320
- // недоступны через Proxy
321
-
322
- // Для методов из RETRYABLE_METHODS оборачиваем retry-логикой
323
- if (typeof prop === "string" && RETRYABLE_METHODS.has(prop)) {
324
- const value = Reflect.get(target, prop, target);
325
- if (typeof value === "function") {
326
- if (!methodCache.has(prop)) {
327
- methodCache.set(prop, withRetry(value, target, prop));
328
- }
329
- return methodCache.get(prop);
330
- }
376
+ get(target, prop) {
377
+ // Передаём target третьим аргументом: геттеры (например, auth)
378
+ // тоже должны исполняться с this === target
379
+ const value = Reflect.get(target, prop, target);
380
+
381
+ if (typeof prop !== "string" || typeof value !== "function") {
382
+ return value;
383
+ }
384
+
385
+ if (!methodCache.has(prop)) {
386
+ const wrapped = RETRYABLE_METHODS.has(prop)
387
+ ? withRetry(value as (...args: any[]) => Promise<any>, target, prop)
388
+ : value.bind(target);
389
+ methodCache.set(prop, wrapped);
331
390
  }
332
391
 
333
- // Для остальных — передаём target чтобы приватные поля работали
334
- return Reflect.get(target, prop, target);
392
+ return methodCache.get(prop);
335
393
  },
336
394
  });
337
395
  }
@@ -361,7 +419,7 @@ export async function reinitializeB24(): Promise<SaveResult> {
361
419
  $b24 = _b24Raw ? wrapB24WithRetry(_b24Raw) : null;
362
420
 
363
421
  if (!_b24Raw || !$b24) {
364
- return { error: true, message: "Не удалось пересоздать $b24 — проверь authB24.json" };
422
+ return { error: true, message: "Не удалось пересоздать $b24 — проверь authB24.json и APP_B24_CLIENT_ID/APP_B24_CLIENT_SECRET" };
365
423
  }
366
424
 
367
425
  setupRefreshCallback(_b24Raw);
@@ -1,5 +1,5 @@
1
1
  import { DateTime } from "luxon";
2
- import { $b24 } from "./b24.ts";
2
+ import { $b24, getResultData } from "./b24.ts";
3
3
  import { logs } from "../logs/logs.ts";
4
4
  import dotEnv from "dotenv";
5
5
 
@@ -54,8 +54,7 @@ export async function checkB24Scope(): Promise<void> {
54
54
 
55
55
  try {
56
56
  const scopeResponse = await $b24.callMethod("scope");
57
- const scopeData = scopeResponse.getData() as any;
58
- const scopes: string[] = scopeData.result ?? [];
57
+ const scopes = getResultData<string[]>(scopeResponse, "scope") ?? [];
59
58
 
60
59
  logs.add(`Права приложения Bitrix24: ${scopes.join(", ")}`, "info");
61
60
 
@@ -104,8 +103,7 @@ export async function errorB24(dataTask: ErrorTaskData): Promise<ErrorB24Result>
104
103
  select: ["ID", "STATUS"],
105
104
  });
106
105
 
107
- const data = response.getData() as any;
108
- const tasks: TaskItem[] = data.result?.tasks ?? [];
106
+ const tasks: TaskItem[] = getResultData<{ tasks?: TaskItem[] }>(response, "tasks.task.list").tasks ?? [];
109
107
 
110
108
  if (tasks.length >= maxTasks) {
111
109
  return {
@@ -128,12 +126,12 @@ export async function errorB24(dataTask: ErrorTaskData): Promise<ErrorB24Result>
128
126
  },
129
127
  });
130
128
 
131
- const createData = createResponse.getData() as any;
129
+ const createdTask = getResultData(createResponse, "tasks.task.add");
132
130
 
133
131
  return {
134
132
  error: false,
135
133
  message: "Задача создана в Битрикс24.",
136
- data: createData.result,
134
+ data: createdTask,
137
135
  };
138
136
  } catch (error: any) {
139
137
  return {
@@ -1,5 +1,5 @@
1
1
  import type { B24OAuth } from "@bitrix24/b24jssdk";
2
- import { $b24 } from "./b24.ts";
2
+ import { $b24, getResultData } from "./b24.ts";
3
3
 
4
4
  // ==================== Типы ====================
5
5
 
@@ -141,7 +141,9 @@ export class Event {
141
141
  filter: { EVENT_NAME: eventName },
142
142
  });
143
143
 
144
- const arrOfflineEvents = response.getData() as OfflineEventsResponse;
144
+ const arrOfflineEvents = getResultData<OfflineEventsResponse["result"]>(response, "event.offline.get");
145
+ // B24 для пустой коллекции может вернуть result: [] — тогда events отсутствует
146
+ const allEvents = arrOfflineEvents?.events ?? [];
145
147
 
146
148
  // Обработка событий коннектора
147
149
  if (eventName === "ONIMCONNECTORMESSAGEADD") {
@@ -150,9 +152,9 @@ export class Event {
150
152
  message: [],
151
153
  };
152
154
 
153
- if (arrOfflineEvents.result.events.length > 0) {
154
- data.processId = arrOfflineEvents.result.process_id;
155
- data.message = arrOfflineEvents.result.events.map((event) => {
155
+ if (allEvents.length > 0) {
156
+ data.processId = arrOfflineEvents.process_id;
157
+ data.message = allEvents.map((event) => {
156
158
  const messageData = event.EVENT_DATA.MESSAGES[0];
157
159
 
158
160
  return {
@@ -178,23 +180,23 @@ export class Event {
178
180
  };
179
181
 
180
182
  // Очищаем события от системного пользователя
181
- const arrEventsToClear = arrOfflineEvents.result.events.filter((event) => event.EVENT_ADDITIONAL.user_id === SYSTEM_USER_ID);
183
+ const arrEventsToClear = allEvents.filter((event) => event.EVENT_ADDITIONAL.user_id === SYSTEM_USER_ID);
182
184
 
183
185
  if (arrEventsToClear.length > 0) {
184
186
  const arrMessageIdToClear = arrEventsToClear.map((event) => event.MESSAGE_ID);
185
187
  await this.b24.callMethod("event.offline.clear", {
186
- process_id: arrOfflineEvents.result.process_id,
188
+ process_id: arrOfflineEvents.process_id,
187
189
  message_id: arrMessageIdToClear,
188
190
  });
189
-
190
- // Удаляем эти события из основного массива
191
- arrOfflineEvents.result.events = arrOfflineEvents.result.events.filter((event) => event.EVENT_ADDITIONAL.user_id !== SYSTEM_USER_ID);
192
191
  }
193
192
 
194
- if (arrOfflineEvents.result.events.length > 0) {
195
- data.processId = arrOfflineEvents.result.process_id;
196
- data.entitysId = arrOfflineEvents.result.events.map((event) => event.EVENT_DATA.FIELDS.ID);
197
- data.arrMessageIdAndEntityId = arrOfflineEvents.result.events.map((event) => ({
193
+ // Работаем только с событиями, не относящимися к системному пользователю
194
+ const activeEvents = allEvents.filter((event) => event.EVENT_ADDITIONAL.user_id !== SYSTEM_USER_ID);
195
+
196
+ if (activeEvents.length > 0) {
197
+ data.processId = arrOfflineEvents.process_id;
198
+ data.entitysId = activeEvents.map((event) => event.EVENT_DATA.FIELDS.ID);
199
+ data.arrMessageIdAndEntityId = activeEvents.map((event) => ({
198
200
  messageId: event.MESSAGE_ID,
199
201
  entityId: event.EVENT_DATA.FIELDS.ID,
200
202
  }));
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@andrey4emk/npm-app-back-b24",
3
- "version": "3.3.1",
3
+ "version": "3.5.0",
4
4
  "description": "Bitrix24 OAuth helpers for Node.js projects",
5
5
  "main": "index.ts",
6
6
  "type": "module",
7
7
  "exports": {
8
- ".": "./index.ts"
8
+ ".": "./index.ts",
9
+ "./logs": "./logs/logs.ts",
10
+ "./fetchRetry": "./utils/fetchRetry.ts"
9
11
  },
10
12
  "scripts": {
11
13
  "test": "echo \"No tests\" && exit 0",
@@ -35,15 +37,14 @@
35
37
  "utils/fetchRetry.ts"
36
38
  ],
37
39
  "dependencies": {
38
- "@bitrix24/b24jssdk": "1.0.4",
40
+ "@bitrix24/b24jssdk": "2.0.0",
39
41
  "@types/express": "5.0.6",
40
42
  "@types/luxon": "3.7.1",
41
43
  "@types/node": "25.0.10",
44
+ "@types/nodemailer": "7.0.9",
42
45
  "conf": "15.0.2",
43
46
  "dotenv": "17.2.3",
44
47
  "luxon": "3.4.4",
45
- "nodemailer": "7.0.6",
46
- "@types/nodemailer": "7.0.9"
47
- },
48
- "devDependencies": {}
48
+ "nodemailer": "7.0.6"
49
+ }
49
50
  }
@@ -1,4 +1,5 @@
1
1
  import type { B24OAuth } from "@bitrix24/b24jssdk";
2
+ import { getResultData } from "../bitrix24/b24.ts";
2
3
 
3
4
  // ==================== Типы ====================
4
5
 
@@ -74,10 +75,10 @@ export class Smsgold {
74
75
 
75
76
  // Получаем публичную ссылку на файл для вставки в сообщение
76
77
  try {
77
- let resLink = await this.b24!.callMethod("disk.file.getExternalLink", {
78
+ const resLink = await this.b24!.callMethod("disk.file.getExternalLink", {
78
79
  id: idFileContract,
79
80
  });
80
- publicLink = (resLink as any).getData().result;
81
+ publicLink = getResultData<string>(resLink, "disk.file.getExternalLink");
81
82
  } catch (error: unknown) {
82
83
  const errMsg = error instanceof Error ? error.message : String(error);
83
84
  return { error: true, message: `Ошибка при получении публичной ссылки на файл: ${errMsg}` };
@@ -139,7 +140,7 @@ export class Smsgold {
139
140
  data: { NAME: fileName },
140
141
  fileContent: [fileName, base64File],
141
142
  });
142
- const data = (res as any).getData().result;
143
+ const data = getResultData(res, "disk.folder.uploadfile");
143
144
  return { error: false, data };
144
145
  } catch (error: unknown) {
145
146
  const errMsg = error instanceof Error ? error.message : String(error);
@@ -32,6 +32,29 @@ export function isNetworkError(error: unknown): boolean {
32
32
  return false;
33
33
  }
34
34
 
35
+ /**
36
+ * Маскирует секреты в URL перед записью в лог.
37
+ *
38
+ * URL входящего вебхука B24 содержит секрет прямо в пути
39
+ * (`https://портал.bitrix24.ru/rest/1/<секрет>/method.json`), а токен авторизации
40
+ * может приходить в query-параметрах. Хост и имя метода сохраняются — они нужны
41
+ * для диагностики, тело секрета заменяется на `***`.
42
+ *
43
+ * @param url — адрес запроса в любом виде, принимаемом fetch
44
+ * @returns строка URL, пригодная для логирования
45
+ */
46
+ export function maskUrl(url: string | URL | Request): string {
47
+ const raw = typeof url === "string" ? url : url instanceof URL ? url.href : url.url;
48
+
49
+ return (
50
+ raw
51
+ // Секрет входящего вебхука: /rest/<id пользователя>/<секрет>/
52
+ .replace(/(\/rest\/\d+\/)[^/?#]+/gi, "$1***")
53
+ // Токены в query-параметрах
54
+ .replace(/([?&](?:auth|access_token|refresh_token|token)=)[^&#]+/gi, "$1***")
55
+ );
56
+ }
57
+
35
58
  /**
36
59
  * Обёртка над fetch с повторными попытками при сетевых ошибках.
37
60
  * Повторяет запрос только при проблемах с сетью (TypeError, ECONNRESET и т.д.),
@@ -63,7 +86,7 @@ export async function fetchRetry(
63
86
  }
64
87
 
65
88
  logs.add(
66
- `fetchRetry: попытка ${attempt}/${retries} не удалась (${lastError.message}), повтор через ${delay}мс — ${String(url)}`,
89
+ `fetchRetry: попытка ${attempt}/${retries} не удалась (${lastError.message}), повтор через ${delay}мс — ${maskUrl(url)}`,
67
90
  "warn"
68
91
  );
69
92