rpc-express-toolkit 4.5.0 → 4.5.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 CHANGED
File without changes
package/README.md CHANGED
@@ -10,6 +10,9 @@
10
10
 
11
11
  JSON-RPC 2.0 toolkit for Express.js with simplified APIs, middleware, schema validation, batch support, and optional Safe Mode type preservation.
12
12
 
13
+ Version **4.5.2**, including the Safe proxy, sanitizes unexpected errors by
14
+ default and tightens invalid envelope handling; see [Changelog](CHANGELOG.md).
15
+
13
16
  ## Which Package Should I Use?
14
17
 
15
18
  - Use `rpc-express-toolkit` if you are building a JSON-RPC endpoint on Express.
@@ -34,7 +37,7 @@ Requirements:
34
37
 
35
38
  ### Compatibility
36
39
 
37
- `rpc-express-toolkit` is tested with Node.js 18, 20, 22, and 24 across Express 4 and Express 5. See [Compatibility](docs/COMPATIBILITY.md) for the current runtime matrix.
40
+ The CI matrix covers Node.js 18, 20, 22, 24, and 26 across Express 4 and Express 5. See [Compatibility](docs/COMPATIBILITY.md) for the configured runtime matrix.
38
41
 
39
42
  ### TypeScript
40
43
 
@@ -226,6 +229,10 @@ const rpc = new RpcEndpoint(app, context, {
226
229
 
227
230
  Calls to methods outside the whitelist are rejected. This is a basic method restriction mechanism, not a complete role or permission system. Project-specific authorization rules can be implemented with middleware hooks such as `beforeCall`; see `README_ADVANCED.md`.
228
231
 
232
+ Client `params` are passed as one object or array argument, never expanded into
233
+ the server-owned application or call context. Base authorization on the
234
+ server-validated context rather than identity claims in client parameters.
235
+
229
236
  ### Safe Import (opt-in)
230
237
 
231
238
  [![npm (proxy)](https://img.shields.io/npm/v/rpc-express-toolkit-safe.svg)](https://www.npmjs.com/package/rpc-express-toolkit-safe)
@@ -250,6 +257,11 @@ const client = new RpcSafeClient('http://localhost:3000/api');
250
257
 
251
258
  This keeps JSON-RPC 2.0 compliance as default for the main entrypoint, while offering a convenient safe-mode import for projects that prefer explicit type disambiguation.
252
259
 
260
+ Safe encoding applies only to `params`, `result`, and `error.data`, not to
261
+ `method`, `id`, or `error.message`. It is type preservation, not authentication
262
+ or encryption. See the [request contract](README_ADVANCED.md#request-contract)
263
+ for notification, ID, and invalid-envelope behavior.
264
+
253
265
  #### Alternative Compatibility Package
254
266
 
255
267
  `rpc-express-toolkit-safe` is a thin compatibility package that re-exports the same safe preset. Use it only when a separate package name is useful for dependency policy or migration; new projects can import `rpc-express-toolkit/safe` directly.
@@ -314,7 +326,14 @@ In `__rpc.capabilities`, `auth` is a boolean indicating whether authentication m
314
326
 
315
327
  ## Full Details
316
328
 
317
- For advanced configuration, middleware, structured logging, safe serialization, error handling, and more, see `README_ADVANCED.md`.
329
+ Unexpected exceptions return only `code: -32603` and `message: "Internal error"`
330
+ by default, for both single calls and batches. Intentional application errors
331
+ with an explicit safe-integer RPC `code` other than `-32603` retain their `message`
332
+ and optional `data`, without an automatically attached stack trace. Treat those
333
+ fields as public. Set `sanitizeErrors: false` only for local debugging; it can
334
+ expose internal messages, exception data, and stack traces to callers.
335
+
336
+ For advanced configuration, middleware, structured logging, safe serialization, error handling, and more, see [Advanced Usage](README_ADVANCED.md).
318
337
 
319
338
  ## Related Projects
320
339
 
@@ -6,14 +6,17 @@
6
6
 
7
7
  | Runtime | Status |
8
8
  | --- | --- |
9
- | Node.js 18.x | CI tested |
10
- | Node.js 20.x | CI tested |
11
- | Node.js 22.x | CI tested |
12
- | Node.js 24.x | CI tested |
13
- | Express 4.x | CI tested |
14
- | Express 5.x | CI tested |
15
-
16
- The GitHub Actions matrix runs the test suite across Node.js 18, 20, 22, and 24 with both Express 4 and Express 5.
9
+ | Node.js 18.x | Configured CI target |
10
+ | Node.js 20.x | Configured CI target |
11
+ | Node.js 22.x | Configured CI target |
12
+ | Node.js 24.x | Configured CI target |
13
+ | Node.js 26.x | Configured CI target |
14
+ | Express 4.x | Configured CI target |
15
+ | Express 5.x | Configured CI target |
16
+
17
+ The GitHub Actions matrix is configured to run the test suite across Node.js 18,
18
+ 20, 22, 24, and 26 with both Express 4 and Express 5. This is the configured
19
+ coverage, not a claim that every target has been rerun for the 4.5.2 release.
17
20
 
18
21
  ## Module And TypeScript Compatibility
19
22
 
@@ -42,6 +45,26 @@ acknowledge the compiler's deprecation notice for the deliberately explicit
42
45
 
43
46
  Package validation runs in CI and as part of `prepublishOnly`, so declaration or packaging regressions block publication.
44
47
 
48
+ Under WSL, run packaging from a native Linux filesystem: Windows-mounted source trees can
49
+ make regular files appear executable (`0755`), which the runner deliberately
50
+ rejects. A temporary source copy with regular file modes preserves this check
51
+ without changing the original checkout.
52
+
53
+ An offline run requires both registry metadata and package tarballs in npm's
54
+ selected cache. A package installed elsewhere, or a cached version listing,
55
+ does not by itself guarantee that npm can install the isolated consumer offline.
56
+ Select the cache for that process with `npm_config_cache`; use
57
+ `npm_config_offline=true` when network access must be disabled.
58
+
59
+ Local validation on 2026-10-12 passed the full packaged-consumer runner for the
60
+ 4.5.2 source: Node.js 24.14.0, npm 11.12.0, TypeScript 6.0.3, Express
61
+ 4.22.2, `@types/express` 5.0.6, and `@types/node` 26.6.2. The offline run used
62
+ `npm_config_before=2026-09-20T00:00:00Z` to select compatible versions already
63
+ cached, without changing manifests or tests. It covered packaging, installation,
64
+ ESM/CommonJS types and runtime, Publint, and ATTW for both the main and Safe proxy
65
+ packages. This does not establish a result for uncached `@types/node` 26.6.4 or
66
+ for every configured CI target.
67
+
45
68
  ## Express Dependency
46
69
 
47
70
  Express is declared as a peer dependency:
@@ -59,6 +82,8 @@ The Express compatibility tests cover:
59
82
  - standard JSON-RPC endpoint calls;
60
83
  - batch requests;
61
84
  - Safe Mode endpoints;
62
- - introspection reporting of the active Express runtime.
85
+ - introspection reporting of the active Express runtime;
86
+ - invalid request envelopes and null IDs/results;
87
+ - default error sanitization, intentional public errors, and the debug opt-out.
63
88
 
64
89
  Broader RPC behavior is covered by the main test suite and is expected to run identically on Express 4 and Express 5. Module and declaration compatibility is covered separately by the packaged consumer validation.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rpc-express-toolkit",
3
- "version": "4.5.0",
3
+ "version": "4.5.2",
4
4
  "description": "JSON-RPC 2.0 toolkit for Express.js with simplified APIs, structured logging, middleware, schema validation, batch support, and optional Safe Mode.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
package/src/batch.js CHANGED
@@ -9,14 +9,6 @@ class BatchHandler {
9
9
  this.endpoint = endpoint;
10
10
  }
11
11
 
12
- serializeErrorData(error, batchIndex) {
13
- const errorWithBatchIndex = addBatchIndex(error, batchIndex);
14
- if (!hasOwn(errorWithBatchIndex, 'data')) {
15
- return undefined;
16
- }
17
- return this.endpoint.serializeBigIntsAndDates(errorWithBatchIndex.data);
18
- }
19
-
20
12
  /**
21
13
  * Check if request is a batch request
22
14
  * @param {any} body
@@ -104,15 +96,10 @@ class BatchHandler {
104
96
  index
105
97
  );
106
98
  } catch (error) {
107
- const errorData = this.serializeErrorData(error, index);
108
99
  return {
109
100
  jsonrpc: '2.0',
110
101
  id: request && hasOwn(request, 'id') ? request.id : null,
111
- error: {
112
- code: error.code || -32603,
113
- message: error.message || 'Internal error',
114
- ...(errorData !== undefined && { data: errorData }),
115
- },
102
+ error: this.endpoint.formatError(error, index),
116
103
  };
117
104
  }
118
105
  });
@@ -314,16 +301,10 @@ class BatchHandler {
314
301
  return null;
315
302
  }
316
303
 
317
- const errorData = this.serializeErrorData(error, batchIndex);
318
-
319
304
  return {
320
305
  jsonrpc: '2.0',
321
306
  id,
322
- error: {
323
- code: error.code || -32603,
324
- message: error.message || 'Internal error',
325
- ...(errorData !== undefined && { data: errorData }),
326
- },
307
+ error: this.endpoint.formatError(error, batchIndex),
327
308
  };
328
309
  }
329
310
  }
package/src/index.d.ts CHANGED
@@ -60,6 +60,8 @@ declare class RpcEndpoint<C = any> {
60
60
  listMethods(): string[];
61
61
  getMetrics(): object;
62
62
 
63
+ formatError(error: unknown, batchIndex?: number): RpcEndpoint.JSONRPCError;
64
+
63
65
  static serveScripts(router: Router, url?: string): void;
64
66
 
65
67
  reply(
@@ -193,6 +195,8 @@ declare namespace RpcEndpoint {
193
195
  logging?: LoggingConfig;
194
196
  warnOnUnsafe?: boolean;
195
197
  strictMode?: boolean;
198
+ /** Hide unexpected error details (default true); false is for local debugging only. */
199
+ sanitizeErrors?: boolean;
196
200
  maxSerializationDepth?: number;
197
201
  maxDeserializationDepth?: number;
198
202
  autoJsonMiddleware?: boolean;
package/src/index.js CHANGED
@@ -31,7 +31,7 @@ const {
31
31
  SchemaBuilder,
32
32
  } = require('./validation');
33
33
  const BatchHandler = require('./batch');
34
- const { hasOwn, validateEnvelope } = require('./protocol');
34
+ const { addBatchIndex, hasOwn, validateEnvelope } = require('./protocol');
35
35
 
36
36
  /**
37
37
  * Default properties to include in error serialization.
@@ -165,6 +165,7 @@ class RpcEndpoint {
165
165
  this.#options.safeEnabled = this.#options.safeEnabled === true; // Default false
166
166
  this.#options.warnOnUnsafe = this.#options.warnOnUnsafe !== false; // Default true
167
167
  this.#options.strictMode = this.#options.strictMode !== false; // Default true
168
+ this.#options.sanitizeErrors = this.#options.sanitizeErrors !== false;
168
169
  this.#options.enableIntrospection =
169
170
  this.#options.enableIntrospection === true; // Default false
170
171
  this.#options.introspectionPrefix =
@@ -499,18 +500,13 @@ class RpcEndpoint {
499
500
  await this.#processSingleRequest(req, res, context, startTime);
500
501
  } catch (error) {
501
502
  this.#logger.error('Endpoint error', {
502
- error: error.message,
503
- stack: error.stack,
503
+ error: error?.message,
504
+ stack: error?.stack,
504
505
  });
505
506
 
506
- res.json({
507
- jsonrpc: '2.0',
507
+ this.reply(res, {
508
508
  id: null,
509
- error: {
510
- code: -32603,
511
- message: 'Internal error',
512
- data: serializeError(error, true),
513
- },
509
+ error: this.formatError(error),
514
510
  });
515
511
  }
516
512
  });
@@ -695,7 +691,7 @@ class RpcEndpoint {
695
691
  });
696
692
  } catch (middlewareError) {
697
693
  this.#logger.error('Error middleware failed', {
698
- error: middlewareError.message,
694
+ error: middlewareError?.message,
699
695
  });
700
696
  }
701
697
 
@@ -704,19 +700,9 @@ class RpcEndpoint {
704
700
  return null;
705
701
  }
706
702
 
707
- const errorData =
708
- hasOwn(err, 'data') && err.data !== undefined
709
- ? this.serializeBigIntsAndDates(err.data)
710
- : undefined;
711
-
712
703
  this.reply(res, {
713
704
  id,
714
- error: {
715
- code: err.code || -32603,
716
- message: err.message || `Internal error`,
717
- ...(errorData !== undefined && { data: errorData }),
718
- error: serializeError(err, true),
719
- },
705
+ error: this.formatError(err),
720
706
  });
721
707
  }
722
708
  // Ensure explicit return for consistent-return rule
@@ -881,6 +867,41 @@ class RpcEndpoint {
881
867
  };
882
868
  }
883
869
 
870
+ /**
871
+ * Build a public RPC error consistently for single and batch requests.
872
+ * Only explicit integer RPC codes identify application errors; native error
873
+ * codes such as ENOENT must not expose unexpected implementation details.
874
+ * @param {any} error
875
+ * @param {number} [batchIndex]
876
+ * @returns {{ code: number, message: string, data?: any, error?: Object }}
877
+ */
878
+ formatError(error, batchIndex) {
879
+ const code = Number.isSafeInteger(error?.code) ? error.code : -32603;
880
+ if (this.#options.sanitizeErrors && code === -32603) {
881
+ return { code: -32603, message: 'Internal error' };
882
+ }
883
+
884
+ let response = {
885
+ code,
886
+ message:
887
+ typeof error?.message === 'string' && error.message
888
+ ? error.message
889
+ : 'Internal error',
890
+ ...(error?.data !== undefined && { data: error.data }),
891
+ };
892
+ if (batchIndex !== undefined) {
893
+ response = addBatchIndex(response, batchIndex);
894
+ }
895
+ if (hasOwn(response, 'data')) {
896
+ response.data = this.serializeBigIntsAndDates(response.data);
897
+ }
898
+ if (code === -32603) {
899
+ // Debug opt-out only: never add raw exceptions to application errors.
900
+ response.error = serializeError(error, true);
901
+ }
902
+ return response;
903
+ }
904
+
884
905
  /**
885
906
  * Send a JSON-RPC 2.0 response.
886
907
  * @param {Response} res The Express response object.
package/src/logger.js CHANGED
@@ -159,10 +159,10 @@ class Logger {
159
159
  method,
160
160
  id,
161
161
  duration: `${duration}ms`,
162
- error: error.message,
163
- code: error.code,
162
+ error: error?.message,
163
+ code: error?.code,
164
164
  stack:
165
- this.levels[this.level] >= this.levels.debug ? error.stack : undefined,
165
+ this.levels[this.level] >= this.levels.debug ? error?.stack : undefined,
166
166
  });
167
167
  }
168
168
 
File without changes
package/src/middleware.js CHANGED
File without changes
package/src/protocol.js CHANGED
@@ -5,17 +5,21 @@ function isObject(value) {
5
5
  }
6
6
 
7
7
  function isValidId(id) {
8
- return id === null || typeof id === 'string' || typeof id === 'number';
8
+ return (
9
+ id === null ||
10
+ typeof id === 'string' ||
11
+ (typeof id === 'number' && Number.isFinite(id))
12
+ );
9
13
  }
10
14
 
11
15
  function isValidParams(params) {
12
16
  return Array.isArray(params) || isObject(params);
13
17
  }
14
18
 
15
- function validationError(message, id = null) {
19
+ function validationError(message) {
16
20
  return {
17
21
  valid: false,
18
- responseId: id,
22
+ responseId: null,
19
23
  error: {
20
24
  code: -32600,
21
25
  message,
@@ -32,37 +36,26 @@ function validateEnvelope(request) {
32
36
  const responseId = hasId && isValidId(request.id) ? request.id : null;
33
37
 
34
38
  if (request.jsonrpc !== '2.0') {
35
- return validationError(
36
- "Invalid Request: 'jsonrpc' must be '2.0'.",
37
- responseId
38
- );
39
+ return validationError("Invalid Request: 'jsonrpc' must be '2.0'.");
39
40
  }
40
41
 
41
42
  if (typeof request.method !== 'string') {
42
- return validationError(
43
- "Invalid Request: 'method' must be a string.",
44
- responseId
45
- );
43
+ return validationError("Invalid Request: 'method' must be a string.");
46
44
  }
47
45
 
48
46
  if (request.method.length === 0) {
49
- return validationError(
50
- "Invalid Request: 'method' must not be empty.",
51
- responseId
52
- );
47
+ return validationError("Invalid Request: 'method' must not be empty.");
53
48
  }
54
49
 
55
50
  if (hasOwn(request, 'params') && !isValidParams(request.params)) {
56
51
  return validationError(
57
- "Invalid Request: 'params' must be an object or array.",
58
- responseId
52
+ "Invalid Request: 'params' must be an object or array."
59
53
  );
60
54
  }
61
55
 
62
56
  if (hasId && !isValidId(request.id)) {
63
57
  return validationError(
64
- "Invalid Request: 'id' must be a string, number, or null.",
65
- null
58
+ "Invalid Request: 'id' must be a string, number, or null."
66
59
  );
67
60
  }
68
61
 
package/src/safe.d.ts CHANGED
File without changes
package/src/safe.js CHANGED
File without changes
package/src/validation.js CHANGED
File without changes