@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 +54 -4
- package/bitrix24/b24.ts +93 -35
- package/bitrix24/errTaskB24.ts +5 -7
- package/bitrix24/eventB24.ts +16 -14
- package/package.json +8 -7
- package/sendMessage/smsgold.ts +4 -3
- package/utils/fetchRetry.ts +24 -1
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
|
|
142
|
+
Экземпляр `$b24` обёрнут Proxy, который автоматически повторяет запросы при сетевых ошибках. Повторные попытки применяются к методам `callMethod`, `callListMethod`, `callBatch`, а также к обновлению токена (`refreshAuth`). Метод `fetchListMethod` из ретраев исключён — это async-генератор, оборачивать его в async-функцию нельзя; постраничные запросы SDK ретраит сам.
|
|
122
143
|
|
|
123
144
|
| Параметр | Значение |
|
|
124
145
|
| -------------- | ----------------------------------------- |
|
|
125
146
|
| Попыток | 5 |
|
|
126
|
-
| Задержка | 500 мс (
|
|
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
|
-
|
|
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()` с уровнем `
|
|
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"
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
317
|
+
// Практически недостижимо (SDK бросает реальную ошибку раньше),
|
|
318
|
+
// но если код всё же пришёл — SDK уже исчерпал свои попытки
|
|
269
319
|
if (error.code === "JSSDK_CALL_ALL_ATTEMPTS_EXHAUSTED") return false;
|
|
320
|
+
}
|
|
270
321
|
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
318
|
-
// Передаём target
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
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);
|
package/bitrix24/errTaskB24.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
129
|
+
const createdTask = getResultData(createResponse, "tasks.task.add");
|
|
132
130
|
|
|
133
131
|
return {
|
|
134
132
|
error: false,
|
|
135
133
|
message: "Задача создана в Битрикс24.",
|
|
136
|
-
data:
|
|
134
|
+
data: createdTask,
|
|
137
135
|
};
|
|
138
136
|
} catch (error: any) {
|
|
139
137
|
return {
|
package/bitrix24/eventB24.ts
CHANGED
|
@@ -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.
|
|
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 (
|
|
154
|
-
data.processId = arrOfflineEvents.
|
|
155
|
-
data.message =
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
+
"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": "
|
|
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
|
-
|
|
47
|
-
},
|
|
48
|
-
"devDependencies": {}
|
|
48
|
+
"nodemailer": "7.0.6"
|
|
49
|
+
}
|
|
49
50
|
}
|
package/sendMessage/smsgold.ts
CHANGED
|
@@ -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
|
-
|
|
78
|
+
const resLink = await this.b24!.callMethod("disk.file.getExternalLink", {
|
|
78
79
|
id: idFileContract,
|
|
79
80
|
});
|
|
80
|
-
publicLink = (resLink
|
|
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
|
|
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);
|
package/utils/fetchRetry.ts
CHANGED
|
@@ -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}мс — ${
|
|
89
|
+
`fetchRetry: попытка ${attempt}/${retries} не удалась (${lastError.message}), повтор через ${delay}мс — ${maskUrl(url)}`,
|
|
67
90
|
"warn"
|
|
68
91
|
);
|
|
69
92
|
|