rpc-express-toolkit 4.2.0 → 4.3.2
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 +135 -9
- package/examples/example.js +0 -0
- package/examples/introspection/client.js +68 -0
- package/examples/introspection/server.js +89 -0
- 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 +95 -56
- package/src/index.d.ts +33 -63
- package/src/index.js +357 -46
- 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 +1 -0
- 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
package/src/index.js
CHANGED
|
@@ -2,8 +2,19 @@ const path = require('path');
|
|
|
2
2
|
const express = require('express');
|
|
3
3
|
const expressPkg = require('express/package.json');
|
|
4
4
|
const NestedError = require('nested-error-stacks');
|
|
5
|
+
const {
|
|
6
|
+
RpcClient,
|
|
7
|
+
RpcError,
|
|
8
|
+
RpcHttpError,
|
|
9
|
+
RpcSafeClient,
|
|
10
|
+
} = require('rpc-toolkit-js-client');
|
|
5
11
|
const pkg = require('../package.json');
|
|
6
12
|
|
|
13
|
+
const rpcClientAssetsPath = path.dirname(
|
|
14
|
+
require.resolve('rpc-toolkit-js-client')
|
|
15
|
+
);
|
|
16
|
+
const DEFAULT_MAX_SERIALIZATION_DEPTH = 100;
|
|
17
|
+
|
|
7
18
|
/** @typedef {import("express").Router} Router */
|
|
8
19
|
/** @typedef {import("express").Request} Request */
|
|
9
20
|
/** @typedef {import("express").Response} Response */
|
|
@@ -16,6 +27,7 @@ const {
|
|
|
16
27
|
SchemaBuilder,
|
|
17
28
|
} = require('./validation');
|
|
18
29
|
const BatchHandler = require('./batch');
|
|
30
|
+
const { hasOwn, validateEnvelope } = require('./protocol');
|
|
19
31
|
|
|
20
32
|
/**
|
|
21
33
|
* Default properties to include in error serialization.
|
|
@@ -93,7 +105,7 @@ class RpcEndpoint {
|
|
|
93
105
|
* The path from which client scripts will be served.
|
|
94
106
|
*/
|
|
95
107
|
static serveScripts(router, url = '/vendor/rpc-client') {
|
|
96
|
-
router.use(url, express.static(
|
|
108
|
+
router.use(url, express.static(rpcClientAssetsPath));
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
/** @type {string} */
|
|
@@ -102,6 +114,12 @@ class RpcEndpoint {
|
|
|
102
114
|
/** @type {{ [name: string]: any }} */
|
|
103
115
|
#methods = {};
|
|
104
116
|
|
|
117
|
+
/** @type {string} */
|
|
118
|
+
#introspectionPrefix = '__rpc';
|
|
119
|
+
|
|
120
|
+
/** @type {boolean} */
|
|
121
|
+
#isInternalRegistration = false;
|
|
122
|
+
|
|
105
123
|
/** @type {Logger} */
|
|
106
124
|
#logger;
|
|
107
125
|
|
|
@@ -143,10 +161,21 @@ class RpcEndpoint {
|
|
|
143
161
|
this.#options.safeEnabled = this.#options.safeEnabled === true; // Default false
|
|
144
162
|
this.#options.warnOnUnsafe = this.#options.warnOnUnsafe !== false; // Default true
|
|
145
163
|
this.#options.strictMode = this.#options.strictMode !== false; // Default true
|
|
164
|
+
this.#options.enableIntrospection =
|
|
165
|
+
this.#options.enableIntrospection === true; // Default false
|
|
166
|
+
this.#options.introspectionPrefix =
|
|
167
|
+
this.#options.introspectionPrefix || '__rpc'; // Default __rpc
|
|
168
|
+
|
|
169
|
+
this.#introspectionPrefix = this.#options.introspectionPrefix;
|
|
146
170
|
|
|
147
171
|
// Validate that the router can handle JSON parsing
|
|
148
172
|
this.#validateJsonMiddleware();
|
|
149
173
|
|
|
174
|
+
// Register introspection methods if enabled
|
|
175
|
+
if (this.#options.enableIntrospection) {
|
|
176
|
+
this.#registerIntrospectionMethods();
|
|
177
|
+
}
|
|
178
|
+
|
|
150
179
|
// Setup built-in middleware if configured
|
|
151
180
|
this.#setupBuiltInMiddleware();
|
|
152
181
|
|
|
@@ -184,6 +213,145 @@ class RpcEndpoint {
|
|
|
184
213
|
}
|
|
185
214
|
}
|
|
186
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Register introspection methods (__rpc.*)
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
#registerIntrospectionMethods() {
|
|
221
|
+
this.#isInternalRegistration = true;
|
|
222
|
+
|
|
223
|
+
// __rpc.listMethods - List all user methods (excludes __rpc.* methods)
|
|
224
|
+
this.addMethod(
|
|
225
|
+
`${this.#introspectionPrefix}.listMethods`,
|
|
226
|
+
async (_req, _context, _params) =>
|
|
227
|
+
Object.keys(this.#methods).filter(
|
|
228
|
+
(name) => !name.startsWith(this.#introspectionPrefix)
|
|
229
|
+
),
|
|
230
|
+
{
|
|
231
|
+
description: 'List all available RPC methods',
|
|
232
|
+
exposeSchema: true,
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// __rpc.describe - Get schema and description of a specific method
|
|
237
|
+
this.addMethod(
|
|
238
|
+
`${this.#introspectionPrefix}.describe`,
|
|
239
|
+
async (_req, _context, params) => {
|
|
240
|
+
const methodName = params?.method;
|
|
241
|
+
|
|
242
|
+
if (!methodName || typeof methodName !== 'string') {
|
|
243
|
+
throw {
|
|
244
|
+
code: -32602,
|
|
245
|
+
message: 'Invalid params: method name required',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Prevent introspection of __rpc.* methods
|
|
250
|
+
if (methodName.startsWith(this.#introspectionPrefix)) {
|
|
251
|
+
throw {
|
|
252
|
+
code: -32601,
|
|
253
|
+
message: 'Cannot describe introspection methods',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const methodConfig = this.#methods[methodName];
|
|
258
|
+
if (!methodConfig) {
|
|
259
|
+
throw { code: -32601, message: `Method not found: ${methodName}` };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check if schema is exposed
|
|
263
|
+
const config = typeof methodConfig === 'function' ? {} : methodConfig;
|
|
264
|
+
if (!config.exposeSchema) {
|
|
265
|
+
throw { code: -32601, message: 'Method schema not available' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
name: methodName,
|
|
270
|
+
schema: config.schema || null,
|
|
271
|
+
description: config.description || '',
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
schema: {
|
|
276
|
+
type: 'object',
|
|
277
|
+
properties: {
|
|
278
|
+
method: { type: 'string' },
|
|
279
|
+
},
|
|
280
|
+
required: ['method'],
|
|
281
|
+
},
|
|
282
|
+
description: 'Get schema and description of a specific method',
|
|
283
|
+
exposeSchema: true,
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// __rpc.describeAll - Get all methods with public schemas
|
|
288
|
+
this.addMethod(
|
|
289
|
+
`${this.#introspectionPrefix}.describeAll`,
|
|
290
|
+
async (_req, _context, _params) => {
|
|
291
|
+
const publicMethods = [];
|
|
292
|
+
|
|
293
|
+
for (const [name, methodConfig] of Object.entries(this.#methods)) {
|
|
294
|
+
// Skip introspection methods
|
|
295
|
+
if (name.startsWith(this.#introspectionPrefix)) continue;
|
|
296
|
+
|
|
297
|
+
const config = typeof methodConfig === 'function' ? {} : methodConfig;
|
|
298
|
+
if (config.exposeSchema) {
|
|
299
|
+
publicMethods.push({
|
|
300
|
+
name,
|
|
301
|
+
schema: config.schema || null,
|
|
302
|
+
description: config.description || '',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return publicMethods;
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
description: 'List all methods with public schemas',
|
|
311
|
+
exposeSchema: true,
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// __rpc.version - Get toolkit version
|
|
316
|
+
this.addMethod(
|
|
317
|
+
`${this.#introspectionPrefix}.version`,
|
|
318
|
+
async (_req, _context, _params) => ({
|
|
319
|
+
toolkit: 'rpc-express-toolkit',
|
|
320
|
+
version: pkg.version,
|
|
321
|
+
expressVersion: expressPkg.version,
|
|
322
|
+
nodeVersion: process.version,
|
|
323
|
+
}),
|
|
324
|
+
{
|
|
325
|
+
description: 'Get RPC toolkit version information',
|
|
326
|
+
exposeSchema: true,
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// __rpc.capabilities - Get server capabilities
|
|
331
|
+
this.addMethod(
|
|
332
|
+
`${this.#introspectionPrefix}.capabilities`,
|
|
333
|
+
async (_req, _context, _params) => ({
|
|
334
|
+
safeMode: this.#options.safeEnabled,
|
|
335
|
+
batch: true,
|
|
336
|
+
introspection: true,
|
|
337
|
+
introspectionPrefix: this.#introspectionPrefix,
|
|
338
|
+
validation: !!this.#options.validation,
|
|
339
|
+
cors: !!this.#options.cors,
|
|
340
|
+
auth: !!this.#options.auth,
|
|
341
|
+
rateLimit: !!this.#options.rateLimit,
|
|
342
|
+
methodCount: Object.keys(this.#methods).filter(
|
|
343
|
+
(n) => !n.startsWith(this.#introspectionPrefix)
|
|
344
|
+
).length,
|
|
345
|
+
}),
|
|
346
|
+
{
|
|
347
|
+
description: 'Get server capabilities and configuration',
|
|
348
|
+
exposeSchema: true,
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
this.#isInternalRegistration = false;
|
|
353
|
+
}
|
|
354
|
+
|
|
187
355
|
/**
|
|
188
356
|
* Validate JSON middleware functionality and Express compatibility
|
|
189
357
|
* @private
|
|
@@ -281,7 +449,13 @@ class RpcEndpoint {
|
|
|
281
449
|
);
|
|
282
450
|
|
|
283
451
|
// Only send response if there are results (non-notification requests)
|
|
284
|
-
if (results
|
|
452
|
+
if (!Array.isArray(results)) {
|
|
453
|
+
res.setHeader(
|
|
454
|
+
'X-RPC-Safe-Enabled',
|
|
455
|
+
this.#options.safeEnabled ? 'true' : 'false'
|
|
456
|
+
);
|
|
457
|
+
res.json(results);
|
|
458
|
+
} else if (results.length > 0) {
|
|
285
459
|
// Align batch responses with single-call header negotiation
|
|
286
460
|
res.setHeader(
|
|
287
461
|
'X-RPC-Safe-Enabled',
|
|
@@ -294,7 +468,7 @@ class RpcEndpoint {
|
|
|
294
468
|
|
|
295
469
|
this.#logger.info('Batch request completed', {
|
|
296
470
|
batchSize: req.body.length,
|
|
297
|
-
responseCount: results.length,
|
|
471
|
+
responseCount: Array.isArray(results) ? results.length : 1,
|
|
298
472
|
duration: Date.now() - startTime,
|
|
299
473
|
});
|
|
300
474
|
|
|
@@ -330,34 +504,26 @@ class RpcEndpoint {
|
|
|
330
504
|
* @param {number} startTime
|
|
331
505
|
*/
|
|
332
506
|
async #processSingleRequest(req, res, context, startTime) {
|
|
333
|
-
const {
|
|
507
|
+
const { method, params, id } = req.body || {};
|
|
508
|
+
const envelope = validateEnvelope(req.body);
|
|
334
509
|
|
|
335
510
|
// Log the incoming request
|
|
336
511
|
this.#logger.rpcCall(method, params, id, req);
|
|
337
512
|
|
|
338
|
-
|
|
339
|
-
if (jsonrpc !== '2.0') {
|
|
513
|
+
if (!envelope.valid) {
|
|
340
514
|
return this.reply(res, {
|
|
341
|
-
id,
|
|
342
|
-
error:
|
|
343
|
-
code: -32600,
|
|
344
|
-
message: `Invalid Request: 'jsonrpc' must be '2.0'.`,
|
|
345
|
-
},
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (typeof method !== 'string') {
|
|
350
|
-
return this.reply(res, {
|
|
351
|
-
id,
|
|
352
|
-
error: {
|
|
353
|
-
code: -32600,
|
|
354
|
-
message: `Invalid Request: 'method' must be a string.`,
|
|
355
|
-
},
|
|
515
|
+
id: envelope.responseId,
|
|
516
|
+
error: envelope.error,
|
|
356
517
|
});
|
|
357
518
|
}
|
|
358
519
|
|
|
359
520
|
const methodConfig = this.#methods[method];
|
|
360
521
|
if (!methodConfig) {
|
|
522
|
+
if (envelope.isNotification) {
|
|
523
|
+
res.status(204).end();
|
|
524
|
+
return null;
|
|
525
|
+
}
|
|
526
|
+
|
|
361
527
|
return this.reply(res, {
|
|
362
528
|
id,
|
|
363
529
|
error: { code: -32601, message: `Method "${method}" not found` },
|
|
@@ -379,6 +545,11 @@ class RpcEndpoint {
|
|
|
379
545
|
this.#options.safeEnabled &&
|
|
380
546
|
!clientSafeHeader
|
|
381
547
|
) {
|
|
548
|
+
if (envelope.isNotification) {
|
|
549
|
+
res.status(204).end();
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
382
553
|
return this.reply(res, {
|
|
383
554
|
id,
|
|
384
555
|
error: {
|
|
@@ -403,7 +574,7 @@ class RpcEndpoint {
|
|
|
403
574
|
};
|
|
404
575
|
|
|
405
576
|
// Deserialize parameters using client's safe options
|
|
406
|
-
const deserializedParams = params
|
|
577
|
+
const deserializedParams = hasOwn(req.body, 'params')
|
|
407
578
|
? this.deserializeBigIntsAndDates(params, deserializationOptions)
|
|
408
579
|
: params;
|
|
409
580
|
|
|
@@ -470,6 +641,11 @@ class RpcEndpoint {
|
|
|
470
641
|
|
|
471
642
|
// Convert any BigInt/Date values before JSON-stringifying
|
|
472
643
|
const safeResult = this.serializeBigIntsAndDates(result);
|
|
644
|
+
if (envelope.isNotification) {
|
|
645
|
+
res.status(204).end();
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
473
649
|
this.reply(res, { id, result: safeResult });
|
|
474
650
|
} catch (err) {
|
|
475
651
|
const duration = Date.now() - startTime;
|
|
@@ -494,6 +670,11 @@ class RpcEndpoint {
|
|
|
494
670
|
});
|
|
495
671
|
}
|
|
496
672
|
|
|
673
|
+
if (envelope.isNotification) {
|
|
674
|
+
res.status(204).end();
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
|
|
497
678
|
this.reply(res, {
|
|
498
679
|
id,
|
|
499
680
|
error: {
|
|
@@ -511,23 +692,60 @@ class RpcEndpoint {
|
|
|
511
692
|
/**
|
|
512
693
|
* Register a new JSON-RPC method.
|
|
513
694
|
* @param {string} name The method name.
|
|
514
|
-
* @param {Function|Object} handlerOrConfig The function to handle calls or config object.
|
|
515
|
-
* @param {Object} [
|
|
695
|
+
* @param {Function|Object} handlerOrConfig The function to handle calls or config object with handler, schema, exposeSchema, description.
|
|
696
|
+
* @param {Object} [optionsOrSchema] Optional config object or schema for backward compatibility.
|
|
516
697
|
*/
|
|
517
|
-
addMethod(name, handlerOrConfig,
|
|
698
|
+
addMethod(name, handlerOrConfig, optionsOrSchema = null) {
|
|
699
|
+
// Prevent users from registering introspection methods
|
|
700
|
+
if (
|
|
701
|
+
name.startsWith(this.#introspectionPrefix) &&
|
|
702
|
+
!this.#isInternalRegistration
|
|
703
|
+
) {
|
|
704
|
+
throw new Error(
|
|
705
|
+
`Method names starting with '${this.#introspectionPrefix}.' are reserved for RPC introspection`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
let methodConfig;
|
|
710
|
+
|
|
518
711
|
if (typeof handlerOrConfig === 'function') {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
712
|
+
// Function handler - optionsOrSchema can be schema (backward compat) or options object
|
|
713
|
+
if (
|
|
714
|
+
optionsOrSchema &&
|
|
715
|
+
(optionsOrSchema.schema ||
|
|
716
|
+
optionsOrSchema.exposeSchema ||
|
|
717
|
+
optionsOrSchema.description)
|
|
718
|
+
) {
|
|
719
|
+
// New format: options object
|
|
720
|
+
methodConfig = {
|
|
721
|
+
handler: handlerOrConfig,
|
|
722
|
+
...optionsOrSchema,
|
|
723
|
+
};
|
|
724
|
+
} else if (optionsOrSchema) {
|
|
725
|
+
// Old format: schema directly
|
|
726
|
+
methodConfig = {
|
|
727
|
+
handler: handlerOrConfig,
|
|
728
|
+
schema: optionsOrSchema,
|
|
729
|
+
};
|
|
730
|
+
} else {
|
|
731
|
+
// Just the handler
|
|
732
|
+
methodConfig = handlerOrConfig;
|
|
733
|
+
}
|
|
522
734
|
} else if (typeof handlerOrConfig === 'object' && handlerOrConfig.handler) {
|
|
523
|
-
|
|
735
|
+
// Config object
|
|
736
|
+
methodConfig = handlerOrConfig;
|
|
524
737
|
} else {
|
|
525
738
|
throw new Error('Invalid handler configuration');
|
|
526
739
|
}
|
|
527
740
|
|
|
741
|
+
this.#methods[name] = methodConfig;
|
|
742
|
+
|
|
743
|
+
const config = typeof methodConfig === 'function' ? {} : methodConfig;
|
|
528
744
|
this.#logger.debug('Method registered', {
|
|
529
745
|
method: name,
|
|
530
|
-
hasSchema: !!schema,
|
|
746
|
+
hasSchema: !!config.schema,
|
|
747
|
+
exposeSchema: !!config.exposeSchema,
|
|
748
|
+
hasDescription: !!config.description,
|
|
531
749
|
});
|
|
532
750
|
}
|
|
533
751
|
|
|
@@ -603,6 +821,14 @@ class RpcEndpoint {
|
|
|
603
821
|
return this.#validator;
|
|
604
822
|
}
|
|
605
823
|
|
|
824
|
+
/**
|
|
825
|
+
* Get endpoint options
|
|
826
|
+
* @returns {Object}
|
|
827
|
+
*/
|
|
828
|
+
get options() {
|
|
829
|
+
return { ...this.#options };
|
|
830
|
+
}
|
|
831
|
+
|
|
606
832
|
/**
|
|
607
833
|
* Get metrics data
|
|
608
834
|
* @returns {Object}
|
|
@@ -658,14 +884,28 @@ class RpcEndpoint {
|
|
|
658
884
|
* @returns {any} A version of `value` safe for JSON serialization.
|
|
659
885
|
*/
|
|
660
886
|
serializeBigIntsAndDates(value) {
|
|
661
|
-
return this.#serializeValue(value, false
|
|
887
|
+
return this.#serializeValue(value, false, false, {
|
|
888
|
+
depth: 0,
|
|
889
|
+
seen: new WeakSet(),
|
|
890
|
+
});
|
|
662
891
|
}
|
|
663
892
|
|
|
664
893
|
/**
|
|
665
894
|
* Internal serialization method with BigInt and Date warning tracking
|
|
666
895
|
* @private
|
|
667
896
|
*/
|
|
668
|
-
#serializeValue(
|
|
897
|
+
#serializeValue(
|
|
898
|
+
value,
|
|
899
|
+
hasBigInt = false,
|
|
900
|
+
hasDate = false,
|
|
901
|
+
state = { depth: 0, seen: new WeakSet() }
|
|
902
|
+
) {
|
|
903
|
+
const maxDepth =
|
|
904
|
+
this.#options.maxSerializationDepth || DEFAULT_MAX_SERIALIZATION_DEPTH;
|
|
905
|
+
if (state.depth > maxDepth) {
|
|
906
|
+
throw new Error('Serialization depth limit exceeded');
|
|
907
|
+
}
|
|
908
|
+
|
|
669
909
|
if (typeof value === 'bigint') {
|
|
670
910
|
// Warn if safeEnabled is disabled and we have BigInt
|
|
671
911
|
if (
|
|
@@ -703,16 +943,47 @@ class RpcEndpoint {
|
|
|
703
943
|
return value;
|
|
704
944
|
}
|
|
705
945
|
if (Array.isArray(value)) {
|
|
946
|
+
if (state.seen.has(value)) {
|
|
947
|
+
throw new Error('Circular reference detected during serialization');
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
state.seen.add(value);
|
|
706
951
|
// Recurse into arrays - don't pass hasBigInt/hasDate to children
|
|
707
952
|
// Each element should be checked independently for warnings
|
|
708
|
-
|
|
953
|
+
try {
|
|
954
|
+
return value.map((v) =>
|
|
955
|
+
this.#serializeValue(v, false, false, {
|
|
956
|
+
depth: state.depth + 1,
|
|
957
|
+
seen: state.seen,
|
|
958
|
+
})
|
|
959
|
+
);
|
|
960
|
+
} finally {
|
|
961
|
+
state.seen.delete(value);
|
|
962
|
+
}
|
|
709
963
|
}
|
|
710
964
|
if (value && typeof value === 'object') {
|
|
965
|
+
if (state.seen.has(value)) {
|
|
966
|
+
throw new Error('Circular reference detected during serialization');
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
state.seen.add(value);
|
|
711
970
|
// Recurse into plain objects - don't pass hasBigInt/hasDate to children
|
|
712
971
|
// Each property should be checked independently for warnings
|
|
713
972
|
const result = {};
|
|
714
|
-
|
|
715
|
-
|
|
973
|
+
try {
|
|
974
|
+
Object.entries(value).forEach(([key, val]) => {
|
|
975
|
+
Object.defineProperty(result, key, {
|
|
976
|
+
value: this.#serializeValue(val, false, false, {
|
|
977
|
+
depth: state.depth + 1,
|
|
978
|
+
seen: state.seen,
|
|
979
|
+
}),
|
|
980
|
+
enumerable: true,
|
|
981
|
+
configurable: true,
|
|
982
|
+
writable: true,
|
|
983
|
+
});
|
|
984
|
+
});
|
|
985
|
+
} finally {
|
|
986
|
+
state.seen.delete(value);
|
|
716
987
|
}
|
|
717
988
|
return result;
|
|
718
989
|
}
|
|
@@ -761,7 +1032,17 @@ class RpcEndpoint {
|
|
|
761
1032
|
* @param {boolean} [options.safeEnabled] Whether to expect safe prefixes for strings and dates.
|
|
762
1033
|
* @returns {any} The re-hydrated value.
|
|
763
1034
|
*/
|
|
764
|
-
deserializeBigIntsAndDates(value, options = null) {
|
|
1035
|
+
deserializeBigIntsAndDates(value, options = null, state = null) {
|
|
1036
|
+
const traversalState = state || {
|
|
1037
|
+
depth: 0,
|
|
1038
|
+
seen: new WeakSet(),
|
|
1039
|
+
};
|
|
1040
|
+
const maxDepth =
|
|
1041
|
+
this.#options.maxDeserializationDepth || DEFAULT_MAX_SERIALIZATION_DEPTH;
|
|
1042
|
+
if (traversalState.depth > maxDepth) {
|
|
1043
|
+
throw new Error('Deserialization depth limit exceeded');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
765
1046
|
// Use provided options or fall back to server options
|
|
766
1047
|
const safeEnabled = options
|
|
767
1048
|
? options.safeEnabled
|
|
@@ -808,17 +1089,47 @@ class RpcEndpoint {
|
|
|
808
1089
|
|
|
809
1090
|
// 2. If it's an array, handle each element
|
|
810
1091
|
if (Array.isArray(value)) {
|
|
811
|
-
|
|
1092
|
+
if (traversalState.seen.has(value)) {
|
|
1093
|
+
throw new Error('Circular reference detected during deserialization');
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
traversalState.seen.add(value);
|
|
1097
|
+
try {
|
|
1098
|
+
return value.map((v) =>
|
|
1099
|
+
this.deserializeBigIntsAndDates(v, options, {
|
|
1100
|
+
depth: traversalState.depth + 1,
|
|
1101
|
+
seen: traversalState.seen,
|
|
1102
|
+
})
|
|
1103
|
+
);
|
|
1104
|
+
} finally {
|
|
1105
|
+
traversalState.seen.delete(value);
|
|
1106
|
+
}
|
|
812
1107
|
}
|
|
813
1108
|
|
|
814
1109
|
// 3. If it's a plain object, recurse into each property
|
|
815
1110
|
if (value && typeof value === 'object') {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
1111
|
+
if (traversalState.seen.has(value)) {
|
|
1112
|
+
throw new Error('Circular reference detected during deserialization');
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
traversalState.seen.add(value);
|
|
1116
|
+
const result = {};
|
|
1117
|
+
try {
|
|
1118
|
+
Object.entries(value).forEach(([key, val]) => {
|
|
1119
|
+
Object.defineProperty(result, key, {
|
|
1120
|
+
value: this.deserializeBigIntsAndDates(val, options, {
|
|
1121
|
+
depth: traversalState.depth + 1,
|
|
1122
|
+
seen: traversalState.seen,
|
|
1123
|
+
}),
|
|
1124
|
+
enumerable: true,
|
|
1125
|
+
configurable: true,
|
|
1126
|
+
writable: true,
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
} finally {
|
|
1130
|
+
traversalState.seen.delete(value);
|
|
1131
|
+
}
|
|
1132
|
+
return result;
|
|
822
1133
|
}
|
|
823
1134
|
|
|
824
1135
|
// 4. For everything else (number, boolean, null, undefined, etc.), return as-is
|
|
@@ -826,13 +1137,13 @@ class RpcEndpoint {
|
|
|
826
1137
|
}
|
|
827
1138
|
}
|
|
828
1139
|
|
|
829
|
-
// Import the client class
|
|
830
|
-
const RpcClient = require('./clients/rpc-client');
|
|
831
|
-
|
|
832
1140
|
// Export both server and client, plus utilities
|
|
833
1141
|
module.exports = RpcEndpoint;
|
|
834
1142
|
module.exports.RpcEndpoint = RpcEndpoint;
|
|
835
1143
|
module.exports.RpcClient = RpcClient;
|
|
1144
|
+
module.exports.RpcError = RpcError;
|
|
1145
|
+
module.exports.RpcHttpError = RpcHttpError;
|
|
1146
|
+
module.exports.RpcSafeClient = RpcSafeClient;
|
|
836
1147
|
module.exports.Logger = Logger;
|
|
837
1148
|
module.exports.MiddlewareManager = MiddlewareManager;
|
|
838
1149
|
module.exports.builtInMiddlewares = builtInMiddlewares;
|
package/src/logger.js
CHANGED
|
@@ -54,9 +54,14 @@ class Logger {
|
|
|
54
54
|
*/
|
|
55
55
|
safeStringify(obj) {
|
|
56
56
|
try {
|
|
57
|
+
const seen = new WeakSet();
|
|
57
58
|
return JSON.stringify(obj, (key, value) => {
|
|
58
59
|
if (typeof value === 'bigint') return `${value.toString()}n`;
|
|
59
60
|
if (value instanceof Date) return value.toISOString();
|
|
61
|
+
if (value && typeof value === 'object') {
|
|
62
|
+
if (seen.has(value)) return '[Circular]';
|
|
63
|
+
seen.add(value);
|
|
64
|
+
}
|
|
60
65
|
return value;
|
|
61
66
|
});
|
|
62
67
|
} catch (e) {
|
|
@@ -156,7 +161,8 @@ class Logger {
|
|
|
156
161
|
duration: `${duration}ms`,
|
|
157
162
|
error: error.message,
|
|
158
163
|
code: error.code,
|
|
159
|
-
stack:
|
|
164
|
+
stack:
|
|
165
|
+
this.levels[this.level] >= this.levels.debug ? error.stack : undefined,
|
|
160
166
|
});
|
|
161
167
|
}
|
|
162
168
|
|
|
@@ -175,14 +181,19 @@ class Logger {
|
|
|
175
181
|
'secret',
|
|
176
182
|
'auth',
|
|
177
183
|
'authorization',
|
|
184
|
+
'credential',
|
|
185
|
+
'cookie',
|
|
186
|
+
'session',
|
|
178
187
|
];
|
|
179
|
-
const
|
|
188
|
+
const seen = new WeakSet();
|
|
180
189
|
|
|
181
190
|
const sanitizeObj = (input) => {
|
|
182
191
|
if (typeof input !== 'object' || input === null) return input;
|
|
192
|
+
if (seen.has(input)) return '[Circular]';
|
|
183
193
|
|
|
194
|
+
seen.add(input);
|
|
184
195
|
const output = Array.isArray(input) ? [] : {};
|
|
185
|
-
|
|
196
|
+
Object.entries(input).forEach(([key, value]) => {
|
|
186
197
|
const isSensitive = sensitiveKeys.some((sensitive) =>
|
|
187
198
|
key.toLowerCase().includes(sensitive)
|
|
188
199
|
);
|
|
@@ -193,11 +204,12 @@ class Logger {
|
|
|
193
204
|
} else {
|
|
194
205
|
output[key] = value;
|
|
195
206
|
}
|
|
196
|
-
}
|
|
207
|
+
});
|
|
208
|
+
seen.delete(input);
|
|
197
209
|
return output;
|
|
198
210
|
};
|
|
199
211
|
|
|
200
|
-
return sanitizeObj(
|
|
212
|
+
return sanitizeObj(params);
|
|
201
213
|
}
|
|
202
214
|
}
|
|
203
215
|
|
package/src/middleware.js
CHANGED
|
File without changes
|