rpc-express-toolkit 4.2.1 → 4.3.3

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/package.json CHANGED
@@ -1,126 +1,124 @@
1
- {
2
- "name": "rpc-express-toolkit",
3
- "version": "4.2.1",
4
- "description": "Enterprise-ready JSON-RPC 2.0 toolkit for Express.js with simplified APIs, structured logging, middleware system, schema validation, comprehensive BigInt/Date support. JSON middleware validation and explicit configuration.",
5
- "main": "src/index.js",
6
- "types": "src/index.d.ts",
7
- "exports": {
8
- ".": {
9
- "types": "./src/index.d.ts",
10
- "require": "./src/index.js",
11
- "import": "./src/index.js"
12
- },
13
- "./safe": {
14
- "types": "./src/safe.d.ts",
15
- "require": "./src/safe.js",
16
- "import": "./src/safe.js"
17
- }
18
- },
19
- "files": [
20
- "src/",
21
- "examples/",
22
- "README.md",
23
- "LICENSE"
24
- ],
25
- "scripts": {
26
- "test": "jest",
27
- "test:coverage": "jest --coverage",
28
- "test:watch": "jest --watch",
29
- "lint": "eslint src/ examples/",
30
- "lint:fix": "eslint src/ examples/ --fix",
31
- "clean": "node -e \"require(\u0027fs\u0027).existsSync(\u0027dist\u0027) \u0026\u0026 require(\u0027fs\u0027).rmSync(\u0027dist\u0027, {recursive: true})\"",
32
- "build": "npm run build:minify",
33
- "build:minify": "npm run build:js \u0026\u0026 npm run build:mjs",
34
- "build:js": "terser src/clients/rpc-client.js -o src/clients/rpc-client.min.js --compress --mangle --comments false --ecma 2020",
35
- "build:mjs": "terser src/clients/rpc-client.mjs -o src/clients/rpc-client.min.mjs --compress --mangle --comments false --module --ecma 2020",
36
- "prepublishOnly": "npm run build",
37
- "precommit": "npm run lint \u0026\u0026 npm test"
38
- },
39
- "repository": {
40
- "type": "git",
41
- "url": "git+https://github.com/n-car/rpc-express-toolkit.git"
42
- },
43
- "keywords": [
44
- "json-rpc",
45
- "jsonrpc",
46
- "json-rpc-2.0",
47
- "express",
48
- "toolkit",
49
- "rpc",
50
- "server",
51
- "client",
52
- "middleware",
53
- "validation",
54
- "schema",
55
- "ajv",
56
- "bigint",
57
- "date",
58
- "enterprise",
59
- "nodejs",
60
- "node",
61
- "browser",
62
- "ssl",
63
- "self-signed",
64
- "secure",
65
- "fetch",
66
- "batch",
67
- "typescript",
68
- "logging",
69
- "cors",
70
- "rate-limit",
71
- "authentication"
72
- ],
73
- "author": "Nicola Carpanese \u003cnicola.carpanese@gmail.com\u003e",
74
- "license": "MIT",
75
- "bugs": {
76
- "url": "https://github.com/n-car/rpc-express-toolkit/issues"
77
- },
78
- "homepage": "https://github.com/n-car/rpc-express-toolkit#readme",
79
- "engines": {
80
- "node": "\u003e=18.0.0"
81
- },
82
- "devDependencies": {
83
- "eslint": "^8.57.1",
84
- "eslint-config-airbnb-base": "^15.0.0",
85
- "eslint-config-prettier": "^9.1.2",
86
- "eslint-plugin-import": "^2.31.0",
87
- "eslint-plugin-prettier": "^5.5.3",
88
- "husky": "^8.0.0",
89
- "jest": "^29.7.0",
90
- "lint-staged": "^13.0.0",
91
- "prettier": "^3.6.2",
92
- "supertest": "^6.3.3",
93
- "terser": "^5.43.1"
94
- },
95
- "dependencies": {
96
- "ajv": "^8.12.0",
97
- "ajv-formats": "^2.1.1",
98
- "express": "^4.21.2",
99
- "nested-error-stacks": "^2.1.1"
100
- },
101
- "optionalDependencies": {
102
- "node-fetch": "^2.7.0"
103
- },
104
- "contributors": [
105
- {
106
- "name": "Nicola Carpanese",
107
- "email": "nicola.carpanese@gmail.com",
108
- "url": "https://github.com/n-car"
109
- }
110
- ],
111
- "husky": {
112
- "hooks": {
113
- "pre-commit": "lint-staged"
114
- }
115
- },
116
- "lint-staged": {
117
- "src/**/*.js": [
118
- "eslint --fix",
119
- "prettier --write"
120
- ],
121
- "examples/**/*.js": [
122
- "eslint --fix",
123
- "prettier --write"
124
- ]
125
- }
126
- }
1
+ {
2
+ "name": "rpc-express-toolkit",
3
+ "version": "4.3.3",
4
+ "description": "Enterprise-ready JSON-RPC 2.0 toolkit for Express.js with simplified APIs, structured logging, middleware system, schema validation, comprehensive BigInt/Date support. JSON middleware validation and explicit configuration.",
5
+ "main": "src/index.js",
6
+ "types": "src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.d.ts",
10
+ "require": "./src/index.js",
11
+ "import": "./src/index.js"
12
+ },
13
+ "./safe": {
14
+ "types": "./src/safe.d.ts",
15
+ "require": "./src/safe.js",
16
+ "import": "./src/safe.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "src/",
21
+ "examples/",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "scripts": {
26
+ "test": "jest",
27
+ "test:coverage": "jest --coverage",
28
+ "test:watch": "jest --watch",
29
+ "lint": "eslint src/ examples/",
30
+ "lint:fix": "eslint src/ examples/ --fix",
31
+ "clean": "node -e \"require(\u0027fs\u0027).existsSync(\u0027dist\u0027) \u0026\u0026 require(\u0027fs\u0027).rmSync(\u0027dist\u0027, {recursive: true})\"",
32
+ "build": "node -e \"require.resolve(\u0027rpc-toolkit-js-client\u0027)\"",
33
+ "prepublishOnly": "npm run build",
34
+ "precommit": "npm run lint \u0026\u0026 npm test"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/n-car/rpc-express-toolkit.git"
39
+ },
40
+ "keywords": [
41
+ "json-rpc",
42
+ "jsonrpc",
43
+ "json-rpc-2.0",
44
+ "express",
45
+ "toolkit",
46
+ "rpc",
47
+ "server",
48
+ "client",
49
+ "middleware",
50
+ "validation",
51
+ "schema",
52
+ "ajv",
53
+ "bigint",
54
+ "date",
55
+ "enterprise",
56
+ "nodejs",
57
+ "node",
58
+ "browser",
59
+ "ssl",
60
+ "self-signed",
61
+ "secure",
62
+ "fetch",
63
+ "batch",
64
+ "typescript",
65
+ "logging",
66
+ "cors",
67
+ "rate-limit",
68
+ "authentication"
69
+ ],
70
+ "author": "Nicola Carpanese \u003cnicola.carpanese@gmail.com\u003e",
71
+ "license": "MIT",
72
+ "bugs": {
73
+ "url": "https://github.com/n-car/rpc-express-toolkit/issues"
74
+ },
75
+ "homepage": "https://github.com/n-car/rpc-express-toolkit#readme",
76
+ "engines": {
77
+ "node": "\u003e=18.0.0"
78
+ },
79
+ "devDependencies": {
80
+ "eslint": "^8.57.1",
81
+ "eslint-config-airbnb-base": "^15.0.0",
82
+ "eslint-config-prettier": "^9.1.2",
83
+ "eslint-plugin-import": "^2.31.0",
84
+ "eslint-plugin-prettier": "^5.5.3",
85
+ "husky": "^8.0.0",
86
+ "jest": "^29.7.0",
87
+ "lint-staged": "^13.0.0",
88
+ "prettier": "^3.6.2",
89
+ "supertest": "^6.3.3",
90
+ "terser": "^5.43.1"
91
+ },
92
+ "dependencies": {
93
+ "ajv": "^8.12.0",
94
+ "ajv-formats": "^2.1.1",
95
+ "express": "^4.21.2",
96
+ "nested-error-stacks": "^2.1.1",
97
+ "rpc-toolkit-js-client": "https://github.com/n-car/rpc-toolkit-js-client/archive/refs/tags/v1.1.0.tar.gz"
98
+ },
99
+ "optionalDependencies": {
100
+ "node-fetch": "^2.7.0"
101
+ },
102
+ "contributors": [
103
+ {
104
+ "name": "Nicola Carpanese",
105
+ "email": "nicola.carpanese@gmail.com",
106
+ "url": "https://github.com/n-car"
107
+ }
108
+ ],
109
+ "husky": {
110
+ "hooks": {
111
+ "pre-commit": "lint-staged"
112
+ }
113
+ },
114
+ "lint-staged": {
115
+ "src/**/*.js": [
116
+ "eslint --fix",
117
+ "prettier --write"
118
+ ],
119
+ "examples/**/*.js": [
120
+ "eslint --fix",
121
+ "prettier --write"
122
+ ]
123
+ }
124
+ }
package/src/batch.js CHANGED
@@ -2,18 +2,28 @@
2
2
  * @file BatchHandler Class
3
3
  * @description Handles JSON-RPC 2.0 batch requests processing multiple requests in a single call
4
4
  */
5
+ const { addBatchIndex, hasOwn, validateEnvelope } = require('./protocol');
6
+
5
7
  class BatchHandler {
6
8
  constructor(endpoint) {
7
9
  this.endpoint = endpoint;
8
10
  }
9
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
+
10
20
  /**
11
21
  * Check if request is a batch request
12
22
  * @param {any} body
13
23
  * @returns {boolean}
14
24
  */
15
25
  isBatchRequest(body) {
16
- return Array.isArray(body) && body.length > 0;
26
+ return Array.isArray(body);
17
27
  }
18
28
 
19
29
  /**
@@ -42,22 +52,6 @@ class BatchHandler {
42
52
  };
43
53
  }
44
54
 
45
- // Check for duplicate IDs in the batch
46
- const ids = batch
47
- .map((req) => req.id)
48
- .filter((id) => id !== undefined && id !== null);
49
- const uniqueIds = new Set(ids);
50
-
51
- if (ids.length !== uniqueIds.size) {
52
- return {
53
- valid: false,
54
- error: {
55
- code: -32600,
56
- message: 'Invalid Request: Duplicate IDs in batch',
57
- },
58
- };
59
- }
60
-
61
55
  return { valid: true };
62
56
  }
63
57
 
@@ -72,13 +66,11 @@ class BatchHandler {
72
66
  async processBatch(batch, req, res, context) {
73
67
  const validation = this.validateBatch(batch);
74
68
  if (!validation.valid) {
75
- return [
76
- {
77
- jsonrpc: '2.0',
78
- id: null,
79
- error: validation.error,
80
- },
81
- ];
69
+ return {
70
+ jsonrpc: '2.0',
71
+ id: null,
72
+ error: validation.error,
73
+ };
82
74
  }
83
75
 
84
76
  // Process all requests in parallel
@@ -86,13 +78,14 @@ class BatchHandler {
86
78
  try {
87
79
  return await this.processSingleRequest(request, req, context, index);
88
80
  } catch (error) {
81
+ const errorData = this.serializeErrorData(error, index);
89
82
  return {
90
83
  jsonrpc: '2.0',
91
- id: request.id || null,
84
+ id: request && hasOwn(request, 'id') ? request.id : null,
92
85
  error: {
93
86
  code: error.code || -32603,
94
87
  message: error.message || 'Internal error',
95
- data: { batchIndex: index },
88
+ ...(errorData !== undefined && { data: errorData }),
96
89
  },
97
90
  };
98
91
  }
@@ -113,38 +106,26 @@ class BatchHandler {
113
106
  * @returns {Promise<Object|null>}
114
107
  */
115
108
  async processSingleRequest(request, req, context, batchIndex) {
116
- const { jsonrpc, method, params, id } = request;
109
+ const { method, params, id } = request || {};
110
+ const envelope = validateEnvelope(request);
117
111
 
118
- // Validate JSON-RPC 2.0 request structure
119
- if (jsonrpc !== '2.0') {
112
+ if (!envelope.valid) {
120
113
  return {
121
114
  jsonrpc: '2.0',
122
- id: id || null,
123
- error: {
124
- code: -32600,
125
- message: `Invalid Request: 'jsonrpc' must be '2.0'`,
126
- data: { batchIndex },
127
- },
128
- };
129
- }
130
-
131
- if (typeof method !== 'string') {
132
- return {
133
- jsonrpc: '2.0',
134
- id: id || null,
135
- error: {
136
- code: -32600,
137
- message: `Invalid Request: 'method' must be a string`,
138
- data: { batchIndex },
139
- },
115
+ id: envelope.responseId,
116
+ error: addBatchIndex(envelope.error, batchIndex),
140
117
  };
141
118
  }
142
119
 
143
120
  const methodConfig = this.endpoint.methods[method];
144
121
  if (!methodConfig) {
122
+ if (envelope.isNotification) {
123
+ return null;
124
+ }
125
+
145
126
  return {
146
127
  jsonrpc: '2.0',
147
- id: id || null,
128
+ id,
148
129
  error: {
149
130
  code: -32601,
150
131
  message: `Method "${method}" not found`,
@@ -156,14 +137,44 @@ class BatchHandler {
156
137
  // Extract handler (support both function and config object)
157
138
  const handler =
158
139
  typeof methodConfig === 'function' ? methodConfig : methodConfig.handler;
140
+ const schema =
141
+ typeof methodConfig === 'object' ? methodConfig.schema : null;
159
142
 
160
143
  // If no id is provided, this is a notification - don't return response
161
- const isNotification = id === undefined || id === null;
144
+ const isNotification = !envelope.hasId;
162
145
 
163
146
  // Determine client safe mode from headers and deserialize params accordingly
164
147
  const clientSafeHeader = req.headers['x-rpc-safe-enabled'];
148
+ if (
149
+ this.endpoint.options.strictMode &&
150
+ this.endpoint.options.safeEnabled &&
151
+ !clientSafeHeader
152
+ ) {
153
+ if (isNotification) {
154
+ return null;
155
+ }
156
+
157
+ return {
158
+ jsonrpc: '2.0',
159
+ id,
160
+ error: {
161
+ code: -32600,
162
+ message:
163
+ 'RPC Compatibility Error: Server requires safe serialization header but client did not provide it.',
164
+ data: {
165
+ serverSafeEnabled: this.endpoint.options.safeEnabled,
166
+ requiredHeader: 'X-RPC-Safe-Enabled',
167
+ strictMode: true,
168
+ solution:
169
+ 'Update client to rpc-express-toolkit v4+ or disable server strict mode',
170
+ batchIndex,
171
+ },
172
+ },
173
+ };
174
+ }
175
+
165
176
  const clientSafeEnabled = clientSafeHeader === 'true';
166
- const deserializedParams = params
177
+ const deserializedParams = hasOwn(request, 'params')
167
178
  ? this.endpoint.deserializeBigIntsAndDates(params, {
168
179
  safeEnabled: clientSafeEnabled,
169
180
  })
@@ -189,6 +200,41 @@ class BatchHandler {
189
200
  );
190
201
  }
191
202
 
203
+ if (schema) {
204
+ if (this.endpoint.middleware) {
205
+ middlewareContext = await this.endpoint.middleware.execute(
206
+ 'beforeValidation',
207
+ middlewareContext
208
+ );
209
+ }
210
+
211
+ const validation = this.endpoint.validator.validate(
212
+ middlewareContext.params,
213
+ schema
214
+ );
215
+ if (!validation.valid) {
216
+ const error = new Error('Validation failed');
217
+ error.code = -32602;
218
+ error.data = {
219
+ validationErrors: validation.errors.map((err) => ({
220
+ field: err.instancePath || err.schemaPath,
221
+ message: err.message,
222
+ value: err.data,
223
+ })),
224
+ };
225
+ throw error;
226
+ }
227
+
228
+ middlewareContext.params = validation.data;
229
+
230
+ if (this.endpoint.middleware) {
231
+ middlewareContext = await this.endpoint.middleware.execute(
232
+ 'afterValidation',
233
+ middlewareContext
234
+ );
235
+ }
236
+ }
237
+
192
238
  // Execute the handler
193
239
  const result = await Promise.resolve(
194
240
  handler(req, context, middlewareContext.params)
@@ -232,13 +278,15 @@ class BatchHandler {
232
278
  return null;
233
279
  }
234
280
 
281
+ const errorData = this.serializeErrorData(error, batchIndex);
282
+
235
283
  return {
236
284
  jsonrpc: '2.0',
237
- id: id || null,
285
+ id,
238
286
  error: {
239
287
  code: error.code || -32603,
240
288
  message: error.message || 'Internal error',
241
- data: { batchIndex },
289
+ ...(errorData !== undefined && { data: errorData }),
242
290
  },
243
291
  };
244
292
  }
@@ -250,9 +298,7 @@ class BatchHandler {
250
298
  * @returns {Object}
251
299
  */
252
300
  getBatchStats(batch) {
253
- const notifications = batch.filter(
254
- (req) => req.id === undefined || req.id === null
255
- ).length;
301
+ const notifications = batch.filter((req) => !hasOwn(req, 'id')).length;
256
302
  const requests = batch.length - notifications;
257
303
  const methods = [...new Set(batch.map((req) => req.method))];
258
304
 
package/src/index.d.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  // Type definitions for rpc-express-toolkit
2
2
 
3
3
  import type { Router, Request, Response } from 'express';
4
+ import {
5
+ RpcClient as SharedRpcClient,
6
+ RpcError as SharedRpcError,
7
+ RpcHttpError as SharedRpcHttpError,
8
+ RpcSafeClient as SharedRpcSafeClient
9
+ } from 'rpc-toolkit-js-client';
10
+ import type {
11
+ RpcBatchRequest as SharedRpcBatchRequest,
12
+ RpcClientOptions as SharedRpcClientOptions
13
+ } from 'rpc-toolkit-js-client';
4
14
 
5
15
  /**
6
16
  * JSON-RPC handler function type
@@ -73,12 +83,9 @@ interface RateLimitConfig {
73
83
  }
74
84
 
75
85
  /**
76
- * Authentication configuration
86
+ * Authentication verifier. Return a truthy value to allow the call.
77
87
  */
78
- interface AuthConfig {
79
- required?: boolean;
80
- verify?: (req: Request) => boolean | Promise<boolean>;
81
- }
88
+ type AuthConfig = (req: Request) => boolean | Promise<boolean>;
82
89
 
83
90
  /**
84
91
  * Schema validation configuration
@@ -106,6 +113,8 @@ interface RpcEndpointOptions {
106
113
  logging?: LoggingConfig;
107
114
  warnOnUnsafe?: boolean;
108
115
  strictMode?: boolean;
116
+ maxSerializationDepth?: number;
117
+ maxDeserializationDepth?: number;
109
118
  autoJsonMiddleware?: boolean;
110
119
  jsonOptions?: {
111
120
  limit?: string;
@@ -123,10 +132,17 @@ interface RpcEndpointOptions {
123
132
  /**
124
133
  * RPC client configuration options
125
134
  */
126
- interface RpcClientOptions {
127
- safeEnabled?: boolean;
128
- warnOnUnsafe?: boolean;
129
- }
135
+ type RpcClientOptions = SharedRpcClientOptions;
136
+ type RpcBatchRequest = SharedRpcBatchRequest;
137
+ type RpcClient = SharedRpcClient;
138
+ type RpcError = SharedRpcError;
139
+ type RpcHttpError = SharedRpcHttpError;
140
+ type RpcSafeClient = SharedRpcSafeClient;
141
+
142
+ declare const RpcClient: typeof SharedRpcClient;
143
+ declare const RpcError: typeof SharedRpcError;
144
+ declare const RpcHttpError: typeof SharedRpcHttpError;
145
+ declare const RpcSafeClient: typeof SharedRpcSafeClient;
130
146
 
131
147
  /**
132
148
  * Deserialization options for safe prefixes
@@ -224,59 +240,6 @@ declare class RpcEndpoint<C = any> {
224
240
  deserializeBigIntsAndDates(value: any, options?: DeserializationOptions): any;
225
241
  }
226
242
 
227
- /**
228
- * JSON-RPC 2.0 Client for Node.js environments.
229
- */
230
- declare class RpcClient {
231
- /**
232
- * Client constructor.
233
- */
234
- constructor(endpoint: string, options?: RpcClientOptions);
235
- constructor(endpoint: string, defaultHeaders?: Record<string, string>, options?: RpcClientOptions);
236
-
237
- /**
238
- * Make a JSON-RPC call to the server.
239
- */
240
- call<T = any>(
241
- method: string,
242
- params?: any,
243
- id?: string | number | null,
244
- overrideHeaders?: Record<string, string>
245
- ): Promise<T>;
246
-
247
- /**
248
- * Make a JSON-RPC notification (no response expected).
249
- */
250
- notify(
251
- method: string,
252
- params?: any,
253
- overrideHeaders?: Record<string, string>
254
- ): Promise<void>;
255
-
256
- /**
257
- * Make a batch JSON-RPC call.
258
- */
259
- batch<T = any>(
260
- requests: Array<{
261
- method: string;
262
- params?: any;
263
- id?: string | number | null;
264
- notification?: boolean;
265
- }>,
266
- overrideHeaders?: Record<string, string>
267
- ): Promise<T[]>;
268
-
269
- /**
270
- * Recursively serialize BigInt and Date to JSON-safe string formats.
271
- */
272
- serializeBigIntsAndDates(value: any): any;
273
-
274
- /**
275
- * Recursively deserialize strings representing BigInt and Date.
276
- */
277
- deserializeBigIntsAndDates(value: any, options?: DeserializationOptions): any;
278
- }
279
-
280
243
  /**
281
244
  * Export types for external use
282
245
  */
@@ -292,9 +255,13 @@ export {
292
255
  ValidationConfig,
293
256
  RpcEndpointOptions,
294
257
  RpcClientOptions,
258
+ RpcBatchRequest,
295
259
  DeserializationOptions,
296
260
  RpcEndpoint,
297
- RpcClient
261
+ RpcClient,
262
+ RpcError,
263
+ RpcHttpError,
264
+ RpcSafeClient
298
265
  };
299
266
 
300
267
  /**
@@ -308,6 +275,9 @@ declare const Main: {
308
275
  serveScripts(router: Router, url?: string): void;
309
276
  RpcEndpoint: typeof RpcEndpoint;
310
277
  RpcClient: typeof RpcClient;
278
+ RpcError: typeof RpcError;
279
+ RpcHttpError: typeof RpcHttpError;
280
+ RpcSafeClient: typeof RpcSafeClient;
311
281
  Logger: any;
312
282
  MiddlewareManager: any;
313
283
  builtInMiddlewares: any;