Anuj-Panthri commited on
Commit
332165f
·
2 Parent(s): 7715c1be1a2bcd

working on making everything work

Browse files
client/lib/Colors/CustomColorPicker.dart CHANGED
@@ -23,7 +23,9 @@ class CustomColorPicker extends StatefulWidget{
23
 
24
  class CustomColorPickerState extends State<CustomColorPicker>{
25
  late Color pickerColor;
 
26
  void initState(){
 
27
  setState(() {
28
  pickerColor=widget.pickerColor;
29
  });
 
23
 
24
  class CustomColorPickerState extends State<CustomColorPicker>{
25
  late Color pickerColor;
26
+ @override
27
  void initState(){
28
+ super.initState();
29
  setState(() {
30
  pickerColor=widget.pickerColor;
31
  });
client/lib/HomeScreen.dart CHANGED
@@ -1,5 +1,7 @@
1
  import 'package:flutter/material.dart';
2
  import 'package:flutter/services.dart'; // for going in fullscreen mode
 
 
3
 
4
  import 'package:shared_preferences/shared_preferences.dart';
5
 
@@ -9,6 +11,7 @@ import 'package:web_socket_channel/status.dart' as status;
9
  import 'package:flutter_colorpicker/flutter_colorpicker.dart';
10
 
11
  import "dart:convert";
 
12
 
13
  void enterFullScreen(){
14
  SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive,overlays: []);
@@ -32,12 +35,19 @@ class HomeScreenState extends State<HomeScreen>{
32
  late double width,height;
33
  late SharedPreferences prefs;
34
  double containerHeight = 0,containerWidth=0;
 
 
 
 
35
  double touchX = 0,touchY=0;
36
- bool touchIsVisible=false;
 
37
  late List<String> roomidList,roomnameList;
38
  String currRoomId="",currRoomName="";
39
  String username="";
40
- String profileColor="";
 
 
41
  final double outsidepadding=15;
42
 
43
  @override
@@ -52,6 +62,24 @@ class HomeScreenState extends State<HomeScreen>{
52
  getPreferences();
53
  // setPreferences();
54
  connectWebsocket();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
  @override
@@ -101,12 +129,12 @@ class HomeScreenState extends State<HomeScreen>{
101
  channel.sink.add(json.encode(data));
102
 
103
 
104
- // // set username
105
- // data = {
106
- // "route": "set_username",
107
- // "username": "anuj",
108
- // };
109
- // channel.sink.add(json.encode(data));
110
 
111
 
112
 
@@ -154,6 +182,18 @@ class HomeScreenState extends State<HomeScreen>{
154
  print(map);
155
  enterRoom(map);
156
  }
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  });
159
 
@@ -324,25 +364,147 @@ class HomeScreenState extends State<HomeScreen>{
324
  );
325
  }
326
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  void detectTouchStart(DragStartDetails details){
328
  // we have to stop see touch out of touch area
329
- print(details.localPosition);
 
 
 
 
 
 
 
330
  setState(() {
331
  touchX = details.globalPosition.dx;
332
  touchY = details.globalPosition.dy;
333
- touchIsVisible=true; // show it
 
 
 
 
 
334
  });
 
335
  }
336
  void detectTouchUpdate(DragUpdateDetails details){
337
  // we have to stop see touch out of touch area
338
-
339
- print(details.localPosition);
 
 
 
 
 
 
 
340
  setState(() {
341
  touchX = details.globalPosition.dx;
342
  touchY = details.globalPosition.dy;
343
  });
344
  // print(profileColor);
345
  // print(details.globalPosition);
 
 
 
 
 
346
  }
347
 
348
  void detectTouchEnd(DragEndDetails details){
@@ -350,7 +512,12 @@ class HomeScreenState extends State<HomeScreen>{
350
  setState(() {
351
  touchX = 0;
352
  touchY = 0;
353
- touchIsVisible=false; // hide it
 
 
 
 
 
354
  });
355
 
356
  }
@@ -460,20 +627,29 @@ class HomeScreenState extends State<HomeScreen>{
460
  ),
461
  ),
462
 
463
- // touch point
464
- Visibility(
465
- visible:touchIsVisible,
466
- child: Positioned(
467
- left:touchX,
468
- top:touchY,
469
- child: Container(
470
- width:50,
471
- height:50,
472
- color:colorFromHex(profileColor),
473
- ),
474
- ),
 
 
 
 
 
 
 
 
 
475
  ),
476
-
477
  ]),
478
 
479
  );
 
1
  import 'package:flutter/material.dart';
2
  import 'package:flutter/services.dart'; // for going in fullscreen mode
3
+ import 'package:flutter/scheduler.dart'; // to add scheduler for touch collision detection
4
+
5
 
6
  import 'package:shared_preferences/shared_preferences.dart';
7
 
 
11
  import 'package:flutter_colorpicker/flutter_colorpicker.dart';
12
 
13
  import "dart:convert";
14
+ import "dart:async";
15
 
16
  void enterFullScreen(){
17
  SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive,overlays: []);
 
35
  late double width,height;
36
  late SharedPreferences prefs;
37
  double containerHeight = 0,containerWidth=0;
38
+
39
+ late Timer sendTouchScheduler;
40
+
41
+ double touchsize=40;
42
  double touchX = 0,touchY=0;
43
+ bool isTouching=false;
44
+
45
  late List<String> roomidList,roomnameList;
46
  String currRoomId="",currRoomName="";
47
  String username="";
48
+ String userid="";
49
+ String profileColor="ff000000";
50
+ List<Map> othersTouchPoints=[];
51
  final double outsidepadding=15;
52
 
53
  @override
 
62
  getPreferences();
63
  // setPreferences();
64
  connectWebsocket();
65
+
66
+ Object data={
67
+ "user": {
68
+ "username": "anne",
69
+ "id": "string",
70
+ },
71
+ "position": {
72
+ "x": 500,
73
+ "y": 500,
74
+ },
75
+ "color": "ff2244bb", // Hex value.
76
+ "intensity": 1, // Vibration intensity.
77
+ };
78
+
79
+ othersTouchPoints=[
80
+ jsonDecode(jsonEncode(data)),
81
+ ];
82
+
83
  }
84
 
85
  @override
 
129
  channel.sink.add(json.encode(data));
130
 
131
 
132
+ // set username
133
+ data = {
134
+ "route": "set_username",
135
+ "username": prefs.getString("username"),
136
+ };
137
+ channel.sink.add(json.encode(data));
138
 
139
 
140
 
 
182
  print(map);
183
  enterRoom(map);
184
  }
185
+ else if(type=="send_vibration_response"){ // handles when we send_touch without being in a room
186
+ print(map);
187
+ ScaffoldMessenger.of(context).hideCurrentSnackBar();
188
+ ScaffoldMessenger.of(context).showSnackBar(
189
+ SnackBar(
190
+ content:Text(map['status'])
191
+ )
192
+ );
193
+ }
194
+ else if(type=="receive_touch"){
195
+ print(map);
196
+ }
197
 
198
  });
199
 
 
364
  );
365
  }
366
 
367
+
368
+ // runs every 1 second
369
+ void startSendTouch(){
370
+ sendTouchScheduler=Timer.periodic(
371
+ Duration(seconds: 1), (timer) async {
372
+ // send touch to server
373
+ Map data;
374
+
375
+ try{
376
+ await channel.ready;
377
+ }
378
+ catch(e){
379
+ print("Can't connect to the web socket server");
380
+ return;
381
+ }
382
+ data={
383
+ "route": "send_touch",
384
+ "id": 1, // Used to indentify vibrations for updating or disabling them
385
+ "type": "enabled", // Whether the vibration is active or not.
386
+ "position": {
387
+ // "x":20,
388
+ // "y":20,
389
+ "x": touchX/width, // send ratio x e.g. : x:700, width:800, then send 0.9
390
+ "y": touchY/height, // send ratio y e.g. : y:400, height:800 then send 0.5
391
+ },
392
+ "color": profileColor, // Hex value. Default: random
393
+ // "intensity"?: 1 // Vibration intensity. Default: 1
394
+ };
395
+
396
+ channel.sink.add(jsonEncode(data));
397
+
398
+
399
+ }
400
+ );
401
+ }
402
+
403
+ void stopSendTouch(){
404
+ sendTouchScheduler.cancel();
405
+ // send one last touch to tell touch is ended
406
+
407
+ }
408
+
409
+ Widget buildTouchPoint({
410
+ required bool visible,
411
+ required double x,
412
+ required double y,
413
+ required double size,
414
+ required Color color
415
+ }){
416
+
417
+ color=color.withOpacity(0.5);
418
+ return Visibility(
419
+ visible:visible,
420
+ child: Positioned(
421
+ left:x-size/2,
422
+ top:y-size/2,
423
+ child: Container(
424
+ width:size,
425
+ height:size,
426
+ decoration: BoxDecoration(
427
+ color:color,
428
+ borderRadius: BorderRadius.circular(50),
429
+ boxShadow: [
430
+ BoxShadow(
431
+ color:color,
432
+ blurRadius: 10,
433
+ spreadRadius: 5,
434
+ ),
435
+ ]),
436
+ ),
437
+ ),
438
+ );
439
+ }
440
+
441
+ void detectTouchCollision({
442
+ required double x,
443
+ required double y,
444
+ }){
445
+ // detect our touch point collision with any othersTouchPoints
446
+ // print(othersTouchPoints);
447
+ // print("listening");
448
+ for(Map map in othersTouchPoints){
449
+
450
+ // print("inside");
451
+ double dx = (map["position"]["x"]-x).abs();
452
+ double dy = (map["position"]["y"]-y).abs();
453
+ // print("x:"+x.toString()+"\ty:"+y.toString());
454
+ // print("dx:"+dx.toString()+"\tdy:"+dy.toString());
455
+ if (dx<=touchsize && dy<=touchsize){
456
+ print("Touch Collision.");
457
+ }
458
+ // else{
459
+ // print("No Touch Collision.");
460
+ // }
461
+ }
462
+ }
463
+
464
  void detectTouchStart(DragStartDetails details){
465
  // we have to stop see touch out of touch area
466
+ // print(details.localPosition);
467
+
468
+ // detect Collision
469
+ detectTouchCollision(
470
+ x:details.globalPosition.dx,
471
+ y:details.globalPosition.dy,
472
+ );
473
+
474
  setState(() {
475
  touchX = details.globalPosition.dx;
476
  touchY = details.globalPosition.dy;
477
+ isTouching=true; // show it
478
+ });
479
+
480
+ // start send Touch Scheduler
481
+ SchedulerBinding.instance.addPostFrameCallback((_) {
482
+ startSendTouch();
483
  });
484
+
485
  }
486
  void detectTouchUpdate(DragUpdateDetails details){
487
  // we have to stop see touch out of touch area
488
+
489
+
490
+ // detect Collision
491
+ detectTouchCollision(
492
+ x:details.globalPosition.dx,
493
+ y:details.globalPosition.dy,
494
+ );
495
+
496
+ // print(details.localPosition);
497
  setState(() {
498
  touchX = details.globalPosition.dx;
499
  touchY = details.globalPosition.dy;
500
  });
501
  // print(profileColor);
502
  // print(details.globalPosition);
503
+
504
+ // // detect Collision
505
+ // SchedulerBinding.instance.addPostFrameCallback((_) {
506
+ // detectTouchCollision();
507
+ // });
508
  }
509
 
510
  void detectTouchEnd(DragEndDetails details){
 
512
  setState(() {
513
  touchX = 0;
514
  touchY = 0;
515
+ isTouching=false; // hide it
516
+ });
517
+
518
+ // stop send Touch Scheduler and send stop touch message
519
+ SchedulerBinding.instance.addPostFrameCallback((_) {
520
+ stopSendTouch();
521
  });
522
 
523
  }
 
627
  ),
628
  ),
629
 
630
+
631
+ // render received touch points
632
+ ...othersTouchPoints.map((map){
633
+ return buildTouchPoint(
634
+ visible:true,
635
+ x:map['position']['x'],
636
+ y:map['position']['y'],
637
+ size:touchsize,
638
+ color:colorFromHex(map['color'])!,
639
+ );
640
+ }).toList(),
641
+
642
+
643
+ // our touch point
644
+ buildTouchPoint(
645
+ visible:isTouching,
646
+ x:touchX,
647
+ y:touchY,
648
+ size:touchsize,
649
+ color:colorFromHex(profileColor)!,
650
+ // color:Colors.red,
651
  ),
652
+
653
  ]),
654
 
655
  );
server/.prettierrc CHANGED
@@ -1,5 +1,6 @@
1
  {
2
  "trailingComma": "es5",
3
  "tabWidth": 4,
4
- "singleQuote": false
 
5
  }
 
1
  {
2
  "trailingComma": "es5",
3
  "tabWidth": 4,
4
+ "singleQuote": false,
5
+ "semi": true
6
  }
server/package-lock.json CHANGED
@@ -11,12 +11,17 @@
11
  "dependencies": {
12
  "@types/ws": "^8.5.9",
13
  "express": "^4.18.2",
 
14
  "ws": "^8.14.2",
15
  "zod": "^3.22.4"
16
  },
17
  "devDependencies": {
 
18
  "@types/express": "^4.17.21",
 
19
  "@types/node": "^20.9.1",
 
 
20
  "nodemon": "^3.0.1",
21
  "prettier": "^3.1.0",
22
  "ts-node": "^10.9.1",
@@ -94,6 +99,12 @@
94
  "@types/node": "*"
95
  }
96
  },
 
 
 
 
 
 
97
  "node_modules/@types/connect": {
98
  "version": "3.4.38",
99
  "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
@@ -139,6 +150,12 @@
139
  "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
140
  "dev": true
141
  },
 
 
 
 
 
 
142
  "node_modules/@types/node": {
143
  "version": "20.9.1",
144
  "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.1.tgz",
@@ -194,6 +211,17 @@
194
  "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
195
  "dev": true
196
  },
 
 
 
 
 
 
 
 
 
 
 
197
  "node_modules/accepts": {
198
  "version": "1.3.8",
199
  "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -227,6 +255,39 @@
227
  "node": ">=0.4.0"
228
  }
229
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  "node_modules/anymatch": {
231
  "version": "3.1.3",
232
  "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -246,17 +307,59 @@
246
  "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
247
  "dev": true
248
  },
 
 
 
 
 
 
249
  "node_modules/array-flatten": {
250
  "version": "1.1.1",
251
  "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
252
  "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
253
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  "node_modules/balanced-match": {
255
  "version": "1.0.2",
256
  "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
257
  "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
258
  "dev": true
259
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  "node_modules/binary-extensions": {
261
  "version": "2.2.0",
262
  "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -324,6 +427,35 @@
324
  "node": ">=8"
325
  }
326
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  "node_modules/bytes": {
328
  "version": "3.1.2",
329
  "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -345,6 +477,85 @@
345
  "url": "https://github.com/sponsors/ljharb"
346
  }
347
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  "node_modules/chokidar": {
349
  "version": "3.5.3",
350
  "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@@ -372,6 +583,35 @@
372
  "fsevents": "~2.3.2"
373
  }
374
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  "node_modules/concat-map": {
376
  "version": "0.0.1",
377
  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -425,6 +665,30 @@
425
  "ms": "^2.1.1"
426
  }
427
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  "node_modules/define-data-property": {
429
  "version": "1.1.1",
430
  "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
@@ -469,6 +733,12 @@
469
  "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
470
  "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
471
  },
 
 
 
 
 
 
472
  "node_modules/encodeurl": {
473
  "version": "1.0.2",
474
  "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -477,11 +747,32 @@
477
  "node": ">= 0.8"
478
  }
479
  },
 
 
 
 
 
 
 
 
 
480
  "node_modules/escape-html": {
481
  "version": "1.0.3",
482
  "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
483
  "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
484
  },
 
 
 
 
 
 
 
 
 
 
 
 
485
  "node_modules/etag": {
486
  "version": "1.8.1",
487
  "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -490,6 +781,22 @@
490
  "node": ">= 0.6"
491
  }
492
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  "node_modules/express": {
494
  "version": "4.18.2",
495
  "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -544,6 +851,14 @@
544
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
545
  "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
546
  },
 
 
 
 
 
 
 
 
547
  "node_modules/fill-range": {
548
  "version": "7.0.1",
549
  "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -586,6 +901,31 @@
586
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
587
  "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
588
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  "node_modules/forwarded": {
590
  "version": "0.2.0",
591
  "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -602,6 +942,12 @@
602
  "node": ">= 0.6"
603
  }
604
  },
 
 
 
 
 
 
605
  "node_modules/fsevents": {
606
  "version": "2.3.3",
607
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -624,6 +970,24 @@
624
  "url": "https://github.com/sponsors/ljharb"
625
  }
626
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  "node_modules/get-intrinsic": {
628
  "version": "1.2.2",
629
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
@@ -638,6 +1002,26 @@
638
  "url": "https://github.com/sponsors/ljharb"
639
  }
640
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
  "node_modules/glob-parent": {
642
  "version": "5.1.2",
643
  "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -714,6 +1098,15 @@
714
  "node": ">= 0.4"
715
  }
716
  },
 
 
 
 
 
 
 
 
 
717
  "node_modules/http-errors": {
718
  "version": "2.0.0",
719
  "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -740,12 +1133,41 @@
740
  "node": ">=0.10.0"
741
  }
742
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
  "node_modules/ignore-by-default": {
744
  "version": "1.0.1",
745
  "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
746
  "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
747
  "dev": true
748
  },
 
 
 
 
 
 
 
 
 
 
749
  "node_modules/inherits": {
750
  "version": "2.0.4",
751
  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -780,6 +1202,15 @@
780
  "node": ">=0.10.0"
781
  }
782
  },
 
 
 
 
 
 
 
 
 
783
  "node_modules/is-glob": {
784
  "version": "4.0.3",
785
  "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -801,20 +1232,93 @@
801
  "node": ">=0.12.0"
802
  }
803
  },
804
- "node_modules/lru-cache": {
805
- "version": "6.0.0",
806
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
807
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
808
  "dev": true,
809
- "dependencies": {
810
- "yallist": "^4.0.0"
811
- },
812
  "engines": {
813
- "node": ">=10"
814
  }
815
  },
816
- "node_modules/make-error": {
817
- "version": "1.3.6",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
818
  "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
819
  "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
820
  "dev": true
@@ -882,11 +1386,140 @@
882
  "node": "*"
883
  }
884
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
885
  "node_modules/ms": {
886
  "version": "2.1.3",
887
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
888
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
889
  },
 
 
 
 
 
 
 
 
 
 
 
 
890
  "node_modules/negotiator": {
891
  "version": "0.6.3",
892
  "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -955,6 +1588,14 @@
955
  "url": "https://github.com/sponsors/ljharb"
956
  }
957
  },
 
 
 
 
 
 
 
 
958
  "node_modules/on-finished": {
959
  "version": "2.4.1",
960
  "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -966,6 +1607,45 @@
966
  "node": ">= 0.8"
967
  }
968
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
969
  "node_modules/parseurl": {
970
  "version": "1.3.3",
971
  "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -974,11 +1654,38 @@
974
  "node": ">= 0.8"
975
  }
976
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
977
  "node_modules/path-to-regexp": {
978
  "version": "0.1.7",
979
  "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
980
  "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
981
  },
 
 
 
 
 
 
 
 
 
982
  "node_modules/picomatch": {
983
  "version": "2.3.1",
984
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -991,6 +1698,41 @@
991
  "url": "https://github.com/sponsors/jonschlinkert"
992
  }
993
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
994
  "node_modules/prettier": {
995
  "version": "3.1.0",
996
  "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
@@ -1006,6 +1748,19 @@
1006
  "url": "https://github.com/prettier/prettier?sponsor=1"
1007
  }
1008
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
1009
  "node_modules/proxy-addr": {
1010
  "version": "2.0.7",
1011
  "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -1038,6 +1793,20 @@
1038
  "url": "https://github.com/sponsors/ljharb"
1039
  }
1040
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1041
  "node_modules/range-parser": {
1042
  "version": "1.2.1",
1043
  "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -1060,6 +1829,21 @@
1060
  "node": ">= 0.8"
1061
  }
1062
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1063
  "node_modules/readdirp": {
1064
  "version": "3.6.0",
1065
  "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -1072,6 +1856,23 @@
1072
  "node": ">=8.10.0"
1073
  }
1074
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1075
  "node_modules/safe-buffer": {
1076
  "version": "5.2.1",
1077
  "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1091,6 +1892,14 @@
1091
  }
1092
  ]
1093
  },
 
 
 
 
 
 
 
 
1094
  "node_modules/safer-buffer": {
1095
  "version": "2.1.2",
1096
  "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1147,6 +1956,15 @@
1147
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1148
  "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
1149
  },
 
 
 
 
 
 
 
 
 
1150
  "node_modules/serve-static": {
1151
  "version": "1.15.0",
1152
  "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
@@ -1205,6 +2023,22 @@
1205
  "node": ">=10"
1206
  }
1207
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1208
  "node_modules/statuses": {
1209
  "version": "2.0.1",
1210
  "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@@ -1213,6 +2047,52 @@
1213
  "node": ">= 0.8"
1214
  }
1215
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1216
  "node_modules/supports-color": {
1217
  "version": "5.5.0",
1218
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -1225,6 +2105,14 @@
1225
  "node": ">=4"
1226
  }
1227
  },
 
 
 
 
 
 
 
 
1228
  "node_modules/to-regex-range": {
1229
  "version": "5.0.1",
1230
  "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -1300,6 +2188,15 @@
1300
  }
1301
  }
1302
  },
 
 
 
 
 
 
 
 
 
1303
  "node_modules/type-is": {
1304
  "version": "1.6.18",
1305
  "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -1366,6 +2263,35 @@
1366
  "node": ">= 0.8"
1367
  }
1368
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1369
  "node_modules/ws": {
1370
  "version": "8.14.2",
1371
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
@@ -1386,12 +2312,63 @@
1386
  }
1387
  }
1388
  },
 
 
 
 
 
 
 
 
 
1389
  "node_modules/yallist": {
1390
  "version": "4.0.0",
1391
  "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
1392
  "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
1393
  "dev": true
1394
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1395
  "node_modules/yn": {
1396
  "version": "3.1.1",
1397
  "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
@@ -1401,6 +2378,18 @@
1401
  "node": ">=6"
1402
  }
1403
  },
 
 
 
 
 
 
 
 
 
 
 
 
1404
  "node_modules/zod": {
1405
  "version": "3.22.4",
1406
  "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
 
11
  "dependencies": {
12
  "@types/ws": "^8.5.9",
13
  "express": "^4.18.2",
14
+ "pino": "^8.16.2",
15
  "ws": "^8.14.2",
16
  "zod": "^3.22.4"
17
  },
18
  "devDependencies": {
19
+ "@types/chai": "^4.3.10",
20
  "@types/express": "^4.17.21",
21
+ "@types/mocha": "^10.0.4",
22
  "@types/node": "^20.9.1",
23
+ "chai": "^4.3.10",
24
+ "mocha": "^10.2.0",
25
  "nodemon": "^3.0.1",
26
  "prettier": "^3.1.0",
27
  "ts-node": "^10.9.1",
 
99
  "@types/node": "*"
100
  }
101
  },
102
+ "node_modules/@types/chai": {
103
+ "version": "4.3.10",
104
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.10.tgz",
105
+ "integrity": "sha512-of+ICnbqjmFCiixUnqRulbylyXQrPqIGf/B3Jax1wIF3DvSheysQxAWvqHhZiW3IQrycvokcLcFQlveGp+vyNg==",
106
+ "dev": true
107
+ },
108
  "node_modules/@types/connect": {
109
  "version": "3.4.38",
110
  "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
 
150
  "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
151
  "dev": true
152
  },
153
+ "node_modules/@types/mocha": {
154
+ "version": "10.0.4",
155
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.4.tgz",
156
+ "integrity": "sha512-xKU7bUjiFTIttpWaIZ9qvgg+22O1nmbA+HRxdlR+u6TWsGfmFdXrheJoK4fFxrHNVIOBDvDNKZG+LYBpMHpX3w==",
157
+ "dev": true
158
+ },
159
  "node_modules/@types/node": {
160
  "version": "20.9.1",
161
  "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.1.tgz",
 
211
  "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
212
  "dev": true
213
  },
214
+ "node_modules/abort-controller": {
215
+ "version": "3.0.0",
216
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
217
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
218
+ "dependencies": {
219
+ "event-target-shim": "^5.0.0"
220
+ },
221
+ "engines": {
222
+ "node": ">=6.5"
223
+ }
224
+ },
225
  "node_modules/accepts": {
226
  "version": "1.3.8",
227
  "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
 
255
  "node": ">=0.4.0"
256
  }
257
  },
258
+ "node_modules/ansi-colors": {
259
+ "version": "4.1.1",
260
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
261
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
262
+ "dev": true,
263
+ "engines": {
264
+ "node": ">=6"
265
+ }
266
+ },
267
+ "node_modules/ansi-regex": {
268
+ "version": "5.0.1",
269
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
270
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
271
+ "dev": true,
272
+ "engines": {
273
+ "node": ">=8"
274
+ }
275
+ },
276
+ "node_modules/ansi-styles": {
277
+ "version": "4.3.0",
278
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
279
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
280
+ "dev": true,
281
+ "dependencies": {
282
+ "color-convert": "^2.0.1"
283
+ },
284
+ "engines": {
285
+ "node": ">=8"
286
+ },
287
+ "funding": {
288
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
289
+ }
290
+ },
291
  "node_modules/anymatch": {
292
  "version": "3.1.3",
293
  "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
 
307
  "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
308
  "dev": true
309
  },
310
+ "node_modules/argparse": {
311
+ "version": "2.0.1",
312
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
313
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
314
+ "dev": true
315
+ },
316
  "node_modules/array-flatten": {
317
  "version": "1.1.1",
318
  "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
319
  "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
320
  },
321
+ "node_modules/assertion-error": {
322
+ "version": "1.1.0",
323
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
324
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
325
+ "dev": true,
326
+ "engines": {
327
+ "node": "*"
328
+ }
329
+ },
330
+ "node_modules/atomic-sleep": {
331
+ "version": "1.0.0",
332
+ "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
333
+ "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
334
+ "engines": {
335
+ "node": ">=8.0.0"
336
+ }
337
+ },
338
  "node_modules/balanced-match": {
339
  "version": "1.0.2",
340
  "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
341
  "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
342
  "dev": true
343
  },
344
+ "node_modules/base64-js": {
345
+ "version": "1.5.1",
346
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
347
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
348
+ "funding": [
349
+ {
350
+ "type": "github",
351
+ "url": "https://github.com/sponsors/feross"
352
+ },
353
+ {
354
+ "type": "patreon",
355
+ "url": "https://www.patreon.com/feross"
356
+ },
357
+ {
358
+ "type": "consulting",
359
+ "url": "https://feross.org/support"
360
+ }
361
+ ]
362
+ },
363
  "node_modules/binary-extensions": {
364
  "version": "2.2.0",
365
  "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
 
427
  "node": ">=8"
428
  }
429
  },
430
+ "node_modules/browser-stdout": {
431
+ "version": "1.3.1",
432
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
433
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
434
+ "dev": true
435
+ },
436
+ "node_modules/buffer": {
437
+ "version": "6.0.3",
438
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
439
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
440
+ "funding": [
441
+ {
442
+ "type": "github",
443
+ "url": "https://github.com/sponsors/feross"
444
+ },
445
+ {
446
+ "type": "patreon",
447
+ "url": "https://www.patreon.com/feross"
448
+ },
449
+ {
450
+ "type": "consulting",
451
+ "url": "https://feross.org/support"
452
+ }
453
+ ],
454
+ "dependencies": {
455
+ "base64-js": "^1.3.1",
456
+ "ieee754": "^1.2.1"
457
+ }
458
+ },
459
  "node_modules/bytes": {
460
  "version": "3.1.2",
461
  "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
 
477
  "url": "https://github.com/sponsors/ljharb"
478
  }
479
  },
480
+ "node_modules/camelcase": {
481
+ "version": "6.3.0",
482
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
483
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
484
+ "dev": true,
485
+ "engines": {
486
+ "node": ">=10"
487
+ },
488
+ "funding": {
489
+ "url": "https://github.com/sponsors/sindresorhus"
490
+ }
491
+ },
492
+ "node_modules/chai": {
493
+ "version": "4.3.10",
494
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz",
495
+ "integrity": "sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==",
496
+ "dev": true,
497
+ "dependencies": {
498
+ "assertion-error": "^1.1.0",
499
+ "check-error": "^1.0.3",
500
+ "deep-eql": "^4.1.3",
501
+ "get-func-name": "^2.0.2",
502
+ "loupe": "^2.3.6",
503
+ "pathval": "^1.1.1",
504
+ "type-detect": "^4.0.8"
505
+ },
506
+ "engines": {
507
+ "node": ">=4"
508
+ }
509
+ },
510
+ "node_modules/chalk": {
511
+ "version": "4.1.2",
512
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
513
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
514
+ "dev": true,
515
+ "dependencies": {
516
+ "ansi-styles": "^4.1.0",
517
+ "supports-color": "^7.1.0"
518
+ },
519
+ "engines": {
520
+ "node": ">=10"
521
+ },
522
+ "funding": {
523
+ "url": "https://github.com/chalk/chalk?sponsor=1"
524
+ }
525
+ },
526
+ "node_modules/chalk/node_modules/has-flag": {
527
+ "version": "4.0.0",
528
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
529
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
530
+ "dev": true,
531
+ "engines": {
532
+ "node": ">=8"
533
+ }
534
+ },
535
+ "node_modules/chalk/node_modules/supports-color": {
536
+ "version": "7.2.0",
537
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
538
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
539
+ "dev": true,
540
+ "dependencies": {
541
+ "has-flag": "^4.0.0"
542
+ },
543
+ "engines": {
544
+ "node": ">=8"
545
+ }
546
+ },
547
+ "node_modules/check-error": {
548
+ "version": "1.0.3",
549
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
550
+ "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==",
551
+ "dev": true,
552
+ "dependencies": {
553
+ "get-func-name": "^2.0.2"
554
+ },
555
+ "engines": {
556
+ "node": "*"
557
+ }
558
+ },
559
  "node_modules/chokidar": {
560
  "version": "3.5.3",
561
  "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
 
583
  "fsevents": "~2.3.2"
584
  }
585
  },
586
+ "node_modules/cliui": {
587
+ "version": "7.0.4",
588
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
589
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
590
+ "dev": true,
591
+ "dependencies": {
592
+ "string-width": "^4.2.0",
593
+ "strip-ansi": "^6.0.0",
594
+ "wrap-ansi": "^7.0.0"
595
+ }
596
+ },
597
+ "node_modules/color-convert": {
598
+ "version": "2.0.1",
599
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
600
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
601
+ "dev": true,
602
+ "dependencies": {
603
+ "color-name": "~1.1.4"
604
+ },
605
+ "engines": {
606
+ "node": ">=7.0.0"
607
+ }
608
+ },
609
+ "node_modules/color-name": {
610
+ "version": "1.1.4",
611
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
612
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
613
+ "dev": true
614
+ },
615
  "node_modules/concat-map": {
616
  "version": "0.0.1",
617
  "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
 
665
  "ms": "^2.1.1"
666
  }
667
  },
668
+ "node_modules/decamelize": {
669
+ "version": "4.0.0",
670
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
671
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
672
+ "dev": true,
673
+ "engines": {
674
+ "node": ">=10"
675
+ },
676
+ "funding": {
677
+ "url": "https://github.com/sponsors/sindresorhus"
678
+ }
679
+ },
680
+ "node_modules/deep-eql": {
681
+ "version": "4.1.3",
682
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
683
+ "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==",
684
+ "dev": true,
685
+ "dependencies": {
686
+ "type-detect": "^4.0.0"
687
+ },
688
+ "engines": {
689
+ "node": ">=6"
690
+ }
691
+ },
692
  "node_modules/define-data-property": {
693
  "version": "1.1.1",
694
  "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
 
733
  "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
734
  "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
735
  },
736
+ "node_modules/emoji-regex": {
737
+ "version": "8.0.0",
738
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
739
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
740
+ "dev": true
741
+ },
742
  "node_modules/encodeurl": {
743
  "version": "1.0.2",
744
  "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
 
747
  "node": ">= 0.8"
748
  }
749
  },
750
+ "node_modules/escalade": {
751
+ "version": "3.1.1",
752
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
753
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
754
+ "dev": true,
755
+ "engines": {
756
+ "node": ">=6"
757
+ }
758
+ },
759
  "node_modules/escape-html": {
760
  "version": "1.0.3",
761
  "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
762
  "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
763
  },
764
+ "node_modules/escape-string-regexp": {
765
+ "version": "4.0.0",
766
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
767
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
768
+ "dev": true,
769
+ "engines": {
770
+ "node": ">=10"
771
+ },
772
+ "funding": {
773
+ "url": "https://github.com/sponsors/sindresorhus"
774
+ }
775
+ },
776
  "node_modules/etag": {
777
  "version": "1.8.1",
778
  "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
 
781
  "node": ">= 0.6"
782
  }
783
  },
784
+ "node_modules/event-target-shim": {
785
+ "version": "5.0.1",
786
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
787
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
788
+ "engines": {
789
+ "node": ">=6"
790
+ }
791
+ },
792
+ "node_modules/events": {
793
+ "version": "3.3.0",
794
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
795
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
796
+ "engines": {
797
+ "node": ">=0.8.x"
798
+ }
799
+ },
800
  "node_modules/express": {
801
  "version": "4.18.2",
802
  "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
 
851
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
852
  "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
853
  },
854
+ "node_modules/fast-redact": {
855
+ "version": "3.3.0",
856
+ "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz",
857
+ "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==",
858
+ "engines": {
859
+ "node": ">=6"
860
+ }
861
+ },
862
  "node_modules/fill-range": {
863
  "version": "7.0.1",
864
  "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
 
901
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
902
  "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
903
  },
904
+ "node_modules/find-up": {
905
+ "version": "5.0.0",
906
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
907
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
908
+ "dev": true,
909
+ "dependencies": {
910
+ "locate-path": "^6.0.0",
911
+ "path-exists": "^4.0.0"
912
+ },
913
+ "engines": {
914
+ "node": ">=10"
915
+ },
916
+ "funding": {
917
+ "url": "https://github.com/sponsors/sindresorhus"
918
+ }
919
+ },
920
+ "node_modules/flat": {
921
+ "version": "5.0.2",
922
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
923
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
924
+ "dev": true,
925
+ "bin": {
926
+ "flat": "cli.js"
927
+ }
928
+ },
929
  "node_modules/forwarded": {
930
  "version": "0.2.0",
931
  "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
 
942
  "node": ">= 0.6"
943
  }
944
  },
945
+ "node_modules/fs.realpath": {
946
+ "version": "1.0.0",
947
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
948
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
949
+ "dev": true
950
+ },
951
  "node_modules/fsevents": {
952
  "version": "2.3.3",
953
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 
970
  "url": "https://github.com/sponsors/ljharb"
971
  }
972
  },
973
+ "node_modules/get-caller-file": {
974
+ "version": "2.0.5",
975
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
976
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
977
+ "dev": true,
978
+ "engines": {
979
+ "node": "6.* || 8.* || >= 10.*"
980
+ }
981
+ },
982
+ "node_modules/get-func-name": {
983
+ "version": "2.0.2",
984
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
985
+ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
986
+ "dev": true,
987
+ "engines": {
988
+ "node": "*"
989
+ }
990
+ },
991
  "node_modules/get-intrinsic": {
992
  "version": "1.2.2",
993
  "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
 
1002
  "url": "https://github.com/sponsors/ljharb"
1003
  }
1004
  },
1005
+ "node_modules/glob": {
1006
+ "version": "7.2.0",
1007
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
1008
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
1009
+ "dev": true,
1010
+ "dependencies": {
1011
+ "fs.realpath": "^1.0.0",
1012
+ "inflight": "^1.0.4",
1013
+ "inherits": "2",
1014
+ "minimatch": "^3.0.4",
1015
+ "once": "^1.3.0",
1016
+ "path-is-absolute": "^1.0.0"
1017
+ },
1018
+ "engines": {
1019
+ "node": "*"
1020
+ },
1021
+ "funding": {
1022
+ "url": "https://github.com/sponsors/isaacs"
1023
+ }
1024
+ },
1025
  "node_modules/glob-parent": {
1026
  "version": "5.1.2",
1027
  "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
 
1098
  "node": ">= 0.4"
1099
  }
1100
  },
1101
+ "node_modules/he": {
1102
+ "version": "1.2.0",
1103
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
1104
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
1105
+ "dev": true,
1106
+ "bin": {
1107
+ "he": "bin/he"
1108
+ }
1109
+ },
1110
  "node_modules/http-errors": {
1111
  "version": "2.0.0",
1112
  "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
 
1133
  "node": ">=0.10.0"
1134
  }
1135
  },
1136
+ "node_modules/ieee754": {
1137
+ "version": "1.2.1",
1138
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
1139
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
1140
+ "funding": [
1141
+ {
1142
+ "type": "github",
1143
+ "url": "https://github.com/sponsors/feross"
1144
+ },
1145
+ {
1146
+ "type": "patreon",
1147
+ "url": "https://www.patreon.com/feross"
1148
+ },
1149
+ {
1150
+ "type": "consulting",
1151
+ "url": "https://feross.org/support"
1152
+ }
1153
+ ]
1154
+ },
1155
  "node_modules/ignore-by-default": {
1156
  "version": "1.0.1",
1157
  "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
1158
  "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
1159
  "dev": true
1160
  },
1161
+ "node_modules/inflight": {
1162
+ "version": "1.0.6",
1163
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
1164
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
1165
+ "dev": true,
1166
+ "dependencies": {
1167
+ "once": "^1.3.0",
1168
+ "wrappy": "1"
1169
+ }
1170
+ },
1171
  "node_modules/inherits": {
1172
  "version": "2.0.4",
1173
  "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
 
1202
  "node": ">=0.10.0"
1203
  }
1204
  },
1205
+ "node_modules/is-fullwidth-code-point": {
1206
+ "version": "3.0.0",
1207
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
1208
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
1209
+ "dev": true,
1210
+ "engines": {
1211
+ "node": ">=8"
1212
+ }
1213
+ },
1214
  "node_modules/is-glob": {
1215
  "version": "4.0.3",
1216
  "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
 
1232
  "node": ">=0.12.0"
1233
  }
1234
  },
1235
+ "node_modules/is-plain-obj": {
1236
+ "version": "2.1.0",
1237
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
1238
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
1239
  "dev": true,
 
 
 
1240
  "engines": {
1241
+ "node": ">=8"
1242
  }
1243
  },
1244
+ "node_modules/is-unicode-supported": {
1245
+ "version": "0.1.0",
1246
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
1247
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
1248
+ "dev": true,
1249
+ "engines": {
1250
+ "node": ">=10"
1251
+ },
1252
+ "funding": {
1253
+ "url": "https://github.com/sponsors/sindresorhus"
1254
+ }
1255
+ },
1256
+ "node_modules/js-yaml": {
1257
+ "version": "4.1.0",
1258
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
1259
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
1260
+ "dev": true,
1261
+ "dependencies": {
1262
+ "argparse": "^2.0.1"
1263
+ },
1264
+ "bin": {
1265
+ "js-yaml": "bin/js-yaml.js"
1266
+ }
1267
+ },
1268
+ "node_modules/locate-path": {
1269
+ "version": "6.0.0",
1270
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
1271
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
1272
+ "dev": true,
1273
+ "dependencies": {
1274
+ "p-locate": "^5.0.0"
1275
+ },
1276
+ "engines": {
1277
+ "node": ">=10"
1278
+ },
1279
+ "funding": {
1280
+ "url": "https://github.com/sponsors/sindresorhus"
1281
+ }
1282
+ },
1283
+ "node_modules/log-symbols": {
1284
+ "version": "4.1.0",
1285
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
1286
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
1287
+ "dev": true,
1288
+ "dependencies": {
1289
+ "chalk": "^4.1.0",
1290
+ "is-unicode-supported": "^0.1.0"
1291
+ },
1292
+ "engines": {
1293
+ "node": ">=10"
1294
+ },
1295
+ "funding": {
1296
+ "url": "https://github.com/sponsors/sindresorhus"
1297
+ }
1298
+ },
1299
+ "node_modules/loupe": {
1300
+ "version": "2.3.7",
1301
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz",
1302
+ "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==",
1303
+ "dev": true,
1304
+ "dependencies": {
1305
+ "get-func-name": "^2.0.1"
1306
+ }
1307
+ },
1308
+ "node_modules/lru-cache": {
1309
+ "version": "6.0.0",
1310
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
1311
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
1312
+ "dev": true,
1313
+ "dependencies": {
1314
+ "yallist": "^4.0.0"
1315
+ },
1316
+ "engines": {
1317
+ "node": ">=10"
1318
+ }
1319
+ },
1320
+ "node_modules/make-error": {
1321
+ "version": "1.3.6",
1322
  "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
1323
  "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
1324
  "dev": true
 
1386
  "node": "*"
1387
  }
1388
  },
1389
+ "node_modules/mocha": {
1390
+ "version": "10.2.0",
1391
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
1392
+ "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
1393
+ "dev": true,
1394
+ "dependencies": {
1395
+ "ansi-colors": "4.1.1",
1396
+ "browser-stdout": "1.3.1",
1397
+ "chokidar": "3.5.3",
1398
+ "debug": "4.3.4",
1399
+ "diff": "5.0.0",
1400
+ "escape-string-regexp": "4.0.0",
1401
+ "find-up": "5.0.0",
1402
+ "glob": "7.2.0",
1403
+ "he": "1.2.0",
1404
+ "js-yaml": "4.1.0",
1405
+ "log-symbols": "4.1.0",
1406
+ "minimatch": "5.0.1",
1407
+ "ms": "2.1.3",
1408
+ "nanoid": "3.3.3",
1409
+ "serialize-javascript": "6.0.0",
1410
+ "strip-json-comments": "3.1.1",
1411
+ "supports-color": "8.1.1",
1412
+ "workerpool": "6.2.1",
1413
+ "yargs": "16.2.0",
1414
+ "yargs-parser": "20.2.4",
1415
+ "yargs-unparser": "2.0.0"
1416
+ },
1417
+ "bin": {
1418
+ "_mocha": "bin/_mocha",
1419
+ "mocha": "bin/mocha.js"
1420
+ },
1421
+ "engines": {
1422
+ "node": ">= 14.0.0"
1423
+ },
1424
+ "funding": {
1425
+ "type": "opencollective",
1426
+ "url": "https://opencollective.com/mochajs"
1427
+ }
1428
+ },
1429
+ "node_modules/mocha/node_modules/brace-expansion": {
1430
+ "version": "2.0.1",
1431
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
1432
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
1433
+ "dev": true,
1434
+ "dependencies": {
1435
+ "balanced-match": "^1.0.0"
1436
+ }
1437
+ },
1438
+ "node_modules/mocha/node_modules/debug": {
1439
+ "version": "4.3.4",
1440
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
1441
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
1442
+ "dev": true,
1443
+ "dependencies": {
1444
+ "ms": "2.1.2"
1445
+ },
1446
+ "engines": {
1447
+ "node": ">=6.0"
1448
+ },
1449
+ "peerDependenciesMeta": {
1450
+ "supports-color": {
1451
+ "optional": true
1452
+ }
1453
+ }
1454
+ },
1455
+ "node_modules/mocha/node_modules/debug/node_modules/ms": {
1456
+ "version": "2.1.2",
1457
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
1458
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
1459
+ "dev": true
1460
+ },
1461
+ "node_modules/mocha/node_modules/diff": {
1462
+ "version": "5.0.0",
1463
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
1464
+ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
1465
+ "dev": true,
1466
+ "engines": {
1467
+ "node": ">=0.3.1"
1468
+ }
1469
+ },
1470
+ "node_modules/mocha/node_modules/has-flag": {
1471
+ "version": "4.0.0",
1472
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
1473
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
1474
+ "dev": true,
1475
+ "engines": {
1476
+ "node": ">=8"
1477
+ }
1478
+ },
1479
+ "node_modules/mocha/node_modules/minimatch": {
1480
+ "version": "5.0.1",
1481
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
1482
+ "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
1483
+ "dev": true,
1484
+ "dependencies": {
1485
+ "brace-expansion": "^2.0.1"
1486
+ },
1487
+ "engines": {
1488
+ "node": ">=10"
1489
+ }
1490
+ },
1491
+ "node_modules/mocha/node_modules/supports-color": {
1492
+ "version": "8.1.1",
1493
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
1494
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
1495
+ "dev": true,
1496
+ "dependencies": {
1497
+ "has-flag": "^4.0.0"
1498
+ },
1499
+ "engines": {
1500
+ "node": ">=10"
1501
+ },
1502
+ "funding": {
1503
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
1504
+ }
1505
+ },
1506
  "node_modules/ms": {
1507
  "version": "2.1.3",
1508
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1509
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1510
  },
1511
+ "node_modules/nanoid": {
1512
+ "version": "3.3.3",
1513
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
1514
+ "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
1515
+ "dev": true,
1516
+ "bin": {
1517
+ "nanoid": "bin/nanoid.cjs"
1518
+ },
1519
+ "engines": {
1520
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1521
+ }
1522
+ },
1523
  "node_modules/negotiator": {
1524
  "version": "0.6.3",
1525
  "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
 
1588
  "url": "https://github.com/sponsors/ljharb"
1589
  }
1590
  },
1591
+ "node_modules/on-exit-leak-free": {
1592
+ "version": "2.1.2",
1593
+ "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
1594
+ "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
1595
+ "engines": {
1596
+ "node": ">=14.0.0"
1597
+ }
1598
+ },
1599
  "node_modules/on-finished": {
1600
  "version": "2.4.1",
1601
  "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
 
1607
  "node": ">= 0.8"
1608
  }
1609
  },
1610
+ "node_modules/once": {
1611
+ "version": "1.4.0",
1612
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1613
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
1614
+ "dev": true,
1615
+ "dependencies": {
1616
+ "wrappy": "1"
1617
+ }
1618
+ },
1619
+ "node_modules/p-limit": {
1620
+ "version": "3.1.0",
1621
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
1622
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
1623
+ "dev": true,
1624
+ "dependencies": {
1625
+ "yocto-queue": "^0.1.0"
1626
+ },
1627
+ "engines": {
1628
+ "node": ">=10"
1629
+ },
1630
+ "funding": {
1631
+ "url": "https://github.com/sponsors/sindresorhus"
1632
+ }
1633
+ },
1634
+ "node_modules/p-locate": {
1635
+ "version": "5.0.0",
1636
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
1637
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
1638
+ "dev": true,
1639
+ "dependencies": {
1640
+ "p-limit": "^3.0.2"
1641
+ },
1642
+ "engines": {
1643
+ "node": ">=10"
1644
+ },
1645
+ "funding": {
1646
+ "url": "https://github.com/sponsors/sindresorhus"
1647
+ }
1648
+ },
1649
  "node_modules/parseurl": {
1650
  "version": "1.3.3",
1651
  "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
 
1654
  "node": ">= 0.8"
1655
  }
1656
  },
1657
+ "node_modules/path-exists": {
1658
+ "version": "4.0.0",
1659
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
1660
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
1661
+ "dev": true,
1662
+ "engines": {
1663
+ "node": ">=8"
1664
+ }
1665
+ },
1666
+ "node_modules/path-is-absolute": {
1667
+ "version": "1.0.1",
1668
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
1669
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
1670
+ "dev": true,
1671
+ "engines": {
1672
+ "node": ">=0.10.0"
1673
+ }
1674
+ },
1675
  "node_modules/path-to-regexp": {
1676
  "version": "0.1.7",
1677
  "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
1678
  "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
1679
  },
1680
+ "node_modules/pathval": {
1681
+ "version": "1.1.1",
1682
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
1683
+ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
1684
+ "dev": true,
1685
+ "engines": {
1686
+ "node": "*"
1687
+ }
1688
+ },
1689
  "node_modules/picomatch": {
1690
  "version": "2.3.1",
1691
  "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
 
1698
  "url": "https://github.com/sponsors/jonschlinkert"
1699
  }
1700
  },
1701
+ "node_modules/pino": {
1702
+ "version": "8.16.2",
1703
+ "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.2.tgz",
1704
+ "integrity": "sha512-2advCDGVEvkKu9TTVSa/kWW7Z3htI/sBKEZpqiHk6ive0i/7f5b1rsU8jn0aimxqfnSz5bj/nOYkwhBUn5xxvg==",
1705
+ "dependencies": {
1706
+ "atomic-sleep": "^1.0.0",
1707
+ "fast-redact": "^3.1.1",
1708
+ "on-exit-leak-free": "^2.1.0",
1709
+ "pino-abstract-transport": "v1.1.0",
1710
+ "pino-std-serializers": "^6.0.0",
1711
+ "process-warning": "^2.0.0",
1712
+ "quick-format-unescaped": "^4.0.3",
1713
+ "real-require": "^0.2.0",
1714
+ "safe-stable-stringify": "^2.3.1",
1715
+ "sonic-boom": "^3.7.0",
1716
+ "thread-stream": "^2.0.0"
1717
+ },
1718
+ "bin": {
1719
+ "pino": "bin.js"
1720
+ }
1721
+ },
1722
+ "node_modules/pino-abstract-transport": {
1723
+ "version": "1.1.0",
1724
+ "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz",
1725
+ "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==",
1726
+ "dependencies": {
1727
+ "readable-stream": "^4.0.0",
1728
+ "split2": "^4.0.0"
1729
+ }
1730
+ },
1731
+ "node_modules/pino-std-serializers": {
1732
+ "version": "6.2.2",
1733
+ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz",
1734
+ "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="
1735
+ },
1736
  "node_modules/prettier": {
1737
  "version": "3.1.0",
1738
  "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
 
1748
  "url": "https://github.com/prettier/prettier?sponsor=1"
1749
  }
1750
  },
1751
+ "node_modules/process": {
1752
+ "version": "0.11.10",
1753
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
1754
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
1755
+ "engines": {
1756
+ "node": ">= 0.6.0"
1757
+ }
1758
+ },
1759
+ "node_modules/process-warning": {
1760
+ "version": "2.3.1",
1761
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.1.tgz",
1762
+ "integrity": "sha512-JjBvFEn7MwFbzUDa2SRtKJSsyO0LlER4V/FmwLMhBlXNbGgGxdyFCxIdMDLerWUycsVUyaoM9QFLvppFy4IWaQ=="
1763
+ },
1764
  "node_modules/proxy-addr": {
1765
  "version": "2.0.7",
1766
  "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
 
1793
  "url": "https://github.com/sponsors/ljharb"
1794
  }
1795
  },
1796
+ "node_modules/quick-format-unescaped": {
1797
+ "version": "4.0.4",
1798
+ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
1799
+ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
1800
+ },
1801
+ "node_modules/randombytes": {
1802
+ "version": "2.1.0",
1803
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
1804
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
1805
+ "dev": true,
1806
+ "dependencies": {
1807
+ "safe-buffer": "^5.1.0"
1808
+ }
1809
+ },
1810
  "node_modules/range-parser": {
1811
  "version": "1.2.1",
1812
  "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
 
1829
  "node": ">= 0.8"
1830
  }
1831
  },
1832
+ "node_modules/readable-stream": {
1833
+ "version": "4.4.2",
1834
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz",
1835
+ "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==",
1836
+ "dependencies": {
1837
+ "abort-controller": "^3.0.0",
1838
+ "buffer": "^6.0.3",
1839
+ "events": "^3.3.0",
1840
+ "process": "^0.11.10",
1841
+ "string_decoder": "^1.3.0"
1842
+ },
1843
+ "engines": {
1844
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1845
+ }
1846
+ },
1847
  "node_modules/readdirp": {
1848
  "version": "3.6.0",
1849
  "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
 
1856
  "node": ">=8.10.0"
1857
  }
1858
  },
1859
+ "node_modules/real-require": {
1860
+ "version": "0.2.0",
1861
+ "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
1862
+ "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
1863
+ "engines": {
1864
+ "node": ">= 12.13.0"
1865
+ }
1866
+ },
1867
+ "node_modules/require-directory": {
1868
+ "version": "2.1.1",
1869
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
1870
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
1871
+ "dev": true,
1872
+ "engines": {
1873
+ "node": ">=0.10.0"
1874
+ }
1875
+ },
1876
  "node_modules/safe-buffer": {
1877
  "version": "5.2.1",
1878
  "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
 
1892
  }
1893
  ]
1894
  },
1895
+ "node_modules/safe-stable-stringify": {
1896
+ "version": "2.4.3",
1897
+ "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz",
1898
+ "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==",
1899
+ "engines": {
1900
+ "node": ">=10"
1901
+ }
1902
+ },
1903
  "node_modules/safer-buffer": {
1904
  "version": "2.1.2",
1905
  "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
 
1956
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1957
  "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
1958
  },
1959
+ "node_modules/serialize-javascript": {
1960
+ "version": "6.0.0",
1961
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
1962
+ "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
1963
+ "dev": true,
1964
+ "dependencies": {
1965
+ "randombytes": "^2.1.0"
1966
+ }
1967
+ },
1968
  "node_modules/serve-static": {
1969
  "version": "1.15.0",
1970
  "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
 
2023
  "node": ">=10"
2024
  }
2025
  },
2026
+ "node_modules/sonic-boom": {
2027
+ "version": "3.7.0",
2028
+ "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz",
2029
+ "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==",
2030
+ "dependencies": {
2031
+ "atomic-sleep": "^1.0.0"
2032
+ }
2033
+ },
2034
+ "node_modules/split2": {
2035
+ "version": "4.2.0",
2036
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
2037
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
2038
+ "engines": {
2039
+ "node": ">= 10.x"
2040
+ }
2041
+ },
2042
  "node_modules/statuses": {
2043
  "version": "2.0.1",
2044
  "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
 
2047
  "node": ">= 0.8"
2048
  }
2049
  },
2050
+ "node_modules/string_decoder": {
2051
+ "version": "1.3.0",
2052
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
2053
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
2054
+ "dependencies": {
2055
+ "safe-buffer": "~5.2.0"
2056
+ }
2057
+ },
2058
+ "node_modules/string-width": {
2059
+ "version": "4.2.3",
2060
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2061
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2062
+ "dev": true,
2063
+ "dependencies": {
2064
+ "emoji-regex": "^8.0.0",
2065
+ "is-fullwidth-code-point": "^3.0.0",
2066
+ "strip-ansi": "^6.0.1"
2067
+ },
2068
+ "engines": {
2069
+ "node": ">=8"
2070
+ }
2071
+ },
2072
+ "node_modules/strip-ansi": {
2073
+ "version": "6.0.1",
2074
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2075
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2076
+ "dev": true,
2077
+ "dependencies": {
2078
+ "ansi-regex": "^5.0.1"
2079
+ },
2080
+ "engines": {
2081
+ "node": ">=8"
2082
+ }
2083
+ },
2084
+ "node_modules/strip-json-comments": {
2085
+ "version": "3.1.1",
2086
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
2087
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
2088
+ "dev": true,
2089
+ "engines": {
2090
+ "node": ">=8"
2091
+ },
2092
+ "funding": {
2093
+ "url": "https://github.com/sponsors/sindresorhus"
2094
+ }
2095
+ },
2096
  "node_modules/supports-color": {
2097
  "version": "5.5.0",
2098
  "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
 
2105
  "node": ">=4"
2106
  }
2107
  },
2108
+ "node_modules/thread-stream": {
2109
+ "version": "2.4.1",
2110
+ "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz",
2111
+ "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==",
2112
+ "dependencies": {
2113
+ "real-require": "^0.2.0"
2114
+ }
2115
+ },
2116
  "node_modules/to-regex-range": {
2117
  "version": "5.0.1",
2118
  "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
 
2188
  }
2189
  }
2190
  },
2191
+ "node_modules/type-detect": {
2192
+ "version": "4.0.8",
2193
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
2194
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
2195
+ "dev": true,
2196
+ "engines": {
2197
+ "node": ">=4"
2198
+ }
2199
+ },
2200
  "node_modules/type-is": {
2201
  "version": "1.6.18",
2202
  "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
 
2263
  "node": ">= 0.8"
2264
  }
2265
  },
2266
+ "node_modules/workerpool": {
2267
+ "version": "6.2.1",
2268
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
2269
+ "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
2270
+ "dev": true
2271
+ },
2272
+ "node_modules/wrap-ansi": {
2273
+ "version": "7.0.0",
2274
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
2275
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
2276
+ "dev": true,
2277
+ "dependencies": {
2278
+ "ansi-styles": "^4.0.0",
2279
+ "string-width": "^4.1.0",
2280
+ "strip-ansi": "^6.0.0"
2281
+ },
2282
+ "engines": {
2283
+ "node": ">=10"
2284
+ },
2285
+ "funding": {
2286
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
2287
+ }
2288
+ },
2289
+ "node_modules/wrappy": {
2290
+ "version": "1.0.2",
2291
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
2292
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
2293
+ "dev": true
2294
+ },
2295
  "node_modules/ws": {
2296
  "version": "8.14.2",
2297
  "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
 
2312
  }
2313
  }
2314
  },
2315
+ "node_modules/y18n": {
2316
+ "version": "5.0.8",
2317
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
2318
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
2319
+ "dev": true,
2320
+ "engines": {
2321
+ "node": ">=10"
2322
+ }
2323
+ },
2324
  "node_modules/yallist": {
2325
  "version": "4.0.0",
2326
  "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
2327
  "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
2328
  "dev": true
2329
  },
2330
+ "node_modules/yargs": {
2331
+ "version": "16.2.0",
2332
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
2333
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
2334
+ "dev": true,
2335
+ "dependencies": {
2336
+ "cliui": "^7.0.2",
2337
+ "escalade": "^3.1.1",
2338
+ "get-caller-file": "^2.0.5",
2339
+ "require-directory": "^2.1.1",
2340
+ "string-width": "^4.2.0",
2341
+ "y18n": "^5.0.5",
2342
+ "yargs-parser": "^20.2.2"
2343
+ },
2344
+ "engines": {
2345
+ "node": ">=10"
2346
+ }
2347
+ },
2348
+ "node_modules/yargs-parser": {
2349
+ "version": "20.2.4",
2350
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
2351
+ "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
2352
+ "dev": true,
2353
+ "engines": {
2354
+ "node": ">=10"
2355
+ }
2356
+ },
2357
+ "node_modules/yargs-unparser": {
2358
+ "version": "2.0.0",
2359
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
2360
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
2361
+ "dev": true,
2362
+ "dependencies": {
2363
+ "camelcase": "^6.0.0",
2364
+ "decamelize": "^4.0.0",
2365
+ "flat": "^5.0.2",
2366
+ "is-plain-obj": "^2.1.0"
2367
+ },
2368
+ "engines": {
2369
+ "node": ">=10"
2370
+ }
2371
+ },
2372
  "node_modules/yn": {
2373
  "version": "3.1.1",
2374
  "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
 
2378
  "node": ">=6"
2379
  }
2380
  },
2381
+ "node_modules/yocto-queue": {
2382
+ "version": "0.1.0",
2383
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
2384
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
2385
+ "dev": true,
2386
+ "engines": {
2387
+ "node": ">=10"
2388
+ },
2389
+ "funding": {
2390
+ "url": "https://github.com/sponsors/sindresorhus"
2391
+ }
2392
+ },
2393
  "node_modules/zod": {
2394
  "version": "3.22.4",
2395
  "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
server/package.json CHANGED
@@ -4,7 +4,7 @@
4
  "description": "",
5
  "main": "build/index.js",
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1",
8
  "build": "tsc",
9
  "start": "node .",
10
  "dev": "nodemon src/index.ts"
@@ -13,8 +13,12 @@
13
  "author": "",
14
  "license": "ISC",
15
  "devDependencies": {
 
16
  "@types/express": "^4.17.21",
 
17
  "@types/node": "^20.9.1",
 
 
18
  "nodemon": "^3.0.1",
19
  "prettier": "^3.1.0",
20
  "ts-node": "^10.9.1",
@@ -23,6 +27,7 @@
23
  "dependencies": {
24
  "@types/ws": "^8.5.9",
25
  "express": "^4.18.2",
 
26
  "ws": "^8.14.2",
27
  "zod": "^3.22.4"
28
  }
 
4
  "description": "",
5
  "main": "build/index.js",
6
  "scripts": {
7
+ "test": "mocha -r ts-node/register 'test/**/*.spec.ts'",
8
  "build": "tsc",
9
  "start": "node .",
10
  "dev": "nodemon src/index.ts"
 
13
  "author": "",
14
  "license": "ISC",
15
  "devDependencies": {
16
+ "@types/chai": "^4.3.10",
17
  "@types/express": "^4.17.21",
18
+ "@types/mocha": "^10.0.4",
19
  "@types/node": "^20.9.1",
20
+ "chai": "^4.3.10",
21
+ "mocha": "^10.2.0",
22
  "nodemon": "^3.0.1",
23
  "prettier": "^3.1.0",
24
  "ts-node": "^10.9.1",
 
27
  "dependencies": {
28
  "@types/ws": "^8.5.9",
29
  "express": "^4.18.2",
30
+ "pino": "^8.16.2",
31
  "ws": "^8.14.2",
32
  "zod": "^3.22.4"
33
  }
server/src/helpers.ts CHANGED
@@ -1,6 +1,7 @@
1
  import * as crypto from "crypto";
2
 
3
  export function generateSessionToken(length: number): string {
 
4
  return crypto.randomBytes(length).toString("hex");
5
  }
6
 
 
1
  import * as crypto from "crypto";
2
 
3
  export function generateSessionToken(length: number): string {
4
+ if (length < 1) throw new Error("invalid length. length must be greater than 0")
5
  return crypto.randomBytes(length).toString("hex");
6
  }
7
 
server/src/index.ts CHANGED
@@ -3,44 +3,70 @@ import * as http from "http";
3
  import * as WebSocket from "ws";
4
  import { HapticLinkServer } from "./socket/hapticLinkServer";
5
  import { registerRoutes } from "./socket/routes";
 
 
6
 
7
- const port: number = parseInt(process.env.PORT as string, 10) || 3000;
 
 
 
 
8
 
9
- const app: Application = express();
10
- const server = http.createServer(app);
11
- const wss = new WebSocket.Server({ server });
12
 
13
- app.get("/", (_req: Request, res: Response) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  res.send("Bonjour");
15
- });
16
 
17
- // Routes are in socket/routes/*.ts
18
- // registerRoutes imports and adds them all to router
19
- const hapticLink = new HapticLinkServer();
20
- registerRoutes(hapticLink);
21
 
22
- // When a user sends a message, the router checks if that route is available
23
- // and then calls the handler. It also generates a User object for them to store
24
- // data for later requests such as an ID
25
- wss.on("connection", (ws: WebSocket) => {
26
- console.log("Client Connected");
 
27
 
28
  ws.on("message", (message: string) => {
29
- console.log(`Received Message: ${message}`);
30
- hapticLink.handleRoute(ws, message);
31
  });
32
 
33
  // When a user disconnects, their account is removed, and they are removed from all groups
34
  ws.on("close", () => {
35
- hapticLink.removeUser(ws);
36
- })
37
  ws.on("error", () => {
38
- hapticLink.removeUser(ws);
39
- })
40
 
41
  ws.send("Welcome");
42
- });
43
 
44
- server.listen(port, () => {
45
- console.log(`Server is running on port ${port}`);
46
- });
 
 
3
  import * as WebSocket from "ws";
4
  import { HapticLinkServer } from "./socket/hapticLinkServer";
5
  import { registerRoutes } from "./socket/routes";
6
+ import WebSocketWrapper from "./socket/WebSocketAdapter";
7
+ import pino from "pino";
8
 
9
+ (() => {
10
+ const args = process.argv;
11
+ if (args.includes("--silent")) {
12
+ return main(false);
13
+ }
14
 
15
+ return main(true);
16
+ })();
 
17
 
18
+ function main(logging: boolean = true) {
19
+ const port: number = parseInt(process.env.PORT as string, 10) || 3000;
20
+ const logger = pino({
21
+ level: logging ? "info" : "silent",
22
+ formatters: {
23
+ bindings(bindings) {
24
+ return {
25
+ level: bindings.level,
26
+ time: bindings.time,
27
+ msg: bindings.msg,
28
+ };
29
+ },
30
+ },
31
+ });
32
+
33
+ const app: Application = express();
34
+ const server = http.createServer(app);
35
+ const wss = new WebSocket.Server({ server });
36
+
37
+ app.get("/", (_req: Request, res: Response) => {
38
  res.send("Bonjour");
39
+ });
40
 
41
+ // Routes are in socket/routes/*.ts
42
+ // registerRoutes imports and adds them all to router
43
+ const hapticLink = new HapticLinkServer();
44
+ registerRoutes(hapticLink);
45
 
46
+ // When a user sends a message, the router checks if that route is available
47
+ // and then calls the handler. It also generates a User object for them to store
48
+ // data for later requests such as an ID
49
+ wss.on("connection", (ws: WebSocket) => {
50
+ logger.info("Client Connected");
51
+ const wsw = new WebSocketWrapper(ws);
52
 
53
  ws.on("message", (message: string) => {
54
+ logger.info(`Received Message: ${message}`);
55
+ hapticLink.handleRoute(wsw, message);
56
  });
57
 
58
  // When a user disconnects, their account is removed, and they are removed from all groups
59
  ws.on("close", () => {
60
+ hapticLink.removeUser(wsw);
61
+ });
62
  ws.on("error", () => {
63
+ hapticLink.removeUser(wsw);
64
+ });
65
 
66
  ws.send("Welcome");
67
+ });
68
 
69
+ server.listen(port, () => {
70
+ logger.info(`Server is running on port ${port}`);
71
+ });
72
+ }
server/src/socket/WebSocketAdapter.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { WebSocket } from "ws";
2
+
3
+ export interface WebSocketInterface {
4
+ send: (data: string | Buffer) => void;
5
+ readyState: () => 0 | 1 | 2 | 3;
6
+ }
7
+
8
+ export default class WebSocketWrapper implements WebSocketInterface {
9
+ ws: WebSocket
10
+
11
+ constructor(ws: WebSocket) {
12
+ this.ws = ws;
13
+ }
14
+
15
+ send(data: string | Buffer) {
16
+ this.ws.send(data);
17
+ }
18
+
19
+ readyState() {
20
+ return this.ws.readyState
21
+ }
22
+ }
server/src/socket/hapticLinkServer.ts CHANGED
@@ -1,39 +1,39 @@
1
- import * as WebSocket from "ws";
2
  import { ZodSchema } from "zod";
3
  import { generateSessionToken } from "../helpers";
4
  import { Room } from "./room";
 
5
 
6
  type RouteHandler<T> = (context: Context<T>) => void;
7
 
8
  export interface Route<T> {
9
  name: string;
10
- schema: ZodSchema<T>
11
  handler: RouteHandler<T>;
12
  }
13
 
14
  export class User {
15
  id: string;
16
  username?: string;
17
- socket: WebSocket;
18
  currentRoom?: Room;
19
 
20
- constructor(socket: WebSocket) {
21
  this.id = generateSessionToken(16);
22
  this.socket = socket;
23
  }
24
  }
25
 
26
  export interface Context<T> {
27
- ws: WebSocket;
28
  user: User;
29
  payload: T;
30
  server: HapticLinkServer;
31
  }
32
 
33
  export class HapticLinkServer {
34
- routes: { [key: string]: Route<any> }
35
- rooms: { [key: string]: Room }
36
- users: Map<WebSocket, User>;
37
 
38
  constructor() {
39
  this.routes = {};
@@ -41,7 +41,7 @@ export class HapticLinkServer {
41
  this.rooms = {};
42
  }
43
 
44
- removeUser(ws: WebSocket): boolean {
45
  const user = this.users.get(ws);
46
  if (!user) return false;
47
  if (user.currentRoom) {
@@ -50,26 +50,39 @@ export class HapticLinkServer {
50
  return true;
51
  }
52
 
53
- addRoute(route: Route<any>): boolean {
54
- if (route.name in this.routes) {
55
- return false
 
 
 
 
56
  }
57
 
58
- this.routes[route.name] = route;
 
 
 
 
59
 
60
  return true;
61
  }
62
 
63
- handleRoute(ws: WebSocket, message: string) {
64
  // Parse JSON
65
  let payload: any;
66
  try {
67
  payload = JSON.parse(message);
68
  } catch (e) {
69
- return ws.send(JSON.stringify({ error: "message not in JSON format" }));
 
 
70
  }
71
 
72
- if (typeof payload != "object" || !Object.keys(payload).includes("route")) {
 
 
 
73
  return ws.send(JSON.stringify({ error: "missing route" }));
74
  }
75
 
@@ -77,7 +90,6 @@ export class HapticLinkServer {
77
  return ws.send(JSON.stringify({ error: "route not found" }));
78
  }
79
 
80
-
81
  const route = this.routes[payload.route];
82
  delete payload.route;
83
 
@@ -95,10 +107,14 @@ export class HapticLinkServer {
95
  ws,
96
  payload,
97
  server: this,
98
- user
 
 
 
 
99
  }
100
 
101
- if (route && route.schema.safeParse(payload).success) {
102
  route.handler(context);
103
  } else {
104
  return ws.send(JSON.stringify({ error: "invalid payload format" }));
 
 
1
  import { ZodSchema } from "zod";
2
  import { generateSessionToken } from "../helpers";
3
  import { Room } from "./room";
4
+ import { WebSocketInterface } from "./WebSocketAdapter";
5
 
6
  type RouteHandler<T> = (context: Context<T>) => void;
7
 
8
  export interface Route<T> {
9
  name: string;
10
+ schema: ZodSchema<T>;
11
  handler: RouteHandler<T>;
12
  }
13
 
14
  export class User {
15
  id: string;
16
  username?: string;
17
+ socket: WebSocketInterface;
18
  currentRoom?: Room;
19
 
20
+ constructor(socket: WebSocketInterface) {
21
  this.id = generateSessionToken(16);
22
  this.socket = socket;
23
  }
24
  }
25
 
26
  export interface Context<T> {
27
+ ws: WebSocketInterface;
28
  user: User;
29
  payload: T;
30
  server: HapticLinkServer;
31
  }
32
 
33
  export class HapticLinkServer {
34
+ routes: { [key: string]: Route<any> };
35
+ rooms: { [key: string]: Room };
36
+ users: Map<WebSocketInterface, User>;
37
 
38
  constructor() {
39
  this.routes = {};
 
41
  this.rooms = {};
42
  }
43
 
44
+ removeUser(ws: WebSocketInterface): boolean {
45
  const user = this.users.get(ws);
46
  if (!user) return false;
47
  if (user.currentRoom) {
 
50
  return true;
51
  }
52
 
53
+ addRoute<T>(
54
+ name: string,
55
+ schema: ZodSchema<T>,
56
+ handler: RouteHandler<T>
57
+ ): boolean {
58
+ if (name in this.routes) {
59
+ return false;
60
  }
61
 
62
+ this.routes[name] = {
63
+ name,
64
+ schema,
65
+ handler,
66
+ };
67
 
68
  return true;
69
  }
70
 
71
+ handleRoute(ws: WebSocketInterface, message: string) {
72
  // Parse JSON
73
  let payload: any;
74
  try {
75
  payload = JSON.parse(message);
76
  } catch (e) {
77
+ return ws.send(
78
+ JSON.stringify({ error: "message not in JSON format" })
79
+ );
80
  }
81
 
82
+ if (
83
+ typeof payload != "object" ||
84
+ !Object.keys(payload).includes("route")
85
+ ) {
86
  return ws.send(JSON.stringify({ error: "missing route" }));
87
  }
88
 
 
90
  return ws.send(JSON.stringify({ error: "route not found" }));
91
  }
92
 
 
93
  const route = this.routes[payload.route];
94
  delete payload.route;
95
 
 
107
  ws,
108
  payload,
109
  server: this,
110
+ user,
111
+ };
112
+
113
+ if (!route) {
114
+ return ws.send(JSON.stringify({ error: "invalid route" }));
115
  }
116
 
117
+ if (route.schema.safeParse(payload).success) {
118
  route.handler(context);
119
  } else {
120
  return ws.send(JSON.stringify({ error: "invalid payload format" }));
server/src/socket/room.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { generateSessionToken } from "../helpers";
2
  import { User } from "./hapticLinkServer";
3
 
4
- interface UserData {
5
  username: string;
6
  id: string;
7
  online: boolean;
@@ -18,6 +18,7 @@ export class Room {
18
 
19
  addUser(user: User) {
20
  // Generate random roomId if one wasn't created.
 
21
  this.users.push(user);
22
  this.updateRoom();
23
  }
 
1
  import { generateSessionToken } from "../helpers";
2
  import { User } from "./hapticLinkServer";
3
 
4
+ export interface UserData {
5
  username: string;
6
  id: string;
7
  online: boolean;
 
18
 
19
  addUser(user: User) {
20
  // Generate random roomId if one wasn't created.
21
+ if (this.users.includes(user)) return;
22
  this.users.push(user);
23
  this.updateRoom();
24
  }
server/src/socket/routes.ts CHANGED
@@ -1,14 +1,14 @@
1
  import { HapticLinkServer } from "./hapticLinkServer";
2
- import JoinRoomRoute from "./routes/join_room";
3
- import LeaveRoomRoute from "./routes/leave_room";
4
- import SendVibrationRoute from "./routes/send_vibration";
5
- import SetUsernameRoute from "./routes/set_username";
6
- import TestConnectionRoute from "./routes/test_connection";
7
 
8
  export function registerRoutes(router: HapticLinkServer) {
9
- router.addRoute(TestConnectionRoute);
10
- router.addRoute(JoinRoomRoute);
11
- router.addRoute(LeaveRoomRoute);
12
- router.addRoute(SendVibrationRoute);
13
- router.addRoute(SetUsernameRoute);
14
  }
 
1
  import { HapticLinkServer } from "./hapticLinkServer";
2
+ import { JoinRoomHandler, JoinRoomSchema } from "./routes/join_room";
3
+ import { LeaveRoomHandler, LeaveRoomSchema } from "./routes/leave_room";
4
+ import { SendVibrationHandler, SendVibrationSchema } from "./routes/send_vibration";
5
+ import { SetUsernameHandler, SetUsernameSchema } from "./routes/set_username";
6
+ import { TestConnnectionSchema, TestConnectionHandler } from "./routes/test_connection";
7
 
8
  export function registerRoutes(router: HapticLinkServer) {
9
+ router.addRoute("test_connection", TestConnnectionSchema, TestConnectionHandler);
10
+ router.addRoute("join_room", JoinRoomSchema, JoinRoomHandler);
11
+ router.addRoute("leave_room", LeaveRoomSchema, LeaveRoomHandler);
12
+ router.addRoute("send_touch", SendVibrationSchema, SendVibrationHandler);
13
+ router.addRoute("set_username", SetUsernameSchema, SetUsernameHandler);
14
  }
server/src/socket/routes/join_room.ts CHANGED
@@ -3,42 +3,47 @@ import { Context, Route } from "../hapticLinkServer";
3
  import { generateSessionToken } from "../../helpers";
4
  import { Room } from "../room";
5
 
6
- interface JoinRoomPayload {
7
  roomId?: string;
8
  username?: string;
9
  }
10
- const JoinRoomSchema = z.object({
11
  roomId: z.string().optional(),
12
  username: z.string().optional(),
13
  })
14
 
15
- const JoinRoomRoute: Route<JoinRoomPayload> = {
16
- name: "join_room",
17
- handler: function(ctx: Context<JoinRoomPayload>) {
18
- // Set username if payload has it
19
- if (ctx.payload.username) {
20
- ctx.user.username = ctx.payload.username;
21
- }
 
 
 
 
 
22
 
23
- let room: Room;
24
-
25
- if (ctx.payload.roomId && !ctx.server.rooms[ctx.payload.roomId]) {
26
- room = new Room(ctx.payload.roomId);
27
- ctx.server.rooms[room.id] = room;
28
- } else if (!ctx.payload.roomId) {
29
- ctx.payload.roomId = generateSessionToken(12);
30
- room = new Room(ctx.payload.roomId);
31
- ctx.server.rooms[room.id] = room;
32
- } else {
33
- room = ctx.server.rooms[ctx.payload.roomId]
 
 
 
34
  }
 
35
 
36
- ctx.user.currentRoom = room;
37
 
38
- // Broadcasts message to room
39
- room.addUser(ctx.user);
40
- },
41
- schema: JoinRoomSchema,
42
  }
43
-
44
- export default JoinRoomRoute;
 
3
  import { generateSessionToken } from "../../helpers";
4
  import { Room } from "../room";
5
 
6
+ export interface JoinRoomPayload {
7
  roomId?: string;
8
  username?: string;
9
  }
10
+ export const JoinRoomSchema = z.object({
11
  roomId: z.string().optional(),
12
  username: z.string().optional(),
13
  })
14
 
15
+ export function JoinRoomHandler(ctx: Context<JoinRoomPayload>) {
16
+ // Set username if payload has it
17
+ if (ctx.payload.username) {
18
+ ctx.user.username = ctx.payload.username;
19
+ }
20
+
21
+ let room: Room;
22
+
23
+ if (ctx.payload.roomId && !ctx.server.rooms[ctx.payload.roomId]) {
24
+ // User sent roomId, but room doesn't exist
25
+ room = new Room(ctx.payload.roomId);
26
+ ctx.server.rooms[room.id] = room;
27
 
28
+ } else if (!ctx.payload.roomId) {
29
+ // Didn't include RoomID, creating new one
30
+ ctx.payload.roomId = generateSessionToken(12);
31
+ room = new Room(ctx.payload.roomId);
32
+ ctx.server.rooms[room.id] = room;
33
+
34
+ } else {
35
+ // RoomId included and room exists, joining...
36
+ room = ctx.server.rooms[ctx.payload.roomId]
37
+ if (room.hasUser(ctx.user.id)) {
38
+ return ctx.ws.send(JSON.stringify({
39
+ "message": "join_room_response",
40
+ "status": "failed, you are already part of this room",
41
+ }))
42
  }
43
+ }
44
 
45
+ ctx.user.currentRoom = room;
46
 
47
+ // Broadcasts message to room
48
+ room.addUser(ctx.user);
 
 
49
  }
 
 
server/src/socket/routes/leave_room.ts CHANGED
@@ -1,33 +1,27 @@
1
-
2
  import { z } from "zod";
3
  import { Context, Route } from "../hapticLinkServer";
4
 
5
- interface LeaveRoomPayload {
6
  roomId: string;
7
  }
8
- const LeaveRoomSchema = z.object({
 
9
  roomId: z.string(),
10
  })
11
 
12
- const LeaveRoomRoute: Route<LeaveRoomPayload> = {
13
- name: "leave_room",
14
- handler: function(ctx: Context<LeaveRoomPayload>) {
15
- const room = ctx.user.currentRoom;
16
 
17
- if (!room) {
18
- return ctx.ws.send(JSON.stringify({
19
- message: "leave_room_response",
20
- status: "you are not part of that room"
21
- }))
22
- } else {
23
- room.removeUserById(ctx.user.id);
24
- return ctx.ws.send(JSON.stringify({
25
- message: "leave_room_response",
26
- status: "room left"
27
- }))
28
- }
29
- },
30
- schema: LeaveRoomSchema,
31
  }
32
-
33
- export default LeaveRoomRoute;
 
 
1
  import { z } from "zod";
2
  import { Context, Route } from "../hapticLinkServer";
3
 
4
+ export interface LeaveRoomPayload {
5
  roomId: string;
6
  }
7
+
8
+ export const LeaveRoomSchema = z.object({
9
  roomId: z.string(),
10
  })
11
 
12
+ export function LeaveRoomHandler(ctx: Context<LeaveRoomPayload>) {
13
+ const room = ctx.user.currentRoom;
 
 
14
 
15
+ if (!room) {
16
+ return ctx.ws.send(JSON.stringify({
17
+ message: "leave_room_response",
18
+ status: "you are not part of that room"
19
+ }))
20
+ } else {
21
+ room.removeUserById(ctx.user.id);
22
+ return ctx.ws.send(JSON.stringify({
23
+ message: "leave_room_response",
24
+ status: "room left"
25
+ }))
26
+ }
 
 
27
  }
 
 
server/src/socket/routes/send_vibration.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { z } from "zod";
2
  import { Context, Route } from "../hapticLinkServer";
3
 
4
- interface SendVibrationPayload {
5
  id: number;
6
  type: "enabled" | "disabled";
7
  position: {
@@ -12,7 +12,7 @@ interface SendVibrationPayload {
12
  intensity?: number;
13
  }
14
 
15
- const SendVibrationSchema = z.object({
16
  id: z.number(),
17
  type: z.union([z.literal("enabled"), z.literal("disabled")]),
18
  position: z.object({
@@ -23,31 +23,29 @@ const SendVibrationSchema = z.object({
23
  intensity: z.number().optional(),
24
  });
25
 
26
- const SendVibrationRoute: Route<SendVibrationPayload> = {
27
- name: "send_touch",
28
- handler: function(ctx: Context<SendVibrationPayload>) {
29
- if (!ctx.user.currentRoom) {
30
- return ctx.ws.send(JSON.stringify({
31
- "message": "send_vibration_response",
32
- "status": "not part of a room",
33
- }))
34
- }
35
 
36
- const p = ctx.payload;
37
 
38
- ctx.user.currentRoom.broadcast(JSON.stringify({
39
- message: "receieve_touch",
40
- id: ctx.user.id + "_" + p.id,
41
- type: p.type,
42
- user: {
43
- username: ctx.user.username,
44
- id: ctx.user.id,
45
- },
46
- color: p.color || "#FF0000",
47
- intensity: p.intensity ?? 1,
48
- }))
49
- },
50
- schema: SendVibrationSchema,
 
 
51
  }
52
-
53
- export default SendVibrationRoute;
 
1
  import { z } from "zod";
2
  import { Context, Route } from "../hapticLinkServer";
3
 
4
+ export interface SendVibrationPayload {
5
  id: number;
6
  type: "enabled" | "disabled";
7
  position: {
 
12
  intensity?: number;
13
  }
14
 
15
+ export const SendVibrationSchema = z.object({
16
  id: z.number(),
17
  type: z.union([z.literal("enabled"), z.literal("disabled")]),
18
  position: z.object({
 
23
  intensity: z.number().optional(),
24
  });
25
 
26
+ export function SendVibrationHandler(ctx: Context<SendVibrationPayload>) {
27
+ if (!ctx.user.currentRoom) {
28
+ return ctx.ws.send(JSON.stringify({
29
+ "message": "send_vibration_response",
30
+ "status": "not part of a room",
31
+ }))
32
+ }
 
 
33
 
34
+ const p = ctx.payload;
35
 
36
+ ctx.user.currentRoom.broadcast(JSON.stringify({
37
+ message: "receive_touch",
38
+ id: ctx.user.id + "_" + p.id,
39
+ type: p.type,
40
+ user: {
41
+ username: ctx.user.username,
42
+ id: ctx.user.id,
43
+ },
44
+ position: {
45
+ x: p.position.x,
46
+ y: p.position.y
47
+ },
48
+ color: p.color || "#FF0000",
49
+ intensity: p.intensity ?? 1,
50
+ }))
51
  }
 
 
server/src/socket/routes/set_username.ts CHANGED
@@ -1,25 +1,19 @@
1
  import { z } from "zod";
2
  import { Context, Route } from "../hapticLinkServer";
3
 
4
- interface SetUsernamePayload {
5
  username: string;
6
  }
7
- const SetUsernameSchema = z.object({
8
  username: z.string(),
9
  })
10
 
11
- const SetUsernameRoute: Route<SetUsernamePayload> = {
12
- name: "set_username",
13
- handler: function(ctx: Context<SetUsernamePayload>) {
14
 
15
- ctx.user.username = ctx.payload.username;
16
-
17
- return ctx.ws.send(JSON.stringify({
18
- "message": "set_username_response",
19
- "status": "success"
20
- }))
21
- },
22
- schema: SetUsernameSchema,
23
  }
24
 
25
- export default SetUsernameRoute;
 
1
  import { z } from "zod";
2
  import { Context, Route } from "../hapticLinkServer";
3
 
4
+ export interface SetUsernamePayload {
5
  username: string;
6
  }
7
+ export const SetUsernameSchema = z.object({
8
  username: z.string(),
9
  })
10
 
11
+ export function SetUsernameHandler(ctx: Context<SetUsernamePayload>) {
12
+ ctx.user.username = ctx.payload.username;
 
13
 
14
+ return ctx.ws.send(JSON.stringify({
15
+ "message": "set_username_response",
16
+ "status": "success"
17
+ }))
 
 
 
 
18
  }
19
 
 
server/src/socket/routes/test_connection.ts CHANGED
@@ -1,25 +1,20 @@
1
  import { z } from "zod";
2
  import { Context, Route } from "../hapticLinkServer";
3
 
4
- interface TestConnectionPayload { }
5
- const TestConnnectionSchema = z.object({})
6
 
7
- const TestConnectionRoute: Route<TestConnectionPayload> = {
8
- name: "test_connection",
9
- handler: function (ctx: Context<TestConnectionPayload>) {
10
- if (ctx.user) {
11
- return ctx.ws.send(JSON.stringify({
12
- "message": "test_connection_response",
13
- "authenticated": true,
14
- "username": ctx.user.username
15
- }))
16
- }
17
  return ctx.ws.send(JSON.stringify({
18
  "message": "test_connection_response",
19
- "authenticated": false,
 
20
  }))
21
- },
22
- schema: TestConnnectionSchema,
 
 
 
23
  }
24
 
25
- export default TestConnectionRoute;
 
1
  import { z } from "zod";
2
  import { Context, Route } from "../hapticLinkServer";
3
 
4
+ export interface TestConnectionPayload { }
5
+ export const TestConnnectionSchema = z.object({})
6
 
7
+ export function TestConnectionHandler(ctx: Context<TestConnectionPayload>) {
8
+ if (ctx.user) {
 
 
 
 
 
 
 
 
9
  return ctx.ws.send(JSON.stringify({
10
  "message": "test_connection_response",
11
+ "authenticated": true,
12
+ "username": ctx.user.username
13
  }))
14
+ }
15
+ return ctx.ws.send(JSON.stringify({
16
+ "message": "test_connection_response",
17
+ "authenticated": false,
18
+ }))
19
  }
20
 
 
server/test/helpers.spec.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { expect } from 'chai';
2
+ import { generateSessionToken } from '../src/helpers';
3
+
4
+ describe('generateSessionToken', () => {
5
+ it('should generate a token of a correct length', () => {
6
+ const length = 16; // Specify the length of the token to generate
7
+ const token = generateSessionToken(length);
8
+
9
+ // Since the token is hexadecimal, its string length should be twice the byte length
10
+ expect(token).to.be.a('string');
11
+ expect(token.length).to.equal(length * 2);
12
+ });
13
+
14
+ it('should generate a unique token each time', () => {
15
+ const tokens = new Set<string>();
16
+ const tokenCount = 100; // Generate a number of tokens to test uniqueness
17
+ const length = 16;
18
+
19
+ for (let i = 0; i < tokenCount; i++) {
20
+ const token = generateSessionToken(length);
21
+ tokens.add(token);
22
+ }
23
+
24
+ // If all tokens are unique, the set's size should match the number of generated tokens
25
+ expect(tokens.size).to.equal(tokenCount);
26
+ });
27
+
28
+ it('should throw an exception if an invalid length is provided', () => {
29
+ const invalidLength = 0; // Zero or negative length or other invalid values could be tested
30
+ expect(() => generateSessionToken(invalidLength)).to.throw();
31
+ });
32
+ });
server/test/index.spec.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { expect } from 'chai';
2
+ import WebSocket from 'ws';
3
+ import express, { Application, Request, Response } from "express";
4
+ import * as http from "http";
5
+ import { HapticLinkServer } from '../src/socket/hapticLinkServer';
6
+ import { registerRoutes } from '../src/socket/routes';
7
+ import WebSocketWrapper from '../src/socket/WebSocketAdapter';
8
+
9
+ describe('WebSocket Server', function() {
10
+ const port: number = parseInt(process.env.PORT as string, 10) || 3000;
11
+ let wsClient: WebSocket;
12
+ let server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>
13
+
14
+ before((done) => {
15
+ const app: Application = express();
16
+ server = http.createServer(app);
17
+ const wss = new WebSocket.Server({ server });
18
+
19
+ app.get("/", (_req: Request, res: Response) => {
20
+ res.send("Bonjour");
21
+ });
22
+
23
+ const hapticLink = new HapticLinkServer();
24
+ registerRoutes(hapticLink);
25
+
26
+ wss.on("connection", (ws: WebSocket) => {
27
+ const wsw = new WebSocketWrapper(ws);
28
+
29
+ ws.on("message", (message: string) => {
30
+ hapticLink.handleRoute(wsw, message);
31
+ });
32
+
33
+ ws.on("close", () => {
34
+ hapticLink.removeUser(wsw);
35
+ });
36
+ ws.on("error", () => {
37
+ hapticLink.removeUser(wsw);
38
+ });
39
+
40
+ ws.send("Welcome");
41
+ });
42
+
43
+ server.listen(port, () => {
44
+ wsClient = new WebSocket('ws://localhost:3000');
45
+ wsClient.on('open', done);
46
+ });
47
+
48
+ server.on("error", (err) => {
49
+ done(err);
50
+ })
51
+
52
+ });
53
+
54
+ after(() => {
55
+ // Close the WebSocket client
56
+ if (wsClient.readyState === WebSocket.OPEN) {
57
+ wsClient.close();
58
+ }
59
+
60
+ server.close();
61
+
62
+ });
63
+
64
+ it('server should start, listen, and respond to test_connection', (done) => {
65
+ // Listen for messages from the server
66
+ wsClient.on('message', (data: string) => {
67
+ expect(JSON.parse(data).message).to.equal("test_connection_response");
68
+ done();
69
+ });
70
+
71
+ // Send a ping message to the server
72
+ wsClient.send(JSON.stringify({ "route": "test_connection" }));
73
+ });
74
+ });
server/test/socket/hapticLinkServer.spec.ts ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { expect } from 'chai';
2
+ import { HapticLinkServer, User, Context } from '../../src/socket/hapticLinkServer';
3
+ import { WebSocketInterface } from '../../src/socket/WebSocketAdapter';
4
+ import { z } from 'zod';
5
+
6
+ export class WebSocketWrapper implements WebSocketInterface {
7
+ sendData: string | Buffer = ""
8
+ state: 0 | 1 | 2 | 3 = 1;
9
+
10
+ constructor() { }
11
+
12
+ send(data: string | Buffer) {
13
+ this.sendData = data
14
+ }
15
+
16
+ readyState() {
17
+ return this.state;
18
+ }
19
+
20
+ }
21
+
22
+ describe("Haptic Link Server", function() {
23
+ let server: HapticLinkServer = new HapticLinkServer();
24
+ const ws = new WebSocketWrapper();
25
+ const user: User = new User(ws);
26
+
27
+ before((done) => {
28
+ done();
29
+ })
30
+
31
+
32
+ after(() => {
33
+
34
+ })
35
+
36
+ it("hapticLinkServer should initialize", (done) => {
37
+ if (!server.users || server.users.size != 0) {
38
+ return done("users map not initialized correctly")
39
+ }
40
+
41
+ if (!server.routes) {
42
+ return done("routes map not initialized correctly")
43
+ }
44
+
45
+ if (!server.rooms) {
46
+ return done("rooms map not initialized correctly")
47
+ }
48
+
49
+ done();
50
+ })
51
+
52
+ it("should add user", (done) => {
53
+ server.users.set(ws, user);
54
+ server.users.set(ws, user);
55
+
56
+ expect(server.users.size).to.equal(1, "user not added");
57
+ done()
58
+ })
59
+
60
+ it("should remove user", (done) => {
61
+ let removed = server.users.delete(ws);
62
+
63
+ expect(server.users.size).to.equal(0, "user not removed");
64
+ expect(removed).to.equal(true, "user not removed");
65
+ done()
66
+ })
67
+
68
+ it("should add route", (done) => {
69
+
70
+ interface TestConnectionPayload { }
71
+ const TestConnnectionSchema = z.object({})
72
+
73
+ function TestConnectionHandler(_ctx: Context<TestConnectionPayload>) {
74
+ return;
75
+ }
76
+
77
+ server.addRoute("test_add_route", TestConnnectionSchema, TestConnectionHandler);
78
+
79
+ const r = server.routes["test_add_route"];
80
+ if (!r) {
81
+ done("failed to add route")
82
+ }
83
+
84
+ done()
85
+ })
86
+
87
+ it("should handle route", (done) => {
88
+ interface TestConnectionPayload { }
89
+ const TestConnnectionSchema = z.object({})
90
+
91
+ let executed = false;
92
+
93
+ function TestConnectionHandler(_ctx: Context<TestConnectionPayload>) {
94
+ executed = true;
95
+ return;
96
+ }
97
+
98
+ server.addRoute("test_route", TestConnnectionSchema, TestConnectionHandler);
99
+ server.handleRoute(ws, JSON.stringify({ route: "test_route" }))
100
+
101
+ expect(executed).to.equal(true, "route not handled");
102
+ done();
103
+ })
104
+ })
server/test/socket/room.spec.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { expect } from "chai";
2
+ import { HapticLinkServer, User } from "../../src/socket/hapticLinkServer";
3
+ import { Room } from "../../src/socket/room";
4
+ import {WebSocketWrapper} from "./hapticLinkServer.spec";
5
+
6
+ describe("Rooms", function() {
7
+ let server: HapticLinkServer = new HapticLinkServer();
8
+ const ws1 = new WebSocketWrapper();
9
+ const ws2 = new WebSocketWrapper();
10
+ const user1: User = new User(ws1);
11
+ const user2: User = new User(ws2);
12
+ const room: Room = new Room()
13
+
14
+ before((done) => {
15
+ done();
16
+ })
17
+
18
+
19
+ after(() => {
20
+ })
21
+
22
+ it("should have a unique random ID", (done) => {
23
+ expect(room.id.length).to.gte(3, "ID not generated")
24
+ done();
25
+ })
26
+
27
+ it("should add/remove user", (done) => {
28
+ room.addUser(user1)
29
+ room.addUser(user1)
30
+ expect(room.users.length).to.equal(1, "user not added")
31
+ room.removeUserById(user1.id);
32
+ expect(room.users.length).to.equal(0, "user not removed")
33
+ done();
34
+ })
35
+
36
+ it("should check if user exists", (done) => {
37
+ room.addUser(user1)
38
+ expect(room.hasUser(user1.id)).to.equal(true, "room didn't find user")
39
+ done();
40
+ })
41
+
42
+ it("should broadcast to all users", (done) => {
43
+ room.addUser(user1)
44
+ room.addUser(user2)
45
+ room.broadcast("hello");
46
+ expect(ws1.sendData).to.equal("hello", "didn't broadcast")
47
+ expect(ws2.sendData).to.equal("hello", "didn't broadcast")
48
+ done();
49
+ })
50
+
51
+ it("should broadcast to all users, but filter a user", (done) => {
52
+ ws1.sendData = ""
53
+ ws2.sendData = ""
54
+ room.addUser(user1)
55
+ room.addUser(user2)
56
+ room.broadcast("hello", [user1.id]);
57
+ expect(ws1.sendData).to.equal("", "didn't filter")
58
+ expect(ws2.sendData).to.equal("hello", "didn't broadcast")
59
+ done();
60
+ })
61
+ });
server/tsconfig.json CHANGED
@@ -96,6 +96,9 @@
96
  // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
97
  /* Completeness */
98
  // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
99
- "skipLibCheck": true /* Skip type checking all .d.ts files. */
100
- }
 
 
 
101
  }
 
96
  // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
97
  /* Completeness */
98
  // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
99
+ "skipLibCheck": true, /* Skip type checking all .d.ts files. */
100
+ },
101
+ "exclude": [
102
+ "test/**/*"
103
+ ]
104
  }