rpc-express-toolkit 4.2.1 → 4.3.3
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/LICENSE +0 -0
- package/README.md +88 -9
- package/examples/example.js +0 -0
- package/examples/introspection/client.js +68 -68
- package/examples/introspection/server.js +89 -89
- package/examples/safe-preset-example.js +0 -0
- package/examples/server-client-example.js +0 -0
- package/examples/ssl-development-example.js +0 -0
- package/package.json +124 -126
- package/src/batch.js +101 -55
- package/src/index.d.ts +33 -63
- package/src/index.js +1158 -1035
- package/src/logger.js +17 -5
- package/src/middleware.js +0 -0
- package/src/protocol.js +97 -0
- package/src/safe.d.ts +0 -0
- package/src/safe.js +0 -0
- package/src/validation.js +222 -222
- package/src/clients/rpc-client.js +0 -403
- package/src/clients/rpc-client.min.js +0 -1
- package/src/clients/rpc-client.min.mjs +0 -1
- package/src/clients/rpc-client.mjs +0 -393
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
/**
|
|
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.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Extend BigInt prototype to handle JSON serialization
|
|
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
|
|
17
|
-
let fetchFn;
|
|
18
|
-
if (typeof fetch !== 'undefined') {
|
|
19
|
-
fetchFn = fetch;
|
|
20
|
-
} else {
|
|
21
|
-
// Try to use node-fetch if available, otherwise use built-in fetch in Node.js 18+
|
|
22
|
-
try {
|
|
23
|
-
fetchFn = require('node-fetch');
|
|
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
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
class RpcClient {
|
|
37
|
-
#endpoint;
|
|
38
|
-
#defaultHeaders;
|
|
39
|
-
#requestId;
|
|
40
|
-
#fetchOptions;
|
|
41
|
-
#options;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @param {string} endpoint The JSON-RPC endpoint URL.
|
|
45
|
-
* @param {Object} [defaultHeaders={}] Optional default headers to include in requests.
|
|
46
|
-
* @param {Object} [options={}] Optional configuration options.
|
|
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.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
|
-
*/
|
|
51
|
-
constructor(endpoint, defaultHeaders = {}, options = {}) {
|
|
52
|
-
this.#endpoint = endpoint;
|
|
53
|
-
this.#defaultHeaders = {
|
|
54
|
-
'Content-Type': 'application/json', // Default header
|
|
55
|
-
...defaultHeaders, // Merge with user-provided defaults
|
|
56
|
-
};
|
|
57
|
-
// Initialize request ID counter with timestamp + random component for uniqueness
|
|
58
|
-
this.#requestId = Date.now() * 1000 + Math.floor(Math.random() * 1000);
|
|
59
|
-
|
|
60
|
-
// Store options
|
|
61
|
-
this.#options = {
|
|
62
|
-
safeEnabled: options.safeEnabled === true, // Default false
|
|
63
|
-
warnOnUnsafe: options.warnOnUnsafe !== false, // Default true
|
|
64
|
-
...options
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// Store fetch options for Node.js environments
|
|
68
|
-
this.#fetchOptions = {};
|
|
69
|
-
// SSL validation: advanced options (agent/ca) have been removed for simplicity and compatibility.
|
|
70
|
-
// To bypass self-signed certificates in development, set:
|
|
71
|
-
// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
72
|
-
// See README for details and best practices.
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Generate a unique request ID
|
|
77
|
-
* @returns {number} Unique request ID
|
|
78
|
-
*/
|
|
79
|
-
#generateId() {
|
|
80
|
-
return ++this.#requestId;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Make a JSON-RPC call to the server.
|
|
85
|
-
* @template T
|
|
86
|
-
* @param {string} method The RPC method name.
|
|
87
|
-
* @param {any} params The parameters to pass to the RPC method.
|
|
88
|
-
* @param {string|number|null} [id] The request ID (auto-generated if not provided).
|
|
89
|
-
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
90
|
-
* @returns {Promise<T>} The result of the RPC call.
|
|
91
|
-
*/
|
|
92
|
-
async call(method, params, id = undefined, overrideHeaders = {}) {
|
|
93
|
-
// Auto-generate ID if not provided (null means notification)
|
|
94
|
-
const requestId = id === undefined ? this.#generateId() : id;
|
|
95
|
-
|
|
96
|
-
// Build the payload according to the spec: params omitted if undefined/null
|
|
97
|
-
const requestBody = {
|
|
98
|
-
jsonrpc: '2.0',
|
|
99
|
-
method,
|
|
100
|
-
id: requestId,
|
|
101
|
-
};
|
|
102
|
-
if (params !== undefined && params !== null) {
|
|
103
|
-
// Serialize parameters to handle BigInt and safe strings
|
|
104
|
-
requestBody.params = this.serializeBigIntsAndDates(params);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const response = await fetchFn(this.#endpoint, {
|
|
109
|
-
method: 'POST',
|
|
110
|
-
headers: {
|
|
111
|
-
...this.#defaultHeaders,
|
|
112
|
-
// Add safe options header so server knows what client expects
|
|
113
|
-
'X-RPC-Safe-Enabled': this.#options.safeEnabled ? 'true' : 'false',
|
|
114
|
-
...overrideHeaders,
|
|
115
|
-
},
|
|
116
|
-
// Since we have BigInt.prototype.toJSON, we don't need manual serialization
|
|
117
|
-
// JSON.stringify will handle BigInt and Date automatically
|
|
118
|
-
body: JSON.stringify(requestBody),
|
|
119
|
-
...this.#fetchOptions, // Include any additional fetch options (e.g., SSL settings)
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
if (!response.ok) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
`HTTP Error: ${response.status} ${response.statusText}`
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const responseBody = await response.json();
|
|
129
|
-
|
|
130
|
-
if (responseBody.error) {
|
|
131
|
-
throw responseBody.error;
|
|
132
|
-
}
|
|
133
|
-
|
|
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';
|
|
155
|
-
|
|
156
|
-
// Create deserialization options based on server's configuration
|
|
157
|
-
const deserializationOptions = {
|
|
158
|
-
safeEnabled: serverSafeEnabled
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Convert back BigInts and Dates in the result using server's options
|
|
162
|
-
return this.deserializeBigIntsAndDates(responseBody.result, deserializationOptions);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
console.error('RPC call failed:', error);
|
|
165
|
-
throw error;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Send a notification (no response expected).
|
|
171
|
-
* @param {string} method The RPC method name.
|
|
172
|
-
* @param {any} params The parameters to pass to the RPC method.
|
|
173
|
-
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
174
|
-
* @returns {Promise<void>} Promise that resolves when notification is sent.
|
|
175
|
-
*/
|
|
176
|
-
async notify(method, params = {}, overrideHeaders = {}) {
|
|
177
|
-
await this.call(method, params, null, overrideHeaders);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Make multiple JSON-RPC calls in a single batch request.
|
|
182
|
-
* @template T
|
|
183
|
-
* @param {Array<{method: string, params?: any, id?: string|number}>} requests Array of request objects.
|
|
184
|
-
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
185
|
-
* @returns {Promise<Array<T>>} Array of results in the same order as requests.
|
|
186
|
-
*/
|
|
187
|
-
async batch(requests, overrideHeaders = {}) {
|
|
188
|
-
const batchRequests = requests.map((req) => {
|
|
189
|
-
const obj = {
|
|
190
|
-
jsonrpc: '2.0',
|
|
191
|
-
method: req.method,
|
|
192
|
-
id: req.id !== undefined ? req.id : this.#generateId(),
|
|
193
|
-
};
|
|
194
|
-
if (req.params !== undefined && req.params !== null) {
|
|
195
|
-
// Serialize parameters to handle BigInt and safe strings
|
|
196
|
-
obj.params = this.serializeBigIntsAndDates(req.params);
|
|
197
|
-
}
|
|
198
|
-
return obj;
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
const response = await fetchFn(this.#endpoint, {
|
|
203
|
-
method: 'POST',
|
|
204
|
-
headers: {
|
|
205
|
-
...this.#defaultHeaders,
|
|
206
|
-
// Add safe options header so server knows what client expects
|
|
207
|
-
'X-RPC-Safe-Enabled': this.#options.safeEnabled ? 'true' : 'false',
|
|
208
|
-
...overrideHeaders,
|
|
209
|
-
},
|
|
210
|
-
body: JSON.stringify(batchRequests),
|
|
211
|
-
...this.#fetchOptions, // Include any additional fetch options (e.g., SSL settings)
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
if (!response.ok) {
|
|
215
|
-
throw new Error(
|
|
216
|
-
`HTTP Error: ${response.status} ${response.statusText}`
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const responseBody = await response.json();
|
|
221
|
-
|
|
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';
|
|
243
|
-
|
|
244
|
-
// Create deserialization options based on server's configuration
|
|
245
|
-
const deserializationOptions = {
|
|
246
|
-
safeEnabled: serverSafeEnabled
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
// Handle batch response
|
|
250
|
-
if (Array.isArray(responseBody)) {
|
|
251
|
-
return responseBody.map((res) => {
|
|
252
|
-
if (res.error) {
|
|
253
|
-
throw res.error;
|
|
254
|
-
}
|
|
255
|
-
return this.deserializeBigIntsAndDates(res.result, deserializationOptions);
|
|
256
|
-
});
|
|
257
|
-
} else {
|
|
258
|
-
// Single response in batch
|
|
259
|
-
if (responseBody.error) {
|
|
260
|
-
throw responseBody.error;
|
|
261
|
-
}
|
|
262
|
-
return [this.deserializeBigIntsAndDates(responseBody.result, deserializationOptions)];
|
|
263
|
-
}
|
|
264
|
-
} catch (error) {
|
|
265
|
-
console.error('Batch RPC call failed:', error);
|
|
266
|
-
throw error;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Recursively convert BigInt values to strings and Date objects to ISO strings
|
|
272
|
-
* so they can be JSON-serialized.
|
|
273
|
-
* @param {any} value
|
|
274
|
-
* @returns {any}
|
|
275
|
-
*/
|
|
276
|
-
serializeBigIntsAndDates(value) {
|
|
277
|
-
if (typeof value === 'bigint') {
|
|
278
|
-
// Convert BigInt to string with 'n' suffix for proper deserialization
|
|
279
|
-
return value.toString() + 'n';
|
|
280
|
-
} else if (value instanceof Date) {
|
|
281
|
-
// Convert Date to ISO string with D: prefix if safeEnabled
|
|
282
|
-
const isoString = value.toISOString();
|
|
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
|
-
}
|
|
292
|
-
} else if (typeof value === 'string') {
|
|
293
|
-
// Add S: prefix if safeEnabled is true
|
|
294
|
-
if (this.#options.safeEnabled) {
|
|
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;
|
|
302
|
-
}
|
|
303
|
-
} else if (Array.isArray(value)) {
|
|
304
|
-
// Recurse into arrays
|
|
305
|
-
return value.map((v) => this.serializeBigIntsAndDates(v));
|
|
306
|
-
} else if (value && typeof value === 'object') {
|
|
307
|
-
// Recurse into plain objects
|
|
308
|
-
const result = {};
|
|
309
|
-
for (const [key, val] of Object.entries(value)) {
|
|
310
|
-
result[key] = this.serializeBigIntsAndDates(val);
|
|
311
|
-
}
|
|
312
|
-
return result;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// If it's neither an array, an object, a Date, nor a bigint, return as-is
|
|
316
|
-
return value;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Recursively convert stringified BigInt values back to BigInt
|
|
321
|
-
* and ISO 8601 date strings back to Date objects.
|
|
322
|
-
*
|
|
323
|
-
* @param {any} value The value to deserialize.
|
|
324
|
-
* @param {Object} [options] Custom deserialization options (uses client options if not provided).
|
|
325
|
-
* @param {boolean} [options.safeEnabled] Whether to expect safe prefixes for strings and dates.
|
|
326
|
-
* @returns {any}
|
|
327
|
-
*/
|
|
328
|
-
deserializeBigIntsAndDates(value, options = null) {
|
|
329
|
-
// Use provided options or fall back to client options
|
|
330
|
-
const safeEnabled = options ? options.safeEnabled : this.#options.safeEnabled;
|
|
331
|
-
|
|
332
|
-
// More comprehensive ISO date regex that handles:
|
|
333
|
-
// - UTC: 2023-01-01T12:00:00.000Z
|
|
334
|
-
// - With timezone: 2023-01-01T12:00:00.000+01:00
|
|
335
|
-
// - Without timezone: 2023-01-01T12:00:00.000 (treated as local)
|
|
336
|
-
const ISO_DATE_REGEX =
|
|
337
|
-
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
|
|
338
|
-
|
|
339
|
-
// 1. Check if it's a string that might be a BigInt, Date, or safe string
|
|
340
|
-
if (typeof value === 'string') {
|
|
341
|
-
// Safe string check: if safeEnabled and starts with S:
|
|
342
|
-
if (safeEnabled && value.startsWith('S:')) {
|
|
343
|
-
return value.substring(2); // Remove 'S:' prefix
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Safe date check: if safeEnabled and starts with D:
|
|
347
|
-
if (safeEnabled && value.startsWith('D:')) {
|
|
348
|
-
const isoString = value.substring(2); // Remove 'D:' prefix
|
|
349
|
-
const date = new Date(isoString);
|
|
350
|
-
// Double-check that we got a valid date
|
|
351
|
-
if (!isNaN(date.getTime())) {
|
|
352
|
-
return date;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// BigInt check: only convert strings that explicitly end with "n"
|
|
357
|
-
// e.g., "42n", "-42n" but NOT "42", "0123456"
|
|
358
|
-
if (/^-?\d+n$/.test(value)) {
|
|
359
|
-
return BigInt(value.slice(0, -1)); // Remove 'n' and convert
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Date check: matches an ISO 8601 string (only if safeEnabled is false)
|
|
363
|
-
if (!safeEnabled && ISO_DATE_REGEX.test(value)) {
|
|
364
|
-
const date = new Date(value);
|
|
365
|
-
// Ensure it's valid
|
|
366
|
-
if (!isNaN(date.getTime())) {
|
|
367
|
-
return date;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// 2. If it's an array, handle each element
|
|
373
|
-
if (Array.isArray(value)) {
|
|
374
|
-
return value.map((v) => this.deserializeBigIntsAndDates(v, options));
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// 3. If it's a plain object, recurse into each property
|
|
378
|
-
if (value && typeof value === 'object') {
|
|
379
|
-
return Object.fromEntries(
|
|
380
|
-
Object.entries(value).map(([key, val]) => [
|
|
381
|
-
key,
|
|
382
|
-
this.deserializeBigIntsAndDates(val, options),
|
|
383
|
-
])
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// 4. Fallback for primitives, etc.
|
|
388
|
-
return value;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export default RpcClient;
|
|
393
|
-
export { RpcClient };
|