@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.
@@ -1,5 +1,6 @@
1
1
  import { DateTime } from "luxon";
2
2
  import { $b24, getResultData } from "./b24.ts";
3
+ import { isB24NetworkError } from "./b24/retry.ts";
3
4
  import { logs } from "../logs/logs.ts";
4
5
  import dotEnv from "dotenv";
5
6
 
@@ -14,6 +15,10 @@ export interface ErrorTaskData {
14
15
  createdBy?: number;
15
16
  responsibleId?: number;
16
17
  deadline?: string;
18
+ /**
19
+ * ID группы задачи. Поле не передано — берётся `B24_ERROR_TASK_GROUP_ID`;
20
+ * явные `null` и `0` означают «без группы» и дефолт не подхватывают
21
+ */
17
22
  groupId?: number | null;
18
23
  accomplices?: number[];
19
24
  maxTasks?: number;
@@ -27,7 +32,8 @@ export interface ErrorTaskData {
27
32
  interface ErrorB24Result {
28
33
  error: boolean;
29
34
  message: string;
30
- data?: any;
35
+ /** Ответ портала на tasks.task.add как есть — форму задаёт сам портал, поэтому unknown */
36
+ data?: unknown;
31
37
  }
32
38
 
33
39
  /** Задача из Bitrix24 */
@@ -40,6 +46,76 @@ interface TaskItem {
40
46
 
41
47
  const appName: string = process.env.APP_NAME || "Задай название приложения в .env";
42
48
 
49
+ /** Имя переменной окружения с группой по умолчанию для задач об ошибках */
50
+ const ERROR_TASK_GROUP_ENV = "B24_ERROR_TASK_GROUP_ID";
51
+
52
+ /** Разобранная группа по умолчанию плюс текст претензии к значению, если она есть */
53
+ interface DefaultGroup {
54
+ /** ID группы либо `null` — «без группы» */
55
+ id: number | null;
56
+ /** Текст строки `warn`; `null` — предупреждать не о чем */
57
+ warning: string | null;
58
+ }
59
+
60
+ /**
61
+ * Разбирает значение `B24_ERROR_TASK_GROUP_ID`.
62
+ *
63
+ * `Number`, а не `parseInt`: последний молча съел бы «196abc» и вернул 196.
64
+ * Непригодное значение группой не становится — задача создаётся без группы,
65
+ * как до 4.0.1.
66
+ */
67
+ function parseDefaultGroupId(raw: string | undefined): DefaultGroup {
68
+ const value = (raw ?? "").trim();
69
+
70
+ if (value === "") {
71
+ return {
72
+ id: null,
73
+ warning: `${ERROR_TASK_GROUP_ENV} не задана — задачи об ошибках создаются без группы`,
74
+ };
75
+ }
76
+
77
+ const parsed = Number(value);
78
+
79
+ if (!Number.isInteger(parsed) || parsed <= 0) {
80
+ return {
81
+ id: null,
82
+ warning: `${ERROR_TASK_GROUP_ENV}: значение '${value}' непригодно (нужно целое больше нуля) — задачи об ошибках создаются без группы`,
83
+ };
84
+ }
85
+
86
+ return { id: parsed, warning: null };
87
+ }
88
+
89
+ /**
90
+ * Значение читается один раз при загрузке модуля — тем же правилом, что и бюджеты
91
+ * таймаутов в `utils/fetchRetry.ts`. Правка `process.env` в рантайме дефолт не двигает.
92
+ */
93
+ const defaultGroup: DefaultGroup = parseDefaultGroupId(process.env[ERROR_TASK_GROUP_ENV]);
94
+
95
+ /** Строка `warn` о непригодном дефолте пишется один раз за процесс, а не на каждый вызов */
96
+ let defaultGroupWarned = false;
97
+
98
+ /**
99
+ * Выбирает группу задачи.
100
+ *
101
+ * Явно переданное значение приоритетнее дефолта: `null` и `0` означают осознанное
102
+ * «без группы» (так же вёл себя прежний `dataTask.groupId || null`), а отсутствие поля
103
+ * и `undefined` — «решай сам», то есть дефолт из окружения.
104
+ *
105
+ * Предупреждение пишется только когда дефолт реально понадобился: у потребителя,
106
+ * который всегда передаёт группу сам, в логе не появится ни строки.
107
+ */
108
+ function resolveGroupId(explicit: number | null | undefined): number | null {
109
+ if (explicit !== undefined) return explicit || null;
110
+
111
+ if (defaultGroup.warning !== null && !defaultGroupWarned) {
112
+ defaultGroupWarned = true;
113
+ logs.add(defaultGroup.warning, "warn");
114
+ }
115
+
116
+ return defaultGroup.id;
117
+ }
118
+
43
119
  // ==================== Функция ====================
44
120
 
45
121
  /**
@@ -65,8 +141,9 @@ export async function checkB24Scope(): Promise<void> {
65
141
  if (missingScopes.length > 0) {
66
142
  logs.add(`Отсутствуют скоупы: ${missingScopes.join(", ")}`, "error");
67
143
  }
68
- } catch (error: any) {
69
- logs.add(`Ошибка при получении скоупов: ${error.message}`, "error");
144
+ } catch (error: unknown) {
145
+ const message = error instanceof Error ? error.message : String(error);
146
+ logs.add(`Ошибка при получении скоупов: ${message}`, "error");
70
147
  }
71
148
  }
72
149
 
@@ -83,13 +160,16 @@ export async function errorB24(dataTask: ErrorTaskData): Promise<ErrorB24Result>
83
160
  };
84
161
  }
85
162
 
163
+ // Резолв группы стоит до `try`: сама функция не бросает, а номер группы нужен
164
+ // в `catch` — без него по строке отказа не понять, куда задача не попала
165
+ const groupId = resolveGroupId(dataTask.groupId);
166
+
86
167
  try {
87
168
  const title = dataTask.title;
88
169
  const description = dataTask.description || "";
89
170
  const createdBy = dataTask.createdBy || 138;
90
171
  const responsibleId = dataTask.responsibleId || 1;
91
172
  const deadline = dataTask.deadline || DateTime.now().plus({ days: 1 }).toISO();
92
- const groupId = dataTask.groupId || null;
93
173
  const accomplices = dataTask.accomplices || [];
94
174
  const maxTasks = dataTask.maxTasks || 100;
95
175
  const ufCrmTask = dataTask.ufCrmTask ?? dataTask.entityTypeAbbr ?? "";
@@ -133,10 +213,24 @@ export async function errorB24(dataTask: ErrorTaskData): Promise<ErrorB24Result>
133
213
  message: "Задача создана в Битрикс24.",
134
214
  data: createdTask,
135
215
  };
136
- } catch (error: any) {
216
+ } catch (error: unknown) {
217
+ const message = error instanceof Error ? error.message : String(error);
218
+
219
+ // Отказ портала иначе не виден нигде: несетевую ошибку retry-слой отдаёт
220
+ // наружу как есть, без единой строки в логе, а `errorB24()` зовут последним
221
+ // средством и её результат обычно не читают — отказ прав или неверный номер
222
+ // группы уходил бы в тишину.
223
+ //
224
+ // Условие обязательно: сетевой отказ retry-слой уже записал сам — строкой
225
+ // про исчерпание попыток либо про отменённый повтор. Без проверки вторая
226
+ // строка ушла бы дублем в чат B24, куда уходит уровень `error`
227
+ if (!isB24NetworkError(error)) {
228
+ logs.add(`errorB24: задача «${dataTask.title}» не создана (группа ${groupId ?? "не задана"}) — ${message}`, "error");
229
+ }
230
+
137
231
  return {
138
232
  error: true,
139
- message: `Не удалось создать задачу в Битрикс24: ${error.message}`,
233
+ message: `Не удалось создать задачу в Битрикс24: ${message}`,
140
234
  };
141
235
  }
142
236
  }
@@ -20,9 +20,21 @@ export interface ConnectorMessage {
20
20
  lineId: string;
21
21
  chatId: string;
22
22
  text: string | null;
23
- file: any[] | null;
24
- attachments: any[] | null;
25
- im: any | null;
23
+ file: TelegramFile[] | null;
24
+ attachments: unknown[] | null;
25
+ im: Record<string, unknown> | null;
26
+ }
27
+
28
+ /**
29
+ * Файл сообщения, приведённый к формату Telegram.
30
+ *
31
+ * Раньше метод отдавал `any[]`, и единственный потребитель (`b24_conector`)
32
+ * объявлял такую же структуру у себя. Тип экспортируется, чтобы объявление было одно
33
+ */
34
+ export interface TelegramFile {
35
+ type: "image" | "video" | "file" | "document";
36
+ downloadLink: string;
37
+ name: string;
26
38
  }
27
39
 
28
40
  /** События коннектора */
@@ -48,7 +60,12 @@ export interface EntityEvents {
48
60
  interface StandardResult {
49
61
  error: boolean;
50
62
  message: string;
51
- data: any;
63
+ /**
64
+ * `ConnectorEvents`, `EntityEvents` либо `null` — какой именно, зависит от имени
65
+ * события, переданного в `get()`. Разобрать это типом нельзя, поэтому `unknown`:
66
+ * потребитель приводит результат сам, как и делал до 4.0.0
67
+ */
68
+ data: unknown;
52
69
  }
53
70
 
54
71
  /** Офлайн-событие Bitrix24 */
@@ -56,7 +73,7 @@ interface OfflineEvent {
56
73
  ID: string;
57
74
  MESSAGE_ID: string;
58
75
  EVENT_NAME: string;
59
- EVENT_DATA: any;
76
+ EVENT_DATA: unknown;
60
77
  EVENT_ADDITIONAL: {
61
78
  user_id: string;
62
79
  };
@@ -105,7 +122,7 @@ const SYSTEM_USER_ID = "138";
105
122
  * внутри записи: защита только одной из них читалась бы как решение «здесь
106
123
  * нормализация не нужна», а словарь на месте списка ронял бы весь вызов.
107
124
  */
108
- function normalizeCollection(value: unknown): any[] {
125
+ function normalizeCollection(value: unknown): unknown[] {
109
126
  if (Array.isArray(value)) return value;
110
127
  if (value && typeof value === "object") return Object.values(value);
111
128
  return [];
@@ -234,15 +251,23 @@ export class Event {
234
251
  /**
235
252
  * Преобразует файлы из формата Bitrix24 в формат TelegramFile
236
253
  */
237
- private convertB24FilesToTelegramFormat(b24Files: any[] | null | undefined): any[] | null {
254
+ private convertB24FilesToTelegramFormat(b24Files: unknown[] | null | undefined): TelegramFile[] | null {
238
255
  if (!b24Files || !Array.isArray(b24Files) || b24Files.length === 0) {
239
256
  return null;
240
257
  }
241
258
 
242
- return b24Files.map((f: any) => {
243
- const fileName = f.name || "";
244
- const link = f.link || f.url || "";
245
- const fileType = this.detectFileType(fileName, f.type);
259
+ return b24Files.map((rawFile) => {
260
+ // Элемент приходит как unknown: сужаем через asRecord, как и запись очереди.
261
+ // Битый элемент (null, строка) даёт пустые имя и ссылку — по той же причине,
262
+ // по которой устойчив разбор записи: исключение здесь остановило бы очередь
263
+ const file = asRecord(rawFile) ?? {};
264
+
265
+ // Проверка типа, а не приведение: нестроковое `name` уронило бы
266
+ // detectFileType на toLowerCase(), get() вернул бы ошибку — и очередь встала
267
+ const fileName = typeof file["name"] === "string" ? file["name"] : "";
268
+ const link = typeof file["link"] === "string" ? file["link"] : typeof file["url"] === "string" ? file["url"] : "";
269
+ const rawType = file["type"];
270
+ const fileType = this.detectFileType(fileName, typeof rawType === "string" ? rawType : undefined);
246
271
 
247
272
  return {
248
273
  type: fileType,
@@ -348,24 +373,33 @@ export class Event {
348
373
  // здесь означало бы, что get() вернёт ошибку, потребитель не вызовет
349
374
  // clear(), и те же события придут следующим опросом — очередь встала бы
350
375
  // навсегда на одном кривом сообщении
351
- data.message = allEvents.flatMap((event) => {
352
- const eventData = event.EVENT_DATA ?? {};
353
-
354
- return normalizeCollection(eventData.MESSAGES)
355
- .filter((messageData) => messageData?.chat && messageData?.message)
356
- .map((messageData) => ({
357
- // Портал отдаёт идентификаторы то строкой, то числом —
358
- // приводим, чтобы тип `string` не врал
359
- messageId: String(event.MESSAGE_ID ?? ""),
360
- eventId: String(event.ID ?? ""),
361
- connectorId: eventData.CONNECTOR,
362
- lineId: eventData.LINE,
363
- chatId: messageData.chat.id,
364
- text: messageData.message.text || null,
365
- file: this.convertB24FilesToTelegramFormat(messageData.message.files),
366
- attachments: messageData.message.attachments || null,
367
- im: messageData.im || null,
368
- }));
376
+ data.message = allEvents.flatMap((rawEvent) => {
377
+ // Запись очереди приходит из normalizeCollection как unknown —
378
+ // сужаем через asRecord, тем же способом, что и classifyEntityEvent
379
+ const record = asRecord(rawEvent) ?? {};
380
+ const eventData = asRecord(record["EVENT_DATA"]) ?? {};
381
+
382
+ return normalizeCollection(eventData["MESSAGES"])
383
+ .map((rawMessage) => asRecord(rawMessage))
384
+ .filter((messageData): messageData is Record<string, unknown> => Boolean(messageData?.["chat"] && messageData?.["message"]))
385
+ .map((messageData) => {
386
+ const chat = asRecord(messageData["chat"]) ?? {};
387
+ const message = asRecord(messageData["message"]) ?? {};
388
+
389
+ return {
390
+ // Портал отдаёт идентификаторы то строкой, то числом —
391
+ // приводим, чтобы тип `string` не врал
392
+ messageId: String(record["MESSAGE_ID"] ?? ""),
393
+ eventId: String(record["ID"] ?? ""),
394
+ connectorId: readId(eventData["CONNECTOR"]) ?? "",
395
+ lineId: readId(eventData["LINE"]) ?? "",
396
+ chatId: readId(chat["id"]) ?? "",
397
+ text: (message["text"] as string | undefined) || null,
398
+ file: this.convertB24FilesToTelegramFormat(message["files"] as unknown[] | undefined),
399
+ attachments: (message["attachments"] as unknown[] | undefined) || null,
400
+ im: (messageData["im"] as Record<string, unknown> | undefined) || null,
401
+ };
402
+ });
369
403
  });
370
404
  }
371
405
 
@@ -382,7 +416,7 @@ export class Event {
382
416
  // Тип OfflineEvent описывает то, что портал обещает, а не то, что присылает:
383
417
  // EVENT_ADDITIONAL и EVENT_DATA приходили и без обязательных полей (T-50).
384
418
  // Параметр колбэка объявлен unknown намеренно — так классификация видит запись
385
- // такой, какая она есть. Глобальная зачистка `any` в этом файле — отдельная задача T-53
419
+ // такой, какая она есть
386
420
  const verdicts = allEvents.map((raw: unknown) => classifyEntityEvent(raw, eventName));
387
421
 
388
422
  // Битую запись обработать нечем, и о её пропаже иначе не узнает никто:
@@ -479,10 +513,11 @@ export class Event {
479
513
  }
480
514
 
481
515
  return { error: false, message: "События получены", data };
482
- } catch (error: any) {
516
+ } catch (error: unknown) {
517
+ const message = error instanceof Error ? error.message : String(error);
483
518
  return {
484
519
  error: true,
485
- message: `Не удалось получить события. ${error.message}`,
520
+ message: `Не удалось получить события. ${message}`,
486
521
  data: null,
487
522
  };
488
523
  }
@@ -502,7 +537,7 @@ export class Event {
502
537
  return { error: false, message: "Список записей пуст, очищать нечего", data: null };
503
538
  }
504
539
 
505
- const params: any = { process_id: processId };
540
+ const params: Record<string, unknown> = { process_id: processId };
506
541
  if (messageId) {
507
542
  params.message_id = messageId;
508
543
  }
@@ -518,10 +553,11 @@ export class Event {
518
553
  }
519
554
 
520
555
  return { error: false, message: "События очищены", data: null };
521
- } catch (error: any) {
556
+ } catch (error: unknown) {
557
+ const message = error instanceof Error ? error.message : String(error);
522
558
  return {
523
559
  error: true,
524
- message: `Не удалось очистить события. ${error.message}`,
560
+ message: `Не удалось очистить события. ${message}`,
525
561
  data: null,
526
562
  };
527
563
  }
package/index.ts CHANGED
@@ -6,9 +6,12 @@ export * from "./bitrix24/errTaskB24.ts";
6
6
  export * from "./bitrix24/eventB24.ts";
7
7
 
8
8
  // Мессенджеры и уведомления
9
+ // `Email` здесь намеренно отсутствует: с 4.0.0 он живёт в подпуте
10
+ // `@andrey4emk/npm-app-back-b24/email`, чтобы optional peer `nodemailer`
11
+ // не требовался проектам, которые почту не шлют. Возврат `export *` сюда
12
+ // вернёт зависимость в баррель — это ловит tests/public-api.test.ts
9
13
  export * from "./sendMessage/chatApp.ts";
10
14
  export * from "./sendMessage/smsgold.ts";
11
- export * from "./sendMessage/email.ts";
12
15
  export * from "./sendMessage/wappi.ts";
13
16
 
14
17
  // Логирование
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@andrey4emk/npm-app-back-b24",
3
- "version": "3.9.0",
3
+ "version": "4.0.1",
4
4
  "description": "Bitrix24 OAuth helpers for Node.js projects",
5
5
  "main": "index.ts",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": "./index.ts",
9
9
  "./logs": "./logs/logs.ts",
10
- "./fetchRetry": "./utils/fetchRetry.ts"
10
+ "./fetchRetry": "./utils/fetchRetry.ts",
11
+ "./email": "./sendMessage/email.ts"
11
12
  },
12
13
  "scripts": {
13
14
  "test": "DOTENV_CONFIG_QUIET=true node --test --test-timeout=30000 \"tests/**/*.test.ts\"",
@@ -49,13 +50,23 @@
49
50
  ],
50
51
  "dependencies": {
51
52
  "@bitrix24/b24jssdk": "2.2.0",
53
+ "conf": "15.1.0",
54
+ "dotenv": "17.4.2",
55
+ "luxon": "3.7.2"
56
+ },
57
+ "devDependencies": {
52
58
  "@types/express": "5.0.6",
53
59
  "@types/luxon": "3.7.4",
54
60
  "@types/node": "25.9.5",
55
61
  "@types/nodemailer": "8.0.1",
56
- "conf": "15.1.0",
57
- "dotenv": "17.4.2",
58
- "luxon": "3.7.2",
59
- "nodemailer": "^9.0.5"
62
+ "nodemailer": "9.0.5"
63
+ },
64
+ "peerDependencies": {
65
+ "nodemailer": "^9.0.5 || ^10.0.0"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "nodemailer": {
69
+ "optional": true
70
+ }
60
71
  }
61
72
  }
@@ -45,6 +45,63 @@ interface FileMessageData extends MessageData {
45
45
  fileName: string;
46
46
  }
47
47
 
48
+ /**
49
+ * Ответ ChatApp в той части, которую разбирает класс.
50
+ *
51
+ * Описан по фактически читаемым полям, а не по всему API: остальное уезжает
52
+ * потребителю в `data` как есть и типом не сужается
53
+ */
54
+ interface ChatAppResponse {
55
+ success?: boolean;
56
+ /** Полезная нагрузка ответа. Форму задаёт сам ChatApp, разбираем на месте */
57
+ data?: unknown;
58
+ /** Дописывается классом в phoneCheckChatApp. В ответе ChatApp этого поля нет */
59
+ check?: unknown;
60
+ }
61
+
62
+ /**
63
+ * Комплект токенов ChatApp — то, что сервис обещает в ответе на выдачу и обновление.
64
+ *
65
+ * Поля необязательные намеренно: обязательность в чужом JSON ничем не подкреплена,
66
+ * а усечённый ответ клал бы `undefined` под типом `string`. Комплект проверяется
67
+ * на месте разбора
68
+ */
69
+ interface ChatAppTokens {
70
+ accessToken?: string;
71
+ accessTokenEndTime?: string;
72
+ refreshToken?: string;
73
+ refreshTokenEndTime?: string;
74
+ }
75
+
76
+ /** Ответ на выдачу или обновление токена */
77
+ interface ChatAppTokenResponse {
78
+ success?: boolean;
79
+ data?: ChatAppTokens;
80
+ }
81
+
82
+ /**
83
+ * Проверяет, что в ответе пришёл весь комплект токенов.
84
+ *
85
+ * Частичный ответ до этой проверки клал `undefined` в `this.auth` под типом `string`:
86
+ * метод отчитывался успехом, а следующее обновление уходило с пустым refresh-токеном
87
+ * и не восстанавливалось до перезапуска процесса
88
+ */
89
+ function isFullTokenSet(tokens: ChatAppTokens | undefined): tokens is ChatAppAuthParam {
90
+ return !!(
91
+ tokens &&
92
+ typeof tokens.accessToken === "string" && tokens.accessToken.length > 0 &&
93
+ typeof tokens.accessTokenEndTime === "string" &&
94
+ typeof tokens.refreshToken === "string" && tokens.refreshToken.length > 0 &&
95
+ typeof tokens.refreshTokenEndTime === "string"
96
+ );
97
+ }
98
+
99
+ /** Ответ на запрос списка лицензий */
100
+ interface ChatAppLicensesResponse {
101
+ success?: boolean;
102
+ licenses?: unknown;
103
+ }
104
+
48
105
  /** Стандартный результат операции */
49
106
  interface ChatAppResult {
50
107
  error: boolean;
@@ -136,7 +193,7 @@ export class ChatApp {
136
193
 
137
194
  const url = `${baseUrl}/chats/${phone}/messages/text`;
138
195
 
139
- let data: any;
196
+ let data: ChatAppResponse;
140
197
  try {
141
198
  const res = await fetchWithTimeout(
142
199
  url,
@@ -152,7 +209,7 @@ export class ChatApp {
152
209
  },
153
210
  FETCH_TIMEOUTS.send
154
211
  );
155
- data = await res.json();
212
+ data = (await res.json()) as ChatAppResponse;
156
213
  } catch (error: unknown) {
157
214
  // Метод исторически без try/catch — перехватываем только таймаут, который сами
158
215
  // и вводим. Остальные ошибки летят наружу как раньше
@@ -210,7 +267,7 @@ export class ChatApp {
210
267
 
211
268
  const url = `${baseUrl}/chats/${phone}/messages/file`;
212
269
 
213
- let data: any;
270
+ let data: ChatAppResponse;
214
271
  try {
215
272
  // Файл по ссылке забирает сам ChatApp, поэтому ответа можно ждать дольше — бюджет transfer
216
273
  const res = await fetchWithTimeout(
@@ -227,7 +284,7 @@ export class ChatApp {
227
284
  },
228
285
  FETCH_TIMEOUTS.transfer
229
286
  );
230
- data = await res.json();
287
+ data = (await res.json()) as ChatAppResponse;
231
288
  } catch (error: unknown) {
232
289
  // Перехватываем только таймаут, остальные ошибки ведут себя как раньше
233
290
  if (!isTimeoutError(error)) throw error;
@@ -280,7 +337,7 @@ export class ChatApp {
280
337
 
281
338
  const url = `${baseUrl}/phones/${phone}/check`;
282
339
 
283
- let data: any;
340
+ let data: ChatAppResponse;
284
341
  try {
285
342
  const res = await fetchWithTimeout(
286
343
  url,
@@ -294,7 +351,7 @@ export class ChatApp {
294
351
  },
295
352
  FETCH_TIMEOUTS.api
296
353
  );
297
- data = await res.json();
354
+ data = (await res.json()) as ChatAppResponse;
298
355
  } catch (error: unknown) {
299
356
  // Перехватываем только таймаут, остальные ошибки ведут себя как раньше
300
357
  if (!isTimeoutError(error)) throw error;
@@ -315,13 +372,14 @@ export class ChatApp {
315
372
  newAuth = this.auth;
316
373
  this.updateToken = false;
317
374
  }
318
- // Ответ разбирается как any — noUncheckedIndexedAccess здесь ничего не ловит,
319
- // а обращение к data.data.exist без проверки давало TypeError мимо всякого try/catch
320
- if (typeof data.data !== "object" || data.data === null) {
375
+ // Полезная нагрузка ответа объявлена unknown: обращение к data.data.exist
376
+ // без проверки давало TypeError мимо всякого try/catch
377
+ const payload = data.data;
378
+ if (typeof payload !== "object" || payload === null) {
321
379
  return { error: true, message: `Ответ ChatApp без поля data при проверке телефона через ${messangerType}`, data, auth: newAuth };
322
380
  }
323
381
 
324
- data.check = data.data.exist;
382
+ data.check = (payload as Record<string, unknown>)["exist"];
325
383
  return { error: false, message: `Телефон успешно проверен в ChatApp через ${messangerType}`, data, auth: newAuth };
326
384
  }
327
385
 
@@ -340,9 +398,14 @@ export class ChatApp {
340
398
  FETCH_TIMEOUTS.quick
341
399
  );
342
400
 
343
- const data: any = await response.json();
401
+ const data = (await response.json()) as ChatAppResponse;
344
402
  if (!data.success) {
345
- await this.refreshTokenChatApp();
403
+ // Результат обновления не проглатываем: раньше «Токен обновлен» уходило
404
+ // и когда обновление провалилось, а мёртвый токен доезжал до отправки
405
+ const refreshResult = await this.refreshTokenChatApp();
406
+ if (refreshResult.error) {
407
+ return { error: true, message: `Не удалось обновить токен ChatApp: ${refreshResult.message ?? ""}`, data: null };
408
+ }
346
409
  return { error: false, message: "Токен обновлен" };
347
410
  }
348
411
  return { error: false, message: "Токен действителен" };
@@ -353,7 +416,6 @@ export class ChatApp {
353
416
  }
354
417
 
355
418
  async makeTokenChatApp(): Promise<{ error: boolean; message?: string; data?: unknown }> {
356
- console.warn("makeTokenChatApp called");
357
419
  try {
358
420
  const response = await fetchWithTimeout(
359
421
  "https://api.chatapp.online/v1/tokens",
@@ -373,15 +435,24 @@ export class ChatApp {
373
435
  FETCH_TIMEOUTS.quick
374
436
  );
375
437
 
376
- const data: any = await response.json();
438
+ const data = (await response.json()) as ChatAppTokenResponse;
377
439
  if (!data.success) {
378
440
  return { error: true, message: "Не удалось получить токен ChatApp", data };
379
441
  }
380
442
 
381
- this.auth.accessToken = data.data.accessToken;
382
- this.auth.accessTokenEndTime = data.data.accessTokenEndTime;
383
- this.auth.refreshToken = data.data.refreshToken;
384
- this.auth.refreshTokenEndTime = data.data.refreshTokenEndTime;
443
+ // Успех без комплекта токенов раньше давал TypeError на undefined и уходил
444
+ // в catch общим сообщением. Отказ явный, флаг ошибки тот же.
445
+ // Проверяем весь комплект, а не наличие контейнера: усечённый ответ клал бы
446
+ // undefined в this.auth, и обновление токена не восстановилось бы до перезапуска
447
+ const tokens = data.data;
448
+ if (!isFullTokenSet(tokens)) {
449
+ return { error: true, message: "ChatApp: в ответе нет данных токена", data };
450
+ }
451
+
452
+ this.auth.accessToken = tokens.accessToken;
453
+ this.auth.accessTokenEndTime = tokens.accessTokenEndTime;
454
+ this.auth.refreshToken = tokens.refreshToken;
455
+ this.auth.refreshTokenEndTime = tokens.refreshTokenEndTime;
385
456
  this.updateToken = true;
386
457
  return { error: false };
387
458
  } catch (error: unknown) {
@@ -406,7 +477,7 @@ export class ChatApp {
406
477
  FETCH_TIMEOUTS.quick
407
478
  );
408
479
 
409
- const data: any = await response.json();
480
+ const data = (await response.json()) as ChatAppTokenResponse;
410
481
  if (!data.success) {
411
482
  const resMakeToken = await this.makeTokenChatApp();
412
483
  if (resMakeToken.error) {
@@ -415,10 +486,16 @@ export class ChatApp {
415
486
  return { error: false };
416
487
  }
417
488
 
418
- this.auth.accessToken = data.data.accessToken;
419
- this.auth.accessTokenEndTime = data.data.accessTokenEndTime;
420
- this.auth.refreshToken = data.data.refreshToken;
421
- this.auth.refreshTokenEndTime = data.data.refreshTokenEndTime;
489
+ // Тот же явный отказ, что и в makeTokenChatApp: успех без комплекта токенов
490
+ const tokens = data.data;
491
+ if (!isFullTokenSet(tokens)) {
492
+ return { error: true, message: "ChatApp: в ответе нет данных токена", data };
493
+ }
494
+
495
+ this.auth.accessToken = tokens.accessToken;
496
+ this.auth.accessTokenEndTime = tokens.accessTokenEndTime;
497
+ this.auth.refreshToken = tokens.refreshToken;
498
+ this.auth.refreshTokenEndTime = tokens.refreshTokenEndTime;
422
499
  this.updateToken = true;
423
500
  return { error: false };
424
501
  } catch (error: unknown) {
@@ -441,7 +518,7 @@ export class ChatApp {
441
518
  },
442
519
  FETCH_TIMEOUTS.api
443
520
  );
444
- const data: any = await response.json();
521
+ const data = (await response.json()) as ChatAppLicensesResponse;
445
522
 
446
523
  if (!data.success) {
447
524
  return { error: true, message: "Не удалось получить лицензии ChatApp", data: null };