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.
- package/README.md +27 -1028
- package/package.json +1 -1
- package/src/clients/rpc-client.js +392 -350
- 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 +10 -14
- package/src/index.js +44 -31
|
@@ -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
|
|
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;#
|
|
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.
|
|
49
|
-
* @param {boolean} [options.
|
|
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
|
-
|
|
63
|
-
|
|
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<
|
|
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
|
|
112
|
-
'X-RPC-
|
|
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
|
|
136
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
|
189
|
-
'X-RPC-
|
|
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
|
|
207
|
-
|
|
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
|
-
|
|
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
|
|
281
|
+
// Convert Date to ISO string with D: prefix if safeEnabled
|
|
248
282
|
const isoString = value.toISOString();
|
|
249
|
-
|
|
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
|
|
252
|
-
if (this.#options.
|
|
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.
|
|
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
|
|
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
|
|
297
|
-
if (
|
|
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
|
|
302
|
-
if (
|
|
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
|
|
318
|
-
if (!
|
|
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
|
-
|
|
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;
|
|
@@ -120,18 +119,15 @@ interface RpcEndpointOptions {
|
|
|
120
119
|
* RPC client configuration options
|
|
121
120
|
*/
|
|
122
121
|
interface RpcClientOptions {
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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<
|
|
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<
|
|
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.
|
|
142
|
-
this.#options.
|
|
143
|
-
this.#options.
|
|
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
|
|
363
|
-
const clientSafeDateEnabled = req.headers['x-rpc-safedate-enabled'] === 'true';
|
|
361
|
+
const clientSafeHeader = req.headers['x-rpc-safe-enabled'];
|
|
364
362
|
|
|
365
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
606
|
-
res.setHeader('X-RPC-
|
|
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
|
|
630
|
-
if (!this.#options.
|
|
631
|
-
this.#logger.warn('BigInt detected in serialization. Consider enabling
|
|
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
|
|
637
|
-
if (!this.#options.
|
|
638
|
-
this.#logger.warn('Date detected in serialization. Consider enabling
|
|
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
|
|
655
|
+
// Convert Date to ISO string with D: prefix if safeEnabled
|
|
641
656
|
const isoString = value.toISOString();
|
|
642
|
-
return this.#options.
|
|
657
|
+
return this.#options.safeEnabled ? `D:${isoString}` : isoString;
|
|
643
658
|
} else if (typeof value === 'string') {
|
|
644
|
-
// Add S: prefix if
|
|
645
|
-
if (this.#options.
|
|
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.
|
|
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
|
|
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
|
|
722
|
-
if (
|
|
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
|
|
727
|
-
if (
|
|
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
|
|
742
|
-
if (!
|
|
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())) {
|