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/README.md +25 -0
- package/REST.d.ts +301 -0
- package/REST.js +810 -0
- package/REST.ts +162 -17
- package/package.json +8 -6
- package/tsconfig.json +0 -1
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
|
-
|
|
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')
|
|
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.
|
|
3
|
+
"version": "1.0.128",
|
|
4
4
|
"description": "",
|
|
5
|
-
"main": "
|
|
6
|
-
"
|
|
5
|
+
"main": "REST.js",
|
|
6
|
+
"types": "REST.d.ts",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": {
|
|
9
|
-
"import": "./REST.
|
|
10
|
-
"types": "./REST.ts"
|
|
10
|
+
"import": "./REST.js",
|
|
11
|
+
"types": "./REST.d.ts"
|
|
11
12
|
}
|
|
12
13
|
},
|
|
13
14
|
"scripts": {
|
|
14
|
-
"
|
|
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",
|