@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.
Files changed (3) hide show
  1. package/README.md +37 -7
  2. package/bitrix24/b24.ts +114 -34
  3. 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
- - **`saveAuthB24Handler(req, res)`** — HTTP-обработчик для сохранения набора токенов из `req.body`.
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
- const { access_token, refresh_token, domain, expires_in, member_id } = req.body;
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
- if (!access_token || !refresh_token || !domain || !expires_in || !member_id) {
114
- res.status(400).json({ status: "error", message: "Не заполнены обязательные поля." });
115
- return;
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
- const result = saveTokens({
119
- access_token,
120
- refresh_token,
121
- domain,
122
- expires_in,
123
- member_id,
124
- expires: Math.floor(Date.now() / 1000) + expires_in,
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
- if (result.error) {
128
- res.status(500).json({ status: "error", message: result.message });
129
- } else {
130
- res.status(201).json({ status: "ok", message: "Токены сохранены. Перезапустите сервер." });
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
- return await $b24.auth.refreshAuth();
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 setInterval> | null = null;
225
+ let proactiveRefreshTimer: ReturnType<typeof setTimeout> | null = null;
182
226
 
183
- /** Запускает проактивное обновление токена с заданным интервалом */
227
+ /** Запускает проактивное обновление токена. При ошибке повтор через 2 мин, при успехе — через 25 мин */
184
228
  function startProactiveRefresh(): void {
185
- if (proactiveRefreshTimer) clearInterval(proactiveRefreshTimer);
229
+ if (proactiveRefreshTimer) {
230
+ clearTimeout(proactiveRefreshTimer);
231
+ proactiveRefreshTimer = null;
232
+ }
186
233
 
187
- proactiveRefreshTimer = setInterval(async () => {
188
- logs.add("Проактивное обновление токена Bitrix24", "debug");
189
- const result = await refreshAndSaveTokens();
190
- if (result.error) {
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
- clearInterval(proactiveRefreshTimer);
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
- const _b24Raw = createB24Instance();
285
- export const $b24 = _b24Raw ? wrapB24WithRetry(_b24Raw) : null;
338
+ let _b24Raw = createB24Instance();
339
+ export let $b24 = _b24Raw ? wrapB24WithRetry(_b24Raw) : null;
286
340
 
287
- if (_b24Raw) {
288
- _b24Raw.setCallbackRefreshAuth(async ({ authData }) => {
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": "2.0.13",
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.5.1",
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",