rpc-express-toolkit 4.0.0 → 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.
@@ -1 +1 @@
1
- let fetchFn;if("undefined"==typeof BigInt||BigInt.prototype.toJSON||(BigInt.prototype.toJSON=function(){return this.toString()}),"undefined"!=typeof fetch)fetchFn=fetch;else try{fetchFn=require("node-fetch")}catch(e){if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("fetch is not available. Please install node-fetch or use Node.js 18+");fetchFn=globalThis.fetch}class RpcClient{#e;#t;#i;#n;#s;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.#s={safeEnabled:!0===i.safeEnabled,warnOnUnsafe:!1!==i.warnOnUnsafe,...i},this.#n={}}#r(){return++this.#i}async call(e,t,i=void 0,n={}){const s={jsonrpc:"2.0",method:e,id:void 0===i?this.#r():i};null!=t&&(s.params=this.serializeBigIntsAndDates(t));try{const e=await fetchFn(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-Safe-Enabled":this.#s.safeEnabled?"true":"false",...n},body:JSON.stringify(s),...this.#n});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.#s.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.#s.safeEnabled&&"true"===i&&this.#s.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 r={safeEnabled:"true"===i};return this.deserializeBigIntsAndDates(t.result,r)}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.#r()};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.#s.safeEnabled?"true":"false",...t},body:JSON.stringify(i),...this.#n});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const n=await e.json(),s=e.headers.get("X-RPC-Safe-Enabled");if(this.#s.safeEnabled&&null===s)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.#s.safeEnabled&&"true"===s&&this.#s.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 r={safeEnabled:"true"===s};if(Array.isArray(n))return n.map(e=>{if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result,r)});if(n.error)throw n.error;return[this.deserializeBigIntsAndDates(n.result,r)]}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.#s.safeEnabled?`D:${t}`:(this.#s.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.#s.safeEnabled?"S:"+e:(this.#s.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,n]of Object.entries(e))t[i]=this.serializeBigIntsAndDates(n);return t}return e}deserializeBigIntsAndDates(e,t=null){const i=t?t.safeEnabled:this.#s.safeEnabled,n=/^\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(!isNaN(i.getTime()))return i}if(/^-?\d+n$/.test(e))return BigInt(e.slice(0,-1));if(!i&&n.test(e)){const t=new Date(e);if(!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;
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 | CorsOptions;
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('express.json() is not available. Please ensure you are using Express 4.16.0 or later, or install body-parser separately.');
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('express.json() did not return a valid middleware function');
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('JSON middleware has invalid signature - expected (req, res, next)');
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 = `JSON middleware validation failed: ${error.message}\n` +
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: require('express/package.json')?.version || 'unknown',
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: require('../package.json').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 (this.#options.strictMode && this.#options.safeEnabled && !clientSafeHeader) {
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: 'RPC Compatibility Error: Server requires safe serialization header but client did not provide it.',
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: 'Update client to rpc-express-toolkit v4+ or disable server strict mode'
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 ? this.deserializeBigIntsAndDates(params, deserializationOptions) : 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('X-RPC-Safe-Enabled', this.#options.safeEnabled ? 'true' : 'false');
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 (!this.#options.safeEnabled && !hasBigInt && this.#options.warnOnUnsafe) {
646
- this.#logger.warn('BigInt detected in serialization. Consider enabling safeEnabled option to avoid potential string/BigInt confusion.');
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() + 'n';
650
- } else if (value instanceof Date) {
681
+ return `${value.toString()}n`;
682
+ }
683
+ if (value instanceof Date) {
651
684
  // Warn if safeEnabled is disabled and we have Date
652
- if (!this.#options.safeEnabled && !hasDate && this.#options.warnOnUnsafe) {
653
- this.#logger.warn('Date detected in serialization. Consider enabling safeEnabled option to avoid potential string/Date confusion.');
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
- } else if (typeof value === 'string') {
697
+ }
698
+ if (typeof value === 'string') {
659
699
  // Add S: prefix if safeEnabled is true
660
700
  if (this.#options.safeEnabled) {
661
- return 'S:' + value;
701
+ return `S:${value}`;
662
702
  }
663
703
  return value;
664
- } else if (Array.isArray(value)) {
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
- } else if (value && typeof value === 'object') {
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)) return obj.some(item => this.#hasDeepBigInt(item));
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 ? options.safeEnabled : this.#options.safeEnabled;
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 ? ` | ${JSON.stringify(meta)}` : '';
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 ? JSON.stringify(result).length : 0,
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 = (obj) => {
164
- if (typeof obj !== 'object' || obj === null) return obj;
165
-
166
- for (const [key, value] of Object.entries(obj)) {
167
- if (
168
- sensitiveKeys.some((sensitive) =>
169
- key.toLowerCase().includes(sensitive)
170
- )
171
- ) {
172
- obj[key] = '[REDACTED]';
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 obj;
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
- let result = context;
46
-
47
- for (const middleware of middlewares) {
48
- try {
49
- const middlewareResult = await middleware(result);
50
-
51
- // If middleware returns an object, merge it with context
52
- if (middlewareResult && typeof middlewareResult === 'object') {
53
- result = { ...result, ...middlewareResult };
54
- }
55
-
56
- // If middleware explicitly returns false, stop execution
57
- if (middlewareResult === false) {
58
- throw new Error('Middleware execution stopped');
59
- }
60
- } catch (error) {
61
- // For error hooks, continue even if middleware fails
62
- if (hook === 'onError') {
63
- console.error('Error middleware failed:', error);
64
- continue;
65
- }
66
- throw error;
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
@@ -1,3 +1,4 @@
1
+ /* eslint max-classes-per-file: off */
1
2
  const Ajv = require('ajv');
2
3
  const addFormats = require('ajv-formats');
3
4