flamerest 1.0.125 → 1.0.128

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/REST.ts CHANGED
@@ -39,6 +39,15 @@ export type CustomAnyRequest = BaseResponse<any>;
39
39
  */
40
40
  export type SavedObject<T> = BaseResponse<T>;
41
41
 
42
+ /**
43
+ * Элемент баланса пользователя
44
+ */
45
+ export interface BalanceItem {
46
+ name: string;
47
+ balance_decimal: string;
48
+ value: number;
49
+ }
50
+
42
51
  /**
43
52
  * Результат авторизации
44
53
  */
@@ -51,12 +60,16 @@ export type Authorized = {
51
60
  lang: string,
52
61
  name: string,
53
62
  role: string,
54
- user_hash?: string
63
+ user_hash?: string,
64
+ balance: 0,
65
+ balance_all: { [key: string]: BalanceItem },
55
66
  };
56
67
  errors: { [key: string]: any };
57
68
  token: string;
58
69
  }
59
70
 
71
+
72
+
60
73
  export class FLAMEREST {
61
74
 
62
75
  /**
@@ -74,6 +87,12 @@ export class FLAMEREST {
74
87
  */
75
88
  public unauthorized_callback: (() => void) | undefined;
76
89
 
90
+ /**
91
+ * Будет вызван, если сервер вернёт статус 503 (Service Unavailable / Maintenance)
92
+ * Может содержать сообщение об ошибке из JSON ответа
93
+ */
94
+ public maintenance_callback: ((errorMessage?: string) => void) | undefined;
95
+
77
96
  /**
78
97
  * Токен приложения конкретного клиента для отправки пуш уведомлений именно ему
79
98
  */
@@ -84,6 +103,12 @@ export class FLAMEREST {
84
103
  */
85
104
  public version: string;
86
105
 
106
+ /**
107
+ * Версия кода клиента (отправляется в заголовке каждого запроса).
108
+ * Можно установить извне: REST.VERSION_CODE = "1.2.3"
109
+ */
110
+ public VERSION_CODE: string = "1.0.0";
111
+
87
112
  /**
88
113
  * Если авторизация по токену, то он сюда подставляется
89
114
  */
@@ -97,7 +122,15 @@ export class FLAMEREST {
97
122
  public token: string | null = null;
98
123
 
99
124
 
100
- constructor(server_address?: string, localhost_endpoint?: string, unauthorized_callback?: () => void, version?: string) {
125
+ /**
126
+ * Конструктор класса FLAMEREST
127
+ * @param {string} server_address Адрес сервера (опционально)
128
+ * @param {string} localhost_endpoint Альтернативный адрес для localhost (опционально)
129
+ * @param {Function} unauthorized_callback Колбек для обработки 401 ошибок (опционально)
130
+ * @param {string} version Версия API (по умолчанию 'v1')
131
+ * @param {Function} maintenance_callback Колбек для обработки 503 ошибок (опционально)
132
+ */
133
+ constructor(server_address?: string, localhost_endpoint?: string, unauthorized_callback?: () => void, version?: string, maintenance_callback?: (errorMessage?: string) => void) {
101
134
  if (typeof window === 'undefined') {
102
135
  // SSR: используем предоставленный адрес или дефолтный
103
136
  this.SERVER = server_address ?? "http://localhost/";
@@ -118,6 +151,7 @@ export class FLAMEREST {
118
151
  this.SERVER = this.SERVER.endsWith('/') ? this.SERVER.substring(0, this.SERVER.length - 1) : this.SERVER;
119
152
 
120
153
  this.unauthorized_callback = unauthorized_callback;
154
+ this.maintenance_callback = maintenance_callback;
121
155
  this.version = version ?? 'v1';
122
156
  }
123
157
 
@@ -141,7 +175,21 @@ export class FLAMEREST {
141
175
  * @param {boolean} isNeedToken нужен ли токен для запроса
142
176
  * @param {Object} customHeaders объект с доп заголовками, которые надо включить в запрос
143
177
  */
144
- request(url: string, params: object | string | FormData, type: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', responseType: 'json' | 'blob' = 'json', isNeedToken = true, customHeaders: Record<string, string> = {}): Promise<BaseResponse<any> & Partial<Paginated>> {
178
+ async request(url: string, params: object | string | FormData, type: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', responseType: 'json' | 'blob' = 'json', isNeedToken = true, customHeaders: Record<string, string> = {}): Promise<BaseResponse<any> & Partial<Paginated>> {
179
+
180
+ // Подставляем сервер автоматом если URL не содержит протокол
181
+ // Не трогаем URL с явным протоколом (http://, https://) или протокол-независимые (//)
182
+ if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('//')) {
183
+ // Убираем начальный "/" если он есть, т.к. SERVER уже без слэша в конце
184
+ if (url[0] === '/') {
185
+ url = url.substring(1);
186
+ }
187
+ // Добавляем SERVER с разделителем "/"
188
+ url = this.SERVER + '/' + url;
189
+ }
190
+
191
+ // Создаём подпись запроса до его модификации
192
+ const SIGN = await this._generateRequestSignature(url, params);
145
193
 
146
194
  // Нормализуем параметры, если они есть
147
195
  if (typeof params === "object" && params !== null) {
@@ -155,10 +203,6 @@ export class FLAMEREST {
155
203
  responseType = 'json';
156
204
  }
157
205
 
158
- // Подставляем сервер автоматом в запрос начинающийся с /
159
- if (url[0] === '/' && url[1] !== '/') {
160
- url = this.SERVER + url;
161
- }
162
206
 
163
207
  const that = this;
164
208
 
@@ -193,6 +237,9 @@ export class FLAMEREST {
193
237
  headers['Content-type'] = 'application/json; charset=utf-8';
194
238
  }
195
239
 
240
+ // Версия кода клиента — уходит с каждым запросом
241
+ headers['version'] = this.VERSION_CODE;
242
+
196
243
  const requestBody: RequestInit = {
197
244
  method: type,
198
245
  mode: 'cors',
@@ -201,17 +248,10 @@ export class FLAMEREST {
201
248
 
202
249
  if (type !== 'GET') {
203
250
  // Принимает и formData тоже и чистую json строку
204
- requestBody.body = params;
251
+ requestBody.body = params as any;
205
252
  }
206
253
 
207
- // Создаём подпись каждого запроса
208
- let SIGN = "empty";
209
- try {
210
- // Используем user_hash для подписи, если он есть
211
- SIGN = await this.hmac_sha256(requestBody.body, user_hash ?? "");
212
- } catch (ex) {
213
- // console.error('Can`t create a request sign');
214
- }
254
+ // Добавляем подпись в заголовки
215
255
  (requestBody.headers as any)['sign'] = SIGN;
216
256
 
217
257
  // Делаем запрос
@@ -244,6 +284,16 @@ export class FLAMEREST {
244
284
  this.unauthorized_callback();
245
285
  }
246
286
  break;
287
+ case 503:
288
+ // Обработка режима обслуживания (maintenance)
289
+ let maintenanceMessage: string | undefined = undefined;
290
+ if (ResolveBody.errors && typeof ResolveBody.errors === 'object' && 'error' in ResolveBody.errors) {
291
+ maintenanceMessage = (ResolveBody.errors as any).error;
292
+ }
293
+ if (typeof this.maintenance_callback === 'function') {
294
+ this.maintenance_callback(maintenanceMessage);
295
+ }
296
+ break;
247
297
  }
248
298
  } catch (exjson) {
249
299
  ResolveBody.errors = await response.text();
@@ -341,6 +391,8 @@ export class FLAMEREST {
341
391
  xhr.open(type, url, true);
342
392
  xhr.responseType = 'json';
343
393
  xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
394
+ // Версия кода клиента — уходит с каждым запросом (fallback без fetch)
395
+ xhr.setRequestHeader('version', this.VERSION_CODE);
344
396
  xhr.withCredentials = false;
345
397
  xhr.send(params as any);
346
398
 
@@ -776,6 +828,98 @@ export class FLAMEREST {
776
828
  return result;
777
829
  }
778
830
 
831
+ /**
832
+ * Вычисляет на лету детерминированную строку, которая будет использоваться как часть ключа для подписи.
833
+ * Ключ генерируется на основе последовательности числовых преобразований, что делает его неочевидным
834
+ * при статическом анализе кода. Этот метод всегда возвращает один и тот же результат.
835
+ * @returns {string} Детерминированно сгенерированная строка.
836
+ */
837
+ getDeterministicKeyPart(): string {
838
+ // Начальная последовательность чисел. Она может быть любой, главное - постоянной.
839
+ const sequence = [5, 12, 23, 8, 17, 3, 29, 14, 31, 2, 11, 28, 7, 20, 1, 16, 22, 5, 13, 19, 4, 25, 10, 30, 9, 21, 6, 18, 27, 15, 24, 26];
840
+ // "Магическое число" для усложнения преобразований.
841
+ const magicNumber = 177;
842
+ let result = '';
843
+
844
+ for (let i = 0; i < sequence.length; i++) {
845
+ const num = sequence[i];
846
+ let charCode;
847
+
848
+ // В зависимости от позиции применяем разные преобразования,
849
+ // чтобы получить символы в разных регистрах и цифры.
850
+ switch (i % 3) {
851
+ case 0:
852
+ // Генерируем заглавную букву (A-Z)
853
+ charCode = (num * magicNumber + i) % 26;
854
+ result += String.fromCharCode(65 + charCode);
855
+ break;
856
+ case 1:
857
+ // Генерируем строчную букву (a-z)
858
+ charCode = (num + magicNumber * (i + 1)) % 26;
859
+ result += String.fromCharCode(97 + charCode);
860
+ break;
861
+ case 2:
862
+ // Генерируем цифру (0-9)
863
+ charCode = (num * (i + 1) + magicNumber) % 10;
864
+ result += String.fromCharCode(48 + charCode);
865
+ break;
866
+ }
867
+ }
868
+ return result;
869
+ }
870
+
871
+ /**
872
+ * Создаёт подпись для запроса на основе его параметров, используя лучшие практики.
873
+ * Собирает все GET и POST параметры, сортирует их и формирует каноническую строку для хеширования.
874
+ * @param url URL запроса, включая GET-параметры.
875
+ * @param params POST-параметры запроса (в виде объекта).
876
+ * @returns Promise<string> Строка подписи.
877
+ */
878
+ private async _generateRequestSignature(url: string, params: object | string | FormData): Promise<string> {
879
+ const secretKey = (this.token ?? '') + this.getDeterministicKeyPart();
880
+
881
+ // Если параметры - это FormData или не объект, мы не можем их надежно обработать для подписи.
882
+ // В этом случае подписываем пустую строку, как и раньше.
883
+ if (params instanceof FormData || typeof params !== 'object' || params === null) {
884
+ return this.hmac_sha256('', secretKey);
885
+ }
886
+
887
+ const allParams: { [key: string]: any } = {};
888
+
889
+ // 1. Извлекаем GET-параметры из URL
890
+ if (url.includes('?')) {
891
+ const queryString = url.split('?')[1];
892
+ const urlParams = new URLSearchParams(queryString);
893
+ urlParams.forEach((value, key) => {
894
+ allParams[key] = value;
895
+ });
896
+ }
897
+
898
+ // 2. Добавляем POST-параметры из тела запроса
899
+ Object.assign(allParams, params);
900
+
901
+ // 3. Создаём каноническую строку
902
+ // Сортируем ключи для консистентности
903
+ const sortedKeys = Object.keys(allParams).sort();
904
+
905
+ // Формируем строку вида "key1=value1&key2=value2..."
906
+ const canonicalString = sortedKeys
907
+ .map(key => {
908
+ const value = allParams[key];
909
+ // Пропускаем null и undefined значения, они не участвуют в подписи
910
+ if (value === null || value === undefined) {
911
+ return '';
912
+ }
913
+ // Если значение - объект (включая массив), заменяем его на слово "object" для консистентности подписи.
914
+ const stringValue = (typeof value === 'object' && value !== null) ? 'object' : String(value);
915
+ return `${encodeURIComponent(key)}=${encodeURIComponent(stringValue)}`;
916
+ })
917
+ .filter(part => part !== '') // Убираем пустые части
918
+ .join('&');
919
+
920
+ return this.hmac_sha256(canonicalString, secretKey);
921
+ }
922
+
779
923
  /**
780
924
  *
781
925
  * @param {string} data
@@ -797,12 +941,13 @@ export class FLAMEREST {
797
941
  if (typeof window === 'undefined') {
798
942
  // Динамически импортируем модуль 'crypto' для Node.js, чтобы избежать ошибок на фронтенде,
799
943
  // где этот модуль недоступен.
944
+ // @ts-ignore
800
945
  const { createHmac } = await import('crypto');
801
946
  const hash = createHmac('sha256', key).update(data).digest('hex');
802
947
  return Promise.resolve(hash);
803
948
  } else {
804
949
 
805
- if (typeof window.crypto?.subtle === 'undefined') throw new Error("Can`t create hash");
950
+ if (typeof window.crypto?.subtle === 'undefined') return Promise.resolve("");
806
951
 
807
952
  return window.crypto.subtle
808
953
  .importKey(
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "flamerest",
3
- "version": "1.0.125",
3
+ "version": "1.0.128",
4
4
  "description": "",
5
- "main": "./REST.ts",
6
- "typings": "./REST.ts",
5
+ "main": "REST.js",
6
+ "types": "REST.d.ts",
7
+ "type": "module",
7
8
  "exports": {
8
9
  ".": {
9
- "import": "./REST.ts",
10
- "types": "./REST.ts"
10
+ "import": "./REST.js",
11
+ "types": "./REST.d.ts"
11
12
  }
12
13
  },
13
14
  "scripts": {
14
- "publishx": "git push && npm --no-git-tag-version version patch && npm publish"
15
+ "build": "tsc REST.ts --declaration --emitDeclarationOnly false --module ESNext --target ES2020",
16
+ "p": "npm run build && git push && npm --no-git-tag-version version patch && npm publish"
15
17
  },
16
18
  "repository": {
17
19
  "type": "git",
package/tsconfig.json CHANGED
@@ -8,7 +8,6 @@
8
8
  ],
9
9
  "module": "ESNext",
10
10
  "types": [
11
- "node"
12
11
  ],
13
12
  "moduleResolution": "Bundler",
14
13
  "strict": true,