rpc-express-toolkit 4.2.1 → 4.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +88 -9
- package/examples/example.js +0 -0
- package/examples/introspection/client.js +68 -68
- package/examples/introspection/server.js +89 -89
- package/examples/safe-preset-example.js +0 -0
- package/examples/server-client-example.js +0 -0
- package/examples/ssl-development-example.js +0 -0
- package/package.json +124 -126
- package/src/batch.js +90 -55
- package/src/index.d.ts +33 -63
- package/src/index.js +1153 -1035
- package/src/logger.js +17 -5
- package/src/middleware.js +0 -0
- package/src/protocol.js +97 -0
- package/src/safe.d.ts +0 -0
- package/src/safe.js +0 -0
- package/src/validation.js +222 -222
- package/src/clients/rpc-client.js +0 -403
- package/src/clients/rpc-client.min.js +0 -1
- package/src/clients/rpc-client.min.mjs +0 -1
- package/src/clients/rpc-client.mjs +0 -393
package/src/index.js
CHANGED
|
@@ -1,1035 +1,1153 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
const express = require('express');
|
|
3
|
-
const expressPkg = require('express/package.json');
|
|
4
|
-
const NestedError = require('nested-error-stacks');
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
/** @type {
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
/** @type {
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
/** @type {
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
/** @type {
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
/** @type {
|
|
124
|
-
#
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
this.use('beforeCall', builtInMiddlewares.
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
this.use(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
// Execute
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
*
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
* @returns {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
*
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
*
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
*
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
if (
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
if (
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
if (
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (
|
|
1004
|
-
return
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const expressPkg = require('express/package.json');
|
|
4
|
+
const NestedError = require('nested-error-stacks');
|
|
5
|
+
const {
|
|
6
|
+
RpcClient,
|
|
7
|
+
RpcError,
|
|
8
|
+
RpcHttpError,
|
|
9
|
+
RpcSafeClient,
|
|
10
|
+
} = require('rpc-toolkit-js-client');
|
|
11
|
+
const pkg = require('../package.json');
|
|
12
|
+
|
|
13
|
+
const rpcClientAssetsPath = path.dirname(
|
|
14
|
+
require.resolve('rpc-toolkit-js-client')
|
|
15
|
+
);
|
|
16
|
+
const DEFAULT_MAX_SERIALIZATION_DEPTH = 100;
|
|
17
|
+
|
|
18
|
+
/** @typedef {import("express").Router} Router */
|
|
19
|
+
/** @typedef {import("express").Request} Request */
|
|
20
|
+
/** @typedef {import("express").Response} Response */
|
|
21
|
+
|
|
22
|
+
const Logger = require('./logger');
|
|
23
|
+
const { MiddlewareManager, builtInMiddlewares } = require('./middleware');
|
|
24
|
+
const {
|
|
25
|
+
SchemaValidator,
|
|
26
|
+
commonSchemas,
|
|
27
|
+
SchemaBuilder,
|
|
28
|
+
} = require('./validation');
|
|
29
|
+
const BatchHandler = require('./batch');
|
|
30
|
+
const { hasOwn, validateEnvelope } = require('./protocol');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Default properties to include in error serialization.
|
|
34
|
+
* @type {Array<string>}
|
|
35
|
+
*/
|
|
36
|
+
const defaultProperties = [
|
|
37
|
+
'stackTraceLimit',
|
|
38
|
+
'cause',
|
|
39
|
+
'code',
|
|
40
|
+
'message',
|
|
41
|
+
'stack',
|
|
42
|
+
'address',
|
|
43
|
+
'dest',
|
|
44
|
+
'errno',
|
|
45
|
+
'info',
|
|
46
|
+
'path',
|
|
47
|
+
'port',
|
|
48
|
+
'syscall',
|
|
49
|
+
'opensslErrorStack',
|
|
50
|
+
'function',
|
|
51
|
+
'library',
|
|
52
|
+
'reason',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Serializes an error into a JSON-compatible format, with optional sanitization.
|
|
57
|
+
* @param {Error | NestedError | Object} error - The error to serialize.
|
|
58
|
+
* @param {boolean} sanitize - Whether to remove sensitive fields.
|
|
59
|
+
* @param {Array<string>} properties - Properties to include in serialization.
|
|
60
|
+
* @returns {Object} The serialized error.
|
|
61
|
+
*/
|
|
62
|
+
const serializeError = (
|
|
63
|
+
error,
|
|
64
|
+
sanitize = false,
|
|
65
|
+
properties = defaultProperties
|
|
66
|
+
) => {
|
|
67
|
+
if (error === null || error === undefined) return error;
|
|
68
|
+
|
|
69
|
+
const result = {};
|
|
70
|
+
const includeProperties = sanitize
|
|
71
|
+
? properties.filter((prop) => !['address', 'path'].includes(prop))
|
|
72
|
+
: properties;
|
|
73
|
+
|
|
74
|
+
includeProperties.forEach((prop) => {
|
|
75
|
+
if (error?.[prop] !== undefined) {
|
|
76
|
+
result[prop] = error[prop];
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (error instanceof NestedError) {
|
|
81
|
+
result.type = 'NestedError';
|
|
82
|
+
if (error.nested)
|
|
83
|
+
result.nested = serializeError(error.nested, sanitize, properties);
|
|
84
|
+
} else if (error instanceof Error) {
|
|
85
|
+
result.type = 'Error';
|
|
86
|
+
} else if (error instanceof Object) {
|
|
87
|
+
result.type = 'Object';
|
|
88
|
+
Object.assign(result, error);
|
|
89
|
+
} else {
|
|
90
|
+
result.type = 'Default';
|
|
91
|
+
result.instance = error;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @template C
|
|
99
|
+
*/
|
|
100
|
+
class RpcEndpoint {
|
|
101
|
+
/**
|
|
102
|
+
* Serve client scripts for making JSON-RPC calls from the browser.
|
|
103
|
+
* @param {Router} router Express.Router instance
|
|
104
|
+
* @param {string} [url="/vendor/rpc-client"]
|
|
105
|
+
* The path from which client scripts will be served.
|
|
106
|
+
*/
|
|
107
|
+
static serveScripts(router, url = '/vendor/rpc-client') {
|
|
108
|
+
router.use(url, express.static(rpcClientAssetsPath));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @type {string} */
|
|
112
|
+
#endpoint;
|
|
113
|
+
|
|
114
|
+
/** @type {{ [name: string]: any }} */
|
|
115
|
+
#methods = {};
|
|
116
|
+
|
|
117
|
+
/** @type {string} */
|
|
118
|
+
#introspectionPrefix = '__rpc';
|
|
119
|
+
|
|
120
|
+
/** @type {boolean} */
|
|
121
|
+
#isInternalRegistration = false;
|
|
122
|
+
|
|
123
|
+
/** @type {Logger} */
|
|
124
|
+
#logger;
|
|
125
|
+
|
|
126
|
+
/** @type {MiddlewareManager} */
|
|
127
|
+
#middleware;
|
|
128
|
+
|
|
129
|
+
/** @type {SchemaValidator} */
|
|
130
|
+
#validator;
|
|
131
|
+
|
|
132
|
+
/** @type {BatchHandler} */
|
|
133
|
+
#batchHandler;
|
|
134
|
+
|
|
135
|
+
/** @type {Object} */
|
|
136
|
+
#options;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {Router} router The Express router to attach the endpoint to.
|
|
140
|
+
* @param {C} context The context object to pass to the method handlers.
|
|
141
|
+
* @param {string|Object} [endpointOrOptions="/api"] The endpoint path or options object.
|
|
142
|
+
* @param {Object} [options={}] Options object if first param is string.
|
|
143
|
+
*/
|
|
144
|
+
constructor(router, context, endpointOrOptions = '/api', options = {}) {
|
|
145
|
+
// Handle both signatures: (router, context, "/api", options) and (router, context, options)
|
|
146
|
+
if (typeof endpointOrOptions === 'string') {
|
|
147
|
+
this.#endpoint = endpointOrOptions;
|
|
148
|
+
this.#options = { ...options };
|
|
149
|
+
} else {
|
|
150
|
+
this.#endpoint = endpointOrOptions.endpoint || '/api';
|
|
151
|
+
this.#options = { ...endpointOrOptions };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Initialize components
|
|
155
|
+
this.#logger = new Logger(this.#options.logging || {});
|
|
156
|
+
this.#middleware = new MiddlewareManager();
|
|
157
|
+
this.#validator = new SchemaValidator(this.#options.validation || {});
|
|
158
|
+
this.#batchHandler = new BatchHandler(this);
|
|
159
|
+
|
|
160
|
+
// Set default options
|
|
161
|
+
this.#options.safeEnabled = this.#options.safeEnabled === true; // Default false
|
|
162
|
+
this.#options.warnOnUnsafe = this.#options.warnOnUnsafe !== false; // Default true
|
|
163
|
+
this.#options.strictMode = this.#options.strictMode !== false; // Default true
|
|
164
|
+
this.#options.enableIntrospection =
|
|
165
|
+
this.#options.enableIntrospection === true; // Default false
|
|
166
|
+
this.#options.introspectionPrefix =
|
|
167
|
+
this.#options.introspectionPrefix || '__rpc'; // Default __rpc
|
|
168
|
+
|
|
169
|
+
this.#introspectionPrefix = this.#options.introspectionPrefix;
|
|
170
|
+
|
|
171
|
+
// Validate that the router can handle JSON parsing
|
|
172
|
+
this.#validateJsonMiddleware();
|
|
173
|
+
|
|
174
|
+
// Register introspection methods if enabled
|
|
175
|
+
if (this.#options.enableIntrospection) {
|
|
176
|
+
this.#registerIntrospectionMethods();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Setup built-in middleware if configured
|
|
180
|
+
this.#setupBuiltInMiddleware();
|
|
181
|
+
|
|
182
|
+
// Wire up routes
|
|
183
|
+
this.#setupRoutes(router, context);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Setup built-in middleware based on options
|
|
188
|
+
*/
|
|
189
|
+
#setupBuiltInMiddleware() {
|
|
190
|
+
const { rateLimit, cors, auth, timing, methodWhitelist } = this.#options;
|
|
191
|
+
|
|
192
|
+
if (timing !== false) {
|
|
193
|
+
this.use('beforeCall', builtInMiddlewares.timing());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (cors) {
|
|
197
|
+
this.use('beforeCall', builtInMiddlewares.cors(cors));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (rateLimit) {
|
|
201
|
+
this.use('beforeCall', builtInMiddlewares.rateLimit(rateLimit));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (auth) {
|
|
205
|
+
this.use('beforeCall', builtInMiddlewares.auth(auth));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (methodWhitelist && Array.isArray(methodWhitelist)) {
|
|
209
|
+
this.use(
|
|
210
|
+
'beforeCall',
|
|
211
|
+
builtInMiddlewares.methodWhitelist(methodWhitelist)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Register introspection methods (__rpc.*)
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
#registerIntrospectionMethods() {
|
|
221
|
+
this.#isInternalRegistration = true;
|
|
222
|
+
|
|
223
|
+
// __rpc.listMethods - List all user methods (excludes __rpc.* methods)
|
|
224
|
+
this.addMethod(
|
|
225
|
+
`${this.#introspectionPrefix}.listMethods`,
|
|
226
|
+
async (_req, _context, _params) =>
|
|
227
|
+
Object.keys(this.#methods).filter(
|
|
228
|
+
(name) => !name.startsWith(this.#introspectionPrefix)
|
|
229
|
+
),
|
|
230
|
+
{
|
|
231
|
+
description: 'List all available RPC methods',
|
|
232
|
+
exposeSchema: true,
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// __rpc.describe - Get schema and description of a specific method
|
|
237
|
+
this.addMethod(
|
|
238
|
+
`${this.#introspectionPrefix}.describe`,
|
|
239
|
+
async (_req, _context, params) => {
|
|
240
|
+
const methodName = params?.method;
|
|
241
|
+
|
|
242
|
+
if (!methodName || typeof methodName !== 'string') {
|
|
243
|
+
throw {
|
|
244
|
+
code: -32602,
|
|
245
|
+
message: 'Invalid params: method name required',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Prevent introspection of __rpc.* methods
|
|
250
|
+
if (methodName.startsWith(this.#introspectionPrefix)) {
|
|
251
|
+
throw {
|
|
252
|
+
code: -32601,
|
|
253
|
+
message: 'Cannot describe introspection methods',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const methodConfig = this.#methods[methodName];
|
|
258
|
+
if (!methodConfig) {
|
|
259
|
+
throw { code: -32601, message: `Method not found: ${methodName}` };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check if schema is exposed
|
|
263
|
+
const config = typeof methodConfig === 'function' ? {} : methodConfig;
|
|
264
|
+
if (!config.exposeSchema) {
|
|
265
|
+
throw { code: -32601, message: 'Method schema not available' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
name: methodName,
|
|
270
|
+
schema: config.schema || null,
|
|
271
|
+
description: config.description || '',
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
schema: {
|
|
276
|
+
type: 'object',
|
|
277
|
+
properties: {
|
|
278
|
+
method: { type: 'string' },
|
|
279
|
+
},
|
|
280
|
+
required: ['method'],
|
|
281
|
+
},
|
|
282
|
+
description: 'Get schema and description of a specific method',
|
|
283
|
+
exposeSchema: true,
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// __rpc.describeAll - Get all methods with public schemas
|
|
288
|
+
this.addMethod(
|
|
289
|
+
`${this.#introspectionPrefix}.describeAll`,
|
|
290
|
+
async (_req, _context, _params) => {
|
|
291
|
+
const publicMethods = [];
|
|
292
|
+
|
|
293
|
+
for (const [name, methodConfig] of Object.entries(this.#methods)) {
|
|
294
|
+
// Skip introspection methods
|
|
295
|
+
if (name.startsWith(this.#introspectionPrefix)) continue;
|
|
296
|
+
|
|
297
|
+
const config = typeof methodConfig === 'function' ? {} : methodConfig;
|
|
298
|
+
if (config.exposeSchema) {
|
|
299
|
+
publicMethods.push({
|
|
300
|
+
name,
|
|
301
|
+
schema: config.schema || null,
|
|
302
|
+
description: config.description || '',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return publicMethods;
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
description: 'List all methods with public schemas',
|
|
311
|
+
exposeSchema: true,
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// __rpc.version - Get toolkit version
|
|
316
|
+
this.addMethod(
|
|
317
|
+
`${this.#introspectionPrefix}.version`,
|
|
318
|
+
async (_req, _context, _params) => ({
|
|
319
|
+
toolkit: 'rpc-express-toolkit',
|
|
320
|
+
version: pkg.version,
|
|
321
|
+
expressVersion: expressPkg.version,
|
|
322
|
+
nodeVersion: process.version,
|
|
323
|
+
}),
|
|
324
|
+
{
|
|
325
|
+
description: 'Get RPC toolkit version information',
|
|
326
|
+
exposeSchema: true,
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// __rpc.capabilities - Get server capabilities
|
|
331
|
+
this.addMethod(
|
|
332
|
+
`${this.#introspectionPrefix}.capabilities`,
|
|
333
|
+
async (_req, _context, _params) => ({
|
|
334
|
+
safeMode: this.#options.safeEnabled,
|
|
335
|
+
batch: true,
|
|
336
|
+
introspection: true,
|
|
337
|
+
introspectionPrefix: this.#introspectionPrefix,
|
|
338
|
+
validation: !!this.#options.validation,
|
|
339
|
+
cors: !!this.#options.cors,
|
|
340
|
+
auth: !!this.#options.auth,
|
|
341
|
+
rateLimit: !!this.#options.rateLimit,
|
|
342
|
+
methodCount: Object.keys(this.#methods).filter(
|
|
343
|
+
(n) => !n.startsWith(this.#introspectionPrefix)
|
|
344
|
+
).length,
|
|
345
|
+
}),
|
|
346
|
+
{
|
|
347
|
+
description: 'Get server capabilities and configuration',
|
|
348
|
+
exposeSchema: true,
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
this.#isInternalRegistration = false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Validate JSON middleware functionality and Express compatibility
|
|
357
|
+
* @private
|
|
358
|
+
*/
|
|
359
|
+
#validateJsonMiddleware() {
|
|
360
|
+
try {
|
|
361
|
+
// Check if express.json is available (Express 4.16.0+)
|
|
362
|
+
if (typeof express.json !== 'function') {
|
|
363
|
+
throw new Error(
|
|
364
|
+
'express.json() is not available. Please ensure you are using Express 4.16.0 or later, or install body-parser separately.'
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Test JSON middleware creation with default options
|
|
369
|
+
const jsonMiddleware = express.json({ limit: '10mb' });
|
|
370
|
+
|
|
371
|
+
if (typeof jsonMiddleware !== 'function') {
|
|
372
|
+
throw new Error(
|
|
373
|
+
'express.json() did not return a valid middleware function'
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Verify middleware has correct signature (req, res, next)
|
|
378
|
+
if (jsonMiddleware.length !== 3) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
'JSON middleware has invalid signature - expected (req, res, next)'
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.#logger.debug('JSON middleware validation passed', {
|
|
385
|
+
expressJsonAvailable: true,
|
|
386
|
+
middlewareValid: true,
|
|
387
|
+
endpoint: this.#endpoint,
|
|
388
|
+
});
|
|
389
|
+
} catch (error) {
|
|
390
|
+
const errorMsg =
|
|
391
|
+
`JSON middleware validation failed: ${error.message}\n` +
|
|
392
|
+
`This router cannot handle JSON-RPC requests properly.\n\n` +
|
|
393
|
+
`Possible solutions:\n` +
|
|
394
|
+
`- Update Express: npm install express@latest (requires >=4.16.0)\n` +
|
|
395
|
+
`- Add JSON parsing middleware manually: router.use(express.json())\n` +
|
|
396
|
+
`- Install body-parser: npm install body-parser`;
|
|
397
|
+
|
|
398
|
+
this.#logger.error('JSON middleware validation failed', {
|
|
399
|
+
error: error.message,
|
|
400
|
+
expressVersion: expressPkg?.version || 'unknown',
|
|
401
|
+
endpoint: this.#endpoint,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
throw new Error(errorMsg);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Setup Express routes
|
|
410
|
+
* @param {Router} router
|
|
411
|
+
* @param {C} context
|
|
412
|
+
*/
|
|
413
|
+
#setupRoutes(router, context) {
|
|
414
|
+
// Health check endpoint
|
|
415
|
+
if (this.#options.healthCheck !== false) {
|
|
416
|
+
router.get(`${this.#endpoint}/health`, (req, res) => {
|
|
417
|
+
res.json({
|
|
418
|
+
status: 'ok',
|
|
419
|
+
timestamp: new Date().toISOString(),
|
|
420
|
+
version: pkg.version,
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Metrics endpoint
|
|
426
|
+
if (this.#options.metrics) {
|
|
427
|
+
router.get(`${this.#endpoint}/metrics`, (req, res) => {
|
|
428
|
+
res.json(this.getMetrics());
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Main JSON-RPC endpoint
|
|
433
|
+
router.post(this.#endpoint, async (req, res) => {
|
|
434
|
+
const startTime = Date.now();
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
// Check if it's a batch request
|
|
438
|
+
if (this.#batchHandler.isBatchRequest(req.body)) {
|
|
439
|
+
this.#logger.info('Batch request received', {
|
|
440
|
+
batchSize: req.body.length,
|
|
441
|
+
ip: req.ip,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const results = await this.#batchHandler.processBatch(
|
|
445
|
+
req.body,
|
|
446
|
+
req,
|
|
447
|
+
res,
|
|
448
|
+
context
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// Only send response if there are results (non-notification requests)
|
|
452
|
+
if (!Array.isArray(results)) {
|
|
453
|
+
res.setHeader(
|
|
454
|
+
'X-RPC-Safe-Enabled',
|
|
455
|
+
this.#options.safeEnabled ? 'true' : 'false'
|
|
456
|
+
);
|
|
457
|
+
res.json(results);
|
|
458
|
+
} else if (results.length > 0) {
|
|
459
|
+
// Align batch responses with single-call header negotiation
|
|
460
|
+
res.setHeader(
|
|
461
|
+
'X-RPC-Safe-Enabled',
|
|
462
|
+
this.#options.safeEnabled ? 'true' : 'false'
|
|
463
|
+
);
|
|
464
|
+
res.json(results);
|
|
465
|
+
} else {
|
|
466
|
+
res.status(204).end(); // No content for all-notification batch
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
this.#logger.info('Batch request completed', {
|
|
470
|
+
batchSize: req.body.length,
|
|
471
|
+
responseCount: Array.isArray(results) ? results.length : 1,
|
|
472
|
+
duration: Date.now() - startTime,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Single request processing
|
|
479
|
+
await this.#processSingleRequest(req, res, context, startTime);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
this.#logger.error('Endpoint error', {
|
|
482
|
+
error: error.message,
|
|
483
|
+
stack: error.stack,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
res.json({
|
|
487
|
+
jsonrpc: '2.0',
|
|
488
|
+
id: null,
|
|
489
|
+
error: {
|
|
490
|
+
code: -32603,
|
|
491
|
+
message: 'Internal error',
|
|
492
|
+
data: serializeError(error, true),
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Process a single JSON-RPC request
|
|
501
|
+
* @param {Request} req
|
|
502
|
+
* @param {Response} res
|
|
503
|
+
* @param {C} context
|
|
504
|
+
* @param {number} startTime
|
|
505
|
+
*/
|
|
506
|
+
async #processSingleRequest(req, res, context, startTime) {
|
|
507
|
+
const { method, params, id } = req.body || {};
|
|
508
|
+
const envelope = validateEnvelope(req.body);
|
|
509
|
+
|
|
510
|
+
// Log the incoming request
|
|
511
|
+
this.#logger.rpcCall(method, params, id, req);
|
|
512
|
+
|
|
513
|
+
if (!envelope.valid) {
|
|
514
|
+
return this.reply(res, {
|
|
515
|
+
id: envelope.responseId,
|
|
516
|
+
error: envelope.error,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const methodConfig = this.#methods[method];
|
|
521
|
+
if (!methodConfig) {
|
|
522
|
+
if (envelope.isNotification) {
|
|
523
|
+
res.status(204).end();
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return this.reply(res, {
|
|
528
|
+
id,
|
|
529
|
+
error: { code: -32601, message: `Method "${method}" not found` },
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const handler =
|
|
534
|
+
typeof methodConfig === 'function' ? methodConfig : methodConfig.handler;
|
|
535
|
+
const schema =
|
|
536
|
+
typeof methodConfig === 'object' ? methodConfig.schema : null;
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
// Check client's safe options from request headers
|
|
540
|
+
const clientSafeHeader = req.headers['x-rpc-safe-enabled'];
|
|
541
|
+
|
|
542
|
+
// Strict mode: se server ha safe enabled ma client non invia header → JSON-RPC ERROR
|
|
543
|
+
if (
|
|
544
|
+
this.#options.strictMode &&
|
|
545
|
+
this.#options.safeEnabled &&
|
|
546
|
+
!clientSafeHeader
|
|
547
|
+
) {
|
|
548
|
+
if (envelope.isNotification) {
|
|
549
|
+
res.status(204).end();
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return this.reply(res, {
|
|
554
|
+
id,
|
|
555
|
+
error: {
|
|
556
|
+
code: -32600, // Invalid Request
|
|
557
|
+
message:
|
|
558
|
+
'RPC Compatibility Error: Server requires safe serialization header but client did not provide it.',
|
|
559
|
+
data: {
|
|
560
|
+
serverSafeEnabled: this.#options.safeEnabled,
|
|
561
|
+
requiredHeader: 'X-RPC-Safe-Enabled',
|
|
562
|
+
strictMode: true,
|
|
563
|
+
solution:
|
|
564
|
+
'Update client to rpc-express-toolkit v4+ or disable server strict mode',
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Use client's safe options for parameter deserialization
|
|
571
|
+
const clientSafeEnabled = clientSafeHeader === 'true';
|
|
572
|
+
const deserializationOptions = {
|
|
573
|
+
safeEnabled: clientSafeEnabled,
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// Deserialize parameters using client's safe options
|
|
577
|
+
const deserializedParams = hasOwn(req.body, 'params')
|
|
578
|
+
? this.deserializeBigIntsAndDates(params, deserializationOptions)
|
|
579
|
+
: params;
|
|
580
|
+
|
|
581
|
+
// Prepare middleware context
|
|
582
|
+
let middlewareContext = {
|
|
583
|
+
req,
|
|
584
|
+
res,
|
|
585
|
+
method,
|
|
586
|
+
params: deserializedParams,
|
|
587
|
+
context,
|
|
588
|
+
id,
|
|
589
|
+
startTime,
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// Execute beforeCall middleware
|
|
593
|
+
middlewareContext = await this.#middleware.execute(
|
|
594
|
+
'beforeCall',
|
|
595
|
+
middlewareContext
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
// Validate parameters if schema is provided
|
|
599
|
+
if (schema) {
|
|
600
|
+
middlewareContext = await this.#middleware.execute(
|
|
601
|
+
'beforeValidation',
|
|
602
|
+
middlewareContext
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const validation = this.#validator.validate(
|
|
606
|
+
middlewareContext.params,
|
|
607
|
+
schema
|
|
608
|
+
);
|
|
609
|
+
if (!validation.valid) {
|
|
610
|
+
const error = new Error('Validation failed');
|
|
611
|
+
error.code = -32602;
|
|
612
|
+
error.data = {
|
|
613
|
+
validationErrors: validation.errors.map((err) => ({
|
|
614
|
+
field: err.instancePath || err.schemaPath,
|
|
615
|
+
message: err.message,
|
|
616
|
+
value: err.data,
|
|
617
|
+
})),
|
|
618
|
+
};
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
middlewareContext.params = validation.data;
|
|
623
|
+
middlewareContext = await this.#middleware.execute(
|
|
624
|
+
'afterValidation',
|
|
625
|
+
middlewareContext
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Invoke the handler
|
|
630
|
+
const result = await Promise.resolve(
|
|
631
|
+
handler(req, context, middlewareContext.params)
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
// Execute afterCall middleware
|
|
635
|
+
middlewareContext.result = result;
|
|
636
|
+
await this.#middleware.execute('afterCall', middlewareContext);
|
|
637
|
+
|
|
638
|
+
// Log success
|
|
639
|
+
const duration = Date.now() - startTime;
|
|
640
|
+
this.#logger.rpcSuccess(method, id, duration, result);
|
|
641
|
+
|
|
642
|
+
// Convert any BigInt/Date values before JSON-stringifying
|
|
643
|
+
const safeResult = this.serializeBigIntsAndDates(result);
|
|
644
|
+
if (envelope.isNotification) {
|
|
645
|
+
res.status(204).end();
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
this.reply(res, { id, result: safeResult });
|
|
650
|
+
} catch (err) {
|
|
651
|
+
const duration = Date.now() - startTime;
|
|
652
|
+
this.#logger.rpcError(method, id, duration, err);
|
|
653
|
+
|
|
654
|
+
// Execute error middleware
|
|
655
|
+
try {
|
|
656
|
+
await this.#middleware.execute('onError', {
|
|
657
|
+
req,
|
|
658
|
+
res,
|
|
659
|
+
method,
|
|
660
|
+
params,
|
|
661
|
+
context,
|
|
662
|
+
id,
|
|
663
|
+
error: err,
|
|
664
|
+
startTime,
|
|
665
|
+
duration,
|
|
666
|
+
});
|
|
667
|
+
} catch (middlewareError) {
|
|
668
|
+
this.#logger.error('Error middleware failed', {
|
|
669
|
+
error: middlewareError.message,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (envelope.isNotification) {
|
|
674
|
+
res.status(204).end();
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
this.reply(res, {
|
|
679
|
+
id,
|
|
680
|
+
error: {
|
|
681
|
+
code: err.code || -32603,
|
|
682
|
+
message: err.message || `Internal error`,
|
|
683
|
+
...(err.data && { data: err.data }),
|
|
684
|
+
error: serializeError(err, true),
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
// Ensure explicit return for consistent-return rule
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Register a new JSON-RPC method.
|
|
694
|
+
* @param {string} name The method name.
|
|
695
|
+
* @param {Function|Object} handlerOrConfig The function to handle calls or config object with handler, schema, exposeSchema, description.
|
|
696
|
+
* @param {Object} [optionsOrSchema] Optional config object or schema for backward compatibility.
|
|
697
|
+
*/
|
|
698
|
+
addMethod(name, handlerOrConfig, optionsOrSchema = null) {
|
|
699
|
+
// Prevent users from registering introspection methods
|
|
700
|
+
if (
|
|
701
|
+
name.startsWith(this.#introspectionPrefix) &&
|
|
702
|
+
!this.#isInternalRegistration
|
|
703
|
+
) {
|
|
704
|
+
throw new Error(
|
|
705
|
+
`Method names starting with '${this.#introspectionPrefix}.' are reserved for RPC introspection`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
let methodConfig;
|
|
710
|
+
|
|
711
|
+
if (typeof handlerOrConfig === 'function') {
|
|
712
|
+
// Function handler - optionsOrSchema can be schema (backward compat) or options object
|
|
713
|
+
if (
|
|
714
|
+
optionsOrSchema &&
|
|
715
|
+
(optionsOrSchema.schema ||
|
|
716
|
+
optionsOrSchema.exposeSchema ||
|
|
717
|
+
optionsOrSchema.description)
|
|
718
|
+
) {
|
|
719
|
+
// New format: options object
|
|
720
|
+
methodConfig = {
|
|
721
|
+
handler: handlerOrConfig,
|
|
722
|
+
...optionsOrSchema,
|
|
723
|
+
};
|
|
724
|
+
} else if (optionsOrSchema) {
|
|
725
|
+
// Old format: schema directly
|
|
726
|
+
methodConfig = {
|
|
727
|
+
handler: handlerOrConfig,
|
|
728
|
+
schema: optionsOrSchema,
|
|
729
|
+
};
|
|
730
|
+
} else {
|
|
731
|
+
// Just the handler
|
|
732
|
+
methodConfig = handlerOrConfig;
|
|
733
|
+
}
|
|
734
|
+
} else if (typeof handlerOrConfig === 'object' && handlerOrConfig.handler) {
|
|
735
|
+
// Config object
|
|
736
|
+
methodConfig = handlerOrConfig;
|
|
737
|
+
} else {
|
|
738
|
+
throw new Error('Invalid handler configuration');
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
this.#methods[name] = methodConfig;
|
|
742
|
+
|
|
743
|
+
const config = typeof methodConfig === 'function' ? {} : methodConfig;
|
|
744
|
+
this.#logger.debug('Method registered', {
|
|
745
|
+
method: name,
|
|
746
|
+
hasSchema: !!config.schema,
|
|
747
|
+
exposeSchema: !!config.exposeSchema,
|
|
748
|
+
hasDescription: !!config.description,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Add middleware for specific hooks
|
|
754
|
+
* @param {string} hook
|
|
755
|
+
* @param {Function} middleware
|
|
756
|
+
*/
|
|
757
|
+
use(hook, middleware) {
|
|
758
|
+
this.#middleware.use(hook, middleware);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Remove method
|
|
763
|
+
* @param {string} name
|
|
764
|
+
*/
|
|
765
|
+
removeMethod(name) {
|
|
766
|
+
delete this.#methods[name];
|
|
767
|
+
this.#logger.debug('Method removed', { method: name });
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Get method configuration
|
|
772
|
+
* @param {string} name
|
|
773
|
+
* @returns {any}
|
|
774
|
+
*/
|
|
775
|
+
getMethod(name) {
|
|
776
|
+
return this.#methods[name];
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* List all registered methods
|
|
781
|
+
* @returns {string[]}
|
|
782
|
+
*/
|
|
783
|
+
listMethods() {
|
|
784
|
+
return Object.keys(this.#methods);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** @returns {string} The endpoint path (e.g. "/api"). */
|
|
788
|
+
get endpoint() {
|
|
789
|
+
return this.#endpoint;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* @returns {{ [name: string]: any }}
|
|
794
|
+
* All registered methods.
|
|
795
|
+
*/
|
|
796
|
+
get methods() {
|
|
797
|
+
return this.#methods;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Get logger instance
|
|
802
|
+
* @returns {Logger}
|
|
803
|
+
*/
|
|
804
|
+
get logger() {
|
|
805
|
+
return this.#logger;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Get middleware manager
|
|
810
|
+
* @returns {MiddlewareManager}
|
|
811
|
+
*/
|
|
812
|
+
get middleware() {
|
|
813
|
+
return this.#middleware;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Get validator instance
|
|
818
|
+
* @returns {SchemaValidator}
|
|
819
|
+
*/
|
|
820
|
+
get validator() {
|
|
821
|
+
return this.#validator;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Get endpoint options
|
|
826
|
+
* @returns {Object}
|
|
827
|
+
*/
|
|
828
|
+
get options() {
|
|
829
|
+
return { ...this.#options };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Get metrics data
|
|
834
|
+
* @returns {Object}
|
|
835
|
+
*/
|
|
836
|
+
getMetrics() {
|
|
837
|
+
return {
|
|
838
|
+
methods: Object.keys(this.#methods).length,
|
|
839
|
+
middleware: {
|
|
840
|
+
beforeCall: this.#middleware.getMiddlewares('beforeCall').length,
|
|
841
|
+
afterCall: this.#middleware.getMiddlewares('afterCall').length,
|
|
842
|
+
onError: this.#middleware.getMiddlewares('onError').length,
|
|
843
|
+
},
|
|
844
|
+
uptime: process.uptime(),
|
|
845
|
+
memory: process.memoryUsage(),
|
|
846
|
+
timestamp: new Date().toISOString(),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Send a JSON-RPC 2.0 response.
|
|
852
|
+
* @param {Response} res The Express response object.
|
|
853
|
+
* @param {{
|
|
854
|
+
* id?: string|number|null,
|
|
855
|
+
* result?: any,
|
|
856
|
+
* error?: { code: number, message: string, data?: any }
|
|
857
|
+
* }} responsePayload The JSON-RPC response fields.
|
|
858
|
+
*/
|
|
859
|
+
reply(res, { id, result, error }) {
|
|
860
|
+
const response = { jsonrpc: '2.0', id: id === undefined ? null : id };
|
|
861
|
+
|
|
862
|
+
if (error) {
|
|
863
|
+
response.error = error;
|
|
864
|
+
} else {
|
|
865
|
+
response.result = result;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Add safe options headers so client knows how to deserialize
|
|
869
|
+
// Send safe options headers to client
|
|
870
|
+
res.setHeader(
|
|
871
|
+
'X-RPC-Safe-Enabled',
|
|
872
|
+
this.#options.safeEnabled ? 'true' : 'false'
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
res.json(response);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Recursively convert BigInt values to strings and Date objects to ISO strings.
|
|
880
|
+
* This is crucial because JSON.stringify() cannot handle BigInt natively
|
|
881
|
+
* and Date objects are best transmitted in a standardized format.
|
|
882
|
+
*
|
|
883
|
+
* @param {any} value The value to serialize.
|
|
884
|
+
* @returns {any} A version of `value` safe for JSON serialization.
|
|
885
|
+
*/
|
|
886
|
+
serializeBigIntsAndDates(value) {
|
|
887
|
+
return this.#serializeValue(value, false, false, {
|
|
888
|
+
depth: 0,
|
|
889
|
+
seen: new WeakSet(),
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Internal serialization method with BigInt and Date warning tracking
|
|
895
|
+
* @private
|
|
896
|
+
*/
|
|
897
|
+
#serializeValue(
|
|
898
|
+
value,
|
|
899
|
+
hasBigInt = false,
|
|
900
|
+
hasDate = false,
|
|
901
|
+
state = { depth: 0, seen: new WeakSet() }
|
|
902
|
+
) {
|
|
903
|
+
const maxDepth =
|
|
904
|
+
this.#options.maxSerializationDepth || DEFAULT_MAX_SERIALIZATION_DEPTH;
|
|
905
|
+
if (state.depth > maxDepth) {
|
|
906
|
+
throw new Error('Serialization depth limit exceeded');
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (typeof value === 'bigint') {
|
|
910
|
+
// Warn if safeEnabled is disabled and we have BigInt
|
|
911
|
+
if (
|
|
912
|
+
!this.#options.safeEnabled &&
|
|
913
|
+
!hasBigInt &&
|
|
914
|
+
this.#options.warnOnUnsafe
|
|
915
|
+
) {
|
|
916
|
+
this.#logger.warn(
|
|
917
|
+
'BigInt detected in serialization. Consider enabling safeEnabled option to avoid potential string/BigInt confusion.'
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
// Convert BigInt to string with 'n' suffix for proper deserialization
|
|
921
|
+
return `${value.toString()}n`;
|
|
922
|
+
}
|
|
923
|
+
if (value instanceof Date) {
|
|
924
|
+
// Warn if safeEnabled is disabled and we have Date
|
|
925
|
+
if (
|
|
926
|
+
!this.#options.safeEnabled &&
|
|
927
|
+
!hasDate &&
|
|
928
|
+
this.#options.warnOnUnsafe
|
|
929
|
+
) {
|
|
930
|
+
this.#logger.warn(
|
|
931
|
+
'Date detected in serialization. Consider enabling safeEnabled option to avoid potential string/Date confusion.'
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
// Convert Date to ISO string with D: prefix if safeEnabled
|
|
935
|
+
const isoString = value.toISOString();
|
|
936
|
+
return this.#options.safeEnabled ? `D:${isoString}` : isoString;
|
|
937
|
+
}
|
|
938
|
+
if (typeof value === 'string') {
|
|
939
|
+
// Add S: prefix if safeEnabled is true
|
|
940
|
+
if (this.#options.safeEnabled) {
|
|
941
|
+
return `S:${value}`;
|
|
942
|
+
}
|
|
943
|
+
return value;
|
|
944
|
+
}
|
|
945
|
+
if (Array.isArray(value)) {
|
|
946
|
+
if (state.seen.has(value)) {
|
|
947
|
+
throw new Error('Circular reference detected during serialization');
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
state.seen.add(value);
|
|
951
|
+
// Recurse into arrays - don't pass hasBigInt/hasDate to children
|
|
952
|
+
// Each element should be checked independently for warnings
|
|
953
|
+
try {
|
|
954
|
+
return value.map((v) =>
|
|
955
|
+
this.#serializeValue(v, false, false, {
|
|
956
|
+
depth: state.depth + 1,
|
|
957
|
+
seen: state.seen,
|
|
958
|
+
})
|
|
959
|
+
);
|
|
960
|
+
} finally {
|
|
961
|
+
state.seen.delete(value);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (value && typeof value === 'object') {
|
|
965
|
+
if (state.seen.has(value)) {
|
|
966
|
+
throw new Error('Circular reference detected during serialization');
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
state.seen.add(value);
|
|
970
|
+
// Recurse into plain objects - don't pass hasBigInt/hasDate to children
|
|
971
|
+
// Each property should be checked independently for warnings
|
|
972
|
+
const result = {};
|
|
973
|
+
try {
|
|
974
|
+
Object.entries(value).forEach(([key, val]) => {
|
|
975
|
+
Object.defineProperty(result, key, {
|
|
976
|
+
value: this.#serializeValue(val, false, false, {
|
|
977
|
+
depth: state.depth + 1,
|
|
978
|
+
seen: state.seen,
|
|
979
|
+
}),
|
|
980
|
+
enumerable: true,
|
|
981
|
+
configurable: true,
|
|
982
|
+
writable: true,
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
} finally {
|
|
986
|
+
state.seen.delete(value);
|
|
987
|
+
}
|
|
988
|
+
return result;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// If it's none of the above, return as-is
|
|
992
|
+
return value;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Check if an object has BigInt values deep inside
|
|
997
|
+
* @private
|
|
998
|
+
*/
|
|
999
|
+
#hasDeepBigInt(obj) {
|
|
1000
|
+
if (typeof obj === 'bigint') return true;
|
|
1001
|
+
if (Array.isArray(obj))
|
|
1002
|
+
return obj.some((item) => this.#hasDeepBigInt(item));
|
|
1003
|
+
if (obj && typeof obj === 'object') {
|
|
1004
|
+
return Object.values(obj).some((val) => this.#hasDeepBigInt(val));
|
|
1005
|
+
}
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Check if an object has Date values deep inside
|
|
1011
|
+
* @private
|
|
1012
|
+
*/
|
|
1013
|
+
#hasDeepDate(obj) {
|
|
1014
|
+
if (obj instanceof Date) return true;
|
|
1015
|
+
if (Array.isArray(obj)) return obj.some((item) => this.#hasDeepDate(item));
|
|
1016
|
+
if (obj && typeof obj === 'object') {
|
|
1017
|
+
return Object.values(obj).some((val) => this.#hasDeepDate(val));
|
|
1018
|
+
}
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Recursively convert stringified BigInts back to BigInt,
|
|
1024
|
+
* and ISO 8601 date strings back to Date objects.
|
|
1025
|
+
*
|
|
1026
|
+
* This is useful if you receive JSON that was serialized
|
|
1027
|
+
* by `serializeBigIntsAndDates()` and want to re-hydrate
|
|
1028
|
+
* the original types.
|
|
1029
|
+
*
|
|
1030
|
+
* @param {any} value The value to deserialize.
|
|
1031
|
+
* @param {Object} [options] Custom deserialization options (uses server options if not provided).
|
|
1032
|
+
* @param {boolean} [options.safeEnabled] Whether to expect safe prefixes for strings and dates.
|
|
1033
|
+
* @returns {any} The re-hydrated value.
|
|
1034
|
+
*/
|
|
1035
|
+
deserializeBigIntsAndDates(value, options = null, state = null) {
|
|
1036
|
+
const traversalState = state || {
|
|
1037
|
+
depth: 0,
|
|
1038
|
+
seen: new WeakSet(),
|
|
1039
|
+
};
|
|
1040
|
+
const maxDepth =
|
|
1041
|
+
this.#options.maxDeserializationDepth || DEFAULT_MAX_SERIALIZATION_DEPTH;
|
|
1042
|
+
if (traversalState.depth > maxDepth) {
|
|
1043
|
+
throw new Error('Deserialization depth limit exceeded');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Use provided options or fall back to server options
|
|
1047
|
+
const safeEnabled = options
|
|
1048
|
+
? options.safeEnabled
|
|
1049
|
+
: this.#options.safeEnabled;
|
|
1050
|
+
|
|
1051
|
+
// More comprehensive ISO date regex that handles:
|
|
1052
|
+
// - UTC: 2023-01-01T12:00:00.000Z
|
|
1053
|
+
// - With timezone: 2023-01-01T12:00:00.000+01:00
|
|
1054
|
+
// - Without timezone: 2023-01-01T12:00:00.000 (treated as local)
|
|
1055
|
+
const ISO_DATE_REGEX =
|
|
1056
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
|
|
1057
|
+
|
|
1058
|
+
// 1. Check if it's a string that might be a BigInt, Date, or safe string
|
|
1059
|
+
if (typeof value === 'string') {
|
|
1060
|
+
// Safe string check: if safeEnabled and starts with S:
|
|
1061
|
+
if (safeEnabled && value.startsWith('S:')) {
|
|
1062
|
+
return value.substring(2); // Remove 'S:' prefix
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Safe date check: if safeEnabled and starts with D:
|
|
1066
|
+
if (safeEnabled && value.startsWith('D:')) {
|
|
1067
|
+
const isoString = value.substring(2); // Remove 'D:' prefix
|
|
1068
|
+
const date = new Date(isoString);
|
|
1069
|
+
// Double-check that we got a valid date
|
|
1070
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1071
|
+
return date;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// BigInt check: matches digits (including negative), ending in "n"
|
|
1076
|
+
if (/^-?\d+n$/.test(value)) {
|
|
1077
|
+
return BigInt(value.slice(0, -1)); // Remove 'n' and convert
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Date check: matches an ISO 8601 string (only if safeEnabled is false)
|
|
1081
|
+
if (!safeEnabled && ISO_DATE_REGEX.test(value)) {
|
|
1082
|
+
const date = new Date(value);
|
|
1083
|
+
// Double-check that we got a valid date
|
|
1084
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1085
|
+
return date;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// 2. If it's an array, handle each element
|
|
1091
|
+
if (Array.isArray(value)) {
|
|
1092
|
+
if (traversalState.seen.has(value)) {
|
|
1093
|
+
throw new Error('Circular reference detected during deserialization');
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
traversalState.seen.add(value);
|
|
1097
|
+
try {
|
|
1098
|
+
return value.map((v) =>
|
|
1099
|
+
this.deserializeBigIntsAndDates(v, options, {
|
|
1100
|
+
depth: traversalState.depth + 1,
|
|
1101
|
+
seen: traversalState.seen,
|
|
1102
|
+
})
|
|
1103
|
+
);
|
|
1104
|
+
} finally {
|
|
1105
|
+
traversalState.seen.delete(value);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// 3. If it's a plain object, recurse into each property
|
|
1110
|
+
if (value && typeof value === 'object') {
|
|
1111
|
+
if (traversalState.seen.has(value)) {
|
|
1112
|
+
throw new Error('Circular reference detected during deserialization');
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
traversalState.seen.add(value);
|
|
1116
|
+
const result = {};
|
|
1117
|
+
try {
|
|
1118
|
+
Object.entries(value).forEach(([key, val]) => {
|
|
1119
|
+
Object.defineProperty(result, key, {
|
|
1120
|
+
value: this.deserializeBigIntsAndDates(val, options, {
|
|
1121
|
+
depth: traversalState.depth + 1,
|
|
1122
|
+
seen: traversalState.seen,
|
|
1123
|
+
}),
|
|
1124
|
+
enumerable: true,
|
|
1125
|
+
configurable: true,
|
|
1126
|
+
writable: true,
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
} finally {
|
|
1130
|
+
traversalState.seen.delete(value);
|
|
1131
|
+
}
|
|
1132
|
+
return result;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// 4. For everything else (number, boolean, null, undefined, etc.), return as-is
|
|
1136
|
+
return value;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Export both server and client, plus utilities
|
|
1141
|
+
module.exports = RpcEndpoint;
|
|
1142
|
+
module.exports.RpcEndpoint = RpcEndpoint;
|
|
1143
|
+
module.exports.RpcClient = RpcClient;
|
|
1144
|
+
module.exports.RpcError = RpcError;
|
|
1145
|
+
module.exports.RpcHttpError = RpcHttpError;
|
|
1146
|
+
module.exports.RpcSafeClient = RpcSafeClient;
|
|
1147
|
+
module.exports.Logger = Logger;
|
|
1148
|
+
module.exports.MiddlewareManager = MiddlewareManager;
|
|
1149
|
+
module.exports.builtInMiddlewares = builtInMiddlewares;
|
|
1150
|
+
module.exports.SchemaValidator = SchemaValidator;
|
|
1151
|
+
module.exports.commonSchemas = commonSchemas;
|
|
1152
|
+
module.exports.SchemaBuilder = SchemaBuilder;
|
|
1153
|
+
module.exports.BatchHandler = BatchHandler;
|