@andrey4emk/npm-app-back-b24 3.8.2 → 4.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.
@@ -25,11 +25,31 @@ interface WappiFileMessageData extends WappiMessageData {
25
25
  fileName: string;
26
26
  }
27
27
 
28
+ /**
29
+ * Ответ Wappi в той части, которую разбирает класс.
30
+ *
31
+ * Описан по фактически читаемым полям, а не по всему API. Поля `check` и `haveContact`
32
+ * в ответе Wappi отсутствуют — их дописывает сам класс в объект ответа, поэтому
33
+ * в интерфейсе они обязаны быть, иначе присваивание не пройдёт тайпчек
34
+ */
35
+ interface WappiResponse {
36
+ status?: string;
37
+ detail?: string;
38
+ on_max?: boolean;
39
+ on_whatsapp?: boolean;
40
+ contact?: unknown;
41
+ /** Дописывается классом: номер пригоден для отправки */
42
+ check?: boolean;
43
+ /** Дописывается классом: контакт найден или создан */
44
+ haveContact?: boolean;
45
+ }
46
+
28
47
  /** Стандартный результат операции */
29
48
  interface WappiResult {
30
49
  error: boolean;
31
50
  message: string;
32
- data: any;
51
+ /** Ответ Wappi как есть — форму задаёт сервис, потребитель приводит сам */
52
+ data: unknown;
33
53
  /**
34
54
  * Ответа не дождались. Отличает «проверка сказала нет» от «проверки не было»:
35
55
  * без этого признака вызывающий код принимает таймаут за отрицательный ответ.
@@ -93,7 +113,7 @@ export class Wappi {
93
113
  url = `https://wappi.pro/maxapi/sync/message/send?${sendOpenLine}profile_id=${profile_id}`;
94
114
  }
95
115
 
96
- let data: any;
116
+ let data: WappiResponse;
97
117
  try {
98
118
  const res = await fetchWithTimeout(
99
119
  url!,
@@ -110,7 +130,7 @@ export class Wappi {
110
130
  },
111
131
  FETCH_TIMEOUTS.send
112
132
  );
113
- data = await res.json();
133
+ data = (await res.json()) as WappiResponse;
114
134
  } catch (error: unknown) {
115
135
  // Метод исторически без try/catch — перехватываем только таймаут, который сами
116
136
  // и вводим, чтобы не появился новый путь исключений там, где его сегодня нет.
@@ -202,7 +222,7 @@ export class Wappi {
202
222
  url = `https://wappi.pro/maxapi/async/message/file/url/send?${sendOpenLine}profile_id=${profile_id}`;
203
223
  }
204
224
 
205
- let data: any;
225
+ let data: WappiResponse;
206
226
  try {
207
227
  // В теле уходит base64 файла — бюджет transfer, а не send
208
228
  const res = await fetchWithTimeout(
@@ -223,7 +243,7 @@ export class Wappi {
223
243
  },
224
244
  FETCH_TIMEOUTS.transfer
225
245
  );
226
- data = await res.json();
246
+ data = (await res.json()) as WappiResponse;
227
247
  } catch (error: unknown) {
228
248
  // Перехватываем только таймаут, остальные ошибки ведут себя как раньше
229
249
  if (!isTimeoutError(error)) throw error;
@@ -265,9 +285,14 @@ export class Wappi {
265
285
  const contactCheck = await this.getContactTelegramWappi(phone);
266
286
 
267
287
  // 2 Если существует, то номер в телеге есть, можем отправлять сообщение
268
- if (!contactCheck.error && contactCheck.data.haveContact) {
269
- contactCheck.data.check = true;
270
- return { error: false, message: `Номер ${phone} проверен для мессенджера ${messangerType}`, data: contactCheck.data };
288
+ // Приведение, а не проверка: `data` объявлен unknown только в публичном типе,
289
+ // внутри класса это всегда разобранный ответ Wappi. На ошибке там null,
290
+ // но до чтения поля не доходит — первым проверяется `!contactCheck.error`,
291
+ // порядок вычисления тот же, что и до 4.0.0
292
+ const contactData = contactCheck.data as WappiResponse;
293
+ if (!contactCheck.error && contactData.haveContact) {
294
+ contactData.check = true;
295
+ return { error: false, message: `Номер ${phone} проверен для мессенджера ${messangerType}`, data: contactData };
271
296
  }
272
297
 
273
298
  // Ответа не дождались — это не «контакта нет». Создавать контакт вслепую нельзя:
@@ -280,14 +305,15 @@ export class Wappi {
280
305
 
281
306
  // 3 Если не существует, то номера в телеге нет, пробуем создать
282
307
  const addContactResult = await this.addContactTelegramWappi(phone);
308
+ const addedData = addContactResult.data as WappiResponse;
283
309
 
284
310
  if (!addContactResult.error) {
285
311
  // Если получается создать, отправляем информацию
286
- addContactResult.data.check = true;
287
- return { error: false, message: `Номер ${phone} успешно добавлен и проверен для мессенджера ${messangerType}`, data: addContactResult.data };
312
+ addedData.check = true;
313
+ return { error: false, message: `Номер ${phone} успешно добавлен и проверен для мессенджера ${messangerType}`, data: addedData };
288
314
  }
289
315
  // Если не получается, то отправляем информацию об ошибке
290
- return { error: true, message: `Не удалось проверить и добавить номер ${phone} в Telegram: ${addContactResult.message}`, data: addContactResult.data };
316
+ return { error: true, message: `Не удалось проверить и добавить номер ${phone} в Telegram: ${addContactResult.message}`, data: addedData };
291
317
  }
292
318
  if (messangerType === "max") {
293
319
  token = this.maxAuth.token;
@@ -295,7 +321,7 @@ export class Wappi {
295
321
  url = `https://wappi.pro/maxapi/sync/contact/check?profile_id=${profile_id}&phone=${phone}`;
296
322
  }
297
323
 
298
- let data: any;
324
+ let data: WappiResponse;
299
325
  try {
300
326
  const res = await fetchWithTimeout(
301
327
  url!,
@@ -308,7 +334,7 @@ export class Wappi {
308
334
  },
309
335
  FETCH_TIMEOUTS.api
310
336
  );
311
- data = await res.json();
337
+ data = (await res.json()) as WappiResponse;
312
338
  } catch (error: unknown) {
313
339
  // Перехватываем только таймаут, остальные ошибки ведут себя как раньше
314
340
  if (!isTimeoutError(error)) throw error;
@@ -353,7 +379,7 @@ export class Wappi {
353
379
  FETCH_TIMEOUTS.api
354
380
  );
355
381
 
356
- const data: any = await res.json();
382
+ const data = (await res.json()) as WappiResponse;
357
383
 
358
384
  if (data.status !== "done") {
359
385
  return { error: true, message: `Ошибка получения контакта ${phone} из Telegram`, data };
@@ -405,7 +431,7 @@ export class Wappi {
405
431
  FETCH_TIMEOUTS.send
406
432
  );
407
433
 
408
- const data: any = await res.json();
434
+ const data = (await res.json()) as WappiResponse;
409
435
  if (data.status !== "done") {
410
436
  return { error: true, message: `Ошибка создания контакта ${phone} в Telegram`, data };
411
437
  }
@@ -31,7 +31,15 @@ function isValidTimeout(value: number): boolean {
31
31
  }
32
32
 
33
33
  /**
34
- * Читает бюджет запроса из переменной окружения `FETCH_TIMEOUT_MS`.
34
+ * Верхняя граница подозрительного бюджета: значение от 1 до 999 мс почти наверняка
35
+ * означает, что человек написал секунды вместо миллисекунд.
36
+ *
37
+ * Ноль сюда не входит — это задокументированное выключение таймаута.
38
+ */
39
+ const SUSPICIOUS_TIMEOUT_MAX_MS = 999;
40
+
41
+ /**
42
+ * Читает бюджет запроса из переменной окружения `name`.
35
43
  *
36
44
  * `dotenv.config()` вызывается в `logs/logs.ts`, а этот модуль импортирует `logs`
37
45
  * первой строкой — значит к моменту чтения `process.env` файл `.env` уже загружен.
@@ -39,15 +47,38 @@ function isValidTimeout(value: number): boolean {
39
47
  * Значение принимается, только если это целое число >= 0 (0 означает «без таймаута»).
40
48
  * Любой мусор в переменной не должен молча превращаться в `NaN` или в обрезанное
41
49
  * `parseInt`-число, поэтому проверяем фактом и падаем на дефолт с записью в лог.
50
+ * Пустая строка и строка из пробелов считаются «переменная не задана» — молча дефолт.
51
+ *
52
+ * Значение от 1 до 999 применяется как есть, но с записью `warn`: столько миллисекунд
53
+ * не хватит ни одному живому запросу, и это типовая путаница секунд с миллисекундами.
54
+ * Молча такое значение пропускать нельзя — на бюджетах отправки оно превращается
55
+ * в массовую недоставку с «неизвестным статусом» и потоком `error` в чат портала.
56
+ *
57
+ * Экспортируется по двум причинам: одинаковая семантика для всех бюджетов пакета
58
+ * (дефолт `fetchRetry` и каждый из `FETCH_TIMEOUTS` читаются одной и той же функцией)
59
+ * и проверяемость тестом без запуска дочернего процесса.
60
+ *
61
+ * Важно: все чтения происходят **один раз при загрузке модуля**. Правка `process.env`
62
+ * в рантайме на уже вычисленные константы не влияет.
63
+ *
64
+ * @param name — имя переменной окружения
65
+ * @param fallback — значение, если переменная не задана или непригодна
66
+ * @returns бюджет в мс
42
67
  */
43
- function readTimeoutFromEnv(): number {
44
- const raw = process.env.FETCH_TIMEOUT_MS;
45
- if (raw === undefined || raw.trim() === "") return FALLBACK_TIMEOUT_MS;
68
+ export function readTimeoutFromEnv(name: string, fallback: number): number {
69
+ const raw = process.env[name];
70
+ if (raw === undefined || raw.trim() === "") return fallback;
46
71
 
47
72
  const parsed = Number(raw);
48
73
  if (!isValidTimeout(parsed)) {
49
- logs.add(`FETCH_TIMEOUT_MS: значение '${raw}' непригодно (нужно целое от 0 до ${MAX_TIMEOUT_MS}) — берём ${FALLBACK_TIMEOUT_MS}мс`, "warn");
50
- return FALLBACK_TIMEOUT_MS;
74
+ logs.add(`${name}: значение '${raw}' непригодно (нужно целое от 0 до ${MAX_TIMEOUT_MS}) — берём ${fallback}мс`, "warn");
75
+ return fallback;
76
+ }
77
+
78
+ // Значение применяем как есть: аварийный рычаг остаётся в руках у человека,
79
+ // мы только делаем подозрительный ввод видимым
80
+ if (parsed >= 1 && parsed <= SUSPICIOUS_TIMEOUT_MAX_MS) {
81
+ logs.add(`${name}: значение '${raw}' похоже на секунды, а нужны миллисекунды — бюджет ${parsed}мс применён как есть`, "warn");
51
82
  }
52
83
 
53
84
  return parsed;
@@ -58,7 +89,13 @@ function readTimeoutFromEnv(): number {
58
89
  * из `FETCH_TIMEOUT_MS`, иначе 60 000. Значение 0 полностью выключает таймаут —
59
90
  * это аварийный рычаг для случая, когда дефолт обрывает живой долгий запрос.
60
91
  */
61
- export const DEFAULT_FETCH_TIMEOUT_MS: number = readTimeoutFromEnv();
92
+ export const DEFAULT_FETCH_TIMEOUT_MS: number = readTimeoutFromEnv("FETCH_TIMEOUT_MS", FALLBACK_TIMEOUT_MS);
93
+
94
+ /** Дефолты именованных бюджетов до чтения окружения, мс */
95
+ const FALLBACK_TIMEOUTS = { quick: 10_000, api: 20_000, send: 30_000, transfer: 60_000 } as const;
96
+
97
+ /** Имя именованного бюджета пакета */
98
+ export type FetchTimeoutName = keyof typeof FALLBACK_TIMEOUTS;
62
99
 
63
100
  /**
64
101
  * Именованные бюджеты для вызовов внутри пакета: смысл каждого запроса тут известен,
@@ -68,8 +105,32 @@ export const DEFAULT_FETCH_TIMEOUT_MS: number = readTimeoutFromEnv();
68
105
  * - `api` — короткое чтение (проверка номера, список лицензий, получение контакта);
69
106
  * - `send` — отправка текста, создание контакта, отправка SMS;
70
107
  * - `transfer` — скачивание файла и отправка файла/вложения.
108
+ *
109
+ * Каждый бюджет переопределяется своей переменной окружения:
110
+ *
111
+ * | Ключ | Переменная | Дефолт, мс |
112
+ * |---|---|---|
113
+ * | `quick` | `FETCH_TIMEOUT_QUICK_MS` | 10 000 |
114
+ * | `api` | `FETCH_TIMEOUT_API_MS` | 20 000 |
115
+ * | `send` | `FETCH_TIMEOUT_SEND_MS` | 30 000 |
116
+ * | `transfer` | `FETCH_TIMEOUT_TRANSFER_MS` | 60 000 |
117
+ *
118
+ * Значения читаются один раз при загрузке модуля: правка `process.env` в рантайме
119
+ * их не двигает, менять бюджет нужно до старта процесса.
120
+ *
121
+ * Значение 0 выключает таймаут для этого бюджета целиком. Это аварийный рычаг,
122
+ * и у отправок (`send`, `transfer`) он снимает заодно защиту «неизвестный статус
123
+ * доставки»: при нуле зависшая отправка снова держится до таймаутов undici,
124
+ * а `unknownDelivery` в `Smsgold` не выставится никогда.
125
+ *
126
+ * Непригодное значение защиту молча не выключает: одна строка `warn` и дефолт.
71
127
  */
72
- export const FETCH_TIMEOUTS = { quick: 10_000, api: 20_000, send: 30_000, transfer: 60_000 } as const;
128
+ export const FETCH_TIMEOUTS: Readonly<Record<FetchTimeoutName, number>> = {
129
+ quick: readTimeoutFromEnv("FETCH_TIMEOUT_QUICK_MS", FALLBACK_TIMEOUTS.quick),
130
+ api: readTimeoutFromEnv("FETCH_TIMEOUT_API_MS", FALLBACK_TIMEOUTS.api),
131
+ send: readTimeoutFromEnv("FETCH_TIMEOUT_SEND_MS", FALLBACK_TIMEOUTS.send),
132
+ transfer: readTimeoutFromEnv("FETCH_TIMEOUT_TRANSFER_MS", FALLBACK_TIMEOUTS.transfer),
133
+ };
73
134
 
74
135
  /** Читает `name` с произвольного значения ошибки, не полагаясь на её тип */
75
136
  function getErrorName(error: unknown): string | null {
@@ -135,7 +196,7 @@ export function isNetworkError(error: unknown): boolean {
135
196
 
136
197
  /**
137
198
  * Коды, доказывающие, что соединение не было установлено: DNS не разрешился либо
138
- * хост отверг подключение. Запрос при таких ошибках гарантированно не дошёл до сервера.
199
+ * хост отверг подключение. Соединение с целевым хостом не состоялось.
139
200
  */
140
201
  const PRE_CONNECTION_ERROR_CODES = ["ENOTFOUND", "EAI_AGAIN", "EAI_NODATA", "ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH", "ENETDOWN"];
141
202