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/src/batch.js CHANGED
@@ -2,6 +2,8 @@
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;
@@ -13,7 +15,7 @@ class BatchHandler {
13
15
  * @returns {boolean}
14
16
  */
15
17
  isBatchRequest(body) {
16
- return Array.isArray(body) && body.length > 0;
18
+ return Array.isArray(body);
17
19
  }
18
20
 
19
21
  /**
@@ -42,22 +44,6 @@ class BatchHandler {
42
44
  };
43
45
  }
44
46
 
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
47
  return { valid: true };
62
48
  }
63
49
 
@@ -72,13 +58,11 @@ class BatchHandler {
72
58
  async processBatch(batch, req, res, context) {
73
59
  const validation = this.validateBatch(batch);
74
60
  if (!validation.valid) {
75
- return [
76
- {
77
- jsonrpc: '2.0',
78
- id: null,
79
- error: validation.error,
80
- },
81
- ];
61
+ return {
62
+ jsonrpc: '2.0',
63
+ id: null,
64
+ error: validation.error,
65
+ };
82
66
  }
83
67
 
84
68
  // Process all requests in parallel
@@ -88,11 +72,11 @@ class BatchHandler {
88
72
  } catch (error) {
89
73
  return {
90
74
  jsonrpc: '2.0',
91
- id: request.id || null,
75
+ id: request && hasOwn(request, 'id') ? request.id : null,
92
76
  error: {
93
77
  code: error.code || -32603,
94
78
  message: error.message || 'Internal error',
95
- data: { batchIndex: index },
79
+ data: addBatchIndex(error, index).data,
96
80
  },
97
81
  };
98
82
  }
@@ -113,38 +97,26 @@ class BatchHandler {
113
97
  * @returns {Promise<Object|null>}
114
98
  */
115
99
  async processSingleRequest(request, req, context, batchIndex) {
116
- const { jsonrpc, method, params, id } = request;
100
+ const { method, params, id } = request || {};
101
+ const envelope = validateEnvelope(request);
117
102
 
118
- // Validate JSON-RPC 2.0 request structure
119
- if (jsonrpc !== '2.0') {
103
+ if (!envelope.valid) {
120
104
  return {
121
105
  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
- },
106
+ id: envelope.responseId,
107
+ error: addBatchIndex(envelope.error, batchIndex),
128
108
  };
129
109
  }
130
110
 
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
- },
140
- };
141
- }
111
+ const methodConfig = this.endpoint.methods[method];
112
+ if (!methodConfig) {
113
+ if (envelope.isNotification) {
114
+ return null;
115
+ }
142
116
 
143
- const handler = this.endpoint.methods[method];
144
- if (!handler) {
145
117
  return {
146
118
  jsonrpc: '2.0',
147
- id: id || null,
119
+ id,
148
120
  error: {
149
121
  code: -32601,
150
122
  message: `Method "${method}" not found`,
@@ -153,13 +125,47 @@ class BatchHandler {
153
125
  };
154
126
  }
155
127
 
128
+ // Extract handler (support both function and config object)
129
+ const handler =
130
+ typeof methodConfig === 'function' ? methodConfig : methodConfig.handler;
131
+ const schema =
132
+ typeof methodConfig === 'object' ? methodConfig.schema : null;
133
+
156
134
  // If no id is provided, this is a notification - don't return response
157
- const isNotification = id === undefined || id === null;
135
+ const isNotification = !envelope.hasId;
158
136
 
159
137
  // Determine client safe mode from headers and deserialize params accordingly
160
138
  const clientSafeHeader = req.headers['x-rpc-safe-enabled'];
139
+ if (
140
+ this.endpoint.options.strictMode &&
141
+ this.endpoint.options.safeEnabled &&
142
+ !clientSafeHeader
143
+ ) {
144
+ if (isNotification) {
145
+ return null;
146
+ }
147
+
148
+ return {
149
+ jsonrpc: '2.0',
150
+ id,
151
+ error: {
152
+ code: -32600,
153
+ message:
154
+ 'RPC Compatibility Error: Server requires safe serialization header but client did not provide it.',
155
+ data: {
156
+ serverSafeEnabled: this.endpoint.options.safeEnabled,
157
+ requiredHeader: 'X-RPC-Safe-Enabled',
158
+ strictMode: true,
159
+ solution:
160
+ 'Update client to rpc-express-toolkit v4+ or disable server strict mode',
161
+ batchIndex,
162
+ },
163
+ },
164
+ };
165
+ }
166
+
161
167
  const clientSafeEnabled = clientSafeHeader === 'true';
162
- const deserializedParams = params
168
+ const deserializedParams = hasOwn(request, 'params')
163
169
  ? this.endpoint.deserializeBigIntsAndDates(params, {
164
170
  safeEnabled: clientSafeEnabled,
165
171
  })
@@ -185,6 +191,41 @@ class BatchHandler {
185
191
  );
186
192
  }
187
193
 
194
+ if (schema) {
195
+ if (this.endpoint.middleware) {
196
+ middlewareContext = await this.endpoint.middleware.execute(
197
+ 'beforeValidation',
198
+ middlewareContext
199
+ );
200
+ }
201
+
202
+ const validation = this.endpoint.validator.validate(
203
+ middlewareContext.params,
204
+ schema
205
+ );
206
+ if (!validation.valid) {
207
+ const error = new Error('Validation failed');
208
+ error.code = -32602;
209
+ error.data = {
210
+ validationErrors: validation.errors.map((err) => ({
211
+ field: err.instancePath || err.schemaPath,
212
+ message: err.message,
213
+ value: err.data,
214
+ })),
215
+ };
216
+ throw error;
217
+ }
218
+
219
+ middlewareContext.params = validation.data;
220
+
221
+ if (this.endpoint.middleware) {
222
+ middlewareContext = await this.endpoint.middleware.execute(
223
+ 'afterValidation',
224
+ middlewareContext
225
+ );
226
+ }
227
+ }
228
+
188
229
  // Execute the handler
189
230
  const result = await Promise.resolve(
190
231
  handler(req, context, middlewareContext.params)
@@ -230,11 +271,11 @@ class BatchHandler {
230
271
 
231
272
  return {
232
273
  jsonrpc: '2.0',
233
- id: id || null,
274
+ id,
234
275
  error: {
235
276
  code: error.code || -32603,
236
277
  message: error.message || 'Internal error',
237
- data: { batchIndex },
278
+ data: addBatchIndex(error, batchIndex).data,
238
279
  },
239
280
  };
240
281
  }
@@ -246,9 +287,7 @@ class BatchHandler {
246
287
  * @returns {Object}
247
288
  */
248
289
  getBatchStats(batch) {
249
- const notifications = batch.filter(
250
- (req) => req.id === undefined || req.id === null
251
- ).length;
290
+ const notifications = batch.filter((req) => !hasOwn(req, 'id')).length;
252
291
  const requests = batch.length - notifications;
253
292
  const methods = [...new Set(batch.map((req) => req.method))];
254
293
 
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;