@andrey4emk/npm-app-back-b24 4.0.2 → 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.
@@ -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": "4.0.2",
3
+ "version": "5.1.0",
4
4
  "description": "Bitrix24 OAuth helpers for Node.js projects",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "keywords": [],
22
22
  "engines": {
23
- "node": ">=20.3.0"
23
+ "node": ">=22.0.0"
24
24
  },
25
25
  "author": "andrey4emk",
26
26
  "license": "MIT",
@@ -35,6 +35,7 @@
35
35
  "bitrix24/b24/config.ts",
36
36
  "bitrix24/b24/state.ts",
37
37
  "bitrix24/b24/retry.ts",
38
+ "bitrix24/b24/nonJsonResponse.ts",
38
39
  "bitrix24/b24/facade.ts",
39
40
  "bitrix24/b24/proxy.ts",
40
41
  "bitrix24/b24/instance.ts",
@@ -46,17 +47,18 @@
46
47
  "sendMessage/email.ts",
47
48
  "sendMessage/wappi.ts",
48
49
  "logs/logs.ts",
50
+ "logs/maskPhones.ts",
49
51
  "utils/fetchRetry.ts"
50
52
  ],
51
53
  "dependencies": {
52
- "@bitrix24/b24jssdk": "2.2.0",
54
+ "@bitrix24/b24jssdk": "3.0.0",
53
55
  "conf": "15.1.0",
54
56
  "dotenv": "17.4.2",
55
57
  "luxon": "3.7.2"
56
58
  },
57
59
  "devDependencies": {
58
60
  "@types/express": "5.0.6",
59
- "@types/luxon": "3.7.4",
61
+ "@types/luxon": "3.7.6",
60
62
  "@types/node": "25.9.5",
61
63
  "@types/nodemailer": "8.0.1",
62
64
  "nodemailer": "9.0.5"
@@ -215,7 +215,7 @@ export class ChatApp {
215
215
  // и вводим. Остальные ошибки летят наружу как раньше
216
216
  if (!isTimeoutError(error)) throw error;
217
217
 
218
- // Уровень error — единственный, который уходит в чат B24. До появления
218
+ // Уровень error — единственный, который уходит в Телеграм через ретранслятор. До появления
219
219
  // таймаута зависание долетало исключением до потребителя и там порождало
220
220
  // задачу через errorB24(); перехватив ошибку здесь, мы обязаны сами оставить
221
221
  // след, иначе правка сделала бы отказ отправки тише, чем он был
@@ -255,7 +255,7 @@ export class Email {
255
255
  * возвращался вызывающему как `error: false`.
256
256
  *
257
257
  * Отказ логируется не здесь: результат уходит вызывающему, а уровень `error`
258
- * дублировался бы в чат B24. От таймаутов отправки в `Smsgold`/`Wappi` случай
258
+ * дублировался бы в Телеграм через ретранслятор. От таймаутов отправки в `Smsgold`/`Wappi` случай
259
259
  * отличается тем, что статус известен — сервер прямо сказал «не принял».
260
260
  */
261
261
  private checkDelivery(to: string, info: SendMailInfo): EmailResult {
@@ -59,8 +59,8 @@ interface SmsResult {
59
59
  export class Smsgold {
60
60
  private user: string;
61
61
  private pass: string;
62
- // Структурный тип, а не B24OAuth: объявляет callMethod сам, поэтому переживёт
63
- // удаление метода из SDK. Сигнатуру конструктора не сужает — она стала шире
62
+ // Структурный тип, а не B24OAuth: объявляет callMethod сам — метода в B24OAuth
63
+ // нет с SDK 3.0.0, поэтому передавать нужно $b24, а не голый B24OAuth
64
64
  private b24: B24MethodCaller | null;
65
65
  // ID папки на диске B24 для вложений SMS — параметр, а не константа модуля
66
66
  private uploadFolderId: number;
@@ -163,7 +163,7 @@ export class Smsgold {
163
163
  } catch (error: unknown) {
164
164
  // Ответа не дождались: запрос уже ушёл, сообщение могло быть доставлено.
165
165
  // Это не «ошибка отправки» — статус неизвестен, и решить это кодом нельзя.
166
- // Единственное место в классе, где пишем в лог: уровень error уходит в чат B24,
166
+ // Единственное место в классе, где пишем в лог: уровень error уходит в Телеграм через ретранслятор,
167
167
  // потому что разбираться должен человек
168
168
  if (isTimeoutError(error)) {
169
169
  logs.add(
@@ -137,7 +137,7 @@ export class Wappi {
137
137
  // Остальные ошибки летят наружу как раньше
138
138
  if (!isTimeoutError(error)) throw error;
139
139
 
140
- // Уровень error — единственный, который уходит в чат B24. До появления
140
+ // Уровень error — единственный, который уходит в Телеграм через ретранслятор. До появления
141
141
  // таймаута зависание долетало исключением до потребителя и там порождало
142
142
  // задачу через errorB24(); перехватив ошибку здесь, мы обязаны сами оставить
143
143
  // след, иначе правка сделала бы отказ отправки тише, чем он был
@@ -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***")