rpc-express-toolkit 3.0.2 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1027
- package/examples/server-client-example.js +26 -26
- package/examples/ssl-development-example.js +18 -49
- package/package.json +18 -5
- package/src/batch.js +10 -1
- package/src/clients/rpc-client.js +139 -86
- package/src/clients/rpc-client.min.js +1 -1
- package/src/clients/rpc-client.min.mjs +1 -1
- package/src/clients/rpc-client.mjs +82 -37
- package/src/index.d.ts +16 -15
- package/src/index.js +118 -60
- package/src/logger.js +35 -15
- package/src/middleware.js +40 -25
- package/src/validation.js +1 -0
|
@@ -1,43 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JSON-RPC 2.0 Client for browser and Node.js environments.
|
|
3
|
-
* Handles BigInt and Date serialization/deserialization automatically
|
|
4
|
-
*
|
|
5
|
-
* Note: We extend BigInt.prototype.toJSON to handle automatic serialization
|
|
6
|
-
* when JSON.stringify() is called, as BigInt doesn't have native JSON support.
|
|
3
|
+
* Handles BigInt and Date serialization/deserialization automatically
|
|
4
|
+
* using explicit serializers (no global prototype extensions).
|
|
7
5
|
*/
|
|
8
6
|
|
|
9
|
-
//
|
|
10
|
-
if (typeof BigInt !== 'undefined' && !BigInt.prototype.toJSON) {
|
|
11
|
-
BigInt.prototype.toJSON = function () {
|
|
12
|
-
return this.toString();
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// Polyfill for Node.js environments that don't have fetch
|
|
7
|
+
// Select fetch implementation (Node 18+ or browser)
|
|
17
8
|
let fetchFn;
|
|
18
|
-
if (typeof
|
|
19
|
-
fetchFn = fetch;
|
|
9
|
+
if (typeof globalThis !== 'undefined' && globalThis.fetch) {
|
|
10
|
+
fetchFn = globalThis.fetch.bind(globalThis);
|
|
20
11
|
} else {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} catch (e) {
|
|
25
|
-
// For Node.js 18+, use global fetch
|
|
26
|
-
if (typeof globalThis !== 'undefined' && globalThis.fetch) {
|
|
27
|
-
fetchFn = globalThis.fetch;
|
|
28
|
-
} else {
|
|
29
|
-
throw new Error(
|
|
30
|
-
'fetch is not available. Please install node-fetch or use Node.js 18+'
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
12
|
+
throw new Error(
|
|
13
|
+
'globalThis.fetch is not available. Node.js 18+ is required.'
|
|
14
|
+
);
|
|
34
15
|
}
|
|
35
16
|
|
|
36
17
|
class RpcClient {
|
|
37
18
|
#endpoint;
|
|
19
|
+
|
|
38
20
|
#defaultHeaders;
|
|
21
|
+
|
|
39
22
|
#requestId;
|
|
23
|
+
|
|
40
24
|
#fetchOptions;
|
|
25
|
+
|
|
41
26
|
#options;
|
|
42
27
|
|
|
43
28
|
/**
|
|
@@ -45,8 +30,8 @@ class RpcClient {
|
|
|
45
30
|
* @param {Object} [defaultHeaders={}] Optional default headers to include in requests.
|
|
46
31
|
* @param {Object} [options={}] Optional configuration options.
|
|
47
32
|
* @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.
|
|
33
|
+
* @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.
|
|
34
|
+
* @param {boolean} [options.warnOnUnsafe=true] Whether to show educational warnings when BigInt/Date objects are serialized without safe mode. Default true.
|
|
50
35
|
*/
|
|
51
36
|
constructor(endpoint, defaultHeaders = {}, options = {}) {
|
|
52
37
|
this.#endpoint = endpoint;
|
|
@@ -59,11 +44,9 @@ class RpcClient {
|
|
|
59
44
|
|
|
60
45
|
// Store options
|
|
61
46
|
this.#options = {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
warnOnUnsafeDate: options.warnOnUnsafeDate !== false, // Default true
|
|
66
|
-
...options
|
|
47
|
+
safeEnabled: options.safeEnabled === true, // Default false
|
|
48
|
+
warnOnUnsafe: options.warnOnUnsafe !== false, // Default true
|
|
49
|
+
...options,
|
|
67
50
|
};
|
|
68
51
|
|
|
69
52
|
// Store fetch options for Node.js environments
|
|
@@ -84,11 +67,12 @@ class RpcClient {
|
|
|
84
67
|
|
|
85
68
|
/**
|
|
86
69
|
* Make a JSON-RPC call to the server.
|
|
70
|
+
* @template T
|
|
87
71
|
* @param {string} method The RPC method name.
|
|
88
72
|
* @param {any} params The parameters to pass to the RPC method.
|
|
89
73
|
* @param {string|number|null} [id] The request ID (auto-generated if not provided).
|
|
90
74
|
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
91
|
-
* @returns {Promise<
|
|
75
|
+
* @returns {Promise<T>} The result of the RPC call.
|
|
92
76
|
*/
|
|
93
77
|
async call(method, params, id = undefined, overrideHeaders = {}) {
|
|
94
78
|
// Auto-generate ID if not provided (null means notification)
|
|
@@ -110,13 +94,11 @@ class RpcClient {
|
|
|
110
94
|
method: 'POST',
|
|
111
95
|
headers: {
|
|
112
96
|
...this.#defaultHeaders,
|
|
113
|
-
// Add safe options
|
|
114
|
-
'X-RPC-
|
|
115
|
-
'X-RPC-SafeDate-Enabled': this.#options.safeDateEnabled ? 'true' : 'false',
|
|
97
|
+
// Add safe options header so server knows what client expects
|
|
98
|
+
'X-RPC-Safe-Enabled': this.#options.safeEnabled ? 'true' : 'false',
|
|
116
99
|
...overrideHeaders,
|
|
117
100
|
},
|
|
118
|
-
//
|
|
119
|
-
// JSON.stringify will handle BigInt and Date automatically
|
|
101
|
+
// Params are pre-serialized (BigInt/Date) via serializeBigIntsAndDates
|
|
120
102
|
body: JSON.stringify(requestBody),
|
|
121
103
|
...this.#fetchOptions, // Include any additional fetch options (e.g., SSL settings)
|
|
122
104
|
});
|
|
@@ -133,18 +115,42 @@ class RpcClient {
|
|
|
133
115
|
throw responseBody.error;
|
|
134
116
|
}
|
|
135
117
|
|
|
136
|
-
// Check server's safe options from response headers
|
|
137
|
-
const
|
|
138
|
-
|
|
118
|
+
// Check server's safe options from response headers - strict compatibility checking
|
|
119
|
+
const serverSafeHeader = response.headers.get('X-RPC-Safe-Enabled');
|
|
120
|
+
|
|
121
|
+
// Errore rigoroso: se client ha safe enabled ma server non risponde con header
|
|
122
|
+
if (this.#options.safeEnabled && serverSafeHeader === null) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
'RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). ' +
|
|
125
|
+
'This may indicate a version mismatch or non-toolkit server. ' +
|
|
126
|
+
'Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Warning: se client ha safe disabled ma server offre compatibilità
|
|
131
|
+
if (
|
|
132
|
+
!this.#options.safeEnabled &&
|
|
133
|
+
serverSafeHeader === 'true' &&
|
|
134
|
+
this.#options.warnOnUnsafe
|
|
135
|
+
) {
|
|
136
|
+
console.warn(
|
|
137
|
+
'⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. ' +
|
|
138
|
+
'Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const serverSafeEnabled = serverSafeHeader === 'true';
|
|
139
143
|
|
|
140
144
|
// Create deserialization options based on server's configuration
|
|
141
145
|
const deserializationOptions = {
|
|
142
|
-
|
|
143
|
-
safeDateEnabled: serverSafeDateEnabled
|
|
146
|
+
safeEnabled: serverSafeEnabled,
|
|
144
147
|
};
|
|
145
148
|
|
|
146
149
|
// Convert back BigInts and Dates in the result using server's options
|
|
147
|
-
return this.deserializeBigIntsAndDates(
|
|
150
|
+
return this.deserializeBigIntsAndDates(
|
|
151
|
+
responseBody.result,
|
|
152
|
+
deserializationOptions
|
|
153
|
+
);
|
|
148
154
|
} catch (error) {
|
|
149
155
|
console.error('RPC call failed:', error);
|
|
150
156
|
throw error;
|
|
@@ -164,9 +170,10 @@ class RpcClient {
|
|
|
164
170
|
|
|
165
171
|
/**
|
|
166
172
|
* Make multiple JSON-RPC calls in a single batch request.
|
|
173
|
+
* @template T
|
|
167
174
|
* @param {Array<{method: string, params?: any, id?: string|number}>} requests Array of request objects.
|
|
168
175
|
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
169
|
-
* @returns {Promise<Array<
|
|
176
|
+
* @returns {Promise<Array<T>>} Array of results in the same order as requests.
|
|
170
177
|
*/
|
|
171
178
|
async batch(requests, overrideHeaders = {}) {
|
|
172
179
|
const batchRequests = requests.map((req) => {
|
|
@@ -187,11 +194,11 @@ class RpcClient {
|
|
|
187
194
|
method: 'POST',
|
|
188
195
|
headers: {
|
|
189
196
|
...this.#defaultHeaders,
|
|
190
|
-
// Add safe options
|
|
191
|
-
'X-RPC-
|
|
192
|
-
'X-RPC-SafeDate-Enabled': this.#options.safeDateEnabled ? 'true' : 'false',
|
|
197
|
+
// Add safe options header so server knows what client expects
|
|
198
|
+
'X-RPC-Safe-Enabled': this.#options.safeEnabled ? 'true' : 'false',
|
|
193
199
|
...overrideHeaders,
|
|
194
200
|
},
|
|
201
|
+
// Params are pre-serialized per item
|
|
195
202
|
body: JSON.stringify(batchRequests),
|
|
196
203
|
...this.#fetchOptions, // Include any additional fetch options (e.g., SSL settings)
|
|
197
204
|
});
|
|
@@ -204,14 +211,35 @@ class RpcClient {
|
|
|
204
211
|
|
|
205
212
|
const responseBody = await response.json();
|
|
206
213
|
|
|
207
|
-
// Check server's safe options from response headers
|
|
208
|
-
const
|
|
209
|
-
|
|
214
|
+
// Check server's safe options from response headers - strict compatibility checking
|
|
215
|
+
const serverSafeHeader = response.headers.get('X-RPC-Safe-Enabled');
|
|
216
|
+
|
|
217
|
+
// Errore rigoroso: se client ha safe enabled ma server non risponde con header
|
|
218
|
+
if (this.#options.safeEnabled && serverSafeHeader === null) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
'RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). ' +
|
|
221
|
+
'This may indicate a version mismatch or non-toolkit server. ' +
|
|
222
|
+
'Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Warning: se client ha safe disabled ma server offre compatibilità
|
|
227
|
+
if (
|
|
228
|
+
!this.#options.safeEnabled &&
|
|
229
|
+
serverSafeHeader === 'true' &&
|
|
230
|
+
this.#options.warnOnUnsafe
|
|
231
|
+
) {
|
|
232
|
+
console.warn(
|
|
233
|
+
'⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. ' +
|
|
234
|
+
'Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.'
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const serverSafeEnabled = serverSafeHeader === 'true';
|
|
210
239
|
|
|
211
240
|
// Create deserialization options based on server's configuration
|
|
212
241
|
const deserializationOptions = {
|
|
213
|
-
|
|
214
|
-
safeDateEnabled: serverSafeDateEnabled
|
|
242
|
+
safeEnabled: serverSafeEnabled,
|
|
215
243
|
};
|
|
216
244
|
|
|
217
245
|
// Handle batch response
|
|
@@ -220,15 +248,22 @@ class RpcClient {
|
|
|
220
248
|
if (res.error) {
|
|
221
249
|
throw res.error;
|
|
222
250
|
}
|
|
223
|
-
return this.deserializeBigIntsAndDates(
|
|
251
|
+
return this.deserializeBigIntsAndDates(
|
|
252
|
+
res.result,
|
|
253
|
+
deserializationOptions
|
|
254
|
+
);
|
|
224
255
|
});
|
|
225
|
-
} else {
|
|
226
|
-
// Single response in batch
|
|
227
|
-
if (responseBody.error) {
|
|
228
|
-
throw responseBody.error;
|
|
229
|
-
}
|
|
230
|
-
return [this.deserializeBigIntsAndDates(responseBody.result, deserializationOptions)];
|
|
231
256
|
}
|
|
257
|
+
// Single response in batch
|
|
258
|
+
if (responseBody.error) {
|
|
259
|
+
throw responseBody.error;
|
|
260
|
+
}
|
|
261
|
+
return [
|
|
262
|
+
this.deserializeBigIntsAndDates(
|
|
263
|
+
responseBody.result,
|
|
264
|
+
deserializationOptions
|
|
265
|
+
),
|
|
266
|
+
];
|
|
232
267
|
} catch (error) {
|
|
233
268
|
console.error('Batch RPC call failed:', error);
|
|
234
269
|
throw error;
|
|
@@ -244,21 +279,40 @@ class RpcClient {
|
|
|
244
279
|
serializeBigIntsAndDates(value) {
|
|
245
280
|
if (typeof value === 'bigint') {
|
|
246
281
|
// Convert BigInt to string with 'n' suffix for proper deserialization
|
|
247
|
-
return value.toString()
|
|
248
|
-
}
|
|
249
|
-
|
|
282
|
+
return `${value.toString()}n`;
|
|
283
|
+
}
|
|
284
|
+
if (value instanceof Date) {
|
|
285
|
+
// Convert Date to ISO string with D: prefix if safeEnabled
|
|
250
286
|
const isoString = value.toISOString();
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if
|
|
255
|
-
|
|
287
|
+
if (this.#options.safeEnabled) {
|
|
288
|
+
return `D:${isoString}`;
|
|
289
|
+
}
|
|
290
|
+
// Show educational warning if enabled
|
|
291
|
+
if (this.#options.warnOnUnsafe) {
|
|
292
|
+
console.warn(
|
|
293
|
+
'⚠️ 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.'
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return isoString;
|
|
297
|
+
}
|
|
298
|
+
if (typeof value === 'string') {
|
|
299
|
+
// Add S: prefix if safeEnabled is true
|
|
300
|
+
if (this.#options.safeEnabled) {
|
|
301
|
+
return `S:${value}`;
|
|
302
|
+
}
|
|
303
|
+
// Show educational warning if enabled and string could be confused with BigInt
|
|
304
|
+
if (this.#options.warnOnUnsafe && /^-?\d+n?$/.test(value)) {
|
|
305
|
+
console.warn(
|
|
306
|
+
`⚠️ String serialization: String "${value}" could be confused with BigInt. Consider enabling safeEnabled for disambiguation or use explicit typing.`
|
|
307
|
+
);
|
|
256
308
|
}
|
|
257
309
|
return value;
|
|
258
|
-
}
|
|
310
|
+
}
|
|
311
|
+
if (Array.isArray(value)) {
|
|
259
312
|
// Recurse into arrays
|
|
260
313
|
return value.map((v) => this.serializeBigIntsAndDates(v));
|
|
261
|
-
}
|
|
314
|
+
}
|
|
315
|
+
if (value && typeof value === 'object') {
|
|
262
316
|
// Recurse into plain objects
|
|
263
317
|
const result = {};
|
|
264
318
|
for (const [key, val] of Object.entries(value)) {
|
|
@@ -277,14 +331,14 @@ class RpcClient {
|
|
|
277
331
|
*
|
|
278
332
|
* @param {any} value The value to deserialize.
|
|
279
333
|
* @param {Object} [options] Custom deserialization options (uses client options if not provided).
|
|
280
|
-
* @param {boolean} [options.
|
|
281
|
-
* @param {boolean} [options.safeDateEnabled] Whether to expect D: prefixed dates.
|
|
334
|
+
* @param {boolean} [options.safeEnabled] Whether to expect safe prefixes for strings and dates.
|
|
282
335
|
* @returns {any}
|
|
283
336
|
*/
|
|
284
337
|
deserializeBigIntsAndDates(value, options = null) {
|
|
285
338
|
// Use provided options or fall back to client options
|
|
286
|
-
const
|
|
287
|
-
|
|
339
|
+
const safeEnabled = options
|
|
340
|
+
? options.safeEnabled
|
|
341
|
+
: this.#options.safeEnabled;
|
|
288
342
|
|
|
289
343
|
// More comprehensive ISO date regex that handles:
|
|
290
344
|
// - UTC: 2023-01-01T12:00:00.000Z
|
|
@@ -295,17 +349,17 @@ class RpcClient {
|
|
|
295
349
|
|
|
296
350
|
// 1. Check if it's a string that might be a BigInt, Date, or safe string
|
|
297
351
|
if (typeof value === 'string') {
|
|
298
|
-
// Safe string check: if
|
|
299
|
-
if (
|
|
352
|
+
// Safe string check: if safeEnabled and starts with S:
|
|
353
|
+
if (safeEnabled && value.startsWith('S:')) {
|
|
300
354
|
return value.substring(2); // Remove 'S:' prefix
|
|
301
355
|
}
|
|
302
356
|
|
|
303
|
-
// Safe date check: if
|
|
304
|
-
if (
|
|
357
|
+
// Safe date check: if safeEnabled and starts with D:
|
|
358
|
+
if (safeEnabled && value.startsWith('D:')) {
|
|
305
359
|
const isoString = value.substring(2); // Remove 'D:' prefix
|
|
306
360
|
const date = new Date(isoString);
|
|
307
361
|
// Double-check that we got a valid date
|
|
308
|
-
if (!isNaN(date.getTime())) {
|
|
362
|
+
if (!Number.isNaN(date.getTime())) {
|
|
309
363
|
return date;
|
|
310
364
|
}
|
|
311
365
|
}
|
|
@@ -315,12 +369,12 @@ class RpcClient {
|
|
|
315
369
|
if (/^-?\d+n$/.test(value)) {
|
|
316
370
|
return BigInt(value.slice(0, -1)); // Remove 'n' and convert
|
|
317
371
|
}
|
|
318
|
-
|
|
319
|
-
// Date check: matches an ISO 8601 string (only if
|
|
320
|
-
if (!
|
|
372
|
+
|
|
373
|
+
// Date check: matches an ISO 8601 string (only if safeEnabled is false)
|
|
374
|
+
if (!safeEnabled && ISO_DATE_REGEX.test(value)) {
|
|
321
375
|
const date = new Date(value);
|
|
322
376
|
// Ensure it's valid
|
|
323
|
-
if (!isNaN(date.getTime())) {
|
|
377
|
+
if (!Number.isNaN(date.getTime())) {
|
|
324
378
|
return date;
|
|
325
379
|
}
|
|
326
380
|
}
|
|
@@ -347,4 +401,3 @@ class RpcClient {
|
|
|
347
401
|
}
|
|
348
402
|
|
|
349
403
|
module.exports = RpcClient;
|
|
350
|
-
module.exports.RpcClient = RpcClient;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
let fetchFn;if("undefined"==typeof
|
|
1
|
+
let fetchFn;if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("globalThis.fetch is not available. Node.js 18+ is required.");fetchFn=globalThis.fetch.bind(globalThis);class RpcClient{#e;#t;#i;#s;#n;constructor(e,t={},i={}){this.#e=e,this.#t={"Content-Type":"application/json",...t},this.#i=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#n={safeEnabled:!0===i.safeEnabled,warnOnUnsafe:!1!==i.warnOnUnsafe,...i},this.#s={}}#a(){return++this.#i}async call(e,t,i=void 0,s={}){const n={jsonrpc:"2.0",method:e,id:void 0===i?this.#a():i};null!=t&&(n.params=this.serializeBigIntsAndDates(t));try{const e=await fetchFn(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-Safe-Enabled":this.#n.safeEnabled?"true":"false",...s},body:JSON.stringify(n),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const t=await e.json();if(t.error)throw t.error;const i=e.headers.get("X-RPC-Safe-Enabled");if(this.#n.safeEnabled&&null===i)throw new Error("RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). This may indicate a version mismatch or non-toolkit server. Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.");!this.#n.safeEnabled&&"true"===i&&this.#n.warnOnUnsafe&&console.warn("⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.");const a={safeEnabled:"true"===i};return this.deserializeBigIntsAndDates(t.result,a)}catch(e){throw console.error("RPC call failed:",e),e}}async notify(e,t={},i={}){await this.call(e,t,null,i)}async batch(e,t={}){const i=e.map(e=>{const t={jsonrpc:"2.0",method:e.method,id:void 0!==e.id?e.id:this.#a()};return void 0!==e.params&&null!==e.params&&(t.params=this.serializeBigIntsAndDates(e.params)),t});try{const e=await fetchFn(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-Safe-Enabled":this.#n.safeEnabled?"true":"false",...t},body:JSON.stringify(i),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const s=await e.json(),n=e.headers.get("X-RPC-Safe-Enabled");if(this.#n.safeEnabled&&null===n)throw new Error("RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). This may indicate a version mismatch or non-toolkit server. Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.");!this.#n.safeEnabled&&"true"===n&&this.#n.warnOnUnsafe&&console.warn("⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.");const a={safeEnabled:"true"===n};if(Array.isArray(s))return s.map(e=>{if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result,a)});if(s.error)throw s.error;return[this.deserializeBigIntsAndDates(s.result,a)]}catch(e){throw console.error("Batch RPC call failed:",e),e}}serializeBigIntsAndDates(e){if("bigint"==typeof e)return`${e.toString()}n`;if(e instanceof Date){const t=e.toISOString();return this.#n.safeEnabled?`D:${t}`:(this.#n.warnOnUnsafe&&console.warn("⚠️ Date serialization: Using plain ISO string format for JSON-RPC 2.0 compliance. Date objects will be deserialized as strings on the receiving end. Consider enabling safeEnabled or using string timestamps for better type safety."),t)}if("string"==typeof e)return this.#n.safeEnabled?`S:${e}`:(this.#n.warnOnUnsafe&&/^-?\d+n?$/.test(e)&&console.warn(`⚠️ String serialization: String "${e}" could be confused with BigInt. Consider enabling safeEnabled for disambiguation or use explicit typing.`),e);if(Array.isArray(e))return e.map(e=>this.serializeBigIntsAndDates(e));if(e&&"object"==typeof e){const t={};for(const[i,s]of Object.entries(e))t[i]=this.serializeBigIntsAndDates(s);return t}return e}deserializeBigIntsAndDates(e,t=null){const i=t?t.safeEnabled:this.#n.safeEnabled,s=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof e){if(i&&e.startsWith("S:"))return e.substring(2);if(i&&e.startsWith("D:")){const t=e.substring(2),i=new Date(t);if(!Number.isNaN(i.getTime()))return i}if(/^-?\d+n$/.test(e))return BigInt(e.slice(0,-1));if(!i&&s.test(e)){const t=new Date(e);if(!Number.isNaN(t.getTime()))return t}}return Array.isArray(e)?e.map(e=>this.deserializeBigIntsAndDates(e,t)):e&&"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,i])=>[e,this.deserializeBigIntsAndDates(i,t)])):e}}module.exports=RpcClient;
|
|
@@ -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())) {
|