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 +0 -0
- package/README.md +21 -2
- package/docs/COMPATIBILITY.md +34 -9
- package/examples/example.js +0 -0
- package/examples/introspection/client.js +0 -0
- package/examples/introspection/server.js +0 -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 +1 -1
- package/src/batch.js +2 -21
- package/src/index.d.ts +4 -0
- package/src/index.js +43 -22
- package/src/logger.js +3 -3
- package/src/memory-rate-limit-store.js +0 -0
- package/src/middleware.js +0 -0
- package/src/protocol.js +12 -19
- package/src/safe.d.ts +0 -0
- package/src/safe.js +0 -0
- package/src/validation.js +0 -0
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
|
-
|
|
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
|
[](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
|
-
|
|
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
|
|
package/docs/COMPATIBILITY.md
CHANGED
|
@@ -6,14 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
| Runtime | Status |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| Node.js 18.x | CI
|
|
10
|
-
| Node.js 20.x | CI
|
|
11
|
-
| Node.js 22.x | CI
|
|
12
|
-
| Node.js 24.x | CI
|
|
13
|
-
|
|
|
14
|
-
| Express
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
package/examples/example.js
CHANGED
|
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.
|
|
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
|
|
503
|
-
stack: error
|
|
503
|
+
error: error?.message,
|
|
504
|
+
stack: error?.stack,
|
|
504
505
|
});
|
|
505
506
|
|
|
506
|
-
|
|
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
|
|
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
|
|
163
|
-
code: error
|
|
162
|
+
error: error?.message,
|
|
163
|
+
code: error?.code,
|
|
164
164
|
stack:
|
|
165
|
-
this.levels[this.level] >= this.levels.debug ? error
|
|
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
|
|
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
|
|
19
|
+
function validationError(message) {
|
|
16
20
|
return {
|
|
17
21
|
valid: false,
|
|
18
|
-
responseId:
|
|
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
|