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
package/src/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const path = require("path");
|
|
4
|
-
const express = require("express");
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const express = require('express');
|
|
5
3
|
|
|
6
4
|
/** @typedef {import("express").Router} Router */
|
|
7
5
|
/** @typedef {import("express").Request} Request */
|
|
@@ -10,7 +8,11 @@ const express = require("express");
|
|
|
10
8
|
const NestedError = require('nested-error-stacks');
|
|
11
9
|
const Logger = require('./logger');
|
|
12
10
|
const { MiddlewareManager, builtInMiddlewares } = require('./middleware');
|
|
13
|
-
const {
|
|
11
|
+
const {
|
|
12
|
+
SchemaValidator,
|
|
13
|
+
commonSchemas,
|
|
14
|
+
SchemaBuilder,
|
|
15
|
+
} = require('./validation');
|
|
14
16
|
const BatchHandler = require('./batch');
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -18,9 +20,22 @@ const BatchHandler = require('./batch');
|
|
|
18
20
|
* @type {Array<string>}
|
|
19
21
|
*/
|
|
20
22
|
const defaultProperties = [
|
|
21
|
-
'stackTraceLimit',
|
|
22
|
-
'
|
|
23
|
-
'
|
|
23
|
+
'stackTraceLimit',
|
|
24
|
+
'cause',
|
|
25
|
+
'code',
|
|
26
|
+
'message',
|
|
27
|
+
'stack',
|
|
28
|
+
'address',
|
|
29
|
+
'dest',
|
|
30
|
+
'errno',
|
|
31
|
+
'info',
|
|
32
|
+
'path',
|
|
33
|
+
'port',
|
|
34
|
+
'syscall',
|
|
35
|
+
'opensslErrorStack',
|
|
36
|
+
'function',
|
|
37
|
+
'library',
|
|
38
|
+
'reason',
|
|
24
39
|
];
|
|
25
40
|
|
|
26
41
|
/**
|
|
@@ -30,31 +45,36 @@ const defaultProperties = [
|
|
|
30
45
|
* @param {Array<string>} properties - Properties to include in serialization.
|
|
31
46
|
* @returns {Object} The serialized error.
|
|
32
47
|
*/
|
|
33
|
-
const serializeError = (
|
|
48
|
+
const serializeError = (
|
|
49
|
+
error,
|
|
50
|
+
sanitize = false,
|
|
51
|
+
properties = defaultProperties
|
|
52
|
+
) => {
|
|
34
53
|
if (error === null || error === undefined) return error;
|
|
35
54
|
|
|
36
55
|
const result = {};
|
|
37
56
|
const includeProperties = sanitize
|
|
38
|
-
|
|
39
|
-
|
|
57
|
+
? properties.filter((prop) => !['address', 'path'].includes(prop))
|
|
58
|
+
: properties;
|
|
40
59
|
|
|
41
|
-
includeProperties.forEach(prop => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
includeProperties.forEach((prop) => {
|
|
61
|
+
if (error?.[prop] !== undefined) {
|
|
62
|
+
result[prop] = error[prop];
|
|
63
|
+
}
|
|
45
64
|
});
|
|
46
65
|
|
|
47
66
|
if (error instanceof NestedError) {
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
result.type = 'NestedError';
|
|
68
|
+
if (error.nested)
|
|
69
|
+
result.nested = serializeError(error.nested, sanitize, properties);
|
|
50
70
|
} else if (error instanceof Error) {
|
|
51
|
-
|
|
71
|
+
result.type = 'Error';
|
|
52
72
|
} else if (error instanceof Object) {
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
result.type = 'Object';
|
|
74
|
+
Object.assign(result, error);
|
|
55
75
|
} else {
|
|
56
|
-
|
|
57
|
-
|
|
76
|
+
result.type = 'Default';
|
|
77
|
+
result.instance = error;
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
return result;
|
|
@@ -70,8 +90,8 @@ class RpcEndpoint {
|
|
|
70
90
|
* @param {string} [url="/vendor/rpc-client"]
|
|
71
91
|
* The path from which client scripts will be served.
|
|
72
92
|
*/
|
|
73
|
-
static serveScripts(router, url =
|
|
74
|
-
router.use(url, express.static(path.join(__dirname,
|
|
93
|
+
static serveScripts(router, url = '/vendor/rpc-client') {
|
|
94
|
+
router.use(url, express.static(path.join(__dirname, 'clients')));
|
|
75
95
|
}
|
|
76
96
|
|
|
77
97
|
/** @type {string} */
|
|
@@ -101,13 +121,13 @@ class RpcEndpoint {
|
|
|
101
121
|
* @param {string|Object} [endpointOrOptions="/api"] The endpoint path or options object.
|
|
102
122
|
* @param {Object} [options={}] Options object if first param is string.
|
|
103
123
|
*/
|
|
104
|
-
constructor(router, context, endpointOrOptions =
|
|
124
|
+
constructor(router, context, endpointOrOptions = '/api', options = {}) {
|
|
105
125
|
// Handle both signatures: (router, context, "/api", options) and (router, context, options)
|
|
106
126
|
if (typeof endpointOrOptions === 'string') {
|
|
107
127
|
this.#endpoint = endpointOrOptions;
|
|
108
128
|
this.#options = { ...options };
|
|
109
129
|
} else {
|
|
110
|
-
this.#endpoint = endpointOrOptions.endpoint ||
|
|
130
|
+
this.#endpoint = endpointOrOptions.endpoint || '/api';
|
|
111
131
|
this.#options = { ...endpointOrOptions };
|
|
112
132
|
}
|
|
113
133
|
|
|
@@ -117,6 +137,15 @@ class RpcEndpoint {
|
|
|
117
137
|
this.#validator = new SchemaValidator(this.#options.validation || {});
|
|
118
138
|
this.#batchHandler = new BatchHandler(this);
|
|
119
139
|
|
|
140
|
+
// Set default options
|
|
141
|
+
this.#options.safeStringEnabled = this.#options.safeStringEnabled !== false; // Default true
|
|
142
|
+
this.#options.safeDateEnabled = this.#options.safeDateEnabled !== false; // Default true
|
|
143
|
+
this.#options.warnOnUnsafeString = this.#options.warnOnUnsafeString !== false; // Default true
|
|
144
|
+
this.#options.warnOnUnsafeDate = this.#options.warnOnUnsafeDate !== false; // Default true
|
|
145
|
+
|
|
146
|
+
// Validate that the router can handle JSON parsing
|
|
147
|
+
this.#validateJsonMiddleware();
|
|
148
|
+
|
|
120
149
|
// Setup built-in middleware if configured
|
|
121
150
|
this.#setupBuiltInMiddleware();
|
|
122
151
|
|
|
@@ -147,23 +176,73 @@ class RpcEndpoint {
|
|
|
147
176
|
}
|
|
148
177
|
|
|
149
178
|
if (methodWhitelist && Array.isArray(methodWhitelist)) {
|
|
150
|
-
this.use(
|
|
179
|
+
this.use(
|
|
180
|
+
'beforeCall',
|
|
181
|
+
builtInMiddlewares.methodWhitelist(methodWhitelist)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Validate JSON middleware functionality and Express compatibility
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
#validateJsonMiddleware() {
|
|
191
|
+
try {
|
|
192
|
+
// Check if express.json is available (Express 4.16.0+)
|
|
193
|
+
if (typeof express.json !== 'function') {
|
|
194
|
+
throw new Error('express.json() is not available. Please ensure you are using Express 4.16.0 or later, or install body-parser separately.');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Test JSON middleware creation with default options
|
|
198
|
+
const jsonMiddleware = express.json({ limit: '10mb' });
|
|
199
|
+
|
|
200
|
+
if (typeof jsonMiddleware !== 'function') {
|
|
201
|
+
throw new Error('express.json() did not return a valid middleware function');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Verify middleware has correct signature (req, res, next)
|
|
205
|
+
if (jsonMiddleware.length !== 3) {
|
|
206
|
+
throw new Error('JSON middleware has invalid signature - expected (req, res, next)');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.#logger.debug('JSON middleware validation passed', {
|
|
210
|
+
expressJsonAvailable: true,
|
|
211
|
+
middlewareValid: true,
|
|
212
|
+
endpoint: this.#endpoint
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const errorMsg = `JSON middleware validation failed: ${error.message}\n` +
|
|
217
|
+
`This router cannot handle JSON-RPC requests properly.\n\n` +
|
|
218
|
+
`Possible solutions:\n` +
|
|
219
|
+
`- Update Express: npm install express@latest (requires >=4.16.0)\n` +
|
|
220
|
+
`- Add JSON parsing middleware manually: router.use(express.json())\n` +
|
|
221
|
+
`- Install body-parser: npm install body-parser`;
|
|
222
|
+
|
|
223
|
+
this.#logger.error('JSON middleware validation failed', {
|
|
224
|
+
error: error.message,
|
|
225
|
+
expressVersion: require('express/package.json')?.version || 'unknown',
|
|
226
|
+
endpoint: this.#endpoint
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
throw new Error(errorMsg);
|
|
151
230
|
}
|
|
152
231
|
}
|
|
153
232
|
|
|
154
233
|
/**
|
|
155
234
|
* Setup Express routes
|
|
156
|
-
* @param {Router} router
|
|
157
|
-
* @param {C} context
|
|
235
|
+
* @param {Router} router
|
|
236
|
+
* @param {C} context
|
|
158
237
|
*/
|
|
159
238
|
#setupRoutes(router, context) {
|
|
160
239
|
// Health check endpoint
|
|
161
240
|
if (this.#options.healthCheck !== false) {
|
|
162
241
|
router.get(`${this.#endpoint}/health`, (req, res) => {
|
|
163
|
-
res.json({
|
|
164
|
-
status: 'ok',
|
|
242
|
+
res.json({
|
|
243
|
+
status: 'ok',
|
|
165
244
|
timestamp: new Date().toISOString(),
|
|
166
|
-
version: require('../package.json').version
|
|
245
|
+
version: require('../package.json').version,
|
|
167
246
|
});
|
|
168
247
|
});
|
|
169
248
|
}
|
|
@@ -178,47 +257,54 @@ class RpcEndpoint {
|
|
|
178
257
|
// Main JSON-RPC endpoint
|
|
179
258
|
router.post(this.#endpoint, async (req, res) => {
|
|
180
259
|
const startTime = Date.now();
|
|
181
|
-
|
|
260
|
+
|
|
182
261
|
try {
|
|
183
262
|
// Check if it's a batch request
|
|
184
263
|
if (this.#batchHandler.isBatchRequest(req.body)) {
|
|
185
264
|
this.#logger.info('Batch request received', {
|
|
186
265
|
batchSize: req.body.length,
|
|
187
|
-
ip: req.ip
|
|
266
|
+
ip: req.ip,
|
|
188
267
|
});
|
|
189
268
|
|
|
190
|
-
const results = await this.#batchHandler.processBatch(
|
|
191
|
-
|
|
269
|
+
const results = await this.#batchHandler.processBatch(
|
|
270
|
+
req.body,
|
|
271
|
+
req,
|
|
272
|
+
res,
|
|
273
|
+
context
|
|
274
|
+
);
|
|
275
|
+
|
|
192
276
|
// Only send response if there are results (non-notification requests)
|
|
193
277
|
if (results.length > 0) {
|
|
194
278
|
res.json(results);
|
|
195
279
|
} else {
|
|
196
280
|
res.status(204).end(); // No content for all-notification batch
|
|
197
281
|
}
|
|
198
|
-
|
|
282
|
+
|
|
199
283
|
this.#logger.info('Batch request completed', {
|
|
200
284
|
batchSize: req.body.length,
|
|
201
285
|
responseCount: results.length,
|
|
202
|
-
duration: Date.now() - startTime
|
|
286
|
+
duration: Date.now() - startTime,
|
|
203
287
|
});
|
|
204
|
-
|
|
288
|
+
|
|
205
289
|
return;
|
|
206
290
|
}
|
|
207
291
|
|
|
208
292
|
// Single request processing
|
|
209
293
|
await this.#processSingleRequest(req, res, context, startTime);
|
|
210
|
-
|
|
211
294
|
} catch (error) {
|
|
212
|
-
this.#logger.error('Endpoint error', {
|
|
213
|
-
|
|
295
|
+
this.#logger.error('Endpoint error', {
|
|
296
|
+
error: error.message,
|
|
297
|
+
stack: error.stack,
|
|
298
|
+
});
|
|
299
|
+
|
|
214
300
|
res.json({
|
|
215
|
-
jsonrpc:
|
|
301
|
+
jsonrpc: '2.0',
|
|
216
302
|
id: null,
|
|
217
|
-
error: {
|
|
218
|
-
code: -32603,
|
|
219
|
-
message:
|
|
220
|
-
data: serializeError(error, true)
|
|
221
|
-
}
|
|
303
|
+
error: {
|
|
304
|
+
code: -32603,
|
|
305
|
+
message: 'Internal error',
|
|
306
|
+
data: serializeError(error, true),
|
|
307
|
+
},
|
|
222
308
|
});
|
|
223
309
|
}
|
|
224
310
|
});
|
|
@@ -226,10 +312,10 @@ class RpcEndpoint {
|
|
|
226
312
|
|
|
227
313
|
/**
|
|
228
314
|
* Process a single JSON-RPC request
|
|
229
|
-
* @param {Request} req
|
|
230
|
-
* @param {Response} res
|
|
231
|
-
* @param {C} context
|
|
232
|
-
* @param {number} startTime
|
|
315
|
+
* @param {Request} req
|
|
316
|
+
* @param {Response} res
|
|
317
|
+
* @param {C} context
|
|
318
|
+
* @param {number} startTime
|
|
233
319
|
*/
|
|
234
320
|
async #processSingleRequest(req, res, context, startTime) {
|
|
235
321
|
const { jsonrpc, method, params, id } = req.body || {};
|
|
@@ -238,17 +324,23 @@ class RpcEndpoint {
|
|
|
238
324
|
this.#logger.rpcCall(method, params, id, req);
|
|
239
325
|
|
|
240
326
|
// Validate JSON-RPC 2.0 request
|
|
241
|
-
if (jsonrpc !==
|
|
327
|
+
if (jsonrpc !== '2.0') {
|
|
242
328
|
return this.reply(res, {
|
|
243
329
|
id,
|
|
244
|
-
error: {
|
|
330
|
+
error: {
|
|
331
|
+
code: -32600,
|
|
332
|
+
message: `Invalid Request: 'jsonrpc' must be '2.0'.`,
|
|
333
|
+
},
|
|
245
334
|
});
|
|
246
335
|
}
|
|
247
336
|
|
|
248
|
-
if (typeof method !==
|
|
337
|
+
if (typeof method !== 'string') {
|
|
249
338
|
return this.reply(res, {
|
|
250
339
|
id,
|
|
251
|
-
error: {
|
|
340
|
+
error: {
|
|
341
|
+
code: -32600,
|
|
342
|
+
message: `Invalid Request: 'method' must be a string.`,
|
|
343
|
+
},
|
|
252
344
|
});
|
|
253
345
|
}
|
|
254
346
|
|
|
@@ -256,53 +348,82 @@ class RpcEndpoint {
|
|
|
256
348
|
if (!methodConfig) {
|
|
257
349
|
return this.reply(res, {
|
|
258
350
|
id,
|
|
259
|
-
error: { code: -32601, message: `Method "${method}" not found` }
|
|
351
|
+
error: { code: -32601, message: `Method "${method}" not found` },
|
|
260
352
|
});
|
|
261
353
|
}
|
|
262
354
|
|
|
263
|
-
const handler =
|
|
264
|
-
|
|
355
|
+
const handler =
|
|
356
|
+
typeof methodConfig === 'function' ? methodConfig : methodConfig.handler;
|
|
357
|
+
const schema =
|
|
358
|
+
typeof methodConfig === 'object' ? methodConfig.schema : null;
|
|
265
359
|
|
|
266
360
|
try {
|
|
361
|
+
// Check client's safe options from request headers
|
|
362
|
+
const clientSafeStringEnabled = req.headers['x-rpc-safestring-enabled'] === 'true';
|
|
363
|
+
const clientSafeDateEnabled = req.headers['x-rpc-safedate-enabled'] === 'true';
|
|
364
|
+
|
|
365
|
+
// Create deserialization options based on client's configuration
|
|
366
|
+
const deserializationOptions = {
|
|
367
|
+
safeStringEnabled: clientSafeStringEnabled,
|
|
368
|
+
safeDateEnabled: clientSafeDateEnabled
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Deserialize parameters using client's safe options
|
|
372
|
+
const deserializedParams = params ? this.deserializeBigIntsAndDates(params, deserializationOptions) : params;
|
|
373
|
+
|
|
267
374
|
// Prepare middleware context
|
|
268
375
|
let middlewareContext = {
|
|
269
376
|
req,
|
|
270
377
|
res,
|
|
271
378
|
method,
|
|
272
|
-
params,
|
|
379
|
+
params: deserializedParams,
|
|
273
380
|
context,
|
|
274
381
|
id,
|
|
275
|
-
startTime
|
|
382
|
+
startTime,
|
|
276
383
|
};
|
|
277
384
|
|
|
278
385
|
// Execute beforeCall middleware
|
|
279
|
-
middlewareContext = await this.#middleware.execute(
|
|
386
|
+
middlewareContext = await this.#middleware.execute(
|
|
387
|
+
'beforeCall',
|
|
388
|
+
middlewareContext
|
|
389
|
+
);
|
|
280
390
|
|
|
281
391
|
// Validate parameters if schema is provided
|
|
282
392
|
if (schema) {
|
|
283
|
-
middlewareContext = await this.#middleware.execute(
|
|
284
|
-
|
|
285
|
-
|
|
393
|
+
middlewareContext = await this.#middleware.execute(
|
|
394
|
+
'beforeValidation',
|
|
395
|
+
middlewareContext
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const validation = this.#validator.validate(
|
|
399
|
+
middlewareContext.params,
|
|
400
|
+
schema
|
|
401
|
+
);
|
|
286
402
|
if (!validation.valid) {
|
|
287
403
|
const error = new Error('Validation failed');
|
|
288
404
|
error.code = -32602;
|
|
289
405
|
error.data = {
|
|
290
|
-
validationErrors: validation.errors.map(err => ({
|
|
406
|
+
validationErrors: validation.errors.map((err) => ({
|
|
291
407
|
field: err.instancePath || err.schemaPath,
|
|
292
408
|
message: err.message,
|
|
293
|
-
value: err.data
|
|
294
|
-
}))
|
|
409
|
+
value: err.data,
|
|
410
|
+
})),
|
|
295
411
|
};
|
|
296
412
|
throw error;
|
|
297
413
|
}
|
|
298
|
-
|
|
414
|
+
|
|
299
415
|
middlewareContext.params = validation.data;
|
|
300
|
-
middlewareContext = await this.#middleware.execute(
|
|
416
|
+
middlewareContext = await this.#middleware.execute(
|
|
417
|
+
'afterValidation',
|
|
418
|
+
middlewareContext
|
|
419
|
+
);
|
|
301
420
|
}
|
|
302
421
|
|
|
303
422
|
// Invoke the handler
|
|
304
|
-
const result = await Promise.resolve(
|
|
305
|
-
|
|
423
|
+
const result = await Promise.resolve(
|
|
424
|
+
handler(req, context, middlewareContext.params)
|
|
425
|
+
);
|
|
426
|
+
|
|
306
427
|
// Execute afterCall middleware
|
|
307
428
|
middlewareContext.result = result;
|
|
308
429
|
await this.#middleware.execute('afterCall', middlewareContext);
|
|
@@ -314,7 +435,6 @@ class RpcEndpoint {
|
|
|
314
435
|
// Convert any BigInt/Date values before JSON-stringifying
|
|
315
436
|
const safeResult = this.serializeBigIntsAndDates(result);
|
|
316
437
|
this.reply(res, { id, result: safeResult });
|
|
317
|
-
|
|
318
438
|
} catch (err) {
|
|
319
439
|
const duration = Date.now() - startTime;
|
|
320
440
|
this.#logger.rpcError(method, id, duration, err);
|
|
@@ -330,20 +450,22 @@ class RpcEndpoint {
|
|
|
330
450
|
id,
|
|
331
451
|
error: err,
|
|
332
452
|
startTime,
|
|
333
|
-
duration
|
|
453
|
+
duration,
|
|
334
454
|
});
|
|
335
455
|
} catch (middlewareError) {
|
|
336
|
-
this.#logger.error('Error middleware failed', {
|
|
456
|
+
this.#logger.error('Error middleware failed', {
|
|
457
|
+
error: middlewareError.message,
|
|
458
|
+
});
|
|
337
459
|
}
|
|
338
460
|
|
|
339
461
|
this.reply(res, {
|
|
340
462
|
id,
|
|
341
|
-
error: {
|
|
342
|
-
code: err.code || -32603,
|
|
343
|
-
message: err.message || `Internal error`,
|
|
463
|
+
error: {
|
|
464
|
+
code: err.code || -32603,
|
|
465
|
+
message: err.message || `Internal error`,
|
|
344
466
|
...(err.data && { data: err.data }),
|
|
345
|
-
error: serializeError(err, true)
|
|
346
|
-
}
|
|
467
|
+
error: serializeError(err, true),
|
|
468
|
+
},
|
|
347
469
|
});
|
|
348
470
|
}
|
|
349
471
|
}
|
|
@@ -356,20 +478,25 @@ class RpcEndpoint {
|
|
|
356
478
|
*/
|
|
357
479
|
addMethod(name, handlerOrConfig, schema = null) {
|
|
358
480
|
if (typeof handlerOrConfig === 'function') {
|
|
359
|
-
this.#methods[name] = schema
|
|
481
|
+
this.#methods[name] = schema
|
|
482
|
+
? { handler: handlerOrConfig, schema }
|
|
483
|
+
: handlerOrConfig;
|
|
360
484
|
} else if (typeof handlerOrConfig === 'object' && handlerOrConfig.handler) {
|
|
361
485
|
this.#methods[name] = handlerOrConfig;
|
|
362
486
|
} else {
|
|
363
487
|
throw new Error('Invalid handler configuration');
|
|
364
488
|
}
|
|
365
489
|
|
|
366
|
-
this.#logger.debug('Method registered', {
|
|
490
|
+
this.#logger.debug('Method registered', {
|
|
491
|
+
method: name,
|
|
492
|
+
hasSchema: !!schema,
|
|
493
|
+
});
|
|
367
494
|
}
|
|
368
495
|
|
|
369
496
|
/**
|
|
370
497
|
* Add middleware for specific hooks
|
|
371
|
-
* @param {string} hook
|
|
372
|
-
* @param {Function} middleware
|
|
498
|
+
* @param {string} hook
|
|
499
|
+
* @param {Function} middleware
|
|
373
500
|
*/
|
|
374
501
|
use(hook, middleware) {
|
|
375
502
|
this.#middleware.use(hook, middleware);
|
|
@@ -377,7 +504,7 @@ class RpcEndpoint {
|
|
|
377
504
|
|
|
378
505
|
/**
|
|
379
506
|
* Remove method
|
|
380
|
-
* @param {string} name
|
|
507
|
+
* @param {string} name
|
|
381
508
|
*/
|
|
382
509
|
removeMethod(name) {
|
|
383
510
|
delete this.#methods[name];
|
|
@@ -386,7 +513,7 @@ class RpcEndpoint {
|
|
|
386
513
|
|
|
387
514
|
/**
|
|
388
515
|
* Get method configuration
|
|
389
|
-
* @param {string} name
|
|
516
|
+
* @param {string} name
|
|
390
517
|
* @returns {any}
|
|
391
518
|
*/
|
|
392
519
|
getMethod(name) {
|
|
@@ -448,11 +575,11 @@ class RpcEndpoint {
|
|
|
448
575
|
middleware: {
|
|
449
576
|
beforeCall: this.#middleware.getMiddlewares('beforeCall').length,
|
|
450
577
|
afterCall: this.#middleware.getMiddlewares('afterCall').length,
|
|
451
|
-
onError: this.#middleware.getMiddlewares('onError').length
|
|
578
|
+
onError: this.#middleware.getMiddlewares('onError').length,
|
|
452
579
|
},
|
|
453
580
|
uptime: process.uptime(),
|
|
454
581
|
memory: process.memoryUsage(),
|
|
455
|
-
timestamp: new Date().toISOString()
|
|
582
|
+
timestamp: new Date().toISOString(),
|
|
456
583
|
};
|
|
457
584
|
}
|
|
458
585
|
|
|
@@ -466,7 +593,7 @@ class RpcEndpoint {
|
|
|
466
593
|
* }} responsePayload The JSON-RPC response fields.
|
|
467
594
|
*/
|
|
468
595
|
reply(res, { id, result, error }) {
|
|
469
|
-
const response = { jsonrpc:
|
|
596
|
+
const response = { jsonrpc: '2.0', id: id === undefined ? null : id };
|
|
470
597
|
|
|
471
598
|
if (error) {
|
|
472
599
|
response.error = error;
|
|
@@ -474,6 +601,10 @@ class RpcEndpoint {
|
|
|
474
601
|
response.result = result;
|
|
475
602
|
}
|
|
476
603
|
|
|
604
|
+
// Add safe options headers so client knows how to deserialize
|
|
605
|
+
res.setHeader('X-RPC-SafeString-Enabled', this.#options.safeStringEnabled ? 'true' : 'false');
|
|
606
|
+
res.setHeader('X-RPC-SafeDate-Enabled', this.#options.safeDateEnabled ? 'true' : 'false');
|
|
607
|
+
|
|
477
608
|
res.json(response);
|
|
478
609
|
}
|
|
479
610
|
|
|
@@ -486,20 +617,45 @@ class RpcEndpoint {
|
|
|
486
617
|
* @returns {any} A version of `value` safe for JSON serialization.
|
|
487
618
|
*/
|
|
488
619
|
serializeBigIntsAndDates(value) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
620
|
+
return this.#serializeValue(value, false);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Internal serialization method with BigInt and Date warning tracking
|
|
625
|
+
* @private
|
|
626
|
+
*/
|
|
627
|
+
#serializeValue(value, hasBigInt = false, hasDate = false) {
|
|
628
|
+
if (typeof value === 'bigint') {
|
|
629
|
+
// Warn if safeStringEnabled is disabled and we have BigInt
|
|
630
|
+
if (!this.#options.safeStringEnabled && !hasBigInt && this.#options.warnOnUnsafeString) {
|
|
631
|
+
this.#logger.warn('BigInt detected in serialization. Consider enabling safeStringEnabled option to avoid potential string/BigInt confusion.');
|
|
632
|
+
}
|
|
633
|
+
// Convert BigInt to string with 'n' suffix for proper deserialization
|
|
634
|
+
return value.toString() + 'n';
|
|
495
635
|
} else if (value instanceof Date) {
|
|
496
|
-
//
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
636
|
+
// Warn if safeDateEnabled is disabled and we have Date
|
|
637
|
+
if (!this.#options.safeDateEnabled && !hasDate && this.#options.warnOnUnsafeDate) {
|
|
638
|
+
this.#logger.warn('Date detected in serialization. Consider enabling safeDateEnabled option to avoid potential string/Date confusion.');
|
|
639
|
+
}
|
|
640
|
+
// Convert Date to ISO string with D: prefix if safeDateEnabled
|
|
641
|
+
const isoString = value.toISOString();
|
|
642
|
+
return this.#options.safeDateEnabled ? `D:${isoString}` : isoString;
|
|
643
|
+
} else if (typeof value === 'string') {
|
|
644
|
+
// Add S: prefix if safeStringEnabled is true
|
|
645
|
+
if (this.#options.safeStringEnabled) {
|
|
646
|
+
return 'S:' + value;
|
|
647
|
+
}
|
|
648
|
+
return value;
|
|
649
|
+
} else if (Array.isArray(value)) {
|
|
650
|
+
// Recurse into arrays - don't pass hasBigInt/hasDate to children
|
|
651
|
+
// Each element should be checked independently for warnings
|
|
652
|
+
return value.map((v) => this.#serializeValue(v, false, false));
|
|
653
|
+
} else if (value && typeof value === 'object') {
|
|
654
|
+
// Recurse into plain objects - don't pass hasBigInt/hasDate to children
|
|
655
|
+
// Each property should be checked independently for warnings
|
|
500
656
|
const result = {};
|
|
501
657
|
for (const [key, val] of Object.entries(value)) {
|
|
502
|
-
result[key] = this
|
|
658
|
+
result[key] = this.#serializeValue(val, false, false);
|
|
503
659
|
}
|
|
504
660
|
return result;
|
|
505
661
|
}
|
|
@@ -508,6 +664,32 @@ class RpcEndpoint {
|
|
|
508
664
|
return value;
|
|
509
665
|
}
|
|
510
666
|
|
|
667
|
+
/**
|
|
668
|
+
* Check if an object has BigInt values deep inside
|
|
669
|
+
* @private
|
|
670
|
+
*/
|
|
671
|
+
#hasDeepBigInt(obj) {
|
|
672
|
+
if (typeof obj === 'bigint') return true;
|
|
673
|
+
if (Array.isArray(obj)) return obj.some(item => this.#hasDeepBigInt(item));
|
|
674
|
+
if (obj && typeof obj === 'object') {
|
|
675
|
+
return Object.values(obj).some(val => this.#hasDeepBigInt(val));
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Check if an object has Date values deep inside
|
|
682
|
+
* @private
|
|
683
|
+
*/
|
|
684
|
+
#hasDeepDate(obj) {
|
|
685
|
+
if (obj instanceof Date) return true;
|
|
686
|
+
if (Array.isArray(obj)) return obj.some(item => this.#hasDeepDate(item));
|
|
687
|
+
if (obj && typeof obj === 'object') {
|
|
688
|
+
return Object.values(obj).some(val => this.#hasDeepDate(val));
|
|
689
|
+
}
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
|
|
511
693
|
/**
|
|
512
694
|
* Recursively convert stringified BigInts back to BigInt,
|
|
513
695
|
* and ISO 8601 date strings back to Date objects.
|
|
@@ -517,25 +699,47 @@ class RpcEndpoint {
|
|
|
517
699
|
* the original types.
|
|
518
700
|
*
|
|
519
701
|
* @param {any} value The value to deserialize.
|
|
702
|
+
* @param {Object} [options] Custom deserialization options (uses server options if not provided).
|
|
703
|
+
* @param {boolean} [options.safeStringEnabled] Whether to expect S: prefixed strings.
|
|
704
|
+
* @param {boolean} [options.safeDateEnabled] Whether to expect D: prefixed dates.
|
|
520
705
|
* @returns {any} The re-hydrated value.
|
|
521
706
|
*/
|
|
522
|
-
deserializeBigIntsAndDates(value) {
|
|
707
|
+
deserializeBigIntsAndDates(value, options = null) {
|
|
708
|
+
// Use provided options or fall back to server options
|
|
709
|
+
const safeStringEnabled = options ? options.safeStringEnabled : this.#options.safeStringEnabled;
|
|
710
|
+
const safeDateEnabled = options ? options.safeDateEnabled : this.#options.safeDateEnabled;
|
|
711
|
+
|
|
523
712
|
// More comprehensive ISO date regex that handles:
|
|
524
713
|
// - UTC: 2023-01-01T12:00:00.000Z
|
|
525
|
-
// - With timezone: 2023-01-01T12:00:00.000+01:00
|
|
714
|
+
// - With timezone: 2023-01-01T12:00:00.000+01:00
|
|
526
715
|
// - Without timezone: 2023-01-01T12:00:00.000 (treated as local)
|
|
527
|
-
const ISO_DATE_REGEX =
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if
|
|
531
|
-
|
|
532
|
-
//
|
|
533
|
-
if (
|
|
534
|
-
return
|
|
716
|
+
const ISO_DATE_REGEX =
|
|
717
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
|
|
718
|
+
|
|
719
|
+
// 1. Check if it's a string that might be a BigInt, Date, or safe string
|
|
720
|
+
if (typeof value === 'string') {
|
|
721
|
+
// Safe string check: if safeStringEnabled and starts with S:
|
|
722
|
+
if (safeStringEnabled && value.startsWith('S:')) {
|
|
723
|
+
return value.substring(2); // Remove 'S:' prefix
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Safe date check: if safeDateEnabled and starts with D:
|
|
727
|
+
if (safeDateEnabled && value.startsWith('D:')) {
|
|
728
|
+
const isoString = value.substring(2); // Remove 'D:' prefix
|
|
729
|
+
const date = new Date(isoString);
|
|
730
|
+
// Double-check that we got a valid date
|
|
731
|
+
if (!isNaN(date.getTime())) {
|
|
732
|
+
return date;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// BigInt check: matches digits (including negative), ending in "n"
|
|
737
|
+
if (/^-?\d+n$/.test(value)) {
|
|
738
|
+
return BigInt(value.slice(0, -1)); // Remove 'n' and convert
|
|
535
739
|
}
|
|
536
740
|
|
|
537
|
-
// Date check: matches an ISO 8601 string
|
|
538
|
-
if (ISO_DATE_REGEX.test(value)) {
|
|
741
|
+
// Date check: matches an ISO 8601 string (only if safeDateEnabled is false)
|
|
742
|
+
if (!safeDateEnabled && ISO_DATE_REGEX.test(value)) {
|
|
539
743
|
const date = new Date(value);
|
|
540
744
|
// Double-check that we got a valid date
|
|
541
745
|
if (!isNaN(date.getTime())) {
|
|
@@ -546,15 +750,15 @@ class RpcEndpoint {
|
|
|
546
750
|
|
|
547
751
|
// 2. If it's an array, handle each element
|
|
548
752
|
if (Array.isArray(value)) {
|
|
549
|
-
return value.map((v) => this.deserializeBigIntsAndDates(v));
|
|
753
|
+
return value.map((v) => this.deserializeBigIntsAndDates(v, options));
|
|
550
754
|
}
|
|
551
755
|
|
|
552
756
|
// 3. If it's a plain object, recurse into each property
|
|
553
|
-
if (value && typeof value ===
|
|
757
|
+
if (value && typeof value === 'object') {
|
|
554
758
|
return Object.fromEntries(
|
|
555
759
|
Object.entries(value).map(([key, val]) => [
|
|
556
760
|
key,
|
|
557
|
-
this.deserializeBigIntsAndDates(val)
|
|
761
|
+
this.deserializeBigIntsAndDates(val, options),
|
|
558
762
|
])
|
|
559
763
|
);
|
|
560
764
|
}
|