rpc-express-toolkit 2.3.3 → 3.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 +221 -9
- package/examples/example.js +17 -18
- package/examples/server-client-example.js +120 -114
- package/examples/ssl-development-example.js +100 -80
- package/package.json +5 -5
- package/src/batch.js +256 -237
- package/src/clients/rpc-client.js +125 -44
- package/src/clients/rpc-client.min.js +1 -1
- package/src/clients/rpc-client.min.mjs +1 -1
- package/src/clients/rpc-client.mjs +159 -96
- package/src/index.d.ts +220 -58
- package/src/index.js +319 -115
- package/src/logger.js +184 -153
- package/src/middleware.js +229 -224
- package/src/validation.js +220 -221
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* JSON-RPC 2.0 Client for browser and Node.js environments
|
|
2
|
+
* JSON-RPC 2.0 Client for browser and Node.js environments.
|
|
3
3
|
* Handles BigInt and Date serialization/deserialization automatically.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* Note: We extend BigInt.prototype.toJSON to handle automatic serialization
|
|
6
6
|
* when JSON.stringify() is called, as BigInt doesn't have native JSON support.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// Extend BigInt prototype to handle JSON serialization
|
|
10
10
|
if (typeof BigInt !== 'undefined' && !BigInt.prototype.toJSON) {
|
|
11
|
-
BigInt.prototype.toJSON = function() {
|
|
11
|
+
BigInt.prototype.toJSON = function () {
|
|
12
|
+
return this.toString();
|
|
13
|
+
};
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
// Polyfill for Node.js environments that don't have fetch
|
|
@@ -18,73 +20,97 @@ if (typeof fetch !== 'undefined') {
|
|
|
18
20
|
} else {
|
|
19
21
|
// Try to use node-fetch if available, otherwise use built-in fetch in Node.js 18+
|
|
20
22
|
try {
|
|
21
|
-
|
|
22
|
-
fetchFn = nodeFetch;
|
|
23
|
+
fetchFn = require('node-fetch');
|
|
23
24
|
} catch (e) {
|
|
24
25
|
// For Node.js 18+, use global fetch
|
|
25
26
|
if (typeof globalThis !== 'undefined' && globalThis.fetch) {
|
|
26
27
|
fetchFn = globalThis.fetch;
|
|
27
28
|
} else {
|
|
28
|
-
throw new Error(
|
|
29
|
+
throw new Error(
|
|
30
|
+
'fetch is not available. Please install node-fetch or use Node.js 18+'
|
|
31
|
+
);
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
class RpcClient {
|
|
34
37
|
#endpoint;
|
|
35
38
|
#defaultHeaders;
|
|
36
39
|
#requestId;
|
|
37
40
|
#fetchOptions;
|
|
41
|
+
#options;
|
|
38
42
|
|
|
39
43
|
/**
|
|
40
44
|
* @param {string} endpoint The JSON-RPC endpoint URL.
|
|
41
45
|
* @param {Object} [defaultHeaders={}] Optional default headers to include in requests.
|
|
42
46
|
* @param {Object} [options={}] Optional configuration options.
|
|
43
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.
|
|
44
50
|
*/
|
|
45
51
|
constructor(endpoint, defaultHeaders = {}, options = {}) {
|
|
46
52
|
this.#endpoint = endpoint;
|
|
47
53
|
this.#defaultHeaders = {
|
|
48
|
-
|
|
54
|
+
'Content-Type': 'application/json', // Default header
|
|
49
55
|
...defaultHeaders, // Merge with user-provided defaults
|
|
50
56
|
};
|
|
51
57
|
// Initialize request ID counter with timestamp + random component for uniqueness
|
|
52
58
|
this.#requestId = Date.now() * 1000 + Math.floor(Math.random() * 1000);
|
|
53
|
-
|
|
59
|
+
|
|
60
|
+
// Store options
|
|
61
|
+
this.#options = {
|
|
62
|
+
safeStringEnabled: options.safeStringEnabled !== false, // Default true
|
|
63
|
+
safeDateEnabled: options.safeDateEnabled !== false, // Default true
|
|
64
|
+
...options
|
|
65
|
+
};
|
|
66
|
+
|
|
54
67
|
// Store fetch options for Node.js environments
|
|
55
68
|
this.#fetchOptions = {};
|
|
56
|
-
// SSL validation:
|
|
57
|
-
//
|
|
69
|
+
// SSL validation: advanced options (agent/ca) have been removed for simplicity and compatibility.
|
|
70
|
+
// To bypass self-signed certificates in development, set:
|
|
58
71
|
// process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
59
|
-
//
|
|
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;
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
/**
|
|
63
84
|
* Make a JSON-RPC call to the server.
|
|
64
85
|
* @param {string} method The RPC method name.
|
|
65
86
|
* @param {any} params The parameters to pass to the RPC method.
|
|
66
|
-
* @param {string|number|null} [id] The request ID (
|
|
87
|
+
* @param {string|number|null} [id] The request ID (auto-generated if not provided).
|
|
67
88
|
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
68
89
|
* @returns {Promise<any>} The result of the RPC call.
|
|
69
90
|
*/
|
|
70
|
-
async call(method, params
|
|
71
|
-
// Auto-generate
|
|
72
|
-
|
|
73
|
-
id = ++this.#requestId;
|
|
74
|
-
}
|
|
91
|
+
async call(method, params, id = undefined, overrideHeaders = {}) {
|
|
92
|
+
// Auto-generate ID if not provided (null means notification)
|
|
93
|
+
const requestId = id === undefined ? this.#generateId() : id;
|
|
75
94
|
|
|
95
|
+
// Build the payload according to the spec: params omitted if undefined/null
|
|
76
96
|
const requestBody = {
|
|
77
|
-
jsonrpc:
|
|
97
|
+
jsonrpc: '2.0',
|
|
78
98
|
method,
|
|
79
|
-
|
|
80
|
-
id,
|
|
99
|
+
id: requestId,
|
|
81
100
|
};
|
|
101
|
+
if (params !== undefined && params !== null) {
|
|
102
|
+
// Serialize parameters to handle BigInt and safe strings
|
|
103
|
+
requestBody.params = this.serializeBigIntsAndDates(params);
|
|
104
|
+
}
|
|
82
105
|
|
|
83
106
|
try {
|
|
84
107
|
const response = await fetchFn(this.#endpoint, {
|
|
85
|
-
method:
|
|
108
|
+
method: 'POST',
|
|
86
109
|
headers: {
|
|
87
110
|
...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',
|
|
88
114
|
...overrideHeaders,
|
|
89
115
|
},
|
|
90
116
|
// Since we have BigInt.prototype.toJSON, we don't need manual serialization
|
|
@@ -94,112 +120,115 @@ export class RpcClient {
|
|
|
94
120
|
});
|
|
95
121
|
|
|
96
122
|
if (!response.ok) {
|
|
97
|
-
throw new Error(
|
|
123
|
+
throw new Error(
|
|
124
|
+
`HTTP Error: ${response.status} ${response.statusText}`
|
|
125
|
+
);
|
|
98
126
|
}
|
|
99
127
|
|
|
100
128
|
const responseBody = await response.json();
|
|
101
129
|
|
|
102
|
-
if (responseBody.error) {
|
|
130
|
+
if (responseBody.error) {
|
|
131
|
+
throw responseBody.error;
|
|
132
|
+
}
|
|
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';
|
|
137
|
+
|
|
138
|
+
// Create deserialization options based on server's configuration
|
|
139
|
+
const deserializationOptions = {
|
|
140
|
+
safeStringEnabled: serverSafeStringEnabled,
|
|
141
|
+
safeDateEnabled: serverSafeDateEnabled
|
|
142
|
+
};
|
|
103
143
|
|
|
104
|
-
// Convert back BigInts and Dates in the result
|
|
105
|
-
return this.deserializeBigIntsAndDates(responseBody.result);
|
|
144
|
+
// Convert back BigInts and Dates in the result using server's options
|
|
145
|
+
return this.deserializeBigIntsAndDates(responseBody.result, deserializationOptions);
|
|
106
146
|
} catch (error) {
|
|
107
|
-
console.error(
|
|
147
|
+
console.error('RPC call failed:', error);
|
|
108
148
|
throw error;
|
|
109
149
|
}
|
|
110
150
|
}
|
|
111
151
|
|
|
112
152
|
/**
|
|
113
|
-
*
|
|
153
|
+
* Send a notification (no response expected).
|
|
114
154
|
* @param {string} method The RPC method name.
|
|
115
155
|
* @param {any} params The parameters to pass to the RPC method.
|
|
116
156
|
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
117
|
-
* @returns {Promise<void>}
|
|
157
|
+
* @returns {Promise<void>} Promise that resolves when notification is sent.
|
|
118
158
|
*/
|
|
119
159
|
async notify(method, params = {}, overrideHeaders = {}) {
|
|
120
|
-
|
|
121
|
-
jsonrpc: "2.0",
|
|
122
|
-
method,
|
|
123
|
-
params,
|
|
124
|
-
// No ID for notifications
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
const response = await fetchFn(this.#endpoint, {
|
|
129
|
-
method: "POST",
|
|
130
|
-
headers: {
|
|
131
|
-
...this.#defaultHeaders,
|
|
132
|
-
...overrideHeaders,
|
|
133
|
-
},
|
|
134
|
-
body: JSON.stringify(requestBody),
|
|
135
|
-
...this.#fetchOptions, // Include any additional fetch options (e.g., SSL settings)
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
if (!response.ok) {
|
|
139
|
-
throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
|
|
140
|
-
}
|
|
141
|
-
} catch (error) {
|
|
142
|
-
console.error("RPC notification failed:", error);
|
|
143
|
-
throw error;
|
|
144
|
-
}
|
|
160
|
+
await this.call(method, params, null, overrideHeaders);
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
/**
|
|
148
|
-
* Make
|
|
149
|
-
* @param {Array<{method: string, params
|
|
164
|
+
* Make multiple JSON-RPC calls in a single batch request.
|
|
165
|
+
* @param {Array<{method: string, params?: any, id?: string|number}>} requests Array of request objects.
|
|
150
166
|
* @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
|
|
151
|
-
* @returns {Promise<Array<any>>} Array of results
|
|
167
|
+
* @returns {Promise<Array<any>>} Array of results in the same order as requests.
|
|
152
168
|
*/
|
|
153
169
|
async batch(requests, overrideHeaders = {}) {
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
jsonrpc:
|
|
170
|
+
const batchRequests = requests.map((req) => {
|
|
171
|
+
const obj = {
|
|
172
|
+
jsonrpc: '2.0',
|
|
157
173
|
method: req.method,
|
|
158
|
-
|
|
174
|
+
id: req.id !== undefined ? req.id : this.#generateId(),
|
|
159
175
|
};
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
body.id = req.id !== undefined ? req.id : ++this.#requestId;
|
|
176
|
+
if (req.params !== undefined && req.params !== null) {
|
|
177
|
+
// Serialize parameters to handle BigInt and safe strings
|
|
178
|
+
obj.params = this.serializeBigIntsAndDates(req.params);
|
|
164
179
|
}
|
|
165
|
-
|
|
166
|
-
return body;
|
|
180
|
+
return obj;
|
|
167
181
|
});
|
|
168
182
|
|
|
169
183
|
try {
|
|
170
184
|
const response = await fetchFn(this.#endpoint, {
|
|
171
|
-
method:
|
|
185
|
+
method: 'POST',
|
|
172
186
|
headers: {
|
|
173
187
|
...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',
|
|
174
191
|
...overrideHeaders,
|
|
175
192
|
},
|
|
176
|
-
body: JSON.stringify(
|
|
193
|
+
body: JSON.stringify(batchRequests),
|
|
177
194
|
...this.#fetchOptions, // Include any additional fetch options (e.g., SSL settings)
|
|
178
195
|
});
|
|
179
196
|
|
|
180
197
|
if (!response.ok) {
|
|
181
|
-
throw new Error(
|
|
198
|
+
throw new Error(
|
|
199
|
+
`HTTP Error: ${response.status} ${response.statusText}`
|
|
200
|
+
);
|
|
182
201
|
}
|
|
183
202
|
|
|
184
203
|
const responseBody = await response.json();
|
|
185
204
|
|
|
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';
|
|
208
|
+
|
|
209
|
+
// Create deserialization options based on server's configuration
|
|
210
|
+
const deserializationOptions = {
|
|
211
|
+
safeStringEnabled: serverSafeStringEnabled,
|
|
212
|
+
safeDateEnabled: serverSafeDateEnabled
|
|
213
|
+
};
|
|
214
|
+
|
|
186
215
|
// Handle batch response
|
|
187
216
|
if (Array.isArray(responseBody)) {
|
|
188
|
-
return responseBody.map(res => {
|
|
217
|
+
return responseBody.map((res) => {
|
|
189
218
|
if (res.error) {
|
|
190
219
|
throw res.error;
|
|
191
220
|
}
|
|
192
|
-
return this.deserializeBigIntsAndDates(res.result);
|
|
221
|
+
return this.deserializeBigIntsAndDates(res.result, deserializationOptions);
|
|
193
222
|
});
|
|
194
223
|
} else {
|
|
195
|
-
// Single response
|
|
224
|
+
// Single response in batch
|
|
196
225
|
if (responseBody.error) {
|
|
197
226
|
throw responseBody.error;
|
|
198
227
|
}
|
|
199
|
-
return [this.deserializeBigIntsAndDates(responseBody.result)];
|
|
228
|
+
return [this.deserializeBigIntsAndDates(responseBody.result, deserializationOptions)];
|
|
200
229
|
}
|
|
201
230
|
} catch (error) {
|
|
202
|
-
console.error(
|
|
231
|
+
console.error('Batch RPC call failed:', error);
|
|
203
232
|
throw error;
|
|
204
233
|
}
|
|
205
234
|
}
|
|
@@ -211,16 +240,23 @@ export class RpcClient {
|
|
|
211
240
|
* @returns {any}
|
|
212
241
|
*/
|
|
213
242
|
serializeBigIntsAndDates(value) {
|
|
214
|
-
if (typeof value ===
|
|
215
|
-
// Convert BigInt to string
|
|
216
|
-
return value.toString();
|
|
243
|
+
if (typeof value === 'bigint') {
|
|
244
|
+
// Convert BigInt to string with 'n' suffix for proper deserialization
|
|
245
|
+
return value.toString() + 'n';
|
|
246
|
+
} else if (value instanceof Date) {
|
|
247
|
+
// Convert Date to ISO string with D: prefix if safeDateEnabled
|
|
248
|
+
const isoString = value.toISOString();
|
|
249
|
+
return this.#options.safeDateEnabled ? `D:${isoString}` : isoString;
|
|
250
|
+
} else if (typeof value === 'string') {
|
|
251
|
+
// Add S: prefix if safeStringEnabled is true
|
|
252
|
+
if (this.#options.safeStringEnabled) {
|
|
253
|
+
return 'S:' + value;
|
|
254
|
+
}
|
|
255
|
+
return value;
|
|
217
256
|
} else if (Array.isArray(value)) {
|
|
218
257
|
// Recurse into arrays
|
|
219
258
|
return value.map((v) => this.serializeBigIntsAndDates(v));
|
|
220
|
-
} else if (value
|
|
221
|
-
// Convert Date to ISO string (UTC)
|
|
222
|
-
return value.toISOString();
|
|
223
|
-
} else if (value && typeof value === "object") {
|
|
259
|
+
} else if (value && typeof value === 'object') {
|
|
224
260
|
// Recurse into plain objects
|
|
225
261
|
const result = {};
|
|
226
262
|
for (const [key, val] of Object.entries(value)) {
|
|
@@ -238,24 +274,48 @@ export class RpcClient {
|
|
|
238
274
|
* and ISO 8601 date strings back to Date objects.
|
|
239
275
|
*
|
|
240
276
|
* @param {any} value The value to deserialize.
|
|
277
|
+
* @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.
|
|
241
280
|
* @returns {any}
|
|
242
281
|
*/
|
|
243
|
-
deserializeBigIntsAndDates(value) {
|
|
282
|
+
deserializeBigIntsAndDates(value, options = null) {
|
|
283
|
+
// 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;
|
|
286
|
+
|
|
244
287
|
// More comprehensive ISO date regex that handles:
|
|
245
288
|
// - UTC: 2023-01-01T12:00:00.000Z
|
|
246
289
|
// - With timezone: 2023-01-01T12:00:00.000+01:00
|
|
247
290
|
// - Without timezone: 2023-01-01T12:00:00.000 (treated as local)
|
|
248
|
-
const ISO_DATE_REGEX =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if
|
|
252
|
-
|
|
253
|
-
//
|
|
254
|
-
if (
|
|
255
|
-
return
|
|
291
|
+
const ISO_DATE_REGEX =
|
|
292
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
|
|
293
|
+
|
|
294
|
+
// 1. Check if it's a string that might be a BigInt, Date, or safe string
|
|
295
|
+
if (typeof value === 'string') {
|
|
296
|
+
// Safe string check: if safeStringEnabled and starts with S:
|
|
297
|
+
if (safeStringEnabled && value.startsWith('S:')) {
|
|
298
|
+
return value.substring(2); // Remove 'S:' prefix
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Safe date check: if safeDateEnabled and starts with D:
|
|
302
|
+
if (safeDateEnabled && value.startsWith('D:')) {
|
|
303
|
+
const isoString = value.substring(2); // Remove 'D:' prefix
|
|
304
|
+
const date = new Date(isoString);
|
|
305
|
+
// Double-check that we got a valid date
|
|
306
|
+
if (!isNaN(date.getTime())) {
|
|
307
|
+
return date;
|
|
308
|
+
}
|
|
256
309
|
}
|
|
257
|
-
|
|
258
|
-
|
|
310
|
+
|
|
311
|
+
// BigInt check: only convert strings that explicitly end with "n"
|
|
312
|
+
// e.g., "42n", "-42n" but NOT "42", "0123456"
|
|
313
|
+
if (/^-?\d+n$/.test(value)) {
|
|
314
|
+
return BigInt(value.slice(0, -1)); // Remove 'n' and convert
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Date check: matches an ISO 8601 string (only if safeDateEnabled is false)
|
|
318
|
+
if (!safeDateEnabled && ISO_DATE_REGEX.test(value)) {
|
|
259
319
|
const date = new Date(value);
|
|
260
320
|
// Ensure it's valid
|
|
261
321
|
if (!isNaN(date.getTime())) {
|
|
@@ -266,15 +326,15 @@ export class RpcClient {
|
|
|
266
326
|
|
|
267
327
|
// 2. If it's an array, handle each element
|
|
268
328
|
if (Array.isArray(value)) {
|
|
269
|
-
return value.map((v) => this.deserializeBigIntsAndDates(v));
|
|
329
|
+
return value.map((v) => this.deserializeBigIntsAndDates(v, options));
|
|
270
330
|
}
|
|
271
331
|
|
|
272
332
|
// 3. If it's a plain object, recurse into each property
|
|
273
|
-
if (value && typeof value ===
|
|
333
|
+
if (value && typeof value === 'object') {
|
|
274
334
|
return Object.fromEntries(
|
|
275
335
|
Object.entries(value).map(([key, val]) => [
|
|
276
336
|
key,
|
|
277
|
-
this.deserializeBigIntsAndDates(val),
|
|
337
|
+
this.deserializeBigIntsAndDates(val, options),
|
|
278
338
|
])
|
|
279
339
|
);
|
|
280
340
|
}
|
|
@@ -283,3 +343,6 @@ export class RpcClient {
|
|
|
283
343
|
return value;
|
|
284
344
|
}
|
|
285
345
|
}
|
|
346
|
+
|
|
347
|
+
export default RpcClient;
|
|
348
|
+
export { RpcClient };
|