@andrey4emk/npm-app-back-b24 2.0.13 → 3.0.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 +37 -7
- package/bitrix24/b24.ts +114 -34
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
$b24,
|
|
34
34
|
saveAuthB24Handler,
|
|
35
35
|
refreshAndSaveTokens,
|
|
36
|
+
reinitializeB24,
|
|
37
|
+
stopProactiveRefresh,
|
|
36
38
|
errorB24,
|
|
37
39
|
event,
|
|
38
40
|
Event,
|
|
@@ -116,16 +118,20 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
116
118
|
|
|
117
119
|
**Retry при сетевых ошибках:**
|
|
118
120
|
|
|
119
|
-
Экземпляр `$b24` обёрнут Proxy, который автоматически повторяет запросы при сетевых ошибках. Повторные попытки применяются к методам `callMethod`, `callListMethod`, `callBatch` и `fetchListMethod
|
|
121
|
+
Экземпляр `$b24` обёрнут Proxy, который автоматически повторяет запросы при сетевых ошибках. Повторные попытки применяются к методам `callMethod`, `callListMethod`, `callBatch` и `fetchListMethod`, а также к обновлению токена (`refreshAuth`).
|
|
120
122
|
|
|
121
|
-
| Параметр | Значение
|
|
122
|
-
| -------------- |
|
|
123
|
-
| Попыток | 5
|
|
124
|
-
| Задержка | 500 мс
|
|
123
|
+
| Параметр | Значение |
|
|
124
|
+
| -------------- | ----------------------------------------- |
|
|
125
|
+
| Попыток | 5 |
|
|
126
|
+
| Задержка | 500 мс (экспоненциально для refreshAuth) |
|
|
125
127
|
|
|
126
128
|
Retry срабатывает только при сетевых проблемах (`ECONNRESET`, `ETIMEDOUT`, `ERR_NETWORK` и т.д.). HTTP-ошибки (400, 500) и ошибки бизнес-логики Bitrix24 **не** вызывают повторных попыток. Каждая неудачная попытка логируется через `logs.add()` с уровнем `error`.
|
|
127
129
|
|
|
128
|
-
|
|
130
|
+
**Проактивное обновление токена:**
|
|
131
|
+
|
|
132
|
+
Токен автоматически обновляется каждые 25 минут. Если обновление не удалось — повтор через 2 минуты (вместо ожидания следующих 25 минут), что предотвращает протухание токена.
|
|
133
|
+
|
|
134
|
+
- **`saveAuthB24Handler(req, res)`** — HTTP-обработчик для сохранения набора токенов из `req.body`. После сохранения автоматически переинициализирует `$b24` — перезапуск сервера не требуется.
|
|
129
135
|
|
|
130
136
|
- **Параметры:**
|
|
131
137
|
- `req` (Express Request) — объект запроса с полем `body`.
|
|
@@ -157,12 +163,36 @@ const response = await fetchRetry("https://api.example.com/data", { method: "GET
|
|
|
157
163
|
}
|
|
158
164
|
```
|
|
159
165
|
|
|
160
|
-
- **`saveTokens(authData)`** — низкоуровневая функция для прямого сохранения токенов в файл.
|
|
166
|
+
- **`saveTokens(authData)`** — низкоуровневая функция для прямого сохранения токенов в файл. Перед записью валидирует данные — если `access_token`, `refresh_token`, `domain` или `expires` пустые/отсутствуют, операция отклоняется.
|
|
161
167
|
|
|
162
168
|
- **Параметры:**
|
|
163
169
|
- `authData` (AuthData) — объект с полями `access_token`, `refresh_token`, `domain`, `expires_in`, `member_id`, `expires`.
|
|
164
170
|
- **Возвращает:** объект `{ error: boolean, message: string }`.
|
|
165
171
|
|
|
172
|
+
- **`reinitializeB24()`** — пересоздаёт экземпляр `$b24` из актуального `authB24.json` без перезапуска сервера. Останавливает текущий проактивный таймер, создаёт новый `B24OAuth`, настраивает колбэк автосохранения, обновляет токены и запускает таймер заново.
|
|
173
|
+
|
|
174
|
+
- **Возвращает:** промис с объектом `{ error: boolean, message: string }`.
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
import { reinitializeB24 } from "@andrey4emk/npm-app-back-b24";
|
|
178
|
+
|
|
179
|
+
const result = await reinitializeB24();
|
|
180
|
+
if (!result.error) {
|
|
181
|
+
console.log("$b24 переинициализирован");
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- **`stopProactiveRefresh()`** — останавливает проактивный таймер обновления токена. Используется при graceful shutdown.
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
import { stopProactiveRefresh } from "@andrey4emk/npm-app-back-b24";
|
|
189
|
+
|
|
190
|
+
process.on("SIGTERM", () => {
|
|
191
|
+
stopProactiveRefresh();
|
|
192
|
+
process.exit(0);
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
166
196
|
### Задачи ошибок
|
|
167
197
|
|
|
168
198
|
- **`errorB24(dataTask)`** — создаёт служебную задачу в Bitrix24 при ошибках/событиях. Использует глобальный `$b24`.
|
package/bitrix24/b24.ts
CHANGED
|
@@ -26,6 +26,8 @@ const CLIENT_SECRET = APP_ENV === "DEV" ? process.env.APP_B24_CLIENT_SECRET_DEV
|
|
|
26
26
|
|
|
27
27
|
/** 25 минут при TTL токена 30 минут */
|
|
28
28
|
const PROACTIVE_REFRESH_INTERVAL_MS = 25 * 60 * 1000;
|
|
29
|
+
/** 2 минуты — ускоренный интервал при ошибке обновления */
|
|
30
|
+
const PROACTIVE_RETRY_ON_ERROR_MS = 2 * 60 * 1000;
|
|
29
31
|
|
|
30
32
|
/** Количество попыток при сетевых ошибках */
|
|
31
33
|
const RETRY_COUNT = 5;
|
|
@@ -94,9 +96,26 @@ function createB24Instance(): B24OAuth | null {
|
|
|
94
96
|
|
|
95
97
|
// ==================== Работа с токенами ====================
|
|
96
98
|
|
|
99
|
+
/** Проверяет, что authData содержит все обязательные непустые поля */
|
|
100
|
+
function isValidAuthData(data: AuthData): boolean {
|
|
101
|
+
return !!(
|
|
102
|
+
data &&
|
|
103
|
+
typeof data.access_token === "string" && data.access_token.length > 0 &&
|
|
104
|
+
typeof data.refresh_token === "string" && data.refresh_token.length > 0 &&
|
|
105
|
+
typeof data.domain === "string" && data.domain.length > 0 &&
|
|
106
|
+
data.expires
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
97
110
|
/** Сохраняет токены в authB24.json, нормализуя домен без протокола */
|
|
98
111
|
export function saveTokens(authData: AuthData): SaveResult {
|
|
99
112
|
try {
|
|
113
|
+
if (!isValidAuthData(authData)) {
|
|
114
|
+
const msg = "Попытка сохранить невалидные токены — операция отклонена";
|
|
115
|
+
logs.add(msg, "error");
|
|
116
|
+
return { error: true, message: msg };
|
|
117
|
+
}
|
|
118
|
+
|
|
100
119
|
confAuthB24.set(APP_ENV, { ...authData, domain: cleanDomain(authData.domain) });
|
|
101
120
|
logs.add("Токены Bitrix24 сохранены", "debug");
|
|
102
121
|
return { error: false, message: "Токены сохранены." };
|
|
@@ -107,27 +126,38 @@ export function saveTokens(authData: AuthData): SaveResult {
|
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
/** HTTP-обработчик для сохранения токенов с фронта */
|
|
110
|
-
export function saveAuthB24Handler(req: Request, res: Response): void {
|
|
111
|
-
|
|
129
|
+
export async function saveAuthB24Handler(req: Request, res: Response): Promise<void> {
|
|
130
|
+
try {
|
|
131
|
+
const { access_token, refresh_token, domain, expires_in, member_id } = req.body;
|
|
112
132
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
if (!access_token || !refresh_token || !domain || !expires_in || !member_id) {
|
|
134
|
+
res.status(400).json({ status: "error", message: "Не заполнены обязательные поля." });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
117
137
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
138
|
+
const result = saveTokens({
|
|
139
|
+
access_token,
|
|
140
|
+
refresh_token,
|
|
141
|
+
domain,
|
|
142
|
+
expires_in,
|
|
143
|
+
member_id,
|
|
144
|
+
expires: Math.floor(Date.now() / 1000) + expires_in,
|
|
145
|
+
});
|
|
126
146
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
147
|
+
if (result.error) {
|
|
148
|
+
res.status(500).json({ status: "error", message: result.message });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const reinitResult = await reinitializeB24();
|
|
153
|
+
if (reinitResult.error) {
|
|
154
|
+
res.status(201).json({ status: "ok", message: `Токены сохранены, но $b24 не переинициализирован: ${reinitResult.message}` });
|
|
155
|
+
} else {
|
|
156
|
+
res.status(201).json({ status: "ok", message: "Токены сохранены и применены." });
|
|
157
|
+
}
|
|
158
|
+
} catch (error: any) {
|
|
159
|
+
logs.add(`Ошибка в saveAuthB24Handler: ${error.message}`, "error");
|
|
160
|
+
res.status(500).json({ status: "error", message: error.message });
|
|
131
161
|
}
|
|
132
162
|
}
|
|
133
163
|
|
|
@@ -149,7 +179,21 @@ async function refreshAuthWithMutex(): Promise<AuthData> {
|
|
|
149
179
|
refreshInProgress = (async () => {
|
|
150
180
|
try {
|
|
151
181
|
if (!$b24) throw new Error("$b24 не инициализирован");
|
|
152
|
-
|
|
182
|
+
|
|
183
|
+
let lastError: unknown;
|
|
184
|
+
for (let attempt = 1; attempt <= RETRY_COUNT; attempt++) {
|
|
185
|
+
try {
|
|
186
|
+
return await $b24.auth.refreshAuth();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
lastError = error;
|
|
189
|
+
if (!isB24NetworkError(error) || attempt === RETRY_COUNT) throw error;
|
|
190
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
191
|
+
const delayMs = RETRY_DELAY_MS * attempt;
|
|
192
|
+
logs.add(`refreshAuth: попытка ${attempt}/${RETRY_COUNT} не удалась (${msg}), повтор через ${delayMs}мс`, "error");
|
|
193
|
+
await delay(delayMs);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
throw lastError;
|
|
153
197
|
} finally {
|
|
154
198
|
refreshInProgress = null;
|
|
155
199
|
}
|
|
@@ -178,27 +222,37 @@ export async function refreshAndSaveTokens(): Promise<SaveResult> {
|
|
|
178
222
|
|
|
179
223
|
// ==================== Проактивное обновление токена ====================
|
|
180
224
|
|
|
181
|
-
let proactiveRefreshTimer: ReturnType<typeof
|
|
225
|
+
let proactiveRefreshTimer: ReturnType<typeof setTimeout> | null = null;
|
|
182
226
|
|
|
183
|
-
/** Запускает проактивное обновление
|
|
227
|
+
/** Запускает проактивное обновление токена. При ошибке — повтор через 2 мин, при успехе — через 25 мин */
|
|
184
228
|
function startProactiveRefresh(): void {
|
|
185
|
-
if (proactiveRefreshTimer)
|
|
229
|
+
if (proactiveRefreshTimer) {
|
|
230
|
+
clearTimeout(proactiveRefreshTimer);
|
|
231
|
+
proactiveRefreshTimer = null;
|
|
232
|
+
}
|
|
186
233
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
logs.add(`Ошибка проактивного обновления токена: ${result.message}`, "error");
|
|
192
|
-
}
|
|
193
|
-
}, PROACTIVE_REFRESH_INTERVAL_MS);
|
|
234
|
+
const scheduleNext = (delayMs: number) => {
|
|
235
|
+
proactiveRefreshTimer = setTimeout(async () => {
|
|
236
|
+
logs.add("Проактивное обновление токена Bitrix24", "debug");
|
|
237
|
+
const result = await refreshAndSaveTokens();
|
|
194
238
|
|
|
239
|
+
if (result.error) {
|
|
240
|
+
logs.add(`Ошибка проактивного обновления токена, повтор через ${PROACTIVE_RETRY_ON_ERROR_MS / 60000} мин`, "error");
|
|
241
|
+
scheduleNext(PROACTIVE_RETRY_ON_ERROR_MS);
|
|
242
|
+
} else {
|
|
243
|
+
scheduleNext(PROACTIVE_REFRESH_INTERVAL_MS);
|
|
244
|
+
}
|
|
245
|
+
}, delayMs);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
scheduleNext(PROACTIVE_REFRESH_INTERVAL_MS);
|
|
195
249
|
logs.add(`Проактивное обновление токена запущено (каждые ${PROACTIVE_REFRESH_INTERVAL_MS / 60000} мин)`, "debug");
|
|
196
250
|
}
|
|
197
251
|
|
|
198
252
|
/** Останавливает проактивное обновление токена */
|
|
199
253
|
export function stopProactiveRefresh(): void {
|
|
200
254
|
if (!proactiveRefreshTimer) return;
|
|
201
|
-
|
|
255
|
+
clearTimeout(proactiveRefreshTimer);
|
|
202
256
|
proactiveRefreshTimer = null;
|
|
203
257
|
logs.add("Проактивное обновление токена остановлено", "debug");
|
|
204
258
|
}
|
|
@@ -281,11 +335,12 @@ function wrapB24WithRetry(b24: B24OAuth): B24OAuth {
|
|
|
281
335
|
|
|
282
336
|
// ==================== Инициализация ====================
|
|
283
337
|
|
|
284
|
-
|
|
285
|
-
export
|
|
338
|
+
let _b24Raw = createB24Instance();
|
|
339
|
+
export let $b24 = _b24Raw ? wrapB24WithRetry(_b24Raw) : null;
|
|
286
340
|
|
|
287
|
-
|
|
288
|
-
|
|
341
|
+
/** Настраивает колбэк автосохранения токенов на экземпляре B24OAuth */
|
|
342
|
+
function setupRefreshCallback(raw: B24OAuth): void {
|
|
343
|
+
raw.setCallbackRefreshAuth(async ({ authData }) => {
|
|
289
344
|
const result = saveTokens(authData);
|
|
290
345
|
if (result.error) {
|
|
291
346
|
logs.add(`Ошибка при автосохранении токенов: ${result.message}`, "error");
|
|
@@ -293,6 +348,31 @@ if (_b24Raw) {
|
|
|
293
348
|
logs.add("Токены автоматически обновлены и сохранены в authB24.json", "debug");
|
|
294
349
|
}
|
|
295
350
|
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Пересоздаёт $b24 из актуального authB24.json без перезапуска сервера */
|
|
354
|
+
export async function reinitializeB24(): Promise<SaveResult> {
|
|
355
|
+
stopProactiveRefresh();
|
|
296
356
|
|
|
357
|
+
_b24Raw = createB24Instance();
|
|
358
|
+
$b24 = _b24Raw ? wrapB24WithRetry(_b24Raw) : null;
|
|
359
|
+
|
|
360
|
+
if (!_b24Raw || !$b24) {
|
|
361
|
+
return { error: true, message: "Не удалось пересоздать $b24 — проверь authB24.json" };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
setupRefreshCallback(_b24Raw);
|
|
365
|
+
|
|
366
|
+
const refreshResult = await refreshAndSaveTokens();
|
|
367
|
+
startProactiveRefresh();
|
|
368
|
+
|
|
369
|
+
logs.add("$b24 переинициализирован с новыми токенами", "debug");
|
|
370
|
+
return refreshResult;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ==================== Начальная инициализация ====================
|
|
374
|
+
|
|
375
|
+
if (_b24Raw) {
|
|
376
|
+
setupRefreshCallback(_b24Raw);
|
|
297
377
|
refreshAndSaveTokens().then(() => startProactiveRefresh());
|
|
298
378
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrey4emk/npm-app-back-b24",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Bitrix24 OAuth helpers for Node.js projects",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"utils/fetchRetry.ts"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@bitrix24/b24jssdk": "^0.
|
|
38
|
+
"@bitrix24/b24jssdk": "^1.0.4",
|
|
39
39
|
"@types/express": "^5.0.6",
|
|
40
40
|
"@types/luxon": "^3.7.1",
|
|
41
41
|
"@types/node": "^25.0.10",
|