rpc-express-toolkit 4.0.0 → 4.1.0
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 +34 -7
- package/examples/safe-preset-example.js +45 -0
- package/examples/server-client-example.js +26 -26
- package/examples/ssl-development-example.js +18 -49
- package/package.json +126 -101
- package/src/batch.js +10 -1
- package/src/clients/rpc-client.js +403 -392
- package/src/clients/rpc-client.min.js +1 -1
- package/src/index.d.ts +6 -1
- package/src/index.js +86 -41
- package/src/logger.js +35 -15
- package/src/middleware.js +40 -25
- package/src/safe.d.ts +30 -0
- package/src/safe.js +54 -0
- package/src/validation.js +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
let fetchFn;if("undefined"==typeof
|
|
1
|
+
let fetchFn;if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("globalThis.fetch is not available. Node.js 18+ is required.");fetchFn=globalThis.fetch.bind(globalThis);class RpcClient{#e;#t;#i;#s;#n;constructor(e,t={},i={}){this.#e=e,this.#t={"Content-Type":"application/json",...t},this.#i=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#n={safeEnabled:!0===i.safeEnabled,warnOnUnsafe:!1!==i.warnOnUnsafe,...i},this.#s={}}#a(){return++this.#i}async call(e,t,i=void 0,s={}){const n={jsonrpc:"2.0",method:e,id:void 0===i?this.#a():i};null!=t&&(n.params=this.serializeBigIntsAndDates(t));try{const e=await fetchFn(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-Safe-Enabled":this.#n.safeEnabled?"true":"false",...s},body:JSON.stringify(n),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const t=await e.json();if(t.error)throw t.error;const i=e.headers.get("X-RPC-Safe-Enabled");if(this.#n.safeEnabled&&null===i)throw new Error("RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). This may indicate a version mismatch or non-toolkit server. Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.");!this.#n.safeEnabled&&"true"===i&&this.#n.warnOnUnsafe&&console.warn("⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.");const a={safeEnabled:"true"===i};return this.deserializeBigIntsAndDates(t.result,a)}catch(e){throw console.error("RPC call failed:",e),e}}async notify(e,t={},i={}){await this.call(e,t,null,i)}async batch(e,t={}){const i=e.map(e=>{const t={jsonrpc:"2.0",method:e.method,id:void 0!==e.id?e.id:this.#a()};return void 0!==e.params&&null!==e.params&&(t.params=this.serializeBigIntsAndDates(e.params)),t});try{const e=await fetchFn(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-Safe-Enabled":this.#n.safeEnabled?"true":"false",...t},body:JSON.stringify(i),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const s=await e.json(),n=e.headers.get("X-RPC-Safe-Enabled");if(this.#n.safeEnabled&&null===n)throw new Error("RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). This may indicate a version mismatch or non-toolkit server. Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.");!this.#n.safeEnabled&&"true"===n&&this.#n.warnOnUnsafe&&console.warn("⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.");const a={safeEnabled:"true"===n};if(Array.isArray(s))return s.map(e=>{if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result,a)});if(s.error)throw s.error;return[this.deserializeBigIntsAndDates(s.result,a)]}catch(e){throw console.error("Batch RPC call failed:",e),e}}serializeBigIntsAndDates(e){if("bigint"==typeof e)return`${e.toString()}n`;if(e instanceof Date){const t=e.toISOString();return this.#n.safeEnabled?`D:${t}`:(this.#n.warnOnUnsafe&&console.warn("⚠️ Date serialization: Using plain ISO string format for JSON-RPC 2.0 compliance. Date objects will be deserialized as strings on the receiving end. Consider enabling safeEnabled or using string timestamps for better type safety."),t)}if("string"==typeof e)return this.#n.safeEnabled?`S:${e}`:(this.#n.warnOnUnsafe&&/^-?\d+n?$/.test(e)&&console.warn(`⚠️ String serialization: String "${e}" could be confused with BigInt. Consider enabling safeEnabled for disambiguation or use explicit typing.`),e);if(Array.isArray(e))return e.map(e=>this.serializeBigIntsAndDates(e));if(e&&"object"==typeof e){const t={};for(const[i,s]of Object.entries(e))t[i]=this.serializeBigIntsAndDates(s);return t}return e}deserializeBigIntsAndDates(e,t=null){const i=t?t.safeEnabled:this.#n.safeEnabled,s=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof e){if(i&&e.startsWith("S:"))return e.substring(2);if(i&&e.startsWith("D:")){const t=e.substring(2),i=new Date(t);if(!Number.isNaN(i.getTime()))return i}if(/^-?\d+n$/.test(e))return BigInt(e.slice(0,-1));if(!i&&s.test(e)){const t=new Date(e);if(!Number.isNaN(t.getTime()))return t}}return Array.isArray(e)?e.map(e=>this.deserializeBigIntsAndDates(e,t)):e&&"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,i])=>[e,this.deserializeBigIntsAndDates(i,t)])):e}}module.exports=RpcClient;
|
package/src/index.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ 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
102
|
safeEnabled?: boolean;
|
|
@@ -113,6 +113,11 @@ interface RpcEndpointOptions {
|
|
|
113
113
|
type?: string | string[] | ((req: any) => boolean);
|
|
114
114
|
verify?: (req: any, res: any, buf: Buffer, encoding: string) => void;
|
|
115
115
|
};
|
|
116
|
+
// Additional options supported by implementation
|
|
117
|
+
methodWhitelist?: string[];
|
|
118
|
+
timing?: boolean;
|
|
119
|
+
metrics?: boolean;
|
|
120
|
+
healthCheck?: boolean;
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
/**
|
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 {
|
|
@@ -190,41 +192,47 @@ class RpcEndpoint {
|
|
|
190
192
|
try {
|
|
191
193
|
// Check if express.json is available (Express 4.16.0+)
|
|
192
194
|
if (typeof express.json !== 'function') {
|
|
193
|
-
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
|
+
);
|
|
194
198
|
}
|
|
195
|
-
|
|
199
|
+
|
|
196
200
|
// Test JSON middleware creation with default options
|
|
197
201
|
const jsonMiddleware = express.json({ limit: '10mb' });
|
|
198
|
-
|
|
202
|
+
|
|
199
203
|
if (typeof jsonMiddleware !== 'function') {
|
|
200
|
-
throw new Error(
|
|
204
|
+
throw new Error(
|
|
205
|
+
'express.json() did not return a valid middleware function'
|
|
206
|
+
);
|
|
201
207
|
}
|
|
202
|
-
|
|
208
|
+
|
|
203
209
|
// Verify middleware has correct signature (req, res, next)
|
|
204
210
|
if (jsonMiddleware.length !== 3) {
|
|
205
|
-
throw new Error(
|
|
211
|
+
throw new Error(
|
|
212
|
+
'JSON middleware has invalid signature - expected (req, res, next)'
|
|
213
|
+
);
|
|
206
214
|
}
|
|
207
|
-
|
|
215
|
+
|
|
208
216
|
this.#logger.debug('JSON middleware validation passed', {
|
|
209
217
|
expressJsonAvailable: true,
|
|
210
218
|
middlewareValid: true,
|
|
211
|
-
endpoint: this.#endpoint
|
|
219
|
+
endpoint: this.#endpoint,
|
|
212
220
|
});
|
|
213
|
-
|
|
214
221
|
} catch (error) {
|
|
215
|
-
const errorMsg =
|
|
222
|
+
const errorMsg =
|
|
223
|
+
`JSON middleware validation failed: ${error.message}\n` +
|
|
216
224
|
`This router cannot handle JSON-RPC requests properly.\n\n` +
|
|
217
225
|
`Possible solutions:\n` +
|
|
218
226
|
`- Update Express: npm install express@latest (requires >=4.16.0)\n` +
|
|
219
227
|
`- Add JSON parsing middleware manually: router.use(express.json())\n` +
|
|
220
228
|
`- Install body-parser: npm install body-parser`;
|
|
221
|
-
|
|
229
|
+
|
|
222
230
|
this.#logger.error('JSON middleware validation failed', {
|
|
223
231
|
error: error.message,
|
|
224
|
-
expressVersion:
|
|
225
|
-
endpoint: this.#endpoint
|
|
232
|
+
expressVersion: expressPkg?.version || 'unknown',
|
|
233
|
+
endpoint: this.#endpoint,
|
|
226
234
|
});
|
|
227
|
-
|
|
235
|
+
|
|
228
236
|
throw new Error(errorMsg);
|
|
229
237
|
}
|
|
230
238
|
}
|
|
@@ -241,7 +249,7 @@ class RpcEndpoint {
|
|
|
241
249
|
res.json({
|
|
242
250
|
status: 'ok',
|
|
243
251
|
timestamp: new Date().toISOString(),
|
|
244
|
-
version:
|
|
252
|
+
version: pkg.version,
|
|
245
253
|
});
|
|
246
254
|
});
|
|
247
255
|
}
|
|
@@ -274,6 +282,11 @@ class RpcEndpoint {
|
|
|
274
282
|
|
|
275
283
|
// Only send response if there are results (non-notification requests)
|
|
276
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
|
+
);
|
|
277
290
|
res.json(results);
|
|
278
291
|
} else {
|
|
279
292
|
res.status(204).end(); // No content for all-notification batch
|
|
@@ -361,30 +374,38 @@ class RpcEndpoint {
|
|
|
361
374
|
const clientSafeHeader = req.headers['x-rpc-safe-enabled'];
|
|
362
375
|
|
|
363
376
|
// Strict mode: se server ha safe enabled ma client non invia header → JSON-RPC ERROR
|
|
364
|
-
if (
|
|
377
|
+
if (
|
|
378
|
+
this.#options.strictMode &&
|
|
379
|
+
this.#options.safeEnabled &&
|
|
380
|
+
!clientSafeHeader
|
|
381
|
+
) {
|
|
365
382
|
return this.reply(res, {
|
|
366
383
|
id,
|
|
367
384
|
error: {
|
|
368
385
|
code: -32600, // Invalid Request
|
|
369
|
-
message:
|
|
386
|
+
message:
|
|
387
|
+
'RPC Compatibility Error: Server requires safe serialization header but client did not provide it.',
|
|
370
388
|
data: {
|
|
371
389
|
serverSafeEnabled: this.#options.safeEnabled,
|
|
372
390
|
requiredHeader: 'X-RPC-Safe-Enabled',
|
|
373
391
|
strictMode: true,
|
|
374
|
-
solution:
|
|
375
|
-
|
|
376
|
-
|
|
392
|
+
solution:
|
|
393
|
+
'Update client to rpc-express-toolkit v4+ or disable server strict mode',
|
|
394
|
+
},
|
|
395
|
+
},
|
|
377
396
|
});
|
|
378
397
|
}
|
|
379
398
|
|
|
380
399
|
// Use client's safe options for parameter deserialization
|
|
381
400
|
const clientSafeEnabled = clientSafeHeader === 'true';
|
|
382
401
|
const deserializationOptions = {
|
|
383
|
-
safeEnabled: clientSafeEnabled
|
|
402
|
+
safeEnabled: clientSafeEnabled,
|
|
384
403
|
};
|
|
385
404
|
|
|
386
405
|
// Deserialize parameters using client's safe options
|
|
387
|
-
const deserializedParams = params
|
|
406
|
+
const deserializedParams = params
|
|
407
|
+
? this.deserializeBigIntsAndDates(params, deserializationOptions)
|
|
408
|
+
: params;
|
|
388
409
|
|
|
389
410
|
// Prepare middleware context
|
|
390
411
|
let middlewareContext = {
|
|
@@ -483,6 +504,8 @@ class RpcEndpoint {
|
|
|
483
504
|
},
|
|
484
505
|
});
|
|
485
506
|
}
|
|
507
|
+
// Ensure explicit return for consistent-return rule
|
|
508
|
+
return null;
|
|
486
509
|
}
|
|
487
510
|
|
|
488
511
|
/**
|
|
@@ -618,7 +641,10 @@ class RpcEndpoint {
|
|
|
618
641
|
|
|
619
642
|
// Add safe options headers so client knows how to deserialize
|
|
620
643
|
// Send safe options headers to client
|
|
621
|
-
res.setHeader(
|
|
644
|
+
res.setHeader(
|
|
645
|
+
'X-RPC-Safe-Enabled',
|
|
646
|
+
this.#options.safeEnabled ? 'true' : 'false'
|
|
647
|
+
);
|
|
622
648
|
|
|
623
649
|
res.json(response);
|
|
624
650
|
}
|
|
@@ -642,30 +668,46 @@ class RpcEndpoint {
|
|
|
642
668
|
#serializeValue(value, hasBigInt = false, hasDate = false) {
|
|
643
669
|
if (typeof value === 'bigint') {
|
|
644
670
|
// Warn if safeEnabled is disabled and we have BigInt
|
|
645
|
-
if (
|
|
646
|
-
this.#
|
|
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
|
+
);
|
|
647
679
|
}
|
|
648
680
|
// Convert BigInt to string with 'n' suffix for proper deserialization
|
|
649
|
-
return value.toString()
|
|
650
|
-
}
|
|
681
|
+
return `${value.toString()}n`;
|
|
682
|
+
}
|
|
683
|
+
if (value instanceof Date) {
|
|
651
684
|
// Warn if safeEnabled is disabled and we have Date
|
|
652
|
-
if (
|
|
653
|
-
this.#
|
|
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
|
+
);
|
|
654
693
|
}
|
|
655
694
|
// Convert Date to ISO string with D: prefix if safeEnabled
|
|
656
695
|
const isoString = value.toISOString();
|
|
657
696
|
return this.#options.safeEnabled ? `D:${isoString}` : isoString;
|
|
658
|
-
}
|
|
697
|
+
}
|
|
698
|
+
if (typeof value === 'string') {
|
|
659
699
|
// Add S: prefix if safeEnabled is true
|
|
660
700
|
if (this.#options.safeEnabled) {
|
|
661
|
-
return
|
|
701
|
+
return `S:${value}`;
|
|
662
702
|
}
|
|
663
703
|
return value;
|
|
664
|
-
}
|
|
704
|
+
}
|
|
705
|
+
if (Array.isArray(value)) {
|
|
665
706
|
// Recurse into arrays - don't pass hasBigInt/hasDate to children
|
|
666
707
|
// Each element should be checked independently for warnings
|
|
667
708
|
return value.map((v) => this.#serializeValue(v, false, false));
|
|
668
|
-
}
|
|
709
|
+
}
|
|
710
|
+
if (value && typeof value === 'object') {
|
|
669
711
|
// Recurse into plain objects - don't pass hasBigInt/hasDate to children
|
|
670
712
|
// Each property should be checked independently for warnings
|
|
671
713
|
const result = {};
|
|
@@ -685,9 +727,10 @@ class RpcEndpoint {
|
|
|
685
727
|
*/
|
|
686
728
|
#hasDeepBigInt(obj) {
|
|
687
729
|
if (typeof obj === 'bigint') return true;
|
|
688
|
-
if (Array.isArray(obj))
|
|
730
|
+
if (Array.isArray(obj))
|
|
731
|
+
return obj.some((item) => this.#hasDeepBigInt(item));
|
|
689
732
|
if (obj && typeof obj === 'object') {
|
|
690
|
-
return Object.values(obj).some(val => this.#hasDeepBigInt(val));
|
|
733
|
+
return Object.values(obj).some((val) => this.#hasDeepBigInt(val));
|
|
691
734
|
}
|
|
692
735
|
return false;
|
|
693
736
|
}
|
|
@@ -698,9 +741,9 @@ class RpcEndpoint {
|
|
|
698
741
|
*/
|
|
699
742
|
#hasDeepDate(obj) {
|
|
700
743
|
if (obj instanceof Date) return true;
|
|
701
|
-
if (Array.isArray(obj)) return obj.some(item => this.#hasDeepDate(item));
|
|
744
|
+
if (Array.isArray(obj)) return obj.some((item) => this.#hasDeepDate(item));
|
|
702
745
|
if (obj && typeof obj === 'object') {
|
|
703
|
-
return Object.values(obj).some(val => this.#hasDeepDate(val));
|
|
746
|
+
return Object.values(obj).some((val) => this.#hasDeepDate(val));
|
|
704
747
|
}
|
|
705
748
|
return false;
|
|
706
749
|
}
|
|
@@ -720,7 +763,9 @@ class RpcEndpoint {
|
|
|
720
763
|
*/
|
|
721
764
|
deserializeBigIntsAndDates(value, options = null) {
|
|
722
765
|
// Use provided options or fall back to server options
|
|
723
|
-
const safeEnabled = options
|
|
766
|
+
const safeEnabled = options
|
|
767
|
+
? options.safeEnabled
|
|
768
|
+
: this.#options.safeEnabled;
|
|
724
769
|
|
|
725
770
|
// More comprehensive ISO date regex that handles:
|
|
726
771
|
// - UTC: 2023-01-01T12:00:00.000Z
|
|
@@ -741,7 +786,7 @@ class RpcEndpoint {
|
|
|
741
786
|
const isoString = value.substring(2); // Remove 'D:' prefix
|
|
742
787
|
const date = new Date(isoString);
|
|
743
788
|
// Double-check that we got a valid date
|
|
744
|
-
if (!isNaN(date.getTime())) {
|
|
789
|
+
if (!Number.isNaN(date.getTime())) {
|
|
745
790
|
return date;
|
|
746
791
|
}
|
|
747
792
|
}
|
|
@@ -755,7 +800,7 @@ class RpcEndpoint {
|
|
|
755
800
|
if (!safeEnabled && ISO_DATE_REGEX.test(value)) {
|
|
756
801
|
const date = new Date(value);
|
|
757
802
|
// Double-check that we got a valid date
|
|
758
|
-
if (!isNaN(date.getTime())) {
|
|
803
|
+
if (!Number.isNaN(date.getTime())) {
|
|
759
804
|
return date;
|
|
760
805
|
}
|
|
761
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/safe.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Router } from 'express';
|
|
2
|
+
import type { RpcEndpoint, RpcClient, RpcEndpointOptions, RpcClientOptions } from './index';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Factory: create a RpcEndpoint with safe defaults (safeEnabled=true, strictMode=true).
|
|
6
|
+
*/
|
|
7
|
+
export function createSafeEndpoint<C = any>(
|
|
8
|
+
router: Router,
|
|
9
|
+
context: C,
|
|
10
|
+
endpoint?: string,
|
|
11
|
+
options?: RpcEndpointOptions
|
|
12
|
+
): RpcEndpoint<C>;
|
|
13
|
+
|
|
14
|
+
export function createSafeEndpoint<C = any>(
|
|
15
|
+
router: Router,
|
|
16
|
+
context: C,
|
|
17
|
+
options?: RpcEndpointOptions
|
|
18
|
+
): RpcEndpoint<C>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Factory: create a RpcClient with safe defaults (safeEnabled=true).
|
|
22
|
+
*/
|
|
23
|
+
export function createSafeClient(
|
|
24
|
+
endpoint: string,
|
|
25
|
+
defaultHeaders?: Record<string, string>,
|
|
26
|
+
options?: RpcClientOptions
|
|
27
|
+
): RpcClient;
|
|
28
|
+
|
|
29
|
+
export * from './index';
|
|
30
|
+
|
package/src/safe.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const Main = require('./index');
|
|
2
|
+
|
|
3
|
+
// Default safe options applied unless explicitly overridden
|
|
4
|
+
const defaultSafeOptions = {
|
|
5
|
+
safeEnabled: true,
|
|
6
|
+
strictMode: true,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a RpcEndpoint with safe defaults (safeEnabled=true, strictMode=true).
|
|
11
|
+
* Preserves both constructor signatures supported by RpcEndpoint.
|
|
12
|
+
*
|
|
13
|
+
* @param {import('express').Router} router
|
|
14
|
+
* @param {any} context
|
|
15
|
+
* @param {string|object} [endpointOrOptions='/api']
|
|
16
|
+
* @param {object} [options]
|
|
17
|
+
*/
|
|
18
|
+
function createSafeEndpoint(
|
|
19
|
+
router,
|
|
20
|
+
context,
|
|
21
|
+
endpointOrOptions = '/api',
|
|
22
|
+
options = {}
|
|
23
|
+
) {
|
|
24
|
+
if (typeof endpointOrOptions === 'string') {
|
|
25
|
+
const finalOptions = { ...defaultSafeOptions, ...options };
|
|
26
|
+
return new Main.RpcEndpoint(
|
|
27
|
+
router,
|
|
28
|
+
context,
|
|
29
|
+
endpointOrOptions,
|
|
30
|
+
finalOptions
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
// options object form
|
|
34
|
+
const final = { ...defaultSafeOptions, ...endpointOrOptions };
|
|
35
|
+
return new Main.RpcEndpoint(router, context, final);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a RpcClient with safe defaults (safeEnabled=true).
|
|
40
|
+
* @param {string} endpoint
|
|
41
|
+
* @param {Record<string,string>} [defaultHeaders]
|
|
42
|
+
* @param {object} [options]
|
|
43
|
+
*/
|
|
44
|
+
function createSafeClient(endpoint, defaultHeaders = {}, options = {}) {
|
|
45
|
+
const finalOptions = { safeEnabled: true, ...options };
|
|
46
|
+
return new Main.RpcClient(endpoint, defaultHeaders, finalOptions);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
// Re-export everything for convenience
|
|
51
|
+
...Main,
|
|
52
|
+
createSafeEndpoint,
|
|
53
|
+
createSafeClient,
|
|
54
|
+
};
|
package/src/validation.js
CHANGED