@andrey4emk/npm-app-back-b24 5.0.0 → 5.1.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 +32 -6
- package/bitrix24/b24/instance.ts +1 -1
- package/bitrix24/b24/retry.ts +1 -1
- package/bitrix24/b24/tokens.ts +1 -1
- package/bitrix24/errTaskB24.ts +1 -1
- package/bitrix24/eventB24.ts +2 -2
- package/logs/logs.ts +153 -115
- package/logs/maskPhones.ts +63 -0
- package/package.json +2 -1
- package/sendMessage/chatApp.ts +1 -1
- package/sendMessage/email.ts +1 -1
- package/sendMessage/smsgold.ts +1 -1
- package/sendMessage/wappi.ts +1 -1
- package/utils/fetchRetry.ts +8 -3
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- [Логирование](#логирование)
|
|
18
18
|
- [fetchRetry и таймауты](#fetchretry-и-таймауты)
|
|
19
19
|
- [Переменные окружения](#переменные-окружения)
|
|
20
|
+
- [Миграция на 5.1.0](#миграция-на-510)
|
|
20
21
|
- [Миграция на 5.0.0](#миграция-на-500)
|
|
21
22
|
- [Миграция на 4.0.0](#миграция-на-400)
|
|
22
23
|
- [Скрипты](#скрипты)
|
|
@@ -239,7 +240,7 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
239
240
|
|
|
240
241
|
Retry срабатывает только при сетевых проблемах (`ECONNRESET`, `ETIMEDOUT`, `ERR_NETWORK` и т.д.). HTTP-ошибки (400, 500) и ошибки бизнес-логики Bitrix24 **не** вызывают повторных попыток.
|
|
241
242
|
|
|
242
|
-
**Уровни логов.** Промежуточные попытки пишутся на `warn`, окончательный отказ — на `error`: исчерпание всех попыток и отмена повтора гейтом. Так один упавший вызов даёт одно сообщение в
|
|
243
|
+
**Уровни логов.** Промежуточные попытки пишутся на `warn`, окончательный отказ — на `error`: исчерпание всех попыток и отмена повтора гейтом. Так один упавший вызов даёт одно сообщение в Телеграм-группу через ретранслятор вместо пяти, но при этом не теряется совсем. Логировать провал обязан сам Proxy: `errorB24()`, `Event` и `Smsgold` ошибку только возвращают вызывающему коду и в лог не пишут.
|
|
243
244
|
|
|
244
245
|
**Создающие вызовы не повторяются Proxy-слоем.** Если в вызове есть метод, создающий сущность или запускающий действие (`*.add`, `*.create`, `*.start`, `*.send`, `*.uploadfile`, `*.import`, `*.register` — в том числе среди команд `callBatch`), retry отменяется, а в лог идёт `повтор отменён`. Причина: транспортная ошибка не говорит, дошёл ли запрос до портала. Обрыв приходит и когда соединение не состоялось, и когда оно случилось после того, как портал уже принял и выполнил запрос — а повтор во втором случае создаёт вторую задачу, второй файл, второй экземпляр бизнес-процесса. Идемпотентные записи (`*.update`, `*.delete`, `*.set`) повторяются как прежде: повторное применение даёт то же состояние.
|
|
245
246
|
|
|
@@ -475,7 +476,7 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
475
476
|
});
|
|
476
477
|
```
|
|
477
478
|
|
|
478
|
-
**Отказ портала виден в логе.** Если портал отказал в создании задачи (нет прав, указанной группы не существует), пишется одна строка уровня `error` с заголовком задачи, номером группы и текстом ошибки. Сетевые сбои этой строки не дают: их уже записал слой повторов, и вторая ушла бы дублем в
|
|
479
|
+
**Отказ портала виден в логе.** Если портал отказал в создании задачи (нет прав, указанной группы не существует), пишется одна строка уровня `error` с заголовком задачи, номером группы и текстом ошибки. Сетевые сбои этой строки не дают: их уже записал слой повторов, и вторая ушла бы дублем в Телеграм-группу через ретранслятор.
|
|
479
480
|
|
|
480
481
|
**Группа задачи по умолчанию.** Если `groupId` не передан, группа берётся из переменной окружения `B24_ERROR_TASK_GROUP_ID`. Явно переданное значение всегда важнее переменной: `null` и `0` означают осознанное «без группы» и дефолт не подхватывают. Переменная читается один раз при загрузке модуля — правка `process.env` в рантайме дефолт не двигает, нужен перезапуск процесса. Если переменная не задана или непригодна (не целое больше нуля), поведение прежнее — задача создаётся без группы, плюс одна строка уровня `warn` за процесс. Строка пишется при первом вызове, которому дефолт реально понадобился: у потребителя, который всегда передаёт группу сам, в логе не появится ничего.
|
|
481
482
|
|
|
@@ -621,7 +622,7 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
621
622
|
|
|
622
623
|
Конструктор сразу разрешает тип мессенджера для `whatsApp` и `telegram` — берёт `messenger[0].type` и держит в полях, вместо того чтобы индексировать массив при каждой отправке. Если конфига нет или список `messenger` пуст, тип остаётся неразрешённым, и **методы этого мессенджера возвращают** `{ error: true, message: "ChatApp: не разрешён тип мессенджера whatsApp (нет конфига или пуст messenger)", data: null }` — вместо запроса на URL с `undefined` в пути. Конструктор при этом **не бросает**: экземпляр обычно создаётся на уровне модуля, и бросок уронил бы загрузку всего модуля отправки вместе с соседними каналами.
|
|
623
624
|
|
|
624
|
-
**Таймаут отправки — статус неизвестен.** Запросы идут с бюджетом времени (10 секунд на работу с токеном, 20 на проверку номера, 30 на отправку сообщения, 60 на отправку файла). Если ответа не дождались, отправка **могла состояться** — методы возвращают `{ error: true, message: "ChatApp: таймаут ... — статус неизвестен", data: null }`, а не бросают исключение. Повторять такую отправку нельзя: клиент получит второе сообщение. Пакет дополнительно пишет строку уровня `error` (она уходит в
|
|
625
|
+
**Таймаут отправки — статус неизвестен.** Запросы идут с бюджетом времени (10 секунд на работу с токеном, 20 на проверку номера, 30 на отправку сообщения, 60 на отправку файла). Если ответа не дождались, отправка **могла состояться** — методы возвращают `{ error: true, message: "ChatApp: таймаут ... — статус неизвестен", data: null }`, а не бросают исключение. Повторять такую отправку нельзя: клиент получит второе сообщение. Пакет дополнительно пишет строку уровня `error` (она уходит в Телеграм-группу через ретранслятор) — решение о повторе принимает человек. Раньше зависание долетало до вызывающего кода исключением через пять минут; теперь оно возвращается объектом через тридцать секунд, поэтому запись в лог обязательна: без неё отказ стал бы тише, чем был.
|
|
625
626
|
|
|
626
627
|
Отказ локальный: пустой `whatsApp` не мешает `telegram`. Но конфиг всё равно нужен для **обоих** мессенджеров, даже если проект пользуется одним — иначе второй канал молча перестанет работать, и узнать об этом можно будет только по `res.error` в логе.
|
|
627
628
|
|
|
@@ -726,7 +727,7 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
726
727
|
}
|
|
727
728
|
```
|
|
728
729
|
|
|
729
|
-
- **Таймаут и неизвестный статус доставки.** Отправка идёт без повторов с бюджетом 30 секунд. Если SMSGold не ответил, запрос уже ушёл — сообщение **могло быть доставлено**, и повторять отправку нельзя. Такой результат отличается от обычной ошибки: `unknownDelivery: true` и текст «SMSGold не ответил за 30с — статус отправки неизвестен». Дополнительно пакет пишет строку уровня `error` (она уходит в
|
|
730
|
+
- **Таймаут и неизвестный статус доставки.** Отправка идёт без повторов с бюджетом 30 секунд. Если SMSGold не ответил, запрос уже ушёл — сообщение **могло быть доставлено**, и повторять отправку нельзя. Такой результат отличается от обычной ошибки: `unknownDelivery: true` и текст «SMSGold не ответил за 30с — статус отправки неизвестен». Дополнительно пакет пишет строку уровня `error` (она уходит в Телеграм-группу через ретранслятор), потому что решение о повторе должен принимать человек, а не код. Это единственное место, где `Smsgold` пишет в лог сам.
|
|
730
731
|
|
|
731
732
|
```js
|
|
732
733
|
const result = await smsClient.sendSms({ phone, message });
|
|
@@ -793,7 +794,7 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
793
794
|
|
|
794
795
|
- **`Wappi`** — класс для отправки сообщений через Wappi API (WhatsApp, Telegram, Max). Экземпляр нужно создать самостоятельно.
|
|
795
796
|
|
|
796
|
-
**Таймаут отправки — статус неизвестен.** Как и у `ChatApp`: бюджет 20 секунд на проверку номера, 30 на сообщение, 60 на файл. При таймауте отправки метод возвращает `{ error: true, message: "Wappi: таймаут ... — статус неизвестен", data: null }` и пишет строку уровня `error` в
|
|
797
|
+
**Таймаут отправки — статус неизвестен.** Как и у `ChatApp`: бюджет 20 секунд на проверку номера, 30 на сообщение, 60 на файл. При таймауте отправки метод возвращает `{ error: true, message: "Wappi: таймаут ... — статус неизвестен", data: null }` и пишет строку уровня `error` (она уходит в Телеграм-группу через ретранслятор) — сообщение могло уйти, повторять нельзя. Отдельно про Telegram: `phoneCheckWappi` при таймауте проверки контакта **не создаёт контакт**, а возвращает ошибку. Иначе не дождавшись ответа мы бы завели контакт на номер, который мог уже быть в адресной книге, и объявили номер проверенным без проверки.
|
|
797
798
|
|
|
798
799
|
```js
|
|
799
800
|
import { Wappi } from "@andrey4emk/npm-app-back-b24";
|
|
@@ -869,11 +870,19 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
869
870
|
- Конфигурация хранится в файле `log.json` (через `conf`).
|
|
870
871
|
- Каждый уровень можно включить/выключить, изменить цвет.
|
|
871
872
|
|
|
873
|
+
**Error-логи в Телеграм.** Если заданы обе переменные `LOG_RELAY_URL` и `LOG_RELAY_KEY`, каждый `logs.add(..., "error")` дополнительно уходит в Телеграм-группу логов через ретранслятор `tg_relay` — независимо от того, включён ли `error` в `log.json`. Тема в группе определяется ключом клиента, уровень, время и имя клиента ставит ретранслятор.
|
|
874
|
+
|
|
875
|
+
- Запрос: `POST LOG_RELAY_URL`, заголовок `Authorization: Bearer <LOG_RELAY_KEY>`, тело `{"level":"error","text":"..."}`. Успех — ответ `202`.
|
|
876
|
+
- В текст идёт сообщение и `jsonData` (`JSON.stringify` с отступами; `null` и `undefined` не дописываются). Пустое сообщение заменяется на `(пустое сообщение)`. Длинный текст обрезается до 3800 символов с пометкой `...(обрезано)`. Логгер не бросает исключений ни на каком `message`.
|
|
877
|
+
- **Телефоны маскируются** — только в Телеграме, в консоли данные исходные: `+7 916 123-45-67`, `89161234567`, `8.916.123.45.67`, `+7 916 12 34 567`, `9161234567` → `+7 ••• •••-••-67`. Разделителем между цифрами считаются пробелы, дефисы и тире, точки, `/` и скобки, но не перевод строки. Числа из 6–9 и 12+ цифр (id, суммы, метки времени) и идентификаторы с латинским префиксом (`DEAL_9161234567`) не трогаются. Известные границы: 10-значный городской номер без префикса (`4951234567`) не маскируется — его не отличить от метки времени в секундах; 10-значное число на `9` (например, ИНН) маскируется.
|
|
878
|
+
- Отправка без ожидания, с бюджетом 5 секунд и без повторов. Отказ ретранслятора (не `202`, таймаут, сетевой сбой) пишется одной строкой `console.error` с префиксом `Logs Relay:`; для `401` — «ключ не принят», для `503` — «ретранслятор останавливается». Сетевой сбой печатается только именем ошибки и кодом причины (`TypeError (ECONNREFUSED)`). Адрес и ключ в строку не попадают.
|
|
879
|
+
- Задана только одна из двух переменных, адрес не `http`/`https` или с логином и паролем, ключ с пробелами или управляющими символами — одна строка `console.warn` при старте (без самого значения) и канал выключен.
|
|
880
|
+
|
|
872
881
|
### fetchRetry и таймауты
|
|
873
882
|
|
|
874
883
|
- **`fetchRetry(url, options?, retries?, delay?, timeoutMs?)`** — обёртка над `fetch` с повторными попытками при сетевых ошибках. HTTP-ошибки (4xx, 5xx) **не** вызывают повторных попыток — повторяются только сетевые сбои (TypeError, ECONNRESET, ETIMEDOUT и т.д.) и собственный таймаут запроса.
|
|
875
884
|
|
|
876
|
-
- **`maskUrl(url)`** — маскирует секреты в адресе перед записью в лог. URL входящего вебхука B24 содержит секрет прямо в пути (`/rest/1/<секрет>/method.json`), а токен авторизации может приходить в query-параметрах (`auth`, `access_token`, `refresh_token`, `token`). Хост и имя метода сохраняются, тело секрета заменяется на `***`. Используется внутри `fetchRetry`; применяйте в своём коде везде, где логируете адреса запросов к B24.
|
|
885
|
+
- **`maskUrl(url)`** — маскирует секреты в адресе перед записью в лог. URL входящего вебхука B24 содержит секрет прямо в пути (`/rest/1/<секрет>/method.json`), а токен авторизации может приходить в query-параметрах (`auth`, `access_token`, `refresh_token`, `token`, `signature`, `sessid`). Так же прячется токен бота Телеграма в пути Bot API: `https://api.telegram.org/bot<id>:<токен>/<метод>` и `/file/bot<id>:<токен>/<путь>` превращаются в `bot<id>:***`. Хост и имя метода сохраняются, тело секрета заменяется на `***`. Используется внутри `fetchRetry`; применяйте в своём коде везде, где логируете адреса запросов к B24.
|
|
877
886
|
|
|
878
887
|
```js
|
|
879
888
|
import { maskUrl } from "@andrey4emk/npm-app-back-b24";
|
|
@@ -1011,6 +1020,8 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
1011
1020
|
| `APP_NAME` | Название приложения (используется в описании задач) |
|
|
1012
1021
|
| `B24_ERROR_TASK_GROUP_ID` | ID группы по умолчанию для задач `errorB24()` (не задана — задача без группы) |
|
|
1013
1022
|
| `CONFIG_DIR` | Путь к директории с конфигами (по умолчанию `../config`) |
|
|
1023
|
+
| `LOG_RELAY_URL` | Адрес `POST /send` ретранслятора error-логов в Телеграм (только парой с `LOG_RELAY_KEY`) |
|
|
1024
|
+
| `LOG_RELAY_KEY` | Ключ клиента ретранслятора; по нему ретранслятор выбирает тему в группе |
|
|
1014
1025
|
| `FETCH_TIMEOUT_MS` | Бюджет одной попытки `fetchRetry`/`fetchWithTimeout`, мс (по умолчанию `60000`; `0` — без таймаута) |
|
|
1015
1026
|
| `FETCH_TIMEOUT_QUICK_MS` | Бюджет `FETCH_TIMEOUTS.quick` — токены, мс (по умолчанию `10000`) |
|
|
1016
1027
|
| `FETCH_TIMEOUT_API_MS` | Бюджет `FETCH_TIMEOUTS.api` — короткое чтение, мс (по умолчанию `20000`) |
|
|
@@ -1031,6 +1042,10 @@ APP_NAME=MyApp
|
|
|
1031
1042
|
CONFIG_DIR=../config
|
|
1032
1043
|
FETCH_TIMEOUT_MS=60000
|
|
1033
1044
|
|
|
1045
|
+
# Error-логи в Телеграм через ретранслятор (только парой; не заданы — канал выключен)
|
|
1046
|
+
LOG_RELAY_URL=http://tg_relay:8080/send
|
|
1047
|
+
LOG_RELAY_KEY=ключ-клиента
|
|
1048
|
+
|
|
1034
1049
|
# Группа по умолчанию для задач об ошибках. ID свой у каждого портала —
|
|
1035
1050
|
# посмотреть в адресе группы на портале. Не задана — задачи создаются без группы
|
|
1036
1051
|
B24_ERROR_TASK_GROUP_ID=196
|
|
@@ -1046,6 +1061,17 @@ CHATAPP_PASS=your-password
|
|
|
1046
1061
|
CHATAPP_APP_ID=your-app-id
|
|
1047
1062
|
```
|
|
1048
1063
|
|
|
1064
|
+
## Миграция на 5.1.0
|
|
1065
|
+
|
|
1066
|
+
Канал error-логов переехал из чата Bitrix24 в Телеграм-группу через ретранслятор `tg_relay`. Публичный API не менялся: `logs.add(message, level?, jsonData?)` как прежде.
|
|
1067
|
+
|
|
1068
|
+
1. **Задать `LOG_RELAY_URL` и `LOG_RELAY_KEY`** в `.env` проекта. Ключ выдаётся на ретрансляторе (`npm run issue-key -- <имя> <тема>`), тема в группе привязана к ключу. Задана одна из двух — канал выключен и при старте одна строка `warn`.
|
|
1069
|
+
2. **Убрать `LOG_B24_WEBHOOK_URL` и `LOG_B24_CHAT_ID`.** Они больше не читаются; пока заданы, при старте пишется одна строка `warn` «канал Bitrix24 снят в 5.1.0».
|
|
1070
|
+
3. **Контейнер проекта — в сеть `tg-relay`**, иначе адрес ретранслятора не резолвится и каждый error даст строку `Logs Relay: ошибка отправки`.
|
|
1071
|
+
4. **Глушилки тестов** — гасить `LOG_RELAY_URL`/`LOG_RELAY_KEY` пустой строкой (не `delete`: dotenv вернул бы значение из `.env`). Старые `LOG_B24_*` в глушилках можно оставить — они только убирают строку `warn`.
|
|
1072
|
+
5. **Телефоны в тексте error-логов маскируются** — только в Телеграме. В консоли и в журнале контейнера данные остаются исходными.
|
|
1073
|
+
6. **`APP_NAME` логгер больше не читает** — имя проекта в сообщении ставит ретранслятор по ключу. Для `errorB24()` переменная по-прежнему нужна.
|
|
1074
|
+
|
|
1049
1075
|
## Миграция на 5.0.0
|
|
1050
1076
|
|
|
1051
1077
|
Выпуск ломающий: пакет переехал на `@bitrix24/b24jssdk` 3.0.0. Вызовы методов фасада (`callMethod`, `callListMethod`, `fetchListMethod`, `callBatch`, `callBatchByChunk`) не меняются.
|
package/bitrix24/b24/instance.ts
CHANGED
|
@@ -105,7 +105,7 @@ export class B24SdkLogHandler implements Handler {
|
|
|
105
105
|
// попытки. На транспортном сбое их пять — по числу наших попыток, повторы SDK
|
|
106
106
|
// выключены (см. SDK_RESTRICTION_PARAMS). На 429/503 и неизвестных 5xx SDK
|
|
107
107
|
// повторяет по-прежнему, там до пятнадцати: три внутри каждой из наших пяти.
|
|
108
|
-
// Уровень error пакета уходит в
|
|
108
|
+
// Уровень error пакета уходит в Телеграм через ретранслятор, поэтому маппим SDK-ERROR в warn:
|
|
109
109
|
// окончательный провал вызова логирует retry-слой, ровно один раз.
|
|
110
110
|
const isQuietClientError = status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
111
111
|
const level = isQuietClientError ? "debug" : record.level >= LogLevel.CRITICAL ? "error" : "warn";
|
package/bitrix24/b24/retry.ts
CHANGED
|
@@ -456,7 +456,7 @@ export async function runWithRetry<T>(fn: () => Promise<T>, label: string, block
|
|
|
456
456
|
const lifted = blockReason ? ` — соединение не состоялось, ограничение (${blockReason}) снято` : "";
|
|
457
457
|
const pause = retryDelayFor(error, attempt);
|
|
458
458
|
|
|
459
|
-
// Промежуточные попытки — warn: в
|
|
459
|
+
// Промежуточные попытки — warn: в Телеграм через ретранслятор уходит только уровень error,
|
|
460
460
|
// иначе один упавший вызов дал бы пять сообщений
|
|
461
461
|
logs.add(`${label}${code}: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${pause}мс${lifted}`, "warn");
|
|
462
462
|
|
package/bitrix24/b24/tokens.ts
CHANGED
|
@@ -190,7 +190,7 @@ function refreshAuthWithMutex(): Promise<AuthData> {
|
|
|
190
190
|
|
|
191
191
|
// Уровень warn, а не error, намеренно: брошенную ниже ошибку поймает
|
|
192
192
|
// refreshAndSaveTokens и напишет свою строку уровня error, а error уходит
|
|
193
|
-
// в
|
|
193
|
+
// в Телеграм через ретранслятор — иначе на одно залипание пришло бы два сообщения
|
|
194
194
|
logs.add(
|
|
195
195
|
`refreshAuth: обмен токена не завершился за ${REFRESH_STUCK_TIMEOUT_MS / 60000} мин — ${mutexPart}, зависший запрос продолжает работу и сохранит токены сам, если ответ придёт`,
|
|
196
196
|
"warn"
|
package/bitrix24/errTaskB24.ts
CHANGED
|
@@ -223,7 +223,7 @@ export async function errorB24(dataTask: ErrorTaskData): Promise<ErrorB24Result>
|
|
|
223
223
|
//
|
|
224
224
|
// Условие обязательно: сетевой отказ retry-слой уже записал сам — строкой
|
|
225
225
|
// про исчерпание попыток либо про отменённый повтор. Без проверки вторая
|
|
226
|
-
// строка ушла бы дублем в
|
|
226
|
+
// строка ушла бы дублем в Телеграм через ретранслятор, куда уходит уровень `error`
|
|
227
227
|
if (!isB24NetworkError(error)) {
|
|
228
228
|
logs.add(`errorB24: задача «${dataTask.title}» не создана (группа ${groupId ?? "не задана"}) — ${message}`, "error");
|
|
229
229
|
}
|
package/bitrix24/eventB24.ts
CHANGED
|
@@ -441,7 +441,7 @@ export class Event {
|
|
|
441
441
|
|
|
442
442
|
// Битой оказалась вся выборка целиком — это уже не одна кривая запись,
|
|
443
443
|
// а похоже на смену формы ответа портала (переименованное поле). Уровень
|
|
444
|
-
// error, потому что строка обязана дойти до
|
|
444
|
+
// error, потому что строка обязана дойти до Телеграма: очередь при этом
|
|
445
445
|
// продолжает вычищаться пакет за пакетом, и локальный лог никто не увидит
|
|
446
446
|
if (verdicts.length > 1 && brokenVerdicts.length === verdicts.length) {
|
|
447
447
|
const firstReason = brokenVerdicts.at(0)?.reason ?? "причина не определена";
|
|
@@ -492,7 +492,7 @@ export class Event {
|
|
|
492
492
|
const message = error instanceof Error ? error.message : String(error);
|
|
493
493
|
|
|
494
494
|
// Уровень warn, а не error: окончательный отказ вызова уже написал
|
|
495
|
-
// строку error retry-слой Proxy, дубль ушёл бы в
|
|
495
|
+
// строку error retry-слой Proxy, дубль ушёл бы в Телеграм через ретранслятор второй раз
|
|
496
496
|
logs.add(
|
|
497
497
|
`event.offline.clear: не удалось снять с резерва записи, их ${messageIdsToClear.length} — ${message}. Записи придут следующим опросом`,
|
|
498
498
|
"warn"
|
package/logs/logs.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import Conf from "conf";
|
|
3
3
|
import dotEnv from "dotenv";
|
|
4
|
+
import { maskPhones } from "./maskPhones.ts";
|
|
4
5
|
|
|
5
6
|
dotEnv.config();
|
|
6
7
|
|
|
@@ -19,28 +20,33 @@ interface LogLevelConfig {
|
|
|
19
20
|
/** Полная конфигурация логирования */
|
|
20
21
|
type LogConfig = Record<LogLevel, LogLevelConfig>;
|
|
21
22
|
|
|
22
|
-
// ==================== Константы
|
|
23
|
+
// ==================== Константы канала в Телеграм ====================
|
|
23
24
|
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const B24_TEXT_LIMIT = 4000;
|
|
25
|
+
/**
|
|
26
|
+
* Предел длины текста, отправляемого в ретранслятор. Совпадает с `ENTRY_TEXT_LIMIT`
|
|
27
|
+
* ретранслятора (`tg_relay/src/format.ts`): длиннее он обрезал бы запись сам, уже после
|
|
28
|
+
* нашей маскировки и без нашей пометки. Сообщение Телеграма — 4096 символов, остаток
|
|
29
|
+
* ретранслятор оставляет под заголовок записи и строку «пропущено N»
|
|
30
|
+
*/
|
|
31
|
+
const RELAY_TEXT_LIMIT = 3800;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Бюджет времени на отправку error-лога в
|
|
34
|
+
* Бюджет времени на отправку одного error-лога в ретранслятор, мс.
|
|
35
35
|
*
|
|
36
36
|
* Своя константа, а не `FETCH_TIMEOUTS` из `utils/fetchRetry.ts`: тот модуль импортирует
|
|
37
37
|
* `logs` первой строкой, и обратный импорт дал бы цикл. Дублирование одного числа
|
|
38
38
|
* дешевле цикла зависимостей.
|
|
39
39
|
*/
|
|
40
|
-
const
|
|
40
|
+
const RELAY_TIMEOUT_MS = 5_000;
|
|
41
|
+
|
|
42
|
+
/** Сколько символов тела неуспешного ответа ретранслятора показывать в `console.error` */
|
|
43
|
+
const RELAY_BODY_PREVIEW = 300;
|
|
41
44
|
|
|
42
|
-
/**
|
|
43
|
-
const
|
|
45
|
+
/** Пояснения к статусам ответа ретранслятора, которые чаще всего придётся разбирать */
|
|
46
|
+
const RELAY_STATUS_HINTS: Readonly<Record<number, string>> = {
|
|
47
|
+
401: "ключ LOG_RELAY_KEY не принят",
|
|
48
|
+
503: "ретранслятор останавливается и не принимает",
|
|
49
|
+
};
|
|
44
50
|
|
|
45
51
|
/**
|
|
46
52
|
* Ошибка — обрыв по бюджету `AbortSignal.timeout`. Проверка локальная, без импорта
|
|
@@ -50,6 +56,41 @@ function isTimeout(error: unknown): boolean {
|
|
|
50
56
|
return (error as { name?: unknown } | null)?.name === "TimeoutError";
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Короткое описание ошибки отправки без текста сообщения: `name` и код или имя причины.
|
|
61
|
+
* Текст сообщения сетевой ошибки может содержать адрес запроса, а адрес и ключ канала
|
|
62
|
+
* в консоль не печатаются
|
|
63
|
+
*/
|
|
64
|
+
function describeSendError(error: unknown): string {
|
|
65
|
+
const name = error instanceof Error ? error.name : typeof error;
|
|
66
|
+
const cause = error instanceof Error ? (error.cause as { code?: unknown; name?: unknown } | null | undefined) : undefined;
|
|
67
|
+
const causeLabel = cause && typeof cause === "object" ? (cause.code ?? cause.name) : undefined;
|
|
68
|
+
return typeof causeLabel === "string" && causeLabel ? `${name} (${causeLabel})` : name;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Ключ ретранслятора — только видимые символы ASCII, без пробелов и управляющих */
|
|
72
|
+
const RELAY_KEY_RE = /^[\x21-\x7e]+$/;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Проверяет адрес ретранслятора: только `http`/`https` и без логина и пароля в адресе.
|
|
76
|
+
* Возвращает причину отказа или `null`. Значение адреса в причину не попадает
|
|
77
|
+
*/
|
|
78
|
+
function relayUrlProblem(raw: string): string | null {
|
|
79
|
+
let parsed: URL;
|
|
80
|
+
try {
|
|
81
|
+
parsed = new URL(raw);
|
|
82
|
+
} catch {
|
|
83
|
+
return "LOG_RELAY_URL не разбирается как адрес";
|
|
84
|
+
}
|
|
85
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
86
|
+
return "LOG_RELAY_URL должен начинаться с http:// или https://";
|
|
87
|
+
}
|
|
88
|
+
if (parsed.username || parsed.password) {
|
|
89
|
+
return "LOG_RELAY_URL не должен содержать логин и пароль";
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
53
94
|
// ==================== Настройки ====================
|
|
54
95
|
|
|
55
96
|
const configDir: string = process.env.CONFIG_DIR || "../config";
|
|
@@ -93,20 +134,43 @@ class LogsAPI {
|
|
|
93
134
|
|
|
94
135
|
private readonly resetColor = "\x1b[0m";
|
|
95
136
|
|
|
96
|
-
/**
|
|
97
|
-
private readonly
|
|
137
|
+
/** Адрес `POST /send` ретранслятора (`LOG_RELAY_URL`); `undefined` — канал выключен */
|
|
138
|
+
private readonly relayUrl: string | undefined;
|
|
98
139
|
|
|
99
|
-
/**
|
|
100
|
-
private readonly
|
|
101
|
-
|
|
102
|
-
/** Имя приложения для заголовка сообщения */
|
|
103
|
-
private readonly appName: string;
|
|
140
|
+
/** Ключ клиента ретранслятора (`LOG_RELAY_KEY`); `undefined` — канал выключен */
|
|
141
|
+
private readonly relayKey: string | undefined;
|
|
104
142
|
|
|
105
143
|
constructor() {
|
|
106
144
|
this.initConfig();
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
145
|
+
const url = process.env.LOG_RELAY_URL?.trim() || undefined;
|
|
146
|
+
const key = process.env.LOG_RELAY_KEY?.trim() || undefined;
|
|
147
|
+
|
|
148
|
+
// Канал включается только парой. Половина пары — ошибка настройки: одна строка
|
|
149
|
+
// предупреждения и канал выключен, а не запрос без ключа или в никуда
|
|
150
|
+
// Непригодные значения тоже выключают канал одной строкой. Сами значения в строку
|
|
151
|
+
// не попадают: адрес с паролем или ключ ушли бы в журнал контейнера
|
|
152
|
+
let enabled = false;
|
|
153
|
+
if (Boolean(url) !== Boolean(key)) {
|
|
154
|
+
console.warn("Logs: задан только один из LOG_RELAY_URL/LOG_RELAY_KEY — отправка error-логов в Телеграм выключена");
|
|
155
|
+
} else if (url && key) {
|
|
156
|
+
const problem = relayUrlProblem(url) ?? (RELAY_KEY_RE.test(key) ? null : "LOG_RELAY_KEY содержит пробелы или управляющие символы");
|
|
157
|
+
if (problem) {
|
|
158
|
+
console.warn(`Logs: ${problem} — отправка error-логов в Телеграм выключена`);
|
|
159
|
+
} else {
|
|
160
|
+
enabled = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
this.relayUrl = enabled ? url : undefined;
|
|
164
|
+
this.relayKey = enabled ? key : undefined;
|
|
165
|
+
|
|
166
|
+
// Старый канал в чат Bitrix24 снят. Строка ровно одна за процесс: конструктор
|
|
167
|
+
// singleton'а вызывается один раз. Без неё обновление пакета молча оставило бы
|
|
168
|
+
// проект без канала error-логов
|
|
169
|
+
if (process.env.LOG_B24_WEBHOOK_URL || process.env.LOG_B24_CHAT_ID) {
|
|
170
|
+
console.warn(
|
|
171
|
+
"Logs: канал Bitrix24 снят в 5.1.0 — LOG_B24_WEBHOOK_URL и LOG_B24_CHAT_ID больше не читаются, error-логи идут через LOG_RELAY_URL/LOG_RELAY_KEY",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
110
174
|
}
|
|
111
175
|
|
|
112
176
|
/** Проверяем и заполняем конфиг дефолтными значениями если данных нет */
|
|
@@ -121,7 +185,7 @@ class LogsAPI {
|
|
|
121
185
|
/**
|
|
122
186
|
* Добавляет сообщение в лог
|
|
123
187
|
*
|
|
124
|
-
* Важно: при уровне `error` сообщение отправляется в
|
|
188
|
+
* Важно: при уровне `error` сообщение отправляется в Телеграм через ретранслятор
|
|
125
189
|
* независимо от настройки `error.enabled` в `log.json`. Настройка `enabled`
|
|
126
190
|
* управляет только выводом в консоль. Это сделано намеренно, чтобы критические
|
|
127
191
|
* ошибки всегда доходили до команды даже при отключённом консольном логировании.
|
|
@@ -132,11 +196,15 @@ class LogsAPI {
|
|
|
132
196
|
*/
|
|
133
197
|
add(message: string | object, level?: LogLevel, jsonData?: unknown): void {
|
|
134
198
|
const levelStr: LogLevel = level || "info";
|
|
135
|
-
|
|
199
|
+
// JSON.stringify отдаёт undefined для undefined, функции и объекта с toJSON → undefined;
|
|
200
|
+
// тогда берём String(), иначе дальше по коду шёл бы не текст
|
|
201
|
+
const messageText = typeof message === "string" ? message : (JSON.stringify(message) ?? String(message));
|
|
136
202
|
|
|
137
|
-
// Отправляем error-логи в
|
|
203
|
+
// Отправляем error-логи в Телеграм независимо от настроек консольного вывода.
|
|
204
|
+
// В консоль ниже уходят исходные message и jsonData — маскировка телефонов
|
|
205
|
+
// касается только текста для Телеграма
|
|
138
206
|
if (levelStr === "error") {
|
|
139
|
-
this.
|
|
207
|
+
this.sendToRelay(messageText, jsonData);
|
|
140
208
|
}
|
|
141
209
|
|
|
142
210
|
// Проверяем конфиг, и если логирование отключено, то выходим
|
|
@@ -161,117 +229,87 @@ class LogsAPI {
|
|
|
161
229
|
}
|
|
162
230
|
|
|
163
231
|
/**
|
|
164
|
-
*
|
|
232
|
+
* Готовит текст error-лога и отправляет его в ретранслятор (fire-and-forget).
|
|
233
|
+
*
|
|
234
|
+
* Телефоны маскируются ДО обрезки по длине: после обрезки хвост номера на границе
|
|
235
|
+
* уже не узнать шаблоном, и он ушёл бы в Телеграм открытым.
|
|
165
236
|
*/
|
|
166
|
-
private
|
|
167
|
-
|
|
168
|
-
}
|
|
237
|
+
private sendToRelay(message: string, jsonData?: unknown): void {
|
|
238
|
+
if (!this.relayUrl || !this.relayKey) return;
|
|
169
239
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
let text = `[b]ERROR[/b] | ${this.appName}\n${message}`;
|
|
186
|
-
|
|
187
|
-
// Добавляем jsonData если есть
|
|
188
|
-
if (jsonData !== undefined) {
|
|
189
|
-
// Безопасная сериализация — логгер не должен бросать исключения
|
|
190
|
-
let jsonStr: string;
|
|
191
|
-
try {
|
|
192
|
-
jsonStr = JSON.stringify(jsonData, null, 2);
|
|
193
|
-
} catch {
|
|
194
|
-
jsonStr = "[Не удалось сериализовать jsonData]";
|
|
240
|
+
// Логгер не должен бросать синхронно: его зовут из catch-блоков потребителей,
|
|
241
|
+
// и бросок отсюда подменил бы исходную ошибку. Отказ — только console.error
|
|
242
|
+
try {
|
|
243
|
+
let text = message.trim() ? message : "(пустое сообщение)";
|
|
244
|
+
|
|
245
|
+
// null и undefined не дописываем: строка «null» в Телеграме ничего не говорит
|
|
246
|
+
if (jsonData != null) {
|
|
247
|
+
// Безопасная сериализация: циклы бросают, функция и undefined дают undefined
|
|
248
|
+
try {
|
|
249
|
+
const serialized = JSON.stringify(jsonData, null, 2) as string | undefined;
|
|
250
|
+
text += `\n${serialized ?? String(jsonData)}`;
|
|
251
|
+
} catch {
|
|
252
|
+
text += "\n[Не удалось сериализовать jsonData]";
|
|
253
|
+
}
|
|
195
254
|
}
|
|
196
|
-
text += `\n[code]${jsonStr}[/code]`;
|
|
197
|
-
}
|
|
198
255
|
|
|
199
|
-
|
|
200
|
-
let isTruncated = false;
|
|
201
|
-
if (text.length > B24_TEXT_LIMIT) {
|
|
202
|
-
const truncatedSuffix = "\n...(обрезано)";
|
|
203
|
-
text = text.slice(0, B24_TEXT_LIMIT - truncatedSuffix.length) + truncatedSuffix;
|
|
204
|
-
isTruncated = true;
|
|
205
|
-
}
|
|
256
|
+
text = maskPhones(text);
|
|
206
257
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
258
|
+
if (text.length > RELAY_TEXT_LIMIT) {
|
|
259
|
+
const truncatedSuffix = "\n...(обрезано)";
|
|
260
|
+
text = text.slice(0, RELAY_TEXT_LIMIT - truncatedSuffix.length) + truncatedSuffix;
|
|
261
|
+
}
|
|
211
262
|
|
|
212
|
-
|
|
263
|
+
void this.postToRelay(this.relayUrl, this.relayKey, text);
|
|
264
|
+
} catch (error: unknown) {
|
|
265
|
+
console.error(`Logs Relay: не удалось подготовить сообщение — ${describeSendError(error)}`);
|
|
266
|
+
}
|
|
213
267
|
}
|
|
214
268
|
|
|
215
269
|
/**
|
|
216
|
-
* Сам запрос
|
|
217
|
-
* не через `logs`, иначе отказ канала порождал бы новый error и зацикливался.
|
|
270
|
+
* Сам запрос к ретранслятору и разбор ответа. Никогда не бросает: все ошибки — в
|
|
271
|
+
* `console.error`, не через `logs`, иначе отказ канала порождал бы новый error и зацикливался.
|
|
272
|
+
*
|
|
273
|
+
* Нативный `fetch`, а не `fetchRetry`/`fetchWithTimeout` — во избежание циклической
|
|
274
|
+
* зависимости, поэтому таймаут выставляется здесь через `AbortSignal.timeout`: вызов идёт
|
|
275
|
+
* без `await`, и без бюджета зависшее соединение никого бы не разбудило. Сигнал действует
|
|
276
|
+
* и на чтение тела ответа — зависшее тело обрывается тем же бюджетом.
|
|
218
277
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
* такая ошибка не доходит — у каждого чтения тела свой `.catch`, поэтому таймаут
|
|
222
|
-
* разбирается прямо там: при `!ok` к статусу дописывается пометка «тело не дочитано»,
|
|
223
|
-
* при 200 пишется отдельная строка «доставка не подтверждена».
|
|
278
|
+
* Ни адрес, ни ключ в строки ошибок не попадают: вместо текста сетевой ошибки
|
|
279
|
+
* (он может содержать адрес) печатается только её имя и код причины.
|
|
224
280
|
*/
|
|
225
|
-
private async
|
|
281
|
+
private async postToRelay(url: string, key: string, text: string): Promise<void> {
|
|
226
282
|
try {
|
|
227
283
|
const response = await fetch(url, {
|
|
228
284
|
method: "POST",
|
|
229
|
-
headers: {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}),
|
|
236
|
-
signal: AbortSignal.timeout(B24_CHAT_TIMEOUT_MS),
|
|
285
|
+
headers: {
|
|
286
|
+
"Content-Type": "application/json",
|
|
287
|
+
Authorization: `Bearer ${key}`,
|
|
288
|
+
},
|
|
289
|
+
body: JSON.stringify({ level: "error", text }),
|
|
290
|
+
signal: AbortSignal.timeout(RELAY_TIMEOUT_MS),
|
|
237
291
|
});
|
|
238
292
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
isTimeout(error) ? `[тело не дочитано за ${B24_CHAT_TIMEOUT_MS / 1000}с]` : "",
|
|
243
|
-
);
|
|
244
|
-
const bodyPart = body ? ` — ${body.slice(0, B24_CHAT_BODY_PREVIEW)}` : "";
|
|
245
|
-
console.error(`Logs B24 Chat: HTTP ${response.status}, сообщение не доставлено${bodyPart}`);
|
|
293
|
+
// 202 — принято в очередь ретранслятора; тело не нужно, соединение освобождаем
|
|
294
|
+
if (response.status === 202) {
|
|
295
|
+
await response.body?.cancel().catch(() => {});
|
|
246
296
|
return;
|
|
247
297
|
}
|
|
248
298
|
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
});
|
|
257
|
-
if (bodyTimedOut) {
|
|
258
|
-
console.error(
|
|
259
|
-
`Logs B24 Chat: HTTP ${response.status}, но ответ не дочитан за ${B24_CHAT_TIMEOUT_MS / 1000}с — доставка не подтверждена`,
|
|
260
|
-
);
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (data && typeof data === "object" && "error" in data) {
|
|
264
|
-
const { error, error_description: description } = data as { error?: unknown; error_description?: unknown };
|
|
265
|
-
const reason = description ? String(description) : String(error);
|
|
266
|
-
console.error(`Logs B24 Chat: портал отклонил сообщение — ${reason}`);
|
|
267
|
-
}
|
|
299
|
+
// Тело — подсказка к причине отказа, его чтение отправку уронить не должно
|
|
300
|
+
const body = await response.text().catch((error: unknown) =>
|
|
301
|
+
isTimeout(error) ? `[тело не дочитано за ${RELAY_TIMEOUT_MS / 1000}с]` : "",
|
|
302
|
+
);
|
|
303
|
+
const bodyPart = body ? ` — ${body.slice(0, RELAY_BODY_PREVIEW)}` : "";
|
|
304
|
+
const hint = RELAY_STATUS_HINTS[response.status];
|
|
305
|
+
const hintPart = hint ? ` (${hint})` : "";
|
|
306
|
+
console.error(`Logs Relay: HTTP ${response.status}, сообщение не доставлено${hintPart}${bodyPart}`);
|
|
268
307
|
} catch (error: unknown) {
|
|
269
308
|
if (isTimeout(error)) {
|
|
270
|
-
console.error(`Logs
|
|
309
|
+
console.error(`Logs Relay: не ответил за ${RELAY_TIMEOUT_MS / 1000}с, сообщение не доставлено`);
|
|
271
310
|
return;
|
|
272
311
|
}
|
|
273
|
-
|
|
274
|
-
console.error(`Logs B24 Chat: ошибка отправки — ${errMsg}`);
|
|
312
|
+
console.error(`Logs Relay: ошибка отправки — ${describeSendError(error)}`);
|
|
275
313
|
}
|
|
276
314
|
}
|
|
277
315
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Маскировка российских телефонных номеров в тексте error-лога перед отправкой
|
|
3
|
+
* в Телеграм через ретранслятор. Внутренний модуль: из `logs.ts` не реэкспортируется
|
|
4
|
+
* и подпути в `exports` не имеет. Побочных эффектов нет.
|
|
5
|
+
*
|
|
6
|
+
* Номер ищется по цифрам, а не по рисунку групп: между любыми двумя цифрами может
|
|
7
|
+
* стоять сколько угодно разделителей — пробел, табуляция, неразрывные и узкие пробелы,
|
|
8
|
+
* дефис и тире всех видов (U+2010–U+2015), точка, косая черта, скобки. Так ловятся
|
|
9
|
+
* `+7 916 123-45-67`, `8.916.123.45.67`, `+7 916 12 34 567`, `8 9161 234567`,
|
|
10
|
+
* `8–916–123–45–67`, а не только «правильная» запись 3-3-2-2.
|
|
11
|
+
*
|
|
12
|
+
* Что ловит:
|
|
13
|
+
* - префикс `+7`, `7`, `8` или `+8` и ещё десять цифр;
|
|
14
|
+
* - мобильный без префикса — десять цифр, первая `9`, можно с открывающей скобкой.
|
|
15
|
+
*
|
|
16
|
+
* Границы:
|
|
17
|
+
* - слева от номера не может стоять цифра, латинская буква, `_` или `+` — поэтому
|
|
18
|
+
* не трогаются куски длинных чисел, `DEAL_9161234567`, `v8.9.16.123.45`.
|
|
19
|
+
* Точка и кириллица слева допустимы: `тел.89161234567` маскируется;
|
|
20
|
+
* - справа от номера не может стоять цифра — 12+ цифр подряд (метки времени в мс,
|
|
21
|
+
* длинные идентификаторы) не трогаются;
|
|
22
|
+
* - перевод строки разделителем не считается: соседние числа отформатированного
|
|
23
|
+
* JSON (`JSON.stringify(..., 2)`) не склеиваются в номер.
|
|
24
|
+
*
|
|
25
|
+
* Известные границы (сознательно):
|
|
26
|
+
* - 10-значный городской номер без префикса (`4951234567`) не маскируется — иначе под
|
|
27
|
+
* маску попали бы метки времени Unix в секундах (10 цифр, первая `1`);
|
|
28
|
+
* - 10-значное число на `9` (например, ИНН `9701234567`) маскируется. Это ошибка
|
|
29
|
+
* в безопасную сторону: лог теряет число, но номер телефона не утекает;
|
|
30
|
+
* - цепочка мелких чисел через точки или пробелы, где после `7`/`8`/`9` набирается
|
|
31
|
+
* нужное число цифр, тоже может попасть под маску.
|
|
32
|
+
*
|
|
33
|
+
* Скорость: разделители и цифры не пересекаются, поэтому каждый повтор однозначен
|
|
34
|
+
* и катастрофического перебора нет (строки в 30–50 тыс. символов — миллисекунды).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Разделитель между цифрами номера (без перевода строки) */
|
|
38
|
+
const SEP = "[ \\t\\u00a0\\u2007\\u202f\\-\\u2010-\\u2015./()]*";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Шаблон номера. Группы: 1–2 — две последние цифры номера с префиксом,
|
|
42
|
+
* 3–4 — две последние цифры мобильного без префикса.
|
|
43
|
+
*/
|
|
44
|
+
const PHONE_RE = new RegExp(
|
|
45
|
+
"(?<![\\dA-Za-z_+])(?:" +
|
|
46
|
+
`(?:\\+7|\\+?8|7)(?:${SEP}\\d){8}${SEP}(\\d)${SEP}(\\d)` +
|
|
47
|
+
"|" +
|
|
48
|
+
`\\(?9(?:${SEP}\\d){7}${SEP}(\\d)${SEP}(\\d)` +
|
|
49
|
+
")(?!\\d)",
|
|
50
|
+
"g",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Заменяет каждый найденный номер на `+7 ••• •••-••-NN`, где NN — две последние цифры.
|
|
55
|
+
* Хвост оставлен, чтобы по логу можно было отличить один номер от другого.
|
|
56
|
+
*/
|
|
57
|
+
export function maskPhones(text: string): string {
|
|
58
|
+
return text.replace(PHONE_RE, (...args: unknown[]) => {
|
|
59
|
+
// args: совпадение, группы 1–4, смещение, исходная строка
|
|
60
|
+
const tail = typeof args[1] === "string" ? `${args[1]}${String(args[2])}` : `${String(args[3])}${String(args[4])}`;
|
|
61
|
+
return `+7 ••• •••-••-${tail}`;
|
|
62
|
+
});
|
|
63
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrey4emk/npm-app-back-b24",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Bitrix24 OAuth helpers for Node.js projects",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"sendMessage/email.ts",
|
|
48
48
|
"sendMessage/wappi.ts",
|
|
49
49
|
"logs/logs.ts",
|
|
50
|
+
"logs/maskPhones.ts",
|
|
50
51
|
"utils/fetchRetry.ts"
|
|
51
52
|
],
|
|
52
53
|
"dependencies": {
|
package/sendMessage/chatApp.ts
CHANGED
|
@@ -215,7 +215,7 @@ export class ChatApp {
|
|
|
215
215
|
// и вводим. Остальные ошибки летят наружу как раньше
|
|
216
216
|
if (!isTimeoutError(error)) throw error;
|
|
217
217
|
|
|
218
|
-
// Уровень error — единственный, который уходит в
|
|
218
|
+
// Уровень error — единственный, который уходит в Телеграм через ретранслятор. До появления
|
|
219
219
|
// таймаута зависание долетало исключением до потребителя и там порождало
|
|
220
220
|
// задачу через errorB24(); перехватив ошибку здесь, мы обязаны сами оставить
|
|
221
221
|
// след, иначе правка сделала бы отказ отправки тише, чем он был
|
package/sendMessage/email.ts
CHANGED
|
@@ -255,7 +255,7 @@ export class Email {
|
|
|
255
255
|
* возвращался вызывающему как `error: false`.
|
|
256
256
|
*
|
|
257
257
|
* Отказ логируется не здесь: результат уходит вызывающему, а уровень `error`
|
|
258
|
-
* дублировался бы в
|
|
258
|
+
* дублировался бы в Телеграм через ретранслятор. От таймаутов отправки в `Smsgold`/`Wappi` случай
|
|
259
259
|
* отличается тем, что статус известен — сервер прямо сказал «не принял».
|
|
260
260
|
*/
|
|
261
261
|
private checkDelivery(to: string, info: SendMailInfo): EmailResult {
|
package/sendMessage/smsgold.ts
CHANGED
|
@@ -163,7 +163,7 @@ export class Smsgold {
|
|
|
163
163
|
} catch (error: unknown) {
|
|
164
164
|
// Ответа не дождались: запрос уже ушёл, сообщение могло быть доставлено.
|
|
165
165
|
// Это не «ошибка отправки» — статус неизвестен, и решить это кодом нельзя.
|
|
166
|
-
// Единственное место в классе, где пишем в лог: уровень error уходит в
|
|
166
|
+
// Единственное место в классе, где пишем в лог: уровень error уходит в Телеграм через ретранслятор,
|
|
167
167
|
// потому что разбираться должен человек
|
|
168
168
|
if (isTimeoutError(error)) {
|
|
169
169
|
logs.add(
|
package/sendMessage/wappi.ts
CHANGED
|
@@ -137,7 +137,7 @@ export class Wappi {
|
|
|
137
137
|
// Остальные ошибки летят наружу как раньше
|
|
138
138
|
if (!isTimeoutError(error)) throw error;
|
|
139
139
|
|
|
140
|
-
// Уровень error — единственный, который уходит в
|
|
140
|
+
// Уровень error — единственный, который уходит в Телеграм через ретранслятор. До появления
|
|
141
141
|
// таймаута зависание долетало исключением до потребителя и там порождало
|
|
142
142
|
// задачу через errorB24(); перехватив ошибку здесь, мы обязаны сами оставить
|
|
143
143
|
// след, иначе правка сделала бы отказ отправки тише, чем он был
|
package/utils/fetchRetry.ts
CHANGED
|
@@ -52,7 +52,7 @@ const SUSPICIOUS_TIMEOUT_MAX_MS = 999;
|
|
|
52
52
|
* Значение от 1 до 999 применяется как есть, но с записью `warn`: столько миллисекунд
|
|
53
53
|
* не хватит ни одному живому запросу, и это типовая путаница секунд с миллисекундами.
|
|
54
54
|
* Молча такое значение пропускать нельзя — на бюджетах отправки оно превращается
|
|
55
|
-
* в массовую недоставку с «неизвестным статусом» и потоком `error` в
|
|
55
|
+
* в массовую недоставку с «неизвестным статусом» и потоком `error` в Телеграм через ретранслятор.
|
|
56
56
|
*
|
|
57
57
|
* Экспортируется по двум причинам: одинаковая семантика для всех бюджетов пакета
|
|
58
58
|
* (дефолт `fetchRetry` и каждый из `FETCH_TIMEOUTS` читаются одной и той же функцией)
|
|
@@ -228,8 +228,10 @@ export function isPreConnectionError(error: unknown): boolean {
|
|
|
228
228
|
*
|
|
229
229
|
* URL входящего вебхука B24 содержит секрет прямо в пути
|
|
230
230
|
* (`https://портал.bitrix24.ru/rest/1/<секрет>/method.json`), а токен авторизации
|
|
231
|
-
* может приходить в query-параметрах.
|
|
232
|
-
*
|
|
231
|
+
* может приходить в query-параметрах. Bot API Телеграма тоже держит токен в пути:
|
|
232
|
+
* `https://api.telegram.org/bot<id>:<токен>/<метод>` и `/file/bot<id>:<токен>/<путь>`.
|
|
233
|
+
* Хост, id бота и имя метода сохраняются — они нужны для диагностики, тело секрета
|
|
234
|
+
* заменяется на `***`.
|
|
233
235
|
*
|
|
234
236
|
* @param url — адрес запроса в любом виде, принимаемом fetch
|
|
235
237
|
* @returns строка URL, пригодная для логирования
|
|
@@ -241,6 +243,9 @@ export function maskUrl(url: string | URL | Request): string {
|
|
|
241
243
|
raw
|
|
242
244
|
// Секрет входящего вебхука: /rest/<id пользователя>/<секрет>/
|
|
243
245
|
.replace(/(\/rest\/\d+\/)[^/?#]+/gi, "$1***")
|
|
246
|
+
// Токен бота Телеграма: /bot<id>:<токен>/ и /file/bot<id>:<токен>/.
|
|
247
|
+
// Двоеточие бывает закодировано как %3A — адрес мог пройти через кодирование
|
|
248
|
+
.replace(/(\/bot\d+(?::|%3A))[^/?#]+/gi, "$1***")
|
|
244
249
|
// Токены и подписи в query-параметрах. signature и sessid нужны отдельно:
|
|
245
250
|
// ссылка на файл B24-диска несёт секрет именно в signature
|
|
246
251
|
.replace(/([?&](?:auth|access_token|refresh_token|token|signature|sessid)=)[^&#]+/gi, "$1***")
|