@etsoo/appscript 1.1.64 → 1.1.68

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.
@@ -3,6 +3,7 @@ import { ApiDataError, IApi, IPData } from '@etsoo/restclient';
3
3
  import { DataTypes, DateUtils } from '@etsoo/shared';
4
4
  import { AddressRegion } from '../address/AddressRegion';
5
5
  import { IActionResult } from '../result/IActionResult';
6
+ import { InitCallResultData } from '../result/InitCallResult';
6
7
  import { IUserData } from '../state/User';
7
8
  import { IAppSettings } from './AppSettings';
8
9
  import { UserRole } from './UserRole';
@@ -87,10 +88,6 @@ export interface ICoreApp<S extends IAppSettings, N, C extends NotificationCallP
87
88
  * User data
88
89
  */
89
90
  userData?: IUserData;
90
- /**
91
- * Passphrase for encryption
92
- */
93
- passphrase?: string;
94
91
  /**
95
92
  * Search input element
96
93
  */
@@ -121,9 +118,10 @@ export interface ICoreApp<S extends IAppSettings, N, C extends NotificationCallP
121
118
  * Decrypt message
122
119
  * @param messageEncrypted Encrypted message
123
120
  * @param passphrase Secret passphrase
121
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
124
122
  * @returns Pure text
125
123
  */
126
- decrypt(messageEncrypted: string, passphrase: string): string;
124
+ decrypt(messageEncrypted: string, passphrase?: string, durationSeconds?: number): string | undefined;
127
125
  /**
128
126
  * Detect IP data, call only one time
129
127
  * @param callback Callback will be called when the IP is ready
@@ -133,9 +131,10 @@ export interface ICoreApp<S extends IAppSettings, N, C extends NotificationCallP
133
131
  * Encrypt message
134
132
  * @param message Message
135
133
  * @param passphrase Secret passphrase
134
+ * @param iterations Iterations, 1000 times, 1 - 99
136
135
  * @returns Result
137
136
  */
138
- encrypt(message: string, passphrase: string): string;
137
+ encrypt(message: string, passphrase?: string, iterations?: number): string;
139
138
  /**
140
139
  * Format date to string
141
140
  * @param input Input date
@@ -216,6 +215,20 @@ export interface ICoreApp<S extends IAppSettings, N, C extends NotificationCallP
216
215
  * @returns Time zone
217
216
  */
218
217
  getTimeZone(): string | undefined;
218
+ /**
219
+ * Hash message, SHA3 or HmacSHA512, 512 as Base64
220
+ * https://cryptojs.gitbook.io/docs/
221
+ * @param message Message
222
+ * @param passphrase Secret passphrase
223
+ */
224
+ hash(message: string, passphrase?: string): string;
225
+ /**
226
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
227
+ * https://cryptojs.gitbook.io/docs/
228
+ * @param message Message
229
+ * @param passphrase Secret passphrase
230
+ */
231
+ hashHex(message: string, passphrase?: string): string;
219
232
  /**
220
233
  * Check use has the specific role permission or not
221
234
  * @param roles Roles to check
@@ -327,10 +340,6 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
327
340
  * User data
328
341
  */
329
342
  userData?: IUserData;
330
- /**
331
- * Passphrase for encryption
332
- */
333
- passphrase?: string;
334
343
  /**
335
344
  * Response token header field name
336
345
  */
@@ -355,6 +364,18 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
355
364
  * Token refresh count down seed
356
365
  */
357
366
  protected refreshCountdownSeed: number;
367
+ /**
368
+ * Device id field name
369
+ */
370
+ protected deviceIdField: string;
371
+ /**
372
+ * Device id
373
+ */
374
+ protected deviceId: string;
375
+ /**
376
+ * Passphrase for encryption
377
+ */
378
+ protected passphrase: string;
358
379
  /**
359
380
  * Protected constructor
360
381
  * @param settings Settings
@@ -364,6 +385,22 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
364
385
  */
365
386
  protected constructor(settings: S, api: IApi, notifier: INotifier<N, C>, name: string);
366
387
  protected setApi(api: IApi): void;
388
+ /**
389
+ * Init call
390
+ * @returns Result
391
+ */
392
+ protected initCall(): Promise<void>;
393
+ /**
394
+ * Init call update
395
+ * @param data Result data
396
+ * @param timestamp Timestamp
397
+ */
398
+ protected initCallUpdate(data: InitCallResultData, timestamp: number): void;
399
+ /**
400
+ * Init call update fields in local storage
401
+ * @returns Fields
402
+ */
403
+ protected initCallUpdateFields(): string[];
367
404
  /**
368
405
  * Alert action result
369
406
  * @param result Action result
@@ -390,9 +427,10 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
390
427
  * Decrypt message
391
428
  * @param messageEncrypted Encrypted message
392
429
  * @param passphrase Secret passphrase
430
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
393
431
  * @returns Pure text
394
432
  */
395
- decrypt(messageEncrypted: string, passphrase: string): string;
433
+ decrypt(messageEncrypted: string, passphrase?: string, durationSeconds?: number): string | undefined;
396
434
  /**
397
435
  * Detect IP data, call only one time
398
436
  * @param callback Callback will be called when the IP is ready
@@ -403,16 +441,17 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
403
441
  * Encrypt message
404
442
  * @param message Message
405
443
  * @param passphrase Secret passphrase
444
+ * @param iterations Iterations, 1000 times, 1 - 99
406
445
  * @returns Result
407
446
  */
408
- encrypt(message: string, passphrase: string): string;
447
+ encrypt(message: string, passphrase?: string, iterations?: number): string;
409
448
  /**
410
449
  * Enchance secret passphrase
411
450
  * @param passphrase Secret passphrase
412
- * @param miliseconds Miliseconds
451
+ * @param timestamp Timestamp
413
452
  * @returns Enhanced passphrase
414
453
  */
415
- protected encryptionEnhance(passphrase: string, miliseconds: string): string;
454
+ protected encryptionEnhance(passphrase: string, timestamp: string): string;
416
455
  /**
417
456
  * Format date to string
418
457
  * @param input Input date
@@ -488,6 +527,20 @@ export declare abstract class CoreApp<S extends IAppSettings, N, C extends Notif
488
527
  * @returns Time zone
489
528
  */
490
529
  getTimeZone(): string | undefined;
530
+ /**
531
+ * Hash message, SHA3 or HmacSHA512, 512 as Base64
532
+ * https://cryptojs.gitbook.io/docs/
533
+ * @param message Message
534
+ * @param passphrase Secret passphrase
535
+ */
536
+ hash(message: string, passphrase?: string): string;
537
+ /**
538
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
539
+ * https://cryptojs.gitbook.io/docs/
540
+ * @param message Message
541
+ * @param passphrase Secret passphrase
542
+ */
543
+ hashHex(message: string, passphrase?: string): string;
491
544
  /**
492
545
  * Check use has the specific role permission or not
493
546
  * @param roles Roles to check
@@ -1,7 +1,7 @@
1
1
  import { NotificationAlign, NotificationMessageType } from '@etsoo/notificationbase';
2
2
  import { ApiDataError } from '@etsoo/restclient';
3
3
  import { DateUtils, DomUtils, NumberUtils, StorageUtils, Utils } from '@etsoo/shared';
4
- import { AES } from 'crypto-js';
4
+ import { AES, algo, enc, HmacSHA512, lib, mode, pad, PBKDF2, SHA3 } from 'crypto-js';
5
5
  import { AddressRegion } from '../address/AddressRegion';
6
6
  import { AddressUtils } from '../address/AddressUtils';
7
7
  import { ActionResultError } from '../result/ActionResultError';
@@ -31,10 +31,19 @@ export class CoreApp {
31
31
  * Token refresh count down seed
32
32
  */
33
33
  this.refreshCountdownSeed = 0;
34
+ /**
35
+ * Device id field name
36
+ */
37
+ this.deviceIdField = 'SmartERPDeviceId';
38
+ /**
39
+ * Passphrase for encryption
40
+ */
41
+ this.passphrase = '***';
34
42
  this.settings = settings;
35
43
  this.api = api;
36
44
  this.notifier = notifier;
37
45
  this.name = name;
46
+ this.deviceId = StorageUtils.getLocalData(this.deviceIdField, '');
38
47
  this.setApi(api);
39
48
  const { currentCulture, currentRegion } = settings;
40
49
  this.changeCulture(currentCulture);
@@ -105,6 +114,81 @@ export class CoreApp {
105
114
  }
106
115
  };
107
116
  }
117
+ /**
118
+ * Init call
119
+ * @returns Result
120
+ */
121
+ async initCall() {
122
+ var _a;
123
+ const data = {
124
+ timestamp: new Date().getTime(),
125
+ deviceId: this.deviceId === '' ? undefined : this.deviceId
126
+ };
127
+ const result = await this.api.put('Auth/WebInitCall', data);
128
+ if (result == null)
129
+ return;
130
+ if (result.data == null) {
131
+ this.notifier.alert(this.get('noData'));
132
+ return;
133
+ }
134
+ if (!result.ok) {
135
+ const seconds = result.data.seconds;
136
+ const validSeconds = result.data.validSeconds;
137
+ if (result.title === 'timeDifferenceInvalid' &&
138
+ seconds != null &&
139
+ validSeconds != null) {
140
+ const title = (_a = this.get('timeDifferenceInvalid')) === null || _a === void 0 ? void 0 : _a.format(seconds.toString(), validSeconds.toString());
141
+ this.notifier.alert(title);
142
+ }
143
+ else {
144
+ this.alertResult(result);
145
+ }
146
+ return;
147
+ }
148
+ this.initCallUpdate(result.data, data.timestamp);
149
+ }
150
+ /**
151
+ * Init call update
152
+ * @param data Result data
153
+ * @param timestamp Timestamp
154
+ */
155
+ initCallUpdate(data, timestamp) {
156
+ if (data.deviceId == null || data.passphrase == null)
157
+ return;
158
+ // Decrypt
159
+ // Should be done within 120 seconds after returning from the backend
160
+ const passphrase = this.decrypt(data.passphrase, timestamp.toString(), 120);
161
+ if (passphrase == null)
162
+ return;
163
+ // Update device id and cache it
164
+ this.deviceId = data.deviceId;
165
+ StorageUtils.setLocalData(this.deviceIdField, this.deviceId);
166
+ // Current passphrase
167
+ this.passphrase = passphrase;
168
+ // Previous passphrase
169
+ if (data.previousPassphrase) {
170
+ const prev = this.decrypt(data.previousPassphrase, timestamp.toString(), 120);
171
+ // Update
172
+ const fields = this.initCallUpdateFields();
173
+ for (const field of fields) {
174
+ const currentValue = StorageUtils.getLocalData(field, '');
175
+ if (currentValue === '' || currentValue.indexOf('+') === -1)
176
+ continue;
177
+ const newValueSource = this.decrypt(currentValue, prev, 12);
178
+ if (newValueSource == null)
179
+ continue;
180
+ const newValue = this.encrypt(newValueSource);
181
+ StorageUtils.setLocalData(field, newValue);
182
+ }
183
+ }
184
+ }
185
+ /**
186
+ * Init call update fields in local storage
187
+ * @returns Fields
188
+ */
189
+ initCallUpdateFields() {
190
+ return [];
191
+ }
108
192
  /**
109
193
  * Alert action result
110
194
  * @param result Action result
@@ -197,13 +281,44 @@ export class CoreApp {
197
281
  * Decrypt message
198
282
  * @param messageEncrypted Encrypted message
199
283
  * @param passphrase Secret passphrase
284
+ * @param durationSeconds Duration seconds, <= 12 will be considered as month
200
285
  * @returns Pure text
201
286
  */
202
- decrypt(messageEncrypted, passphrase) {
287
+ decrypt(messageEncrypted, passphrase, durationSeconds) {
288
+ // Timestamp splitter
203
289
  const pos = messageEncrypted.indexOf('+');
204
- const miliseconds = messageEncrypted.substring(0, pos);
290
+ if (pos === -1 || messageEncrypted.length <= 66)
291
+ return undefined;
292
+ const timestamp = messageEncrypted.substring(0, pos);
205
293
  const message = messageEncrypted.substring(pos + 1);
206
- return AES.decrypt(message, this.encryptionEnhance(passphrase, miliseconds)).toString();
294
+ if (durationSeconds != null && durationSeconds > 0) {
295
+ const milseconds = Utils.charsToNumber(timestamp);
296
+ if (isNaN(milseconds) || milseconds < 1)
297
+ return undefined;
298
+ const timespan = new Date().substract(new Date(milseconds));
299
+ if ((durationSeconds <= 12 &&
300
+ timespan.totalMonths > durationSeconds) ||
301
+ (durationSeconds > 12 &&
302
+ timespan.totalSeconds > durationSeconds))
303
+ return undefined;
304
+ }
305
+ // Iterations
306
+ const iterations = parseInt(message.substring(0, 2), 10);
307
+ if (isNaN(iterations))
308
+ return undefined;
309
+ const salt = enc.Hex.parse(message.substring(2, 34));
310
+ const iv = enc.Hex.parse(message.substring(34, 66));
311
+ const encrypted = message.substring(66);
312
+ const key = PBKDF2(this.encryptionEnhance(passphrase !== null && passphrase !== void 0 ? passphrase : this.passphrase, timestamp), salt, {
313
+ keySize: 8,
314
+ hasher: algo.SHA256,
315
+ iterations: 1000 * iterations
316
+ });
317
+ return AES.decrypt(encrypted, key, {
318
+ iv,
319
+ padding: pad.Pkcs7,
320
+ mode: mode.CBC
321
+ }).toString(enc.Utf8);
207
322
  }
208
323
  /**
209
324
  * Detect IP data, call only one time
@@ -242,25 +357,44 @@ export class CoreApp {
242
357
  * Encrypt message
243
358
  * @param message Message
244
359
  * @param passphrase Secret passphrase
360
+ * @param iterations Iterations, 1000 times, 1 - 99
245
361
  * @returns Result
246
362
  */
247
- encrypt(message, passphrase) {
248
- const miliseconds = Utils.numberToChars(new Date().getTime());
249
- return (miliseconds +
363
+ encrypt(message, passphrase, iterations) {
364
+ // Default 1 * 1000
365
+ iterations !== null && iterations !== void 0 ? iterations : (iterations = 1);
366
+ // Timestamp
367
+ const timestamp = Utils.numberToChars(new Date().getTime());
368
+ const bits = 16; // 128 / 8
369
+ const salt = lib.WordArray.random(bits);
370
+ const key = PBKDF2(this.encryptionEnhance(passphrase !== null && passphrase !== void 0 ? passphrase : this.passphrase, timestamp), salt, {
371
+ keySize: 8,
372
+ hasher: algo.SHA256,
373
+ iterations: 1000 * iterations
374
+ });
375
+ const iv = lib.WordArray.random(bits);
376
+ return (timestamp +
250
377
  '+' +
251
- AES.encrypt(message, this.encryptionEnhance(passphrase, miliseconds)).toString());
378
+ iterations.toString().padStart(2, '0') +
379
+ salt.toString(enc.Hex) +
380
+ iv.toString(enc.Hex) +
381
+ AES.encrypt(message, key, {
382
+ iv,
383
+ padding: pad.Pkcs7,
384
+ mode: mode.CBC
385
+ }).toString() // enc.Base64
386
+ );
252
387
  }
253
388
  /**
254
389
  * Enchance secret passphrase
255
390
  * @param passphrase Secret passphrase
256
- * @param miliseconds Miliseconds
391
+ * @param timestamp Timestamp
257
392
  * @returns Enhanced passphrase
258
393
  */
259
- encryptionEnhance(passphrase, miliseconds) {
260
- var _a;
261
- passphrase += miliseconds;
394
+ encryptionEnhance(passphrase, timestamp) {
395
+ passphrase += timestamp;
262
396
  passphrase += passphrase.length.toString();
263
- return passphrase + ((_a = this.passphrase) !== null && _a !== void 0 ? _a : '');
397
+ return passphrase;
264
398
  }
265
399
  /**
266
400
  * Format date to string
@@ -322,7 +456,19 @@ export class CoreApp {
322
456
  * @param forceToLocal Force to local labels
323
457
  */
324
458
  formatResult(result, forceToLocal) {
325
- if ((result.title == null || forceToLocal) && result.type != null) {
459
+ const title = result.title;
460
+ if (title && /^\w+$/.test(title)) {
461
+ const key = title.formatInitial(false);
462
+ const localTitle = this.get(key);
463
+ if (localTitle) {
464
+ result.title = localTitle;
465
+ // Hold the original title in type when type is null
466
+ if (result.type == null)
467
+ result.type = title;
468
+ }
469
+ }
470
+ else if ((title == null || forceToLocal) && result.type != null) {
471
+ // Get label from type
326
472
  const key = result.type.formatInitial(false);
327
473
  result.title = this.get(key);
328
474
  }
@@ -388,6 +534,30 @@ export class CoreApp {
388
534
  // settings.timeZone = Utils.getTimeZone()
389
535
  return (_a = this.settings.timeZone) !== null && _a !== void 0 ? _a : (_b = this.ipData) === null || _b === void 0 ? void 0 : _b.timezone;
390
536
  }
537
+ /**
538
+ * Hash message, SHA3 or HmacSHA512, 512 as Base64
539
+ * https://cryptojs.gitbook.io/docs/
540
+ * @param message Message
541
+ * @param passphrase Secret passphrase
542
+ */
543
+ hash(message, passphrase) {
544
+ if (passphrase == null)
545
+ return SHA3(message, { outputLength: 512 }).toString(enc.Base64);
546
+ else
547
+ return HmacSHA512(message, passphrase).toString(enc.Base64);
548
+ }
549
+ /**
550
+ * Hash message Hex, SHA3 or HmacSHA512, 512 as Base64
551
+ * https://cryptojs.gitbook.io/docs/
552
+ * @param message Message
553
+ * @param passphrase Secret passphrase
554
+ */
555
+ hashHex(message, passphrase) {
556
+ if (passphrase == null)
557
+ return SHA3(message, { outputLength: 512 }).toString(enc.Hex);
558
+ else
559
+ return HmacSHA512(message, passphrase).toString(enc.Hex);
560
+ }
391
561
  /**
392
562
  * Check use has the specific role permission or not
393
563
  * @param roles Roles to check
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Init call dto
3
+ */
4
+ export declare type InitCallDto = {
5
+ /**
6
+ * Device id
7
+ */
8
+ deviceId?: string;
9
+ /**
10
+ * Timestamp
11
+ */
12
+ timestamp: number;
13
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -59,6 +59,7 @@
59
59
  "status": "Status",
60
60
  "submit": "Submit",
61
61
  "success": "Success",
62
+ "timeDifferenceInvalid": "The time difference between the device and the server is {0}, which exceeds the limit of {1} seconds. Please adjust the device time. If it is abnormal, please inform the administrator",
62
63
  "tokenExpiry": "Your session is about to expire. Click the Cancel button to continue",
63
64
  "yes": "Yes",
64
65
  "unknownError": "Unknown Error",
@@ -59,6 +59,7 @@
59
59
  "status": "状态",
60
60
  "submit": "提交",
61
61
  "success": "成功",
62
+ "timeDifferenceInvalid": "设备时间和服务器时间差为{0},超过{1}秒的限制,请调整设备时间,如果异常请告知管理员",
62
63
  "tokenExpiry": "您的会话即将过期。点击 取消 按钮继续使用",
63
64
  "yes": "是",
64
65
  "unknownError": "未知错误",
@@ -59,6 +59,7 @@
59
59
  "status": "狀態",
60
60
  "submit": "提交",
61
61
  "success": "成功",
62
+ "timeDifferenceInvalid": "設備時間和服務器時間差為{0},超過{1}秒的限制,請調整設備時間,如果異常請告知管理員",
62
63
  "tokenExpiry": "您的會話即將過期。點擊 取消 按鈕繼續使用",
63
64
  "yes": "是",
64
65
  "unknownError": "未知錯誤",
@@ -16,6 +16,7 @@ export * from './def/ListItem';
16
16
  export * from './dto/IdDto';
17
17
  export * from './dto/IdLabelDto';
18
18
  export * from './dto/IdLabelPrimaryDto';
19
+ export * from './dto/InitCallDto';
19
20
  export * from './dto/UpdateDto';
20
21
  export * from './i18n/enUS';
21
22
  export * from './i18n/zhCN';
@@ -25,6 +26,7 @@ export type { IApi, IApiPayload } from '@etsoo/restclient';
25
26
  export * from './result/ActionResult';
26
27
  export * from './result/ActionResultError';
27
28
  export * from './result/IActionResult';
29
+ export * from './result/InitCallResult';
28
30
  export * from './state/Culture';
29
31
  export * from './state/State';
30
32
  export * from './state/User';
package/lib/mjs/index.js CHANGED
@@ -22,6 +22,7 @@ export * from './def/ListItem';
22
22
  export * from './dto/IdDto';
23
23
  export * from './dto/IdLabelDto';
24
24
  export * from './dto/IdLabelPrimaryDto';
25
+ export * from './dto/InitCallDto';
25
26
  export * from './dto/UpdateDto';
26
27
  // i18n
27
28
  export * from './i18n/enUS';
@@ -33,6 +34,7 @@ export { ApiAuthorizationScheme, createClient } from '@etsoo/restclient';
33
34
  export * from './result/ActionResult';
34
35
  export * from './result/ActionResultError';
35
36
  export * from './result/IActionResult';
37
+ export * from './result/InitCallResult';
36
38
  // state
37
39
  export * from './state/Culture';
38
40
  export * from './state/State';
@@ -48,11 +48,11 @@ export interface IActionResult<D extends IResultData = IResultData> {
48
48
  /**
49
49
  * Trace id
50
50
  */
51
- readonly traceId?: string;
51
+ traceId?: string;
52
52
  /**
53
53
  * Type
54
54
  */
55
- readonly type: string;
55
+ type: string;
56
56
  /**
57
57
  * Success or not
58
58
  */
@@ -0,0 +1,30 @@
1
+ import { IActionResult, IResultData } from './IActionResult';
2
+ /**
3
+ * Init call result data
4
+ */
5
+ export interface InitCallResultData extends IResultData {
6
+ /**
7
+ * Device id
8
+ */
9
+ deviceId?: string;
10
+ /**
11
+ * Secret passphrase
12
+ */
13
+ passphrase?: string;
14
+ /**
15
+ * Previous secret passphrase
16
+ */
17
+ previousPassphrase?: string;
18
+ /**
19
+ * Actual seconds gap
20
+ */
21
+ seconds?: number;
22
+ /**
23
+ * Valid seconds gap
24
+ */
25
+ validSeconds?: number;
26
+ }
27
+ /**
28
+ * Init call result
29
+ */
30
+ export declare type InitCallResult = IActionResult<InitCallResultData>;
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etsoo/appscript",
3
- "version": "1.1.64",
3
+ "version": "1.1.68",
4
4
  "description": "Applications shared TypeScript framework",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/mjs/index.js",
@@ -23,7 +23,7 @@
23
23
  "testMatch": [
24
24
  "<rootDir>/__tests__/**/*.ts"
25
25
  ],
26
- "testEnvironment": "node",
26
+ "testEnvironment": "jsdom",
27
27
  "transform": {
28
28
  ".+\\.jsx?$": "babel-jest",
29
29
  ".+\\.tsx?$": "ts-jest"
@@ -65,9 +65,9 @@
65
65
  "@babel/preset-env": "^7.16.4",
66
66
  "@babel/runtime-corejs3": "^7.16.3",
67
67
  "@types/jest": "^27.0.3",
68
- "@typescript-eslint/eslint-plugin": "^5.5.0",
69
- "@typescript-eslint/parser": "^5.5.0",
70
- "eslint": "^8.4.0",
68
+ "@typescript-eslint/eslint-plugin": "^5.6.0",
69
+ "@typescript-eslint/parser": "^5.6.0",
70
+ "eslint": "^8.4.1",
71
71
  "eslint-config-airbnb-base": "^15.0.0",
72
72
  "eslint-plugin-import": "^2.25.3",
73
73
  "jest": "^27.4.3",