@andrey4emk/npm-app-back-b24 3.9.0 → 4.0.1
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 +108 -4
- package/bitrix24/b24/facade.ts +14 -1
- package/bitrix24/b24/proxy.ts +51 -9
- package/bitrix24/b24/retry.ts +19 -6
- package/bitrix24/b24/tokens.ts +8 -6
- package/bitrix24/b24/types.ts +27 -0
- package/bitrix24/b24.ts +43 -12
- package/bitrix24/errTaskB24.ts +100 -6
- package/bitrix24/eventB24.ts +71 -35
- package/index.ts +4 -1
- package/package.json +17 -6
- package/sendMessage/chatApp.ts +101 -24
- package/sendMessage/smsgold.ts +11 -2
- package/sendMessage/wappi.ts +41 -15
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- [Логирование](#логирование)
|
|
18
18
|
- [fetchRetry и таймауты](#fetchretry-и-таймауты)
|
|
19
19
|
- [Переменные окружения](#переменные-окружения)
|
|
20
|
+
- [Миграция на 4.0.0](#миграция-на-400)
|
|
20
21
|
- [Скрипты](#скрипты)
|
|
21
22
|
- [Лицензия](#лицензия)
|
|
22
23
|
|
|
@@ -26,6 +27,15 @@
|
|
|
26
27
|
npm install @andrey4emk/npm-app-back-b24
|
|
27
28
|
```
|
|
28
29
|
|
|
30
|
+
`nodemailer` — **опциональная peer-зависимость**: её ставит потребитель, и только если пользуется классом `Email`. Всем остальным она не нужна и не приезжает.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Только тем, кто шлёт почту
|
|
34
|
+
npm install nodemailer@9.0.5
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Типы (`@types/node`, `@types/luxon` и прочие) пакет **не транслирует** — с 4.0.0 они лежат в его `devDependencies`. Потребителю, который гоняет `tsc`, нужны свои: подробности в разделе [Миграция на 4.0.0](#миграция-на-400).
|
|
38
|
+
|
|
29
39
|
### Точки входа
|
|
30
40
|
|
|
31
41
|
| Импорт | Что отдаёт | Поднимает OAuth-модуль |
|
|
@@ -33,6 +43,7 @@ npm install @andrey4emk/npm-app-back-b24
|
|
|
33
43
|
| `@andrey4emk/npm-app-back-b24` | всё | да |
|
|
34
44
|
| `@andrey4emk/npm-app-back-b24/logs` | `logs` | нет |
|
|
35
45
|
| `@andrey4emk/npm-app-back-b24/fetchRetry` | `fetchRetry`, `fetchWithTimeout`, `isNetworkError`, `isPreConnectionError`, `isTimeoutError`, `isAbortError`, `maskUrl`, `FETCH_TIMEOUTS`, `DEFAULT_FETCH_TIMEOUT_MS` | нет |
|
|
46
|
+
| `@andrey4emk/npm-app-back-b24/email` | `Email` | нет |
|
|
36
47
|
|
|
37
48
|
Корневой импорт — barrel: он тянет модуль OAuth, который при загрузке читает `authB24.json` и, если приложение авторизовано, запускает таймер проактивного обновления токена. Проектам, которые ходят в B24 по **входящему вебхуку** или берут из пакета только логгер и `fetchRetry`, удобнее импортировать по подпутям — тогда OAuth-модуль не загружается вообще.
|
|
38
49
|
|
|
@@ -42,6 +53,13 @@ import { logs } from "@andrey4emk/npm-app-back-b24/logs";
|
|
|
42
53
|
import { fetchRetry, maskUrl } from "@andrey4emk/npm-app-back-b24/fetchRetry";
|
|
43
54
|
```
|
|
44
55
|
|
|
56
|
+
Подпуть `./email` стоит особняком: `Email` вынесен из barrel'а не ради экономии импорта, а ради зависимости. Он единственный тянет `nodemailer`, и пока класс лежал в barrel'е, почтовая библиотека приезжала во все восемь проектов — включая те шесть, что почту не шлют.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// Почту шлём — ставим nodemailer и берём Email из подпути
|
|
60
|
+
import { Email } from "@andrey4emk/npm-app-back-b24/email";
|
|
61
|
+
```
|
|
62
|
+
|
|
45
63
|
Корневой импорт при этом остаётся безопасным: если `APP_B24_CLIENT_ID` и `APP_B24_CLIENT_SECRET` не заданы, пакет считает, что OAuth в проекте не используется, молча выставляет `$b24 = null` и пишет об этом только в `debug`.
|
|
46
64
|
|
|
47
65
|
### Строгость типов
|
|
@@ -101,7 +119,6 @@ import {
|
|
|
101
119
|
Event,
|
|
102
120
|
ChatApp,
|
|
103
121
|
Smsgold,
|
|
104
|
-
Email,
|
|
105
122
|
Wappi,
|
|
106
123
|
logs,
|
|
107
124
|
fetchRetry,
|
|
@@ -112,6 +129,9 @@ import {
|
|
|
112
129
|
FETCH_TIMEOUTS,
|
|
113
130
|
} from "@andrey4emk/npm-app-back-b24";
|
|
114
131
|
|
|
132
|
+
// Email живёт в подпути: он единственный тянет optional peer nodemailer
|
|
133
|
+
import { Email } from "@andrey4emk/npm-app-back-b24/email";
|
|
134
|
+
|
|
115
135
|
// $b24 — готовый экземпляр B24OAuth (или null, если токены не настроены)
|
|
116
136
|
if ($b24) {
|
|
117
137
|
const result = await $b24.callMethod("crm.contact.list", { limit: 5 });
|
|
@@ -422,7 +442,7 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
422
442
|
| `createdBy` | number | `138` | ID создателя |
|
|
423
443
|
| `responsibleId` | number | `1` | ID ответственного |
|
|
424
444
|
| `deadline` | ISO string | `DateTime.now() + 1 день` | Дедлайн в ISO формате |
|
|
425
|
-
| `groupId` | number\|null | `null`
|
|
445
|
+
| `groupId` | number\|null | `B24_ERROR_TASK_GROUP_ID` или `null` | ID группы; не передан — берётся переменная окружения |
|
|
426
446
|
| `accomplices` | number[] | `[]` | Массив ID соисполнителей |
|
|
427
447
|
| `maxTasks` | number | `100` | Максимум существующих задач с таким названием |
|
|
428
448
|
| `ufCrmTask` | string\|string[] | `""` | Значение для `UF_CRM_TASK` (массив или строка) |
|
|
@@ -439,6 +459,10 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
439
459
|
});
|
|
440
460
|
```
|
|
441
461
|
|
|
462
|
+
**Отказ портала виден в логе.** Если портал отказал в создании задачи (нет прав, указанной группы не существует), пишется одна строка уровня `error` с заголовком задачи, номером группы и текстом ошибки. Сетевые сбои этой строки не дают: их уже записал слой повторов, и вторая ушла бы дублем в чат B24.
|
|
463
|
+
|
|
464
|
+
**Группа задачи по умолчанию.** Если `groupId` не передан, группа берётся из переменной окружения `B24_ERROR_TASK_GROUP_ID`. Явно переданное значение всегда важнее переменной: `null` и `0` означают осознанное «без группы» и дефолт не подхватывают. Переменная читается один раз при загрузке модуля — правка `process.env` в рантайме дефолт не двигает, нужен перезапуск процесса. Если переменная не задана или непригодна (не целое больше нуля), поведение прежнее — задача создаётся без группы, плюс одна строка уровня `warn` за процесс. Строка пишется при первом вызове, которому дефолт реально понадобился: у потребителя, который всегда передаёт группу сам, в логе не появится ничего.
|
|
465
|
+
|
|
442
466
|
- **`checkB24Scope()`** — проверяет и логирует права (скоупы) приложения Bitrix24. Вызывается автоматически при старте, если `$b24` инициализирован.
|
|
443
467
|
|
|
444
468
|
### События
|
|
@@ -704,7 +728,7 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
704
728
|
- **`Email`** — класс для отправки email через SMTP Яндекса. Экземпляр нужно создать самостоятельно.
|
|
705
729
|
|
|
706
730
|
```js
|
|
707
|
-
import { Email } from "@andrey4emk/npm-app-back-b24";
|
|
731
|
+
import { Email } from "@andrey4emk/npm-app-back-b24/email";
|
|
708
732
|
|
|
709
733
|
const emailClient = new Email({
|
|
710
734
|
user: "your-email@yandex.ru",
|
|
@@ -743,7 +767,9 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
743
767
|
- **Отвергнутый получатель — это ошибка.** Ответ транспорта разбирается: адрес ищется в `accepted` и `rejected`, элементы принимаются и строкой, и объектом `{ name, address }`. Текст ошибки несёт адрес и ответ сервера. Сверка идёт по **нормализованному** адресу: снимается регистр, снимаются угловые скобки, домен приводится к punycode. Иначе живой адрес объявлялся бы недоставленным — почтовый сервер возвращает адреса из конверта, а не в том виде, как их передали: `ivan@пример.рф` приходит обратно как `ivan@xn--e1afmkfd.xn--p1ai`, а `<a@b.ru>` — как `a@b.ru`. Если транспорт не сообщил ни `accepted`, ни `rejected`, результат считается успешным и в лог уходит одна строка `warn`: объявить доставленное письмо ошибкой было бы хуже исходного дефекта.
|
|
744
768
|
- **Входной объект не мутируется.** Прежняя реализация записывала вложения обратно в переданный `dataMail`, затирая `attachments` вызывающего.
|
|
745
769
|
- **Второй аргумент конструктора — подмена транспорта.** `new Email(auth, transport)` принимает любой объект с методом `sendMail(options)` (тип `MailTransport`). Нужен тестам и потребителю со своим SMTP; без него класс поднимает транспорт Яндекса сам.
|
|
746
|
-
-
|
|
770
|
+
- **Класс живёт в подпути `@andrey4emk/npm-app-back-b24/email`, а не в корне.** Из barrel'а он убран в 4.0.0.
|
|
771
|
+
- Транспорт — `nodemailer`, **опциональная peer-зависимость** с диапазоном `^9.0.5 || ^10.0.0`. Ставит его потребитель. Нижняя граница — CVE (GHSA-p6gq-j5cr-w38f, уязвимы версии `<= 9.0.0`), верхняя — проверенная совместимость с десяткой.
|
|
772
|
+
- `@types/nodemailer` нужен **только на девятке**. У `nodemailer` 10 типы свои, и TypeScript предпочтёт их; отдельный пакет типов там лишний.
|
|
747
773
|
- Вложения передаются готовым `Buffer` — файл по `fileUrl` пакет скачивает сам. Поля `path` и `href` намеренно не поддерживаются: nodemailer не должен ходить по URL. Скачивание идёт через `fetchWithTimeout` с бюджетом 60 секунд.
|
|
748
774
|
- Таймауты SMTP-транспорта переопределены: соединение 15 с, приветствие 10 с, сокет 120 с. Дефолты nodemailer (2 мин / 30 с / 10 мин) слишком щедры — письмо на Яндекс уходит за секунды, а 10 минут молчания сокета блокируют вызывающую очередь.
|
|
749
775
|
|
|
@@ -967,6 +993,7 @@ const sent = await fetchWithTimeout("https://api.example.com/send", { method: "P
|
|
|
967
993
|
| `APP_B24_CLIENT_SECRET` | Client Secret приложения Bitrix24 |
|
|
968
994
|
| `APP_ENV` | Определяет окружение, используется как ключ секции авторизации |
|
|
969
995
|
| `APP_NAME` | Название приложения (используется в описании задач) |
|
|
996
|
+
| `B24_ERROR_TASK_GROUP_ID` | ID группы по умолчанию для задач `errorB24()` (не задана — задача без группы) |
|
|
970
997
|
| `CONFIG_DIR` | Путь к директории с конфигами (по умолчанию `../config`) |
|
|
971
998
|
| `FETCH_TIMEOUT_MS` | Бюджет одной попытки `fetchRetry`/`fetchWithTimeout`, мс (по умолчанию `60000`; `0` — без таймаута) |
|
|
972
999
|
| `FETCH_TIMEOUT_QUICK_MS` | Бюджет `FETCH_TIMEOUTS.quick` — токены, мс (по умолчанию `10000`) |
|
|
@@ -988,6 +1015,10 @@ APP_NAME=MyApp
|
|
|
988
1015
|
CONFIG_DIR=../config
|
|
989
1016
|
FETCH_TIMEOUT_MS=60000
|
|
990
1017
|
|
|
1018
|
+
# Группа по умолчанию для задач об ошибках. ID свой у каждого портала —
|
|
1019
|
+
# посмотреть в адресе группы на портале. Не задана — задачи создаются без группы
|
|
1020
|
+
B24_ERROR_TASK_GROUP_ID=196
|
|
1021
|
+
|
|
991
1022
|
# Именованные бюджеты пакета — задавать только при необходимости, иначе действуют дефолты
|
|
992
1023
|
# FETCH_TIMEOUT_QUICK_MS=10000
|
|
993
1024
|
# FETCH_TIMEOUT_API_MS=20000
|
|
@@ -999,6 +1030,79 @@ CHATAPP_PASS=your-password
|
|
|
999
1030
|
CHATAPP_APP_ID=your-app-id
|
|
1000
1031
|
```
|
|
1001
1032
|
|
|
1033
|
+
## Миграция на 4.0.0
|
|
1034
|
+
|
|
1035
|
+
Выпуск ломающий. Правок у потребителя четыре, все механические.
|
|
1036
|
+
|
|
1037
|
+
### 1. `Email` переехал в подпуть
|
|
1038
|
+
|
|
1039
|
+
Из общего импорта его убрать, добавить отдельной строкой:
|
|
1040
|
+
|
|
1041
|
+
```diff
|
|
1042
|
+
-import { $b24, logs, Email } from "@andrey4emk/npm-app-back-b24";
|
|
1043
|
+
+import { $b24, logs } from "@andrey4emk/npm-app-back-b24";
|
|
1044
|
+
+import { Email } from "@andrey4emk/npm-app-back-b24/email";
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
Затрагивает два проекта: `b24_conector/server/api/sendMessage.ts` и `b24_desktop/server/api/sendMessage.js` (первая строка в обоих). Больше `Email` из пакета никто не импортирует.
|
|
1048
|
+
|
|
1049
|
+
### 2. `nodemailer` — теперь зависимость потребителя
|
|
1050
|
+
|
|
1051
|
+
Кто пользуется `Email`, ставит его себе: `pnpm add nodemailer@9.0.5` (оба проекта на pnpm). Диапазон peer — `^9.0.5 || ^10.0.0`. Тем же двум проектам `@types/nodemailer` в `devDependencies`, если остаются на девятке; на десятке типы приходят с самим пакетом.
|
|
1052
|
+
|
|
1053
|
+
Шаг обязателен обоим, но по разным причинам:
|
|
1054
|
+
|
|
1055
|
+
- **`b24_conector`** — `nodemailer` в его `package.json` отсутствует вовсе, библиотека приезжала транзитивно из наших `dependencies`. После 4.0.0 не приедет, и первый же импорт `server/api/sendMessage.ts` упадёт с `ERR_MODULE_NOT_FOUND`. Вместе с почтой встанут `ChatApp`, `Wappi` и SemySMS — они живут в том же модуле. Пропустить шаг нельзя;
|
|
1056
|
+
- **`b24_desktop`** — зависимость уже прописана в его `package.json`, поставленная версия `9.0.5` подходит под диапазон. Здесь достаточно сверки.
|
|
1057
|
+
|
|
1058
|
+
Под pnpm несовпадение опционального peer не блокирует установку: менеджер пишет `WARN` и завершает работу с нулевым кодом. Кому нужен жёсткий блок — `strict-peer-dependencies=true` в своём `.npmrc`.
|
|
1059
|
+
|
|
1060
|
+
Остальным шести проектам делать нечего — почтовая библиотека к ним больше не приезжает.
|
|
1061
|
+
|
|
1062
|
+
### 3. `@types/*` больше не приезжают из пакета
|
|
1063
|
+
|
|
1064
|
+
Они переехали в `devDependencies` и потребителю не транслируются. Кто гоняет `tsc`, держит свои:
|
|
1065
|
+
|
|
1066
|
+
- `@types/node` — всем;
|
|
1067
|
+
- `@types/luxon` — всем, кто импортирует корень (`errTaskB24.ts` тянет `luxon` в рантайме);
|
|
1068
|
+
- `@types/express` — **больше не нужен из-за нас никому**: пакет типы `express` не импортирует вовсе.
|
|
1069
|
+
|
|
1070
|
+
Без `@types/luxon` тайпчек упадёт с `TS7016` на `bitrix24/errTaskB24.ts`. Семь из восьми живых потребителей его уже держат.
|
|
1071
|
+
|
|
1072
|
+
### 4. Изменённые публичные типы
|
|
1073
|
+
|
|
1074
|
+
`any` в поверхности пакета заменён на `unknown` — читающий поле напрямую добавляет приведение.
|
|
1075
|
+
|
|
1076
|
+
| Что | Было | Стало |
|
|
1077
|
+
| ---------------------------------------------- | ------- | --------------------------- |
|
|
1078
|
+
| `data` у `event.get()` и `event.clear()` | `any` | `unknown` |
|
|
1079
|
+
| `ConnectorMessage.file` | `any[] \| null` | `TelegramFile[] \| null` |
|
|
1080
|
+
| `ConnectorMessage.attachments` | `any[] \| null` | `unknown[] \| null` |
|
|
1081
|
+
| `ConnectorMessage.im` | `any \| null` | `Record<string, unknown> \| null` |
|
|
1082
|
+
| `data` у методов `Wappi` | `any` | `unknown` |
|
|
1083
|
+
| `data` у `errorB24()` | `any` | `unknown` |
|
|
1084
|
+
|
|
1085
|
+
```diff
|
|
1086
|
+
-const { data } = await event.get("ONCRMDEALUPDATE");
|
|
1087
|
+
-for (const id of data.entitysId) { /* ... */ }
|
|
1088
|
+
+const { data } = await event.get("ONCRMDEALUPDATE");
|
|
1089
|
+
+const events = data as EntityEvents;
|
|
1090
|
+
+for (const id of events.entitysId) { /* ... */ }
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
Тип `TelegramFile` экспортируется из пакета — своё объявление в `b24_conector` можно убрать, но замена не drop-in: у них поле объявлено `name?: string`, в пакете оно `name: string`. Разбор пакета всегда отдаёт строку (на битом элементе — пустую), поэтому чтение вида `fileInfo.name || fallback` продолжит работать. Сверки потребуют их собственные места, где `TelegramFile` создаётся, — в том числе очередь ретраев `confB24Outbox.ts`: объект без `name` в наш тип не ляжет и упадёт тайпчеком.
|
|
1094
|
+
|
|
1095
|
+
`saveAuthB24Handler` больше не типизирован типами `express`, но по-прежнему принимает `Request`/`Response` без правок: `app.post("/saveAuthB24", saveAuthB24Handler)` работает как раньше.
|
|
1096
|
+
|
|
1097
|
+
Дефолты дженериков `callMethod`, `callBatch`, `fetchListMethod` и `getResultData` остались `any` намеренно — код вида `chunk.forEach((x) => x.ID)` продолжает компилироваться.
|
|
1098
|
+
|
|
1099
|
+
### Что изменилось само, правок не требует
|
|
1100
|
+
|
|
1101
|
+
- Вместе с `Email` из барреля уехали и его типы — `MailTransport` и `SendMailInfo`. Теперь они в подпути `@andrey4emk/npm-app-back-b24/email`. Из потребителей их никто не импортирует.
|
|
1102
|
+
- `saveAuthB24Handler` на запрос **без тела** отвечает `400 Не заполнены обязательные поля.` вместо прежних `500`. Раньше разбор пустого тела падал и уходил в общий `catch`.
|
|
1103
|
+
- Он же отвечает `400` на **нестроковые поля** тела: объект в `access_token`, число в `member_id`, нечисловой `expires_in`. Раньше такой запрос давал `500` — а в 4.0.0 до этой правки успевал затереть рабочий `config/authB24.json` строкой `"[object Object]"` и ответить `201`. Файл токенов в этом случае не трогается.
|
|
1104
|
+
- `expires_in`, пришедший строкой (`"3600"`), считается правильно. Раньше склейка строк в расчёте `expires` давала абсурдную дату.
|
|
1105
|
+
|
|
1002
1106
|
## Скрипты
|
|
1003
1107
|
|
|
1004
1108
|
- `npm run test` — прогон юнит-тестов из `tests/` через встроенный `node --test`. Дополнительных зависимостей не нужно: `.ts` запускаются на нативном стирании типов Node, поэтому прогону нужен Node 22.6+ (`engines: ">=20.3.0"` — это требование к потребителю пакета, а не к разработке). Сети тесты не касаются, в тарбол папка не попадает.
|
package/bitrix24/b24/facade.ts
CHANGED
|
@@ -31,6 +31,11 @@ export const FETCH_LIST_MAX_PAGES = 20000;
|
|
|
31
31
|
* Функция превращает такой ответ в понятную ошибку вместо падения на undefined.
|
|
32
32
|
* Также отсекает случай, когда запрос успешен, но result пустой (null/undefined).
|
|
33
33
|
*
|
|
34
|
+
* Дефолт дженерика — `any`, и это осознанно: потребители пишут
|
|
35
|
+
* `getResultData(response, "tasks.task.add").task.id` без параметра типа.
|
|
36
|
+
* Замена дефолта на `unknown` сломала бы каждое такое обращение, ничего не дав
|
|
37
|
+
* взамен: узкий тип задаётся на месте вызова — `getResultData<Deal>(response, method)`.
|
|
38
|
+
*
|
|
34
39
|
* @param response — результат $b24.callMethod()
|
|
35
40
|
* @param methodName — имя метода B24, попадёт в текст ошибки
|
|
36
41
|
*/
|
|
@@ -82,8 +87,16 @@ export function readV2Envelope(response: AjaxResult, method: string): V2Envelope
|
|
|
82
87
|
* идемпотентности (`getRetryBlockReason`) видит те же аргументы, что и раньше,
|
|
83
88
|
* — retry оборачивает фасад снаружи, а преобразование в объект опций
|
|
84
89
|
* происходит уже внутри.
|
|
90
|
+
*
|
|
91
|
+
* `any` в сигнатурах ниже (`callMethod<T = any>`, `params?: any`, `AsyncGenerator<any[]>`,
|
|
92
|
+
* `Array<any>`, а также `call.make<any>` внутри `fetchListMethod` — тип страницы
|
|
93
|
+
* задаёт вызывающий) оставлен НАМЕРЕННО и снятию по T-53 не подлежит. Это дефолты дженериков
|
|
94
|
+
* публичной поверхности, дословно повторяющие `AbstractB24`: на них стоит код потребителей
|
|
95
|
+
* (`for await (const chunk of $b24.fetchListMethod(...)) chunk.forEach((x) => x.ID)`).
|
|
96
|
+
* `unknown` в дефолте уронил бы каждое такое место, не дав взамен ничего — узкий тип
|
|
97
|
+
* задаётся параметром типа на месте вызова (`callMethod<Deal>(...)`).
|
|
85
98
|
*/
|
|
86
|
-
export function createFacade(target: B24OAuth):
|
|
99
|
+
export function createFacade(target: B24OAuth): FacadeMethods {
|
|
87
100
|
// Геттер actions при каждом обращении проверяет инициализацию экземпляра —
|
|
88
101
|
// читаем его в момент вызова, а не один раз при создании фасада
|
|
89
102
|
const v2 = () => target.actions.v2;
|
package/bitrix24/b24/proxy.ts
CHANGED
|
@@ -1,21 +1,61 @@
|
|
|
1
1
|
import type { B24OAuth } from "@bitrix24/b24jssdk";
|
|
2
2
|
import { createFacade } from "./facade.ts";
|
|
3
3
|
import { withRetry } from "./retry.ts";
|
|
4
|
-
import type {
|
|
4
|
+
import type { AsyncFn } from "./retry.ts";
|
|
5
|
+
import type { B24Client, FacadeMethods } from "./types.ts";
|
|
5
6
|
|
|
6
|
-
// ==================== Константы ====================
|
|
7
|
+
// ==================== Константы и типы ====================
|
|
8
|
+
|
|
9
|
+
/** Имя метода, который Proxy резолвит в реализацию пакета */
|
|
10
|
+
type FacadeMethodName = keyof FacadeMethods;
|
|
7
11
|
|
|
8
12
|
/** Методы фасада, оборачиваемые retry-логикой (fetchListMethod — async-генератор, его оборачивать нельзя) */
|
|
9
|
-
|
|
13
|
+
const RETRYABLE_METHOD_NAMES = ["callMethod", "callListMethod", "callBatch"] as const;
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Имена, которые Proxy резолвит в собственную реализацию пакета поверх `actions.v2.*`.
|
|
13
17
|
*
|
|
14
|
-
* Шире, чем
|
|
18
|
+
* Шире, чем RETRYABLE_METHOD_NAMES: `fetchListMethod` и `callBatchByChunk` мы реализуем,
|
|
15
19
|
* но не ретраим. `callBatchByChunk` в наборе обязателен — пока хоть один deprecated-метод
|
|
16
20
|
* SDK достижим через `$b24`, он будет писать предупреждение об устаревании.
|
|
21
|
+
*
|
|
22
|
+
* `satisfies` ловит опечатку и переименование: имя, которого нет среди методов фасада,
|
|
23
|
+
* уронит тайпчек здесь, а не молча уедет мимо Proxy в SDK.
|
|
24
|
+
*/
|
|
25
|
+
const FACADE_METHOD_NAMES = ["callMethod", "callListMethod", "fetchListMethod", "callBatch", "callBatchByChunk"] as const satisfies readonly FacadeMethodName[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Обратная страховка к `satisfies` выше: ловит НЕдостачу.
|
|
29
|
+
* Забыли имя — `Exclude` перестанет быть `never`, и тип станет `never`,
|
|
30
|
+
* а `true` в него не присвоится. Значение нигде не используется
|
|
17
31
|
*/
|
|
18
|
-
|
|
32
|
+
const _facadeNamesAreExhaustive: Exclude<FacadeMethodName, (typeof FACADE_METHOD_NAMES)[number]> extends never ? true : never = true;
|
|
33
|
+
void _facadeNamesAreExhaustive;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Имя метода фасада, который дополнительно оборачивается retry.
|
|
37
|
+
*
|
|
38
|
+
* Уже, чем `FacadeMethodName`: `fetchListMethod` — async-генератор, он возвращает
|
|
39
|
+
* не `Promise`, под `AsyncFn` не подходит и в `withRetry` не попадает даже по типу.
|
|
40
|
+
* Тип выведен из самого списка, поэтому список и тип разойтись не могут
|
|
41
|
+
*/
|
|
42
|
+
type RetryableMethodName = (typeof RETRYABLE_METHOD_NAMES)[number];
|
|
43
|
+
|
|
44
|
+
/** Тип `ReadonlySet<string>`, а не `Set<RetryableMethodName>`: `has()` вызывается с произвольным ключом Proxy */
|
|
45
|
+
export const RETRYABLE_METHODS: ReadonlySet<string> = new Set<string>(RETRYABLE_METHOD_NAMES);
|
|
46
|
+
|
|
47
|
+
/** См. FACADE_METHOD_NAMES */
|
|
48
|
+
export const FACADE_METHODS: ReadonlySet<string> = new Set<string>(FACADE_METHOD_NAMES);
|
|
49
|
+
|
|
50
|
+
/** Метод резолвится в реализацию пакета, а не SDK */
|
|
51
|
+
function isFacadeMethod(prop: string): prop is FacadeMethodName {
|
|
52
|
+
return FACADE_METHODS.has(prop);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Метод фасада оборачивается retry-логикой */
|
|
56
|
+
function isRetryableMethod(prop: string): prop is RetryableMethodName {
|
|
57
|
+
return RETRYABLE_METHODS.has(prop);
|
|
58
|
+
}
|
|
19
59
|
|
|
20
60
|
// ==================== Proxy-обёртка ====================
|
|
21
61
|
|
|
@@ -40,10 +80,12 @@ export function wrapB24WithRetry(b24: B24OAuth): B24Client {
|
|
|
40
80
|
get(target, prop) {
|
|
41
81
|
// Имена фасада проверяем до Reflect.get: пока SDK ещё объявляет свои
|
|
42
82
|
// deprecated-методы, иначе мы отдавали бы их, а не свои
|
|
43
|
-
if (typeof prop === "string" &&
|
|
83
|
+
if (typeof prop === "string" && isFacadeMethod(prop)) {
|
|
44
84
|
if (!methodCache.has(prop)) {
|
|
45
|
-
|
|
46
|
-
|
|
85
|
+
// Индексируем внутри ветки: после сужения prop до RetryableMethodName
|
|
86
|
+
// из типа facade[prop] уходит fetchListMethod, и withRetry принимает
|
|
87
|
+
// остаток без приведения
|
|
88
|
+
methodCache.set(prop, isRetryableMethod(prop) ? withRetry(facade[prop], target, prop) : facade[prop]);
|
|
47
89
|
}
|
|
48
90
|
|
|
49
91
|
return methodCache.get(prop);
|
|
@@ -59,7 +101,7 @@ export function wrapB24WithRetry(b24: B24OAuth): B24Client {
|
|
|
59
101
|
|
|
60
102
|
if (!methodCache.has(prop)) {
|
|
61
103
|
const wrapped = RETRYABLE_METHODS.has(prop)
|
|
62
|
-
? withRetry(value as
|
|
104
|
+
? withRetry(value as AsyncFn, target, prop)
|
|
63
105
|
: value.bind(target);
|
|
64
106
|
methodCache.set(prop, wrapped);
|
|
65
107
|
}
|
package/bitrix24/b24/retry.ts
CHANGED
|
@@ -2,6 +2,18 @@ import { AjaxError, RefreshTokenError, SdkError } from "@bitrix24/b24jssdk";
|
|
|
2
2
|
import { logs } from "../../logs/logs.ts";
|
|
3
3
|
import { isNetworkError, isPreConnectionError } from "../../utils/fetchRetry.ts";
|
|
4
4
|
|
|
5
|
+
// ==================== Типы ====================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Любая асинхронная функция.
|
|
9
|
+
*
|
|
10
|
+
* `never[]` в параметрах делает тип надмножеством всех сигнатур: `never`
|
|
11
|
+
* присваивается чему угодно, поэтому под ограничение подходит и `callMethod`,
|
|
12
|
+
* и `callBatch`. Возврат `Promise<unknown>` отсекает `fetchListMethod` —
|
|
13
|
+
* это async-генератор, и оборачивать его retry-обёрткой нельзя (`for await` ломается).
|
|
14
|
+
*/
|
|
15
|
+
export type AsyncFn = (...args: never[]) => Promise<unknown>;
|
|
16
|
+
|
|
5
17
|
// ==================== Константы ====================
|
|
6
18
|
|
|
7
19
|
/** Количество попыток при сетевых ошибках */
|
|
@@ -302,7 +314,7 @@ function joinReasons(creating: string[], reserving: string[]): string | null {
|
|
|
302
314
|
* не удалось, вызов считается создающим. Ошибиться в сторону лишней осторожности
|
|
303
315
|
* дешевле — потребитель получит ошибку вместо тихого дубликата.
|
|
304
316
|
*/
|
|
305
|
-
export function getRetryBlockReason(sdkMethod: string, args:
|
|
317
|
+
export function getRetryBlockReason(sdkMethod: string, args: readonly unknown[]): string | null {
|
|
306
318
|
const first = args[0];
|
|
307
319
|
|
|
308
320
|
// callMethod(method, params) и callListMethod(method, params, ...)
|
|
@@ -390,8 +402,9 @@ export async function runWithRetry<T>(fn: () => Promise<T>, label: string, block
|
|
|
390
402
|
const proven = isPreConnectionError(error);
|
|
391
403
|
|
|
392
404
|
// Окончательные отказы логируем на error: собственные модули пакета
|
|
393
|
-
// (
|
|
394
|
-
// но не пишут в лог — без этих строк сбой $b24 не виден
|
|
405
|
+
// (Event, Smsgold) ошибку только возвращают вызывающему коду,
|
|
406
|
+
// но не пишут в лог — без этих строк сбой $b24 не виден нигде.
|
|
407
|
+
// errorB24 пишет сама только несетевой отказ, сетевой ждёт от нас
|
|
395
408
|
|
|
396
409
|
// Ветка обмена токена стоит до проверки исчерпания попыток: иначе она
|
|
397
410
|
// сработала бы только на пятой попытке, ради чего всё и затевалось
|
|
@@ -430,10 +443,10 @@ export async function runWithRetry<T>(fn: () => Promise<T>, label: string, block
|
|
|
430
443
|
}
|
|
431
444
|
|
|
432
445
|
/** Оборачивает async-функцию retry-логикой при сетевых ошибках */
|
|
433
|
-
export function withRetry<T extends (
|
|
434
|
-
return (async (...args:
|
|
446
|
+
export function withRetry<T extends AsyncFn>(fn: T, context: object, methodName: string): T {
|
|
447
|
+
return (async (...args: Parameters<T>) => {
|
|
435
448
|
// Состав вызова между попытками не меняется — разбираем один раз
|
|
436
|
-
const blockReason = getRetryBlockReason(methodName, args);
|
|
449
|
+
const blockReason = getRetryBlockReason(methodName, args as readonly unknown[]);
|
|
437
450
|
return runWithRetry(() => fn.apply(context, args), `$b24.${methodName}`, blockReason);
|
|
438
451
|
}) as T;
|
|
439
452
|
}
|
package/bitrix24/b24/tokens.ts
CHANGED
|
@@ -64,9 +64,10 @@ export function saveTokens(authData: AuthData): SaveResult {
|
|
|
64
64
|
confAuthB24.set(APP_ENV, { ...authData, domain: cleanDomain(authData.domain) });
|
|
65
65
|
logs.add("Токены Bitrix24 сохранены", "debug");
|
|
66
66
|
return { error: false, message: "Токены сохранены." };
|
|
67
|
-
} catch (error:
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
} catch (error: unknown) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
logs.add(`Ошибка сохранения токенов: ${message}`, "error");
|
|
70
|
+
return { error: true, message };
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -234,9 +235,10 @@ export async function refreshAndSaveTokens(): Promise<SaveResult> {
|
|
|
234
235
|
try {
|
|
235
236
|
const authData = await refreshAuthWithMutex();
|
|
236
237
|
return saveTokens(authData);
|
|
237
|
-
} catch (error:
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
} catch (error: unknown) {
|
|
239
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
240
|
+
logs.add(`Ошибка обновления токенов: ${message}`, "error");
|
|
241
|
+
return { error: true, message };
|
|
240
242
|
}
|
|
241
243
|
}
|
|
242
244
|
|
package/bitrix24/b24/types.ts
CHANGED
|
@@ -8,6 +8,26 @@ export interface SaveResult {
|
|
|
8
8
|
message: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Минимальный контракт HTTP-запроса, который читает `saveAuthB24Handler`.
|
|
13
|
+
*
|
|
14
|
+
* Своя структура вместо `Request` из `@types/express`: три потребителя
|
|
15
|
+
* (`b24_rnp`, `b24_ai`, `marketing_vk_ads`) `express` не ставят вовсе и жили
|
|
16
|
+
* на нашей копии типов, приезжавшей транзитивно. С 4.0.0 `@types/*` в
|
|
17
|
+
* `devDependencies`, поэтому такой копии больше нет.
|
|
18
|
+
*
|
|
19
|
+
* Express подставляет сюда свой `Request` без правок у потребителя:
|
|
20
|
+
* параметры контравариантны, и `Request.body: any` подходит под `body: unknown`.
|
|
21
|
+
*/
|
|
22
|
+
export interface AuthSaveRequest {
|
|
23
|
+
body: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Минимальный контракт HTTP-ответа, который пишет `saveAuthB24Handler` */
|
|
27
|
+
export interface AuthSaveResponse {
|
|
28
|
+
status(code: number): { json(body: unknown): unknown };
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
/**
|
|
12
32
|
* `B24OAuth` плюс пять методов, которые пакет реализует сам поверх `actions.v2.*`.
|
|
13
33
|
*
|
|
@@ -24,6 +44,13 @@ export interface SaveResult {
|
|
|
24
44
|
* SDK 2.2.0 останется зелёным, то есть пропажа обнаружится только у потребителя.
|
|
25
45
|
* Страховка от «уборки дублей» — `tests/client-type.test.ts`: он читает текст этого
|
|
26
46
|
* файла и падает, если хоть одно объявление отсюда исчезло.
|
|
47
|
+
*
|
|
48
|
+
* `any` в сигнатурах оставлен НАМЕРЕННО и снятию по T-53 не подлежит: это дефолты
|
|
49
|
+
* дженериков публичной поверхности, дословно повторяющие `AbstractB24`. На них стоит
|
|
50
|
+
* код потребителей — `for await (const chunk of $b24.fetchListMethod(...))
|
|
51
|
+
* chunk.forEach((x) => x.ID)` и `callBatch([["crm.deal.get", { id }]])`. `unknown`
|
|
52
|
+
* в дефолте уронил бы каждое такое место, ничего не дав взамен: узкий тип задаётся
|
|
53
|
+
* параметром типа на месте вызова (`callMethod<Deal>(...)`).
|
|
27
54
|
*/
|
|
28
55
|
export interface B24Client extends B24OAuth {
|
|
29
56
|
callMethod<T = any>(method: string, params?: object, start?: number): Promise<AjaxResult<T>>;
|
package/bitrix24/b24.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type { B24OAuth } from "@bitrix24/b24jssdk";
|
|
2
|
-
import type { Request, Response } from "express";
|
|
3
2
|
import { logs } from "../logs/logs.ts";
|
|
4
3
|
|
|
5
4
|
import { createB24Instance } from "./b24/instance.ts";
|
|
6
5
|
import { wrapB24WithRetry } from "./b24/proxy.ts";
|
|
7
6
|
import { $b24, setB24Instance } from "./b24/state.ts";
|
|
8
7
|
import { refreshAndSaveTokens, resetRefreshMutex, saveTokens, startProactiveRefresh, stopProactiveRefresh } from "./b24/tokens.ts";
|
|
9
|
-
import type { SaveResult } from "./b24/types.ts";
|
|
8
|
+
import type { AuthSaveRequest, AuthSaveResponse, SaveResult } from "./b24/types.ts";
|
|
10
9
|
|
|
11
10
|
// ==================== Реэкспорт публичного API ====================
|
|
12
11
|
|
|
@@ -62,12 +61,43 @@ export async function reinitializeB24(): Promise<SaveResult> {
|
|
|
62
61
|
return refreshResult;
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
/** Непустая строка — единственная форма, пригодная для поля токена */
|
|
65
|
+
function isFilledString(value: unknown): value is string {
|
|
66
|
+
return typeof value === "string" && value.length > 0;
|
|
67
|
+
}
|
|
69
68
|
|
|
70
|
-
|
|
69
|
+
/**
|
|
70
|
+
* HTTP-обработчик для сохранения токенов с фронта.
|
|
71
|
+
*
|
|
72
|
+
* Типы запроса и ответа — собственные структурные (`AuthSaveRequest`/`AuthSaveResponse`),
|
|
73
|
+
* а не `Request`/`Response` из `@types/express`: пакет больше не тянет типы express
|
|
74
|
+
* потребителю. Express подставляет свои объекты сюда без правок на его стороне
|
|
75
|
+
*/
|
|
76
|
+
export async function saveAuthB24Handler(req: AuthSaveRequest, res: AuthSaveResponse): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
// req.body теперь unknown — разбираем явно, деструктуризация по нему не пройдёт
|
|
79
|
+
const body = (req.body ?? {}) as Record<string, unknown>;
|
|
80
|
+
const access_token = body["access_token"];
|
|
81
|
+
const refresh_token = body["refresh_token"];
|
|
82
|
+
const domain = body["domain"];
|
|
83
|
+
const expires_in = body["expires_in"];
|
|
84
|
+
const member_id = body["member_id"];
|
|
85
|
+
|
|
86
|
+
// expires_in участвует и в самом объекте токенов, и в расчёте expires —
|
|
87
|
+
// приводим один раз в отдельную переменную
|
|
88
|
+
const expiresIn = Number(expires_in);
|
|
89
|
+
|
|
90
|
+
// Проверяем именно тип, а не «истинность» после String(): приведение объекта
|
|
91
|
+
// дало бы непустую строку "[object Object]", она прошла бы валидацию
|
|
92
|
+
// в saveTokens и затёрла рабочие токены в authB24.json
|
|
93
|
+
if (
|
|
94
|
+
!isFilledString(access_token) ||
|
|
95
|
+
!isFilledString(refresh_token) ||
|
|
96
|
+
!isFilledString(domain) ||
|
|
97
|
+
!isFilledString(member_id) ||
|
|
98
|
+
!Number.isFinite(expiresIn) ||
|
|
99
|
+
expiresIn <= 0
|
|
100
|
+
) {
|
|
71
101
|
res.status(400).json({ status: "error", message: "Не заполнены обязательные поля." });
|
|
72
102
|
return;
|
|
73
103
|
}
|
|
@@ -76,9 +106,9 @@ export async function saveAuthB24Handler(req: Request, res: Response): Promise<v
|
|
|
76
106
|
access_token,
|
|
77
107
|
refresh_token,
|
|
78
108
|
domain,
|
|
79
|
-
expires_in,
|
|
109
|
+
expires_in: expiresIn,
|
|
80
110
|
member_id,
|
|
81
|
-
expires: Math.floor(Date.now() / 1000) +
|
|
111
|
+
expires: Math.floor(Date.now() / 1000) + expiresIn,
|
|
82
112
|
});
|
|
83
113
|
|
|
84
114
|
if (result.error) {
|
|
@@ -100,9 +130,10 @@ export async function saveAuthB24Handler(req: Request, res: Response): Promise<v
|
|
|
100
130
|
} else {
|
|
101
131
|
res.status(201).json({ status: "ok", message: "Токены сохранены и применены." });
|
|
102
132
|
}
|
|
103
|
-
} catch (error:
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
} catch (error: unknown) {
|
|
134
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
135
|
+
logs.add(`Ошибка в saveAuthB24Handler: ${message}`, "error");
|
|
136
|
+
res.status(500).json({ status: "error", message });
|
|
106
137
|
}
|
|
107
138
|
}
|
|
108
139
|
|