rpc-express-toolkit 3.0.2 → 4.0.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.
@@ -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;#n;#s;#a;constructor(e,t={},n={}){this.#e=e,this.#t={"Content-Type":"application/json",...t},this.#n=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#a={safeStringEnabled:!1!==n.safeStringEnabled,safeDateEnabled:!1!==n.safeDateEnabled,warnOnUnsafeString:!1!==n.warnOnUnsafeString,warnOnUnsafeDate:!1!==n.warnOnUnsafeDate,...n},this.#s={}}#r(){return++this.#n}async call(e,t,n=void 0,s={}){const a={jsonrpc:"2.0",method:e,id:void 0===n?this.#r():n};null!=t&&(a.params=this.serializeBigIntsAndDates(t));try{const e=await fetchFn(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-SafeString-Enabled":this.#a.safeStringEnabled?"true":"false","X-RPC-SafeDate-Enabled":this.#a.safeDateEnabled?"true":"false",...s},body:JSON.stringify(a),...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 n="true"===e.headers.get("X-RPC-SafeString-Enabled"),r={safeStringEnabled:n,safeDateEnabled:"true"===e.headers.get("X-RPC-SafeDate-Enabled")};return this.deserializeBigIntsAndDates(t.result,r)}catch(e){throw console.error("RPC call failed:",e),e}}async notify(e,t={},n={}){await this.call(e,t,null,n)}async batch(e,t={}){const n=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-SafeString-Enabled":this.#a.safeStringEnabled?"true":"false","X-RPC-SafeDate-Enabled":this.#a.safeDateEnabled?"true":"false",...t},body:JSON.stringify(n),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const s=await e.json(),a="true"===e.headers.get("X-RPC-SafeString-Enabled"),r={safeStringEnabled:a,safeDateEnabled:"true"===e.headers.get("X-RPC-SafeDate-Enabled")};if(Array.isArray(s))return s.map(e=>{if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result,r)});if(s.error)throw s.error;return[this.deserializeBigIntsAndDates(s.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.#a.safeDateEnabled?`D:${t}`:t}if("string"==typeof e)return this.#a.safeStringEnabled?"S:"+e:e;if(Array.isArray(e))return e.map(e=>this.serializeBigIntsAndDates(e));if(e&&"object"==typeof e){const t={};for(const[n,s]of Object.entries(e))t[n]=this.serializeBigIntsAndDates(s);return t}return e}deserializeBigIntsAndDates(e,t=null){const n=t?t.safeStringEnabled:this.#a.safeStringEnabled,s=t?t.safeDateEnabled:this.#a.safeDateEnabled,a=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof e){if(n&&e.startsWith("S:"))return e.substring(2);if(s&&e.startsWith("D:")){const t=e.substring(2),n=new Date(t);if(!isNaN(n.getTime()))return n}if(/^-?\d+n$/.test(e))return BigInt(e.slice(0,-1));if(!s&&a.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,n])=>[e,this.deserializeBigIntsAndDates(n,t)])):e}}module.exports=RpcClient,module.exports.RpcClient=RpcClient;
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 +1 @@
1
- let e;if("undefined"==typeof BigInt||BigInt.prototype.toJSON||(BigInt.prototype.toJSON=function(){return this.toString()}),"undefined"!=typeof fetch)e=fetch;else try{e=require("node-fetch")}catch(t){if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("fetch is not available. Please install node-fetch or use Node.js 18+");e=globalThis.fetch}class t{#e;#t;#s;#r;#a;constructor(e,t={},s={}){this.#e=e,this.#t={"Content-Type":"application/json",...t},this.#s=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#a={safeStringEnabled:!1!==s.safeStringEnabled,safeDateEnabled:!1!==s.safeDateEnabled,...s},this.#r={}}#i(){return++this.#s}async call(t,s,r=void 0,a={}){const i={jsonrpc:"2.0",method:t,id:void 0===r?this.#i():r};null!=s&&(i.params=this.serializeBigIntsAndDates(s));try{const t=await e(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-SafeString-Enabled":this.#a.safeStringEnabled?"true":"false","X-RPC-SafeDate-Enabled":this.#a.safeDateEnabled?"true":"false",...a},body:JSON.stringify(i),...this.#r});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const s=await t.json();if(s.error)throw s.error;const r="true"===t.headers.get("X-RPC-SafeString-Enabled"),n={safeStringEnabled:r,safeDateEnabled:"true"===t.headers.get("X-RPC-SafeDate-Enabled")};return this.deserializeBigIntsAndDates(s.result,n)}catch(e){throw console.error("RPC call failed:",e),e}}async notify(e,t={},s={}){await this.call(e,t,null,s)}async batch(t,s={}){const r=t.map(e=>{const t={jsonrpc:"2.0",method:e.method,id:void 0!==e.id?e.id:this.#i()};return void 0!==e.params&&null!==e.params&&(t.params=this.serializeBigIntsAndDates(e.params)),t});try{const t=await e(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-SafeString-Enabled":this.#a.safeStringEnabled?"true":"false","X-RPC-SafeDate-Enabled":this.#a.safeDateEnabled?"true":"false",...s},body:JSON.stringify(r),...this.#r});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const a=await t.json(),i="true"===t.headers.get("X-RPC-SafeString-Enabled"),n={safeStringEnabled:i,safeDateEnabled:"true"===t.headers.get("X-RPC-SafeDate-Enabled")};if(Array.isArray(a))return a.map(e=>{if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result,n)});if(a.error)throw a.error;return[this.deserializeBigIntsAndDates(a.result,n)]}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.#a.safeDateEnabled?`D:${t}`:t}if("string"==typeof e)return this.#a.safeStringEnabled?"S:"+e:e;if(Array.isArray(e))return e.map(e=>this.serializeBigIntsAndDates(e));if(e&&"object"==typeof e){const t={};for(const[s,r]of Object.entries(e))t[s]=this.serializeBigIntsAndDates(r);return t}return e}deserializeBigIntsAndDates(e,t=null){const s=t?t.safeStringEnabled:this.#a.safeStringEnabled,r=t?t.safeDateEnabled:this.#a.safeDateEnabled,a=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof e){if(s&&e.startsWith("S:"))return e.substring(2);if(r&&e.startsWith("D:")){const t=e.substring(2),s=new Date(t);if(!isNaN(s.getTime()))return s}if(/^-?\d+n$/.test(e))return BigInt(e.slice(0,-1));if(!r&&a.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,s])=>[e,this.deserializeBigIntsAndDates(s,t)])):e}}export default t;export{t as RpcClient};
1
+ let e;if("undefined"==typeof BigInt||BigInt.prototype.toJSON||(BigInt.prototype.toJSON=function(){return this.toString()}),"undefined"!=typeof fetch)e=fetch;else try{e=require("node-fetch")}catch(t){if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("fetch is not available. Please install node-fetch or use Node.js 18+");e=globalThis.fetch}class t{#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={}}#r(){return++this.#i}async call(t,i,s=void 0,n={}){const r={jsonrpc:"2.0",method:t,id:void 0===s?this.#r():s};null!=i&&(r.params=this.serializeBigIntsAndDates(i));try{const t=await e(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-Safe-Enabled":this.#n.safeEnabled?"true":"false",...n},body:JSON.stringify(r),...this.#s});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const i=await t.json();if(i.error)throw i.error;const s=t.headers.get("X-RPC-Safe-Enabled");if(this.#n.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.#n.safeEnabled&&"true"===s&&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"===s};return this.deserializeBigIntsAndDates(i.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(t,i={}){const s=t.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 t=await e(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-Safe-Enabled":this.#n.safeEnabled?"true":"false",...i},body:JSON.stringify(s),...this.#s});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const n=await t.json(),r=t.headers.get("X-RPC-Safe-Enabled");if(this.#n.safeEnabled&&null===r)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"===r&&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"===r};if(Array.isArray(n))return n.map(e=>{if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result,a)});if(n.error)throw n.error;return[this.deserializeBigIntsAndDates(n.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(!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(!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}}export default t;export{t as RpcClient};
@@ -45,8 +45,8 @@ class RpcClient {
45
45
  * @param {Object} [defaultHeaders={}] Optional default headers to include in requests.
46
46
  * @param {Object} [options={}] Optional configuration options.
47
47
  * @param {boolean} [options.rejectUnauthorized=true] Whether to reject unauthorized SSL certificates. Set to false for development with self-signed certificates.
48
- * @param {boolean} [options.safeStringEnabled=true] Whether to prefix strings with 'S:' to avoid confusion with BigInt values.
49
- * @param {boolean} [options.safeDateEnabled=true] Whether to prefix dates with 'D:' to avoid confusion with ISO string values.
48
+ * @param {boolean} [options.safeEnabled=false] Whether to use safe serialization for both strings and dates to avoid confusion with BigInt/ISO values. Default false for JSON-RPC 2.0 compliance.
49
+ * @param {boolean} [options.warnOnUnsafe=true] Whether to show educational warnings when BigInt/Date objects are serialized without safe mode. Default true.
50
50
  */
51
51
  constructor(endpoint, defaultHeaders = {}, options = {}) {
52
52
  this.#endpoint = endpoint;
@@ -59,8 +59,8 @@ class RpcClient {
59
59
 
60
60
  // Store options
61
61
  this.#options = {
62
- safeStringEnabled: options.safeStringEnabled !== false, // Default true
63
- safeDateEnabled: options.safeDateEnabled !== false, // Default true
62
+ safeEnabled: options.safeEnabled === true, // Default false
63
+ warnOnUnsafe: options.warnOnUnsafe !== false, // Default true
64
64
  ...options
65
65
  };
66
66
 
@@ -82,11 +82,12 @@ class RpcClient {
82
82
 
83
83
  /**
84
84
  * Make a JSON-RPC call to the server.
85
+ * @template T
85
86
  * @param {string} method The RPC method name.
86
87
  * @param {any} params The parameters to pass to the RPC method.
87
88
  * @param {string|number|null} [id] The request ID (auto-generated if not provided).
88
89
  * @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
89
- * @returns {Promise<any>} The result of the RPC call.
90
+ * @returns {Promise<T>} The result of the RPC call.
90
91
  */
91
92
  async call(method, params, id = undefined, overrideHeaders = {}) {
92
93
  // Auto-generate ID if not provided (null means notification)
@@ -108,9 +109,8 @@ class RpcClient {
108
109
  method: 'POST',
109
110
  headers: {
110
111
  ...this.#defaultHeaders,
111
- // Add safe options headers so server knows what client expects
112
- 'X-RPC-SafeString-Enabled': this.#options.safeStringEnabled ? 'true' : 'false',
113
- 'X-RPC-SafeDate-Enabled': this.#options.safeDateEnabled ? 'true' : 'false',
112
+ // Add safe options header so server knows what client expects
113
+ 'X-RPC-Safe-Enabled': this.#options.safeEnabled ? 'true' : 'false',
114
114
  ...overrideHeaders,
115
115
  },
116
116
  // Since we have BigInt.prototype.toJSON, we don't need manual serialization
@@ -131,14 +131,31 @@ class RpcClient {
131
131
  throw responseBody.error;
132
132
  }
133
133
 
134
- // Check server's safe options from response headers
135
- const serverSafeStringEnabled = response.headers.get('X-RPC-SafeString-Enabled') === 'true';
136
- const serverSafeDateEnabled = response.headers.get('X-RPC-SafeDate-Enabled') === 'true';
134
+ // Check server's safe options from response headers - strict compatibility checking
135
+ const serverSafeHeader = response.headers.get('X-RPC-Safe-Enabled');
136
+
137
+ // Errore rigoroso: se client ha safe enabled ma server non risponde con header
138
+ if (this.#options.safeEnabled && serverSafeHeader === null) {
139
+ throw new Error(
140
+ 'RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). ' +
141
+ 'This may indicate a version mismatch or non-toolkit server. ' +
142
+ 'Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.'
143
+ );
144
+ }
145
+
146
+ // Warning: se client ha safe disabled ma server offre compatibilità
147
+ if (!this.#options.safeEnabled && serverSafeHeader === 'true' && this.#options.warnOnUnsafe) {
148
+ console.warn(
149
+ '⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. ' +
150
+ 'Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.'
151
+ );
152
+ }
153
+
154
+ const serverSafeEnabled = serverSafeHeader === 'true';
137
155
 
138
156
  // Create deserialization options based on server's configuration
139
157
  const deserializationOptions = {
140
- safeStringEnabled: serverSafeStringEnabled,
141
- safeDateEnabled: serverSafeDateEnabled
158
+ safeEnabled: serverSafeEnabled
142
159
  };
143
160
 
144
161
  // Convert back BigInts and Dates in the result using server's options
@@ -162,9 +179,10 @@ class RpcClient {
162
179
 
163
180
  /**
164
181
  * Make multiple JSON-RPC calls in a single batch request.
182
+ * @template T
165
183
  * @param {Array<{method: string, params?: any, id?: string|number}>} requests Array of request objects.
166
184
  * @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
167
- * @returns {Promise<Array<any>>} Array of results in the same order as requests.
185
+ * @returns {Promise<Array<T>>} Array of results in the same order as requests.
168
186
  */
169
187
  async batch(requests, overrideHeaders = {}) {
170
188
  const batchRequests = requests.map((req) => {
@@ -185,9 +203,8 @@ class RpcClient {
185
203
  method: 'POST',
186
204
  headers: {
187
205
  ...this.#defaultHeaders,
188
- // Add safe options headers so server knows what client expects
189
- 'X-RPC-SafeString-Enabled': this.#options.safeStringEnabled ? 'true' : 'false',
190
- 'X-RPC-SafeDate-Enabled': this.#options.safeDateEnabled ? 'true' : 'false',
206
+ // Add safe options header so server knows what client expects
207
+ 'X-RPC-Safe-Enabled': this.#options.safeEnabled ? 'true' : 'false',
191
208
  ...overrideHeaders,
192
209
  },
193
210
  body: JSON.stringify(batchRequests),
@@ -202,14 +219,31 @@ class RpcClient {
202
219
 
203
220
  const responseBody = await response.json();
204
221
 
205
- // Check server's safe options from response headers
206
- const serverSafeStringEnabled = response.headers.get('X-RPC-SafeString-Enabled') === 'true';
207
- const serverSafeDateEnabled = response.headers.get('X-RPC-SafeDate-Enabled') === 'true';
222
+ // Check server's safe options from response headers - strict compatibility checking
223
+ const serverSafeHeader = response.headers.get('X-RPC-Safe-Enabled');
224
+
225
+ // Errore rigoroso: se client ha safe enabled ma server non risponde con header
226
+ if (this.#options.safeEnabled && serverSafeHeader === null) {
227
+ throw new Error(
228
+ 'RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). ' +
229
+ 'This may indicate a version mismatch or non-toolkit server. ' +
230
+ 'Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.'
231
+ );
232
+ }
233
+
234
+ // Warning: se client ha safe disabled ma server offre compatibilità
235
+ if (!this.#options.safeEnabled && serverSafeHeader === 'true' && this.#options.warnOnUnsafe) {
236
+ console.warn(
237
+ '⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. ' +
238
+ 'Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.'
239
+ );
240
+ }
241
+
242
+ const serverSafeEnabled = serverSafeHeader === 'true';
208
243
 
209
244
  // Create deserialization options based on server's configuration
210
245
  const deserializationOptions = {
211
- safeStringEnabled: serverSafeStringEnabled,
212
- safeDateEnabled: serverSafeDateEnabled
246
+ safeEnabled: serverSafeEnabled
213
247
  };
214
248
 
215
249
  // Handle batch response
@@ -244,15 +278,28 @@ class RpcClient {
244
278
  // Convert BigInt to string with 'n' suffix for proper deserialization
245
279
  return value.toString() + 'n';
246
280
  } else if (value instanceof Date) {
247
- // Convert Date to ISO string with D: prefix if safeDateEnabled
281
+ // Convert Date to ISO string with D: prefix if safeEnabled
248
282
  const isoString = value.toISOString();
249
- return this.#options.safeDateEnabled ? `D:${isoString}` : isoString;
283
+ if (this.#options.safeEnabled) {
284
+ return `D:${isoString}`;
285
+ } else {
286
+ // Show educational warning if enabled
287
+ if (this.#options.warnOnUnsafe) {
288
+ 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.');
289
+ }
290
+ return isoString;
291
+ }
250
292
  } else if (typeof value === 'string') {
251
- // Add S: prefix if safeStringEnabled is true
252
- if (this.#options.safeStringEnabled) {
293
+ // Add S: prefix if safeEnabled is true
294
+ if (this.#options.safeEnabled) {
253
295
  return 'S:' + value;
296
+ } else {
297
+ // Show educational warning if enabled and string could be confused with BigInt
298
+ if (this.#options.warnOnUnsafe && /^-?\d+n?$/.test(value)) {
299
+ console.warn(`⚠️ String serialization: String "${value}" could be confused with BigInt. Consider enabling safeEnabled for disambiguation or use explicit typing.`);
300
+ }
301
+ return value;
254
302
  }
255
- return value;
256
303
  } else if (Array.isArray(value)) {
257
304
  // Recurse into arrays
258
305
  return value.map((v) => this.serializeBigIntsAndDates(v));
@@ -275,14 +322,12 @@ class RpcClient {
275
322
  *
276
323
  * @param {any} value The value to deserialize.
277
324
  * @param {Object} [options] Custom deserialization options (uses client options if not provided).
278
- * @param {boolean} [options.safeStringEnabled] Whether to expect S: prefixed strings.
279
- * @param {boolean} [options.safeDateEnabled] Whether to expect D: prefixed dates.
325
+ * @param {boolean} [options.safeEnabled] Whether to expect safe prefixes for strings and dates.
280
326
  * @returns {any}
281
327
  */
282
328
  deserializeBigIntsAndDates(value, options = null) {
283
329
  // Use provided options or fall back to client options
284
- const safeStringEnabled = options ? options.safeStringEnabled : this.#options.safeStringEnabled;
285
- const safeDateEnabled = options ? options.safeDateEnabled : this.#options.safeDateEnabled;
330
+ const safeEnabled = options ? options.safeEnabled : this.#options.safeEnabled;
286
331
 
287
332
  // More comprehensive ISO date regex that handles:
288
333
  // - UTC: 2023-01-01T12:00:00.000Z
@@ -293,13 +338,13 @@ class RpcClient {
293
338
 
294
339
  // 1. Check if it's a string that might be a BigInt, Date, or safe string
295
340
  if (typeof value === 'string') {
296
- // Safe string check: if safeStringEnabled and starts with S:
297
- if (safeStringEnabled && value.startsWith('S:')) {
341
+ // Safe string check: if safeEnabled and starts with S:
342
+ if (safeEnabled && value.startsWith('S:')) {
298
343
  return value.substring(2); // Remove 'S:' prefix
299
344
  }
300
345
 
301
- // Safe date check: if safeDateEnabled and starts with D:
302
- if (safeDateEnabled && value.startsWith('D:')) {
346
+ // Safe date check: if safeEnabled and starts with D:
347
+ if (safeEnabled && value.startsWith('D:')) {
303
348
  const isoString = value.substring(2); // Remove 'D:' prefix
304
349
  const date = new Date(isoString);
305
350
  // Double-check that we got a valid date
@@ -314,8 +359,8 @@ class RpcClient {
314
359
  return BigInt(value.slice(0, -1)); // Remove 'n' and convert
315
360
  }
316
361
 
317
- // Date check: matches an ISO 8601 string (only if safeDateEnabled is false)
318
- if (!safeDateEnabled && ISO_DATE_REGEX.test(value)) {
362
+ // Date check: matches an ISO 8601 string (only if safeEnabled is false)
363
+ if (!safeEnabled && ISO_DATE_REGEX.test(value)) {
319
364
  const date = new Date(value);
320
365
  // Ensure it's valid
321
366
  if (!isNaN(date.getTime())) {
package/src/index.d.ts CHANGED
@@ -99,14 +99,13 @@ interface RpcEndpointOptions {
99
99
  cors?: boolean | CorsOptions;
100
100
  auth?: AuthConfig;
101
101
  validation?: ValidationConfig;
102
- safeStringEnabled?: boolean;
103
- safeDateEnabled?: boolean;
102
+ safeEnabled?: boolean;
104
103
  crossConfigurationEnabled?: boolean;
105
104
  enableSchema?: boolean;
106
105
  rateLimit?: RateLimitConfig;
107
106
  logging?: LoggingConfig;
108
- warnOnUnsafeString?: boolean;
109
- warnOnUnsafeDate?: boolean;
107
+ warnOnUnsafe?: boolean;
108
+ strictMode?: boolean;
110
109
  autoJsonMiddleware?: boolean;
111
110
  jsonOptions?: {
112
111
  limit?: string;
@@ -120,18 +119,15 @@ interface RpcEndpointOptions {
120
119
  * RPC client configuration options
121
120
  */
122
121
  interface RpcClientOptions {
123
- safeStringEnabled?: boolean;
124
- safeDateEnabled?: boolean;
125
- warnOnUnsafeString?: boolean;
126
- warnOnUnsafeDate?: boolean;
122
+ safeEnabled?: boolean;
123
+ warnOnUnsafe?: boolean;
127
124
  }
128
125
 
129
126
  /**
130
127
  * Deserialization options for safe prefixes
131
128
  */
132
129
  interface DeserializationOptions {
133
- safeStringEnabled?: boolean;
134
- safeDateEnabled?: boolean;
130
+ safeEnabled?: boolean;
135
131
  }
136
132
 
137
133
  /**
@@ -236,12 +232,12 @@ declare class RpcClient {
236
232
  /**
237
233
  * Make a JSON-RPC call to the server.
238
234
  */
239
- call(
235
+ call<T = any>(
240
236
  method: string,
241
237
  params?: any,
242
238
  id?: string | number | null,
243
239
  overrideHeaders?: Record<string, string>
244
- ): Promise<any>;
240
+ ): Promise<T>;
245
241
 
246
242
  /**
247
243
  * Make a JSON-RPC notification (no response expected).
@@ -255,7 +251,7 @@ declare class RpcClient {
255
251
  /**
256
252
  * Make a batch JSON-RPC call.
257
253
  */
258
- batch(
254
+ batch<T = any>(
259
255
  requests: Array<{
260
256
  method: string;
261
257
  params?: any;
@@ -263,7 +259,7 @@ declare class RpcClient {
263
259
  notification?: boolean;
264
260
  }>,
265
261
  overrideHeaders?: Record<string, string>
266
- ): Promise<any[]>;
262
+ ): Promise<T[]>;
267
263
 
268
264
  /**
269
265
  * Recursively serialize BigInt and Date to JSON-safe string formats.
package/src/index.js CHANGED
@@ -138,10 +138,9 @@ class RpcEndpoint {
138
138
  this.#batchHandler = new BatchHandler(this);
139
139
 
140
140
  // Set default options
141
- this.#options.safeStringEnabled = this.#options.safeStringEnabled !== false; // Default true
142
- this.#options.safeDateEnabled = this.#options.safeDateEnabled !== false; // Default true
143
- this.#options.warnOnUnsafeString = this.#options.warnOnUnsafeString !== false; // Default true
144
- this.#options.warnOnUnsafeDate = this.#options.warnOnUnsafeDate !== false; // Default true
141
+ this.#options.safeEnabled = this.#options.safeEnabled === true; // Default false
142
+ this.#options.warnOnUnsafe = this.#options.warnOnUnsafe !== false; // Default true
143
+ this.#options.strictMode = this.#options.strictMode !== false; // Default true
145
144
 
146
145
  // Validate that the router can handle JSON parsing
147
146
  this.#validateJsonMiddleware();
@@ -359,13 +358,29 @@ class RpcEndpoint {
359
358
 
360
359
  try {
361
360
  // Check client's safe options from request headers
362
- const clientSafeStringEnabled = req.headers['x-rpc-safestring-enabled'] === 'true';
363
- const clientSafeDateEnabled = req.headers['x-rpc-safedate-enabled'] === 'true';
361
+ const clientSafeHeader = req.headers['x-rpc-safe-enabled'];
364
362
 
365
- // Create deserialization options based on client's configuration
363
+ // Strict mode: se server ha safe enabled ma client non invia header → JSON-RPC ERROR
364
+ if (this.#options.strictMode && this.#options.safeEnabled && !clientSafeHeader) {
365
+ return this.reply(res, {
366
+ id,
367
+ error: {
368
+ code: -32600, // Invalid Request
369
+ message: 'RPC Compatibility Error: Server requires safe serialization header but client did not provide it.',
370
+ data: {
371
+ serverSafeEnabled: this.#options.safeEnabled,
372
+ requiredHeader: 'X-RPC-Safe-Enabled',
373
+ strictMode: true,
374
+ solution: 'Update client to rpc-express-toolkit v4+ or disable server strict mode'
375
+ }
376
+ }
377
+ });
378
+ }
379
+
380
+ // Use client's safe options for parameter deserialization
381
+ const clientSafeEnabled = clientSafeHeader === 'true';
366
382
  const deserializationOptions = {
367
- safeStringEnabled: clientSafeStringEnabled,
368
- safeDateEnabled: clientSafeDateEnabled
383
+ safeEnabled: clientSafeEnabled
369
384
  };
370
385
 
371
386
  // Deserialize parameters using client's safe options
@@ -602,8 +617,8 @@ class RpcEndpoint {
602
617
  }
603
618
 
604
619
  // Add safe options headers so client knows how to deserialize
605
- res.setHeader('X-RPC-SafeString-Enabled', this.#options.safeStringEnabled ? 'true' : 'false');
606
- res.setHeader('X-RPC-SafeDate-Enabled', this.#options.safeDateEnabled ? 'true' : 'false');
620
+ // Send safe options headers to client
621
+ res.setHeader('X-RPC-Safe-Enabled', this.#options.safeEnabled ? 'true' : 'false');
607
622
 
608
623
  res.json(response);
609
624
  }
@@ -626,23 +641,23 @@ class RpcEndpoint {
626
641
  */
627
642
  #serializeValue(value, hasBigInt = false, hasDate = false) {
628
643
  if (typeof value === 'bigint') {
629
- // Warn if safeStringEnabled is disabled and we have BigInt
630
- if (!this.#options.safeStringEnabled && !hasBigInt && this.#options.warnOnUnsafeString) {
631
- this.#logger.warn('BigInt detected in serialization. Consider enabling safeStringEnabled option to avoid potential string/BigInt confusion.');
644
+ // 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.');
632
647
  }
633
648
  // Convert BigInt to string with 'n' suffix for proper deserialization
634
649
  return value.toString() + 'n';
635
650
  } else if (value instanceof Date) {
636
- // Warn if safeDateEnabled is disabled and we have Date
637
- if (!this.#options.safeDateEnabled && !hasDate && this.#options.warnOnUnsafeDate) {
638
- this.#logger.warn('Date detected in serialization. Consider enabling safeDateEnabled option to avoid potential string/Date confusion.');
651
+ // 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.');
639
654
  }
640
- // Convert Date to ISO string with D: prefix if safeDateEnabled
655
+ // Convert Date to ISO string with D: prefix if safeEnabled
641
656
  const isoString = value.toISOString();
642
- return this.#options.safeDateEnabled ? `D:${isoString}` : isoString;
657
+ return this.#options.safeEnabled ? `D:${isoString}` : isoString;
643
658
  } else if (typeof value === 'string') {
644
- // Add S: prefix if safeStringEnabled is true
645
- if (this.#options.safeStringEnabled) {
659
+ // Add S: prefix if safeEnabled is true
660
+ if (this.#options.safeEnabled) {
646
661
  return 'S:' + value;
647
662
  }
648
663
  return value;
@@ -700,14 +715,12 @@ class RpcEndpoint {
700
715
  *
701
716
  * @param {any} value The value to deserialize.
702
717
  * @param {Object} [options] Custom deserialization options (uses server options if not provided).
703
- * @param {boolean} [options.safeStringEnabled] Whether to expect S: prefixed strings.
704
- * @param {boolean} [options.safeDateEnabled] Whether to expect D: prefixed dates.
718
+ * @param {boolean} [options.safeEnabled] Whether to expect safe prefixes for strings and dates.
705
719
  * @returns {any} The re-hydrated value.
706
720
  */
707
721
  deserializeBigIntsAndDates(value, options = null) {
708
722
  // Use provided options or fall back to server options
709
- const safeStringEnabled = options ? options.safeStringEnabled : this.#options.safeStringEnabled;
710
- const safeDateEnabled = options ? options.safeDateEnabled : this.#options.safeDateEnabled;
723
+ const safeEnabled = options ? options.safeEnabled : this.#options.safeEnabled;
711
724
 
712
725
  // More comprehensive ISO date regex that handles:
713
726
  // - UTC: 2023-01-01T12:00:00.000Z
@@ -718,13 +731,13 @@ class RpcEndpoint {
718
731
 
719
732
  // 1. Check if it's a string that might be a BigInt, Date, or safe string
720
733
  if (typeof value === 'string') {
721
- // Safe string check: if safeStringEnabled and starts with S:
722
- if (safeStringEnabled && value.startsWith('S:')) {
734
+ // Safe string check: if safeEnabled and starts with S:
735
+ if (safeEnabled && value.startsWith('S:')) {
723
736
  return value.substring(2); // Remove 'S:' prefix
724
737
  }
725
738
 
726
- // Safe date check: if safeDateEnabled and starts with D:
727
- if (safeDateEnabled && value.startsWith('D:')) {
739
+ // Safe date check: if safeEnabled and starts with D:
740
+ if (safeEnabled && value.startsWith('D:')) {
728
741
  const isoString = value.substring(2); // Remove 'D:' prefix
729
742
  const date = new Date(isoString);
730
743
  // Double-check that we got a valid date
@@ -738,8 +751,8 @@ class RpcEndpoint {
738
751
  return BigInt(value.slice(0, -1)); // Remove 'n' and convert
739
752
  }
740
753
 
741
- // Date check: matches an ISO 8601 string (only if safeDateEnabled is false)
742
- if (!safeDateEnabled && ISO_DATE_REGEX.test(value)) {
754
+ // Date check: matches an ISO 8601 string (only if safeEnabled is false)
755
+ if (!safeEnabled && ISO_DATE_REGEX.test(value)) {
743
756
  const date = new Date(value);
744
757
  // Double-check that we got a valid date
745
758
  if (!isNaN(date.getTime())) {