rpc-express-toolkit 3.0.2 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1027
- package/examples/server-client-example.js +26 -26
- package/examples/ssl-development-example.js +18 -49
- package/package.json +18 -5
- package/src/batch.js +10 -1
- package/src/clients/rpc-client.js +139 -86
- package/src/clients/rpc-client.min.js +1 -1
- package/src/clients/rpc-client.min.mjs +1 -1
- package/src/clients/rpc-client.mjs +82 -37
- package/src/index.d.ts +16 -15
- package/src/index.js +118 -60
- package/src/logger.js +35 -15
- package/src/middleware.js +40 -25
- package/src/validation.js +1 -0
package/src/index.d.ts
CHANGED
|
@@ -96,17 +96,16 @@ interface RpcEndpointOptions {
|
|
|
96
96
|
prefix?: string;
|
|
97
97
|
maxBodySize?: string;
|
|
98
98
|
timeout?: number;
|
|
99
|
-
cors?: boolean |
|
|
99
|
+
cors?: boolean | CorsConfig;
|
|
100
100
|
auth?: AuthConfig;
|
|
101
101
|
validation?: ValidationConfig;
|
|
102
|
-
|
|
103
|
-
safeDateEnabled?: boolean;
|
|
102
|
+
safeEnabled?: boolean;
|
|
104
103
|
crossConfigurationEnabled?: boolean;
|
|
105
104
|
enableSchema?: boolean;
|
|
106
105
|
rateLimit?: RateLimitConfig;
|
|
107
106
|
logging?: LoggingConfig;
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
warnOnUnsafe?: boolean;
|
|
108
|
+
strictMode?: boolean;
|
|
110
109
|
autoJsonMiddleware?: boolean;
|
|
111
110
|
jsonOptions?: {
|
|
112
111
|
limit?: string;
|
|
@@ -114,24 +113,26 @@ interface RpcEndpointOptions {
|
|
|
114
113
|
type?: string | string[] | ((req: any) => boolean);
|
|
115
114
|
verify?: (req: any, res: any, buf: Buffer, encoding: string) => void;
|
|
116
115
|
};
|
|
116
|
+
// Additional options supported by implementation
|
|
117
|
+
methodWhitelist?: string[];
|
|
118
|
+
timing?: boolean;
|
|
119
|
+
metrics?: boolean;
|
|
120
|
+
healthCheck?: boolean;
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
/**
|
|
120
124
|
* RPC client configuration options
|
|
121
125
|
*/
|
|
122
126
|
interface RpcClientOptions {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
warnOnUnsafeString?: boolean;
|
|
126
|
-
warnOnUnsafeDate?: boolean;
|
|
127
|
+
safeEnabled?: boolean;
|
|
128
|
+
warnOnUnsafe?: boolean;
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
/**
|
|
130
132
|
* Deserialization options for safe prefixes
|
|
131
133
|
*/
|
|
132
134
|
interface DeserializationOptions {
|
|
133
|
-
|
|
134
|
-
safeDateEnabled?: boolean;
|
|
135
|
+
safeEnabled?: boolean;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
/**
|
|
@@ -236,12 +237,12 @@ declare class RpcClient {
|
|
|
236
237
|
/**
|
|
237
238
|
* Make a JSON-RPC call to the server.
|
|
238
239
|
*/
|
|
239
|
-
call(
|
|
240
|
+
call<T = any>(
|
|
240
241
|
method: string,
|
|
241
242
|
params?: any,
|
|
242
243
|
id?: string | number | null,
|
|
243
244
|
overrideHeaders?: Record<string, string>
|
|
244
|
-
): Promise<
|
|
245
|
+
): Promise<T>;
|
|
245
246
|
|
|
246
247
|
/**
|
|
247
248
|
* Make a JSON-RPC notification (no response expected).
|
|
@@ -255,7 +256,7 @@ declare class RpcClient {
|
|
|
255
256
|
/**
|
|
256
257
|
* Make a batch JSON-RPC call.
|
|
257
258
|
*/
|
|
258
|
-
batch(
|
|
259
|
+
batch<T = any>(
|
|
259
260
|
requests: Array<{
|
|
260
261
|
method: string;
|
|
261
262
|
params?: any;
|
|
@@ -263,7 +264,7 @@ declare class RpcClient {
|
|
|
263
264
|
notification?: boolean;
|
|
264
265
|
}>,
|
|
265
266
|
overrideHeaders?: Record<string, string>
|
|
266
|
-
): Promise<
|
|
267
|
+
): Promise<T[]>;
|
|
267
268
|
|
|
268
269
|
/**
|
|
269
270
|
* Recursively serialize BigInt and Date to JSON-safe string formats.
|
package/src/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const express = require('express');
|
|
3
|
+
const expressPkg = require('express/package.json');
|
|
4
|
+
const NestedError = require('nested-error-stacks');
|
|
5
|
+
const pkg = require('../package.json');
|
|
3
6
|
|
|
4
7
|
/** @typedef {import("express").Router} Router */
|
|
5
8
|
/** @typedef {import("express").Request} Request */
|
|
6
9
|
/** @typedef {import("express").Response} Response */
|
|
7
10
|
|
|
8
|
-
const NestedError = require('nested-error-stacks');
|
|
9
11
|
const Logger = require('./logger');
|
|
10
12
|
const { MiddlewareManager, builtInMiddlewares } = require('./middleware');
|
|
11
13
|
const {
|
|
@@ -138,10 +140,9 @@ class RpcEndpoint {
|
|
|
138
140
|
this.#batchHandler = new BatchHandler(this);
|
|
139
141
|
|
|
140
142
|
// Set default options
|
|
141
|
-
this.#options.
|
|
142
|
-
this.#options.
|
|
143
|
-
this.#options.
|
|
144
|
-
this.#options.warnOnUnsafeDate = this.#options.warnOnUnsafeDate !== false; // Default true
|
|
143
|
+
this.#options.safeEnabled = this.#options.safeEnabled === true; // Default false
|
|
144
|
+
this.#options.warnOnUnsafe = this.#options.warnOnUnsafe !== false; // Default true
|
|
145
|
+
this.#options.strictMode = this.#options.strictMode !== false; // Default true
|
|
145
146
|
|
|
146
147
|
// Validate that the router can handle JSON parsing
|
|
147
148
|
this.#validateJsonMiddleware();
|
|
@@ -191,41 +192,47 @@ class RpcEndpoint {
|
|
|
191
192
|
try {
|
|
192
193
|
// Check if express.json is available (Express 4.16.0+)
|
|
193
194
|
if (typeof express.json !== 'function') {
|
|
194
|
-
throw new Error(
|
|
195
|
+
throw new Error(
|
|
196
|
+
'express.json() is not available. Please ensure you are using Express 4.16.0 or later, or install body-parser separately.'
|
|
197
|
+
);
|
|
195
198
|
}
|
|
196
|
-
|
|
199
|
+
|
|
197
200
|
// Test JSON middleware creation with default options
|
|
198
201
|
const jsonMiddleware = express.json({ limit: '10mb' });
|
|
199
|
-
|
|
202
|
+
|
|
200
203
|
if (typeof jsonMiddleware !== 'function') {
|
|
201
|
-
throw new Error(
|
|
204
|
+
throw new Error(
|
|
205
|
+
'express.json() did not return a valid middleware function'
|
|
206
|
+
);
|
|
202
207
|
}
|
|
203
|
-
|
|
208
|
+
|
|
204
209
|
// Verify middleware has correct signature (req, res, next)
|
|
205
210
|
if (jsonMiddleware.length !== 3) {
|
|
206
|
-
throw new Error(
|
|
211
|
+
throw new Error(
|
|
212
|
+
'JSON middleware has invalid signature - expected (req, res, next)'
|
|
213
|
+
);
|
|
207
214
|
}
|
|
208
|
-
|
|
215
|
+
|
|
209
216
|
this.#logger.debug('JSON middleware validation passed', {
|
|
210
217
|
expressJsonAvailable: true,
|
|
211
218
|
middlewareValid: true,
|
|
212
|
-
endpoint: this.#endpoint
|
|
219
|
+
endpoint: this.#endpoint,
|
|
213
220
|
});
|
|
214
|
-
|
|
215
221
|
} catch (error) {
|
|
216
|
-
const errorMsg =
|
|
222
|
+
const errorMsg =
|
|
223
|
+
`JSON middleware validation failed: ${error.message}\n` +
|
|
217
224
|
`This router cannot handle JSON-RPC requests properly.\n\n` +
|
|
218
225
|
`Possible solutions:\n` +
|
|
219
226
|
`- Update Express: npm install express@latest (requires >=4.16.0)\n` +
|
|
220
227
|
`- Add JSON parsing middleware manually: router.use(express.json())\n` +
|
|
221
228
|
`- Install body-parser: npm install body-parser`;
|
|
222
|
-
|
|
229
|
+
|
|
223
230
|
this.#logger.error('JSON middleware validation failed', {
|
|
224
231
|
error: error.message,
|
|
225
|
-
expressVersion:
|
|
226
|
-
endpoint: this.#endpoint
|
|
232
|
+
expressVersion: expressPkg?.version || 'unknown',
|
|
233
|
+
endpoint: this.#endpoint,
|
|
227
234
|
});
|
|
228
|
-
|
|
235
|
+
|
|
229
236
|
throw new Error(errorMsg);
|
|
230
237
|
}
|
|
231
238
|
}
|
|
@@ -242,7 +249,7 @@ class RpcEndpoint {
|
|
|
242
249
|
res.json({
|
|
243
250
|
status: 'ok',
|
|
244
251
|
timestamp: new Date().toISOString(),
|
|
245
|
-
version:
|
|
252
|
+
version: pkg.version,
|
|
246
253
|
});
|
|
247
254
|
});
|
|
248
255
|
}
|
|
@@ -275,6 +282,11 @@ class RpcEndpoint {
|
|
|
275
282
|
|
|
276
283
|
// Only send response if there are results (non-notification requests)
|
|
277
284
|
if (results.length > 0) {
|
|
285
|
+
// Align batch responses with single-call header negotiation
|
|
286
|
+
res.setHeader(
|
|
287
|
+
'X-RPC-Safe-Enabled',
|
|
288
|
+
this.#options.safeEnabled ? 'true' : 'false'
|
|
289
|
+
);
|
|
278
290
|
res.json(results);
|
|
279
291
|
} else {
|
|
280
292
|
res.status(204).end(); // No content for all-notification batch
|
|
@@ -359,17 +371,41 @@ class RpcEndpoint {
|
|
|
359
371
|
|
|
360
372
|
try {
|
|
361
373
|
// Check client's safe options from request headers
|
|
362
|
-
const
|
|
363
|
-
|
|
374
|
+
const clientSafeHeader = req.headers['x-rpc-safe-enabled'];
|
|
375
|
+
|
|
376
|
+
// Strict mode: se server ha safe enabled ma client non invia header → JSON-RPC ERROR
|
|
377
|
+
if (
|
|
378
|
+
this.#options.strictMode &&
|
|
379
|
+
this.#options.safeEnabled &&
|
|
380
|
+
!clientSafeHeader
|
|
381
|
+
) {
|
|
382
|
+
return this.reply(res, {
|
|
383
|
+
id,
|
|
384
|
+
error: {
|
|
385
|
+
code: -32600, // Invalid Request
|
|
386
|
+
message:
|
|
387
|
+
'RPC Compatibility Error: Server requires safe serialization header but client did not provide it.',
|
|
388
|
+
data: {
|
|
389
|
+
serverSafeEnabled: this.#options.safeEnabled,
|
|
390
|
+
requiredHeader: 'X-RPC-Safe-Enabled',
|
|
391
|
+
strictMode: true,
|
|
392
|
+
solution:
|
|
393
|
+
'Update client to rpc-express-toolkit v4+ or disable server strict mode',
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
}
|
|
364
398
|
|
|
365
|
-
//
|
|
399
|
+
// Use client's safe options for parameter deserialization
|
|
400
|
+
const clientSafeEnabled = clientSafeHeader === 'true';
|
|
366
401
|
const deserializationOptions = {
|
|
367
|
-
|
|
368
|
-
safeDateEnabled: clientSafeDateEnabled
|
|
402
|
+
safeEnabled: clientSafeEnabled,
|
|
369
403
|
};
|
|
370
404
|
|
|
371
405
|
// Deserialize parameters using client's safe options
|
|
372
|
-
const deserializedParams = params
|
|
406
|
+
const deserializedParams = params
|
|
407
|
+
? this.deserializeBigIntsAndDates(params, deserializationOptions)
|
|
408
|
+
: params;
|
|
373
409
|
|
|
374
410
|
// Prepare middleware context
|
|
375
411
|
let middlewareContext = {
|
|
@@ -468,6 +504,8 @@ class RpcEndpoint {
|
|
|
468
504
|
},
|
|
469
505
|
});
|
|
470
506
|
}
|
|
507
|
+
// Ensure explicit return for consistent-return rule
|
|
508
|
+
return null;
|
|
471
509
|
}
|
|
472
510
|
|
|
473
511
|
/**
|
|
@@ -602,8 +640,11 @@ class RpcEndpoint {
|
|
|
602
640
|
}
|
|
603
641
|
|
|
604
642
|
// Add safe options headers so client knows how to deserialize
|
|
605
|
-
|
|
606
|
-
res.setHeader(
|
|
643
|
+
// Send safe options headers to client
|
|
644
|
+
res.setHeader(
|
|
645
|
+
'X-RPC-Safe-Enabled',
|
|
646
|
+
this.#options.safeEnabled ? 'true' : 'false'
|
|
647
|
+
);
|
|
607
648
|
|
|
608
649
|
res.json(response);
|
|
609
650
|
}
|
|
@@ -626,31 +667,47 @@ class RpcEndpoint {
|
|
|
626
667
|
*/
|
|
627
668
|
#serializeValue(value, hasBigInt = false, hasDate = false) {
|
|
628
669
|
if (typeof value === 'bigint') {
|
|
629
|
-
// Warn if
|
|
630
|
-
if (
|
|
631
|
-
this.#
|
|
670
|
+
// Warn if safeEnabled is disabled and we have BigInt
|
|
671
|
+
if (
|
|
672
|
+
!this.#options.safeEnabled &&
|
|
673
|
+
!hasBigInt &&
|
|
674
|
+
this.#options.warnOnUnsafe
|
|
675
|
+
) {
|
|
676
|
+
this.#logger.warn(
|
|
677
|
+
'BigInt detected in serialization. Consider enabling safeEnabled option to avoid potential string/BigInt confusion.'
|
|
678
|
+
);
|
|
632
679
|
}
|
|
633
680
|
// Convert BigInt to string with 'n' suffix for proper deserialization
|
|
634
|
-
return value.toString()
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
if
|
|
638
|
-
|
|
681
|
+
return `${value.toString()}n`;
|
|
682
|
+
}
|
|
683
|
+
if (value instanceof Date) {
|
|
684
|
+
// Warn if safeEnabled is disabled and we have Date
|
|
685
|
+
if (
|
|
686
|
+
!this.#options.safeEnabled &&
|
|
687
|
+
!hasDate &&
|
|
688
|
+
this.#options.warnOnUnsafe
|
|
689
|
+
) {
|
|
690
|
+
this.#logger.warn(
|
|
691
|
+
'Date detected in serialization. Consider enabling safeEnabled option to avoid potential string/Date confusion.'
|
|
692
|
+
);
|
|
639
693
|
}
|
|
640
|
-
// Convert Date to ISO string with D: prefix if
|
|
694
|
+
// Convert Date to ISO string with D: prefix if safeEnabled
|
|
641
695
|
const isoString = value.toISOString();
|
|
642
|
-
return this.#options.
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if
|
|
646
|
-
|
|
696
|
+
return this.#options.safeEnabled ? `D:${isoString}` : isoString;
|
|
697
|
+
}
|
|
698
|
+
if (typeof value === 'string') {
|
|
699
|
+
// Add S: prefix if safeEnabled is true
|
|
700
|
+
if (this.#options.safeEnabled) {
|
|
701
|
+
return `S:${value}`;
|
|
647
702
|
}
|
|
648
703
|
return value;
|
|
649
|
-
}
|
|
704
|
+
}
|
|
705
|
+
if (Array.isArray(value)) {
|
|
650
706
|
// Recurse into arrays - don't pass hasBigInt/hasDate to children
|
|
651
707
|
// Each element should be checked independently for warnings
|
|
652
708
|
return value.map((v) => this.#serializeValue(v, false, false));
|
|
653
|
-
}
|
|
709
|
+
}
|
|
710
|
+
if (value && typeof value === 'object') {
|
|
654
711
|
// Recurse into plain objects - don't pass hasBigInt/hasDate to children
|
|
655
712
|
// Each property should be checked independently for warnings
|
|
656
713
|
const result = {};
|
|
@@ -670,9 +727,10 @@ class RpcEndpoint {
|
|
|
670
727
|
*/
|
|
671
728
|
#hasDeepBigInt(obj) {
|
|
672
729
|
if (typeof obj === 'bigint') return true;
|
|
673
|
-
if (Array.isArray(obj))
|
|
730
|
+
if (Array.isArray(obj))
|
|
731
|
+
return obj.some((item) => this.#hasDeepBigInt(item));
|
|
674
732
|
if (obj && typeof obj === 'object') {
|
|
675
|
-
return Object.values(obj).some(val => this.#hasDeepBigInt(val));
|
|
733
|
+
return Object.values(obj).some((val) => this.#hasDeepBigInt(val));
|
|
676
734
|
}
|
|
677
735
|
return false;
|
|
678
736
|
}
|
|
@@ -683,9 +741,9 @@ class RpcEndpoint {
|
|
|
683
741
|
*/
|
|
684
742
|
#hasDeepDate(obj) {
|
|
685
743
|
if (obj instanceof Date) return true;
|
|
686
|
-
if (Array.isArray(obj)) return obj.some(item => this.#hasDeepDate(item));
|
|
744
|
+
if (Array.isArray(obj)) return obj.some((item) => this.#hasDeepDate(item));
|
|
687
745
|
if (obj && typeof obj === 'object') {
|
|
688
|
-
return Object.values(obj).some(val => this.#hasDeepDate(val));
|
|
746
|
+
return Object.values(obj).some((val) => this.#hasDeepDate(val));
|
|
689
747
|
}
|
|
690
748
|
return false;
|
|
691
749
|
}
|
|
@@ -700,14 +758,14 @@ class RpcEndpoint {
|
|
|
700
758
|
*
|
|
701
759
|
* @param {any} value The value to deserialize.
|
|
702
760
|
* @param {Object} [options] Custom deserialization options (uses server options if not provided).
|
|
703
|
-
* @param {boolean} [options.
|
|
704
|
-
* @param {boolean} [options.safeDateEnabled] Whether to expect D: prefixed dates.
|
|
761
|
+
* @param {boolean} [options.safeEnabled] Whether to expect safe prefixes for strings and dates.
|
|
705
762
|
* @returns {any} The re-hydrated value.
|
|
706
763
|
*/
|
|
707
764
|
deserializeBigIntsAndDates(value, options = null) {
|
|
708
765
|
// Use provided options or fall back to server options
|
|
709
|
-
const
|
|
710
|
-
|
|
766
|
+
const safeEnabled = options
|
|
767
|
+
? options.safeEnabled
|
|
768
|
+
: this.#options.safeEnabled;
|
|
711
769
|
|
|
712
770
|
// More comprehensive ISO date regex that handles:
|
|
713
771
|
// - UTC: 2023-01-01T12:00:00.000Z
|
|
@@ -718,17 +776,17 @@ class RpcEndpoint {
|
|
|
718
776
|
|
|
719
777
|
// 1. Check if it's a string that might be a BigInt, Date, or safe string
|
|
720
778
|
if (typeof value === 'string') {
|
|
721
|
-
// Safe string check: if
|
|
722
|
-
if (
|
|
779
|
+
// Safe string check: if safeEnabled and starts with S:
|
|
780
|
+
if (safeEnabled && value.startsWith('S:')) {
|
|
723
781
|
return value.substring(2); // Remove 'S:' prefix
|
|
724
782
|
}
|
|
725
783
|
|
|
726
|
-
// Safe date check: if
|
|
727
|
-
if (
|
|
784
|
+
// Safe date check: if safeEnabled and starts with D:
|
|
785
|
+
if (safeEnabled && value.startsWith('D:')) {
|
|
728
786
|
const isoString = value.substring(2); // Remove 'D:' prefix
|
|
729
787
|
const date = new Date(isoString);
|
|
730
788
|
// Double-check that we got a valid date
|
|
731
|
-
if (!isNaN(date.getTime())) {
|
|
789
|
+
if (!Number.isNaN(date.getTime())) {
|
|
732
790
|
return date;
|
|
733
791
|
}
|
|
734
792
|
}
|
|
@@ -738,11 +796,11 @@ class RpcEndpoint {
|
|
|
738
796
|
return BigInt(value.slice(0, -1)); // Remove 'n' and convert
|
|
739
797
|
}
|
|
740
798
|
|
|
741
|
-
// Date check: matches an ISO 8601 string (only if
|
|
742
|
-
if (!
|
|
799
|
+
// Date check: matches an ISO 8601 string (only if safeEnabled is false)
|
|
800
|
+
if (!safeEnabled && ISO_DATE_REGEX.test(value)) {
|
|
743
801
|
const date = new Date(value);
|
|
744
802
|
// Double-check that we got a valid date
|
|
745
|
-
if (!isNaN(date.getTime())) {
|
|
803
|
+
if (!Number.isNaN(date.getTime())) {
|
|
746
804
|
return date;
|
|
747
805
|
}
|
|
748
806
|
}
|
package/src/logger.js
CHANGED
|
@@ -47,6 +47,24 @@ class Logger {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Safely stringify objects that may contain BigInt/Date
|
|
52
|
+
* @param {any} obj
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
safeStringify(obj) {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.stringify(obj, (key, value) => {
|
|
58
|
+
if (typeof value === 'bigint') return `${value.toString()}n`;
|
|
59
|
+
if (value instanceof Date) return value.toISOString();
|
|
60
|
+
return value;
|
|
61
|
+
});
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// Fallback: best-effort toString
|
|
64
|
+
return String(obj);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
/**
|
|
51
69
|
* Generic log method
|
|
52
70
|
* @param {string} level
|
|
@@ -63,7 +81,7 @@ class Logger {
|
|
|
63
81
|
} else {
|
|
64
82
|
const logMessage = `${this.prefix} ${logData.timestamp} [${logData.level}] ${message}`;
|
|
65
83
|
const metaStr =
|
|
66
|
-
Object.keys(meta).length > 0 ? ` | ${
|
|
84
|
+
Object.keys(meta).length > 0 ? ` | ${this.safeStringify(meta)}` : '';
|
|
67
85
|
console[level === 'debug' || level === 'trace' ? 'log' : level](
|
|
68
86
|
`${logMessage}${metaStr}`
|
|
69
87
|
);
|
|
@@ -120,7 +138,7 @@ class Logger {
|
|
|
120
138
|
id,
|
|
121
139
|
duration: `${duration}ms`,
|
|
122
140
|
resultType: typeof result,
|
|
123
|
-
resultSize: result ?
|
|
141
|
+
resultSize: result ? this.safeStringify(result).length : 0,
|
|
124
142
|
});
|
|
125
143
|
}
|
|
126
144
|
|
|
@@ -160,21 +178,23 @@ class Logger {
|
|
|
160
178
|
];
|
|
161
179
|
const sanitized = Array.isArray(params) ? [...params] : { ...params };
|
|
162
180
|
|
|
163
|
-
const sanitizeObj = (
|
|
164
|
-
if (typeof
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
) {
|
|
172
|
-
|
|
173
|
-
} else if (typeof value === 'object') {
|
|
174
|
-
sanitizeObj(value);
|
|
181
|
+
const sanitizeObj = (input) => {
|
|
182
|
+
if (typeof input !== 'object' || input === null) return input;
|
|
183
|
+
|
|
184
|
+
const output = Array.isArray(input) ? [] : {};
|
|
185
|
+
for (const [key, value] of Object.entries(input)) {
|
|
186
|
+
const isSensitive = sensitiveKeys.some((sensitive) =>
|
|
187
|
+
key.toLowerCase().includes(sensitive)
|
|
188
|
+
);
|
|
189
|
+
if (isSensitive) {
|
|
190
|
+
output[key] = '[REDACTED]';
|
|
191
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
192
|
+
output[key] = sanitizeObj(value);
|
|
193
|
+
} else {
|
|
194
|
+
output[key] = value;
|
|
175
195
|
}
|
|
176
196
|
}
|
|
177
|
-
return
|
|
197
|
+
return output;
|
|
178
198
|
};
|
|
179
199
|
|
|
180
200
|
return sanitizeObj(sanitized);
|
package/src/middleware.js
CHANGED
|
@@ -42,32 +42,47 @@ class MiddlewareManager {
|
|
|
42
42
|
*/
|
|
43
43
|
async execute(hook, context) {
|
|
44
44
|
const middlewares = this.middlewares[hook] || [];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
const isErrorHook = hook === 'onError';
|
|
46
|
+
|
|
47
|
+
const finalResult = await middlewares.reduce(
|
|
48
|
+
(promise, middleware) =>
|
|
49
|
+
promise.then(async (acc) => {
|
|
50
|
+
// If a previous middleware requested stop, propagate without executing others
|
|
51
|
+
if (acc && acc.__middlewareStopped) {
|
|
52
|
+
return acc;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const middlewareResult = await middleware(acc);
|
|
56
|
+
|
|
57
|
+
if (middlewareResult === false) {
|
|
58
|
+
// Mark chain as stopped
|
|
59
|
+
return { ...acc, __middlewareStopped: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If middleware returns an object, merge it with context
|
|
63
|
+
if (middlewareResult && typeof middlewareResult === 'object') {
|
|
64
|
+
return { ...acc, ...middlewareResult };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return acc;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (isErrorHook) {
|
|
70
|
+
console.error('Error middleware failed:', error);
|
|
71
|
+
return acc;
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
Promise.resolve(context)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Do not expose internal marker
|
|
80
|
+
if (finalResult && finalResult.__middlewareStopped) {
|
|
81
|
+
const rest = { ...finalResult };
|
|
82
|
+
delete rest.__middlewareStopped;
|
|
83
|
+
return rest;
|
|
68
84
|
}
|
|
69
|
-
|
|
70
|
-
return result;
|
|
85
|
+
return finalResult;
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
/**
|
package/src/validation.js
CHANGED