rpc-express-toolkit 4.4.2 → 4.5.1

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
@@ -201,7 +201,7 @@ Safe type disambiguation is disabled by default for maximum JSON-RPC 2.0 compati
201
201
 
202
202
  ## Authentication And Method Restrictions
203
203
 
204
- Authentication is supported through the built-in `auth` middleware. The `auth` option is a function that receives the Express request. Returning a truthy value allows the RPC call; returning a falsy value rejects it with JSON-RPC error `-32001` and message `Authentication required`. Throwing also rejects the call and returns a JSON-RPC error based on the thrown error.
204
+ Authentication is supported through the built-in `auth` middleware. The `auth` option is a function that receives the Express request. Returning a truthy value allows the RPC call; a truthy non-boolean value is available to handlers as the optional fourth argument's `authenticatedUser`. Returning a falsy value rejects the call with JSON-RPC error `-32001` and message `Authentication required`. Throwing also rejects the call and returns a JSON-RPC error based on the thrown error.
205
205
 
206
206
  ```js
207
207
  const rpc = new RpcEndpoint(app, context, {
@@ -211,7 +211,9 @@ const rpc = new RpcEndpoint(app, context, {
211
211
  },
212
212
  });
213
213
 
214
- rpc.addMethod('ping', () => 'pong');
214
+ rpc.addMethod('whoami', (_req, _context, _params, callContext) =>
215
+ callContext.authenticatedUser
216
+ );
215
217
  ```
216
218
 
217
219
  For simple method-level restriction, use `methodWhitelist`:
File without changes
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.4.2",
3
+ "version": "4.5.1",
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
@@ -52,6 +52,26 @@ class BatchHandler {
52
52
  };
53
53
  }
54
54
 
55
+ if (!this.endpoint.options.enableBatch) {
56
+ return {
57
+ valid: false,
58
+ error: {
59
+ code: -32600,
60
+ message: 'Invalid Request: Batch requests are not enabled',
61
+ },
62
+ };
63
+ }
64
+
65
+ if (batch.length > this.endpoint.options.maxBatchSize) {
66
+ return {
67
+ valid: false,
68
+ error: {
69
+ code: -32600,
70
+ message: `Invalid Request: Batch size exceeds maximum of ${this.endpoint.options.maxBatchSize}`,
71
+ },
72
+ };
73
+ }
74
+
55
75
  return { valid: true };
56
76
  }
57
77
 
@@ -76,7 +96,13 @@ class BatchHandler {
76
96
  // Process all requests in parallel
77
97
  const promises = batch.map(async (request, index) => {
78
98
  try {
79
- return await this.processSingleRequest(request, req, context, index);
99
+ return await this.processSingleRequest(
100
+ request,
101
+ req,
102
+ res,
103
+ context,
104
+ index
105
+ );
80
106
  } catch (error) {
81
107
  const errorData = this.serializeErrorData(error, index);
82
108
  return {
@@ -101,11 +127,12 @@ class BatchHandler {
101
127
  * Process a single request within a batch
102
128
  * @param {Object} request
103
129
  * @param {Object} req
130
+ * @param {Object} res
104
131
  * @param {any} context
105
132
  * @param {number} batchIndex
106
133
  * @returns {Promise<Object|null>}
107
134
  */
108
- async processSingleRequest(request, req, context, batchIndex) {
135
+ async processSingleRequest(request, req, res, context, batchIndex) {
109
136
  const { method, params, id } = request || {};
110
137
  const envelope = validateEnvelope(request);
111
138
 
@@ -183,7 +210,7 @@ class BatchHandler {
183
210
  // Initialize middleware context
184
211
  let middlewareContext = {
185
212
  req,
186
- res: null, // No response object for batch items
213
+ res,
187
214
  method,
188
215
  params: deserializedParams,
189
216
  context,
@@ -236,14 +263,23 @@ class BatchHandler {
236
263
  }
237
264
 
238
265
  // Execute the handler
239
- const result = await Promise.resolve(
240
- handler(req, context, middlewareContext.params)
266
+ let result = await Promise.resolve(
267
+ handler(
268
+ req,
269
+ middlewareContext.context,
270
+ middlewareContext.params,
271
+ middlewareContext
272
+ )
241
273
  );
242
274
 
243
275
  // Execute after middleware
244
276
  if (this.endpoint.middleware) {
245
277
  middlewareContext.result = result;
246
- await this.endpoint.middleware.execute('afterCall', middlewareContext);
278
+ middlewareContext = await this.endpoint.middleware.execute(
279
+ 'afterCall',
280
+ middlewareContext
281
+ );
282
+ result = middlewareContext.result;
247
283
  }
248
284
 
249
285
  // For notifications, return null (will be filtered out)
package/src/index.d.ts CHANGED
@@ -93,7 +93,8 @@ declare namespace RpcEndpoint {
93
93
  export type JSONRPCHandler<C> = (
94
94
  req: Request,
95
95
  context: C,
96
- params: any
96
+ params: any,
97
+ callContext?: MiddlewareContext<C>
97
98
  ) => any | Promise<any>;
98
99
 
99
100
  /** Method configuration object. */
@@ -153,10 +154,19 @@ declare namespace RpcEndpoint {
153
154
  message?: string;
154
155
  standardHeaders?: boolean;
155
156
  legacyHeaders?: boolean;
157
+ store?: RateLimitStore;
156
158
  }
157
159
 
158
- /** Authentication verifier. Return a truthy value to allow the call. */
159
- export type AuthConfig = (req: Request) => boolean | Promise<boolean>;
160
+ /** Authentication verifier. A truthy non-boolean result becomes authenticatedUser. */
161
+ export type AuthConfig = (req: Request) => unknown | Promise<unknown>;
162
+
163
+ export interface RateLimitStore {
164
+ increment(
165
+ key: string,
166
+ windowMs: number,
167
+ now: number
168
+ ): number | Promise<number>;
169
+ }
160
170
 
161
171
  /** Schema validation configuration. */
162
172
  export interface ValidationConfig {
@@ -198,6 +208,8 @@ declare namespace RpcEndpoint {
198
208
  healthCheck?: boolean;
199
209
  enableIntrospection?: boolean;
200
210
  introspectionPrefix?: string;
211
+ enableBatch?: boolean;
212
+ maxBatchSize?: number;
201
213
  }
202
214
 
203
215
  /** RPC client configuration options. */
@@ -227,6 +239,7 @@ declare namespace RpcEndpoint {
227
239
  batchIndex?: number;
228
240
  isNotification?: boolean;
229
241
  startTime?: number;
242
+ authenticatedUser?: unknown;
230
243
  [key: string]: any;
231
244
  }
232
245
 
@@ -258,6 +271,10 @@ declare namespace RpcEndpoint {
258
271
  new (...args: any[]): MiddlewareManager;
259
272
  };
260
273
 
274
+ export const MemoryRateLimitStore: {
275
+ new (): RateLimitStore;
276
+ };
277
+
261
278
  export const builtInMiddlewares: any;
262
279
 
263
280
  export interface SchemaValidationResult<T = any> {
package/src/index.js CHANGED
@@ -20,7 +20,11 @@ const DEFAULT_MAX_SERIALIZATION_DEPTH = 100;
20
20
  /** @typedef {import("express").Response} Response */
21
21
 
22
22
  const Logger = require('./logger');
23
- const { MiddlewareManager, builtInMiddlewares } = require('./middleware');
23
+ const {
24
+ MiddlewareManager,
25
+ MemoryRateLimitStore,
26
+ builtInMiddlewares,
27
+ } = require('./middleware');
24
28
  const {
25
29
  SchemaValidator,
26
30
  commonSchemas,
@@ -165,6 +169,12 @@ class RpcEndpoint {
165
169
  this.#options.enableIntrospection === true; // Default false
166
170
  this.#options.introspectionPrefix =
167
171
  this.#options.introspectionPrefix || '__rpc'; // Default __rpc
172
+ this.#options.enableBatch = this.#options.enableBatch !== false;
173
+ this.#options.maxBatchSize =
174
+ Number.isInteger(this.#options.maxBatchSize) &&
175
+ this.#options.maxBatchSize > 0
176
+ ? this.#options.maxBatchSize
177
+ : 100;
168
178
 
169
179
  this.#introspectionPrefix = this.#options.introspectionPrefix;
170
180
 
@@ -334,7 +344,7 @@ class RpcEndpoint {
334
344
  `${this.#introspectionPrefix}.capabilities`,
335
345
  async (_req, _context, _params) => ({
336
346
  safeMode: this.#options.safeEnabled,
337
- batch: true,
347
+ batch: this.#options.enableBatch,
338
348
  introspection: true,
339
349
  introspectionPrefix: this.#introspectionPrefix,
340
350
  validation: !!this.#options.validation,
@@ -431,6 +441,14 @@ class RpcEndpoint {
431
441
  });
432
442
  }
433
443
 
444
+ if (this.#options.cors) {
445
+ const corsMiddleware = builtInMiddlewares.cors(this.#options.cors);
446
+ router.options(this.#endpoint, async (req, res) => {
447
+ await corsMiddleware({ req, res });
448
+ res.status(204).end();
449
+ });
450
+ }
451
+
434
452
  // Main JSON-RPC endpoint
435
453
  router.post(this.#endpoint, async (req, res) => {
436
454
  const startTime = Date.now();
@@ -629,13 +647,22 @@ class RpcEndpoint {
629
647
  }
630
648
 
631
649
  // Invoke the handler
632
- const result = await Promise.resolve(
633
- handler(req, context, middlewareContext.params)
650
+ let result = await Promise.resolve(
651
+ handler(
652
+ req,
653
+ middlewareContext.context,
654
+ middlewareContext.params,
655
+ middlewareContext
656
+ )
634
657
  );
635
658
 
636
659
  // Execute afterCall middleware
637
660
  middlewareContext.result = result;
638
- await this.#middleware.execute('afterCall', middlewareContext);
661
+ middlewareContext = await this.#middleware.execute(
662
+ 'afterCall',
663
+ middlewareContext
664
+ );
665
+ result = middlewareContext.result;
639
666
 
640
667
  // Log success
641
668
  const duration = Date.now() - startTime;
@@ -1153,6 +1180,7 @@ module.exports.RpcHttpError = RpcHttpError;
1153
1180
  module.exports.RpcSafeClient = RpcSafeClient;
1154
1181
  module.exports.Logger = Logger;
1155
1182
  module.exports.MiddlewareManager = MiddlewareManager;
1183
+ module.exports.MemoryRateLimitStore = MemoryRateLimitStore;
1156
1184
  module.exports.builtInMiddlewares = builtInMiddlewares;
1157
1185
  module.exports.SchemaValidator = SchemaValidator;
1158
1186
  module.exports.commonSchemas = commonSchemas;
package/src/logger.js CHANGED
File without changes
@@ -0,0 +1,28 @@
1
+ /**
2
+ * In-process sliding-window store used by the rate limiter by default.
3
+ * Multi-process and multi-instance deployments should provide a shared store
4
+ * implementing the same increment() contract.
5
+ */
6
+ class MemoryRateLimitStore {
7
+ constructor() {
8
+ this.requests = new Map();
9
+ }
10
+
11
+ /**
12
+ * Record a hit and return the number of hits still inside the window.
13
+ * @param {string} key
14
+ * @param {number} windowMs
15
+ * @param {number} [now]
16
+ * @returns {Promise<number>}
17
+ */
18
+ async increment(key, windowMs, now = Date.now()) {
19
+ const validRequests = (this.requests.get(key) || []).filter(
20
+ (time) => now - time < windowMs
21
+ );
22
+ validRequests.push(now);
23
+ this.requests.set(key, validRequests);
24
+ return validRequests.length;
25
+ }
26
+ }
27
+
28
+ module.exports = MemoryRateLimitStore;
package/src/middleware.js CHANGED
@@ -3,6 +3,8 @@
3
3
  * @description Handles middleware execution for JSON-RPC requests including built-in middleware
4
4
  */
5
5
 
6
+ const MemoryRateLimitStore = require('./memory-rate-limit-store');
7
+
6
8
  /**
7
9
  * Middleware manager for handling RPC middleware
8
10
  */
@@ -138,33 +140,27 @@ const builtInMiddlewares = {
138
140
  const max = options.max || 100; // 100 requests per window
139
141
  const message = options.message || 'Too many requests';
140
142
 
141
- const requests = new Map();
143
+ const store = options.store || new MemoryRateLimitStore();
144
+
145
+ if (!store || typeof store.increment !== 'function') {
146
+ throw new TypeError(
147
+ 'Rate limit store must implement increment(key, windowMs, now)'
148
+ );
149
+ }
142
150
 
143
151
  return async (context) => {
144
152
  const { req } = context;
145
153
  const key = req.ip || req.connection?.remoteAddress || 'unknown';
146
154
  const now = Date.now();
147
155
 
148
- // Clean old entries
149
- if (requests.has(key)) {
150
- const userRequests = requests.get(key);
151
- const validRequests = userRequests.filter(
152
- (time) => now - time < windowMs
153
- );
154
- requests.set(key, validRequests);
155
- }
156
+ const requestCount = await store.increment(key, windowMs, now);
156
157
 
157
- const userRequests = requests.get(key) || [];
158
-
159
- if (userRequests.length >= max) {
158
+ if (requestCount > max) {
160
159
  const error = new Error(message);
161
160
  error.code = -32000; // Custom JSON-RPC error code
162
161
  throw error;
163
162
  }
164
163
 
165
- userRequests.push(now);
166
- requests.set(key, userRequests);
167
-
168
164
  return context;
169
165
  };
170
166
  },
@@ -177,15 +173,19 @@ const builtInMiddlewares = {
177
173
  auth(authFunction) {
178
174
  return async (context) => {
179
175
  const { req } = context;
180
- const isAuthenticated = await authFunction(req);
176
+ const authenticatedUser = await authFunction(req);
181
177
 
182
- if (!isAuthenticated) {
178
+ if (!authenticatedUser) {
183
179
  const error = new Error('Authentication required');
184
180
  error.code = -32001;
185
181
  throw error;
186
182
  }
187
183
 
188
- return context;
184
+ return {
185
+ ...context,
186
+ authenticatedUser:
187
+ authenticatedUser === true ? undefined : authenticatedUser,
188
+ };
189
189
  };
190
190
  },
191
191
 
@@ -195,16 +195,37 @@ const builtInMiddlewares = {
195
195
  * @returns {Function}
196
196
  */
197
197
  cors(options = {}) {
198
- const origin = options.origin || '*';
199
- const methods = options.methods || ['POST'];
200
- const headers = options.headers || ['Content-Type'];
198
+ const config = options === true ? {} : options;
199
+ const origin = config.origin ?? '*';
200
+ const methods = config.methods || ['POST'];
201
+ const headers = config.headers || config.allowedHeaders || ['Content-Type'];
201
202
 
202
203
  return async (context) => {
203
- const { res } = context;
204
+ const { req, res } = context;
205
+ const requestOrigin = req?.headers?.origin;
206
+ let allowedOrigin = origin;
204
207
 
205
- res.header('Access-Control-Allow-Origin', origin);
208
+ if (origin === false) {
209
+ return context;
210
+ }
211
+
212
+ if (Array.isArray(origin)) {
213
+ if (!requestOrigin || !origin.includes(requestOrigin)) {
214
+ return context;
215
+ }
216
+ allowedOrigin = requestOrigin;
217
+ res.vary('Origin');
218
+ } else if (origin === true) {
219
+ allowedOrigin = requestOrigin || '*';
220
+ if (requestOrigin) res.vary('Origin');
221
+ }
222
+
223
+ res.header('Access-Control-Allow-Origin', allowedOrigin);
206
224
  res.header('Access-Control-Allow-Methods', methods.join(', '));
207
225
  res.header('Access-Control-Allow-Headers', headers.join(', '));
226
+ if (config.credentials === true) {
227
+ res.header('Access-Control-Allow-Credentials', 'true');
228
+ }
208
229
 
209
230
  return context;
210
231
  };
@@ -241,4 +262,8 @@ const builtInMiddlewares = {
241
262
  },
242
263
  };
243
264
 
244
- module.exports = { MiddlewareManager, builtInMiddlewares };
265
+ module.exports = {
266
+ MiddlewareManager,
267
+ MemoryRateLimitStore,
268
+ builtInMiddlewares,
269
+ };
package/src/protocol.js CHANGED
File without changes
package/src/safe.d.ts CHANGED
@@ -12,6 +12,7 @@ declare namespace Safe {
12
12
  export import RpcHttpError = Main.RpcHttpError;
13
13
  export import Logger = Main.Logger;
14
14
  export import MiddlewareManager = Main.MiddlewareManager;
15
+ export import MemoryRateLimitStore = Main.MemoryRateLimitStore;
15
16
  export import builtInMiddlewares = Main.builtInMiddlewares;
16
17
  export import SchemaValidator = Main.SchemaValidator;
17
18
  export import commonSchemas = Main.commonSchemas;
@@ -27,6 +28,7 @@ declare namespace Safe {
27
28
  export import LoggingConfig = Main.LoggingConfig;
28
29
  export import CorsConfig = Main.CorsConfig;
29
30
  export import RateLimitConfig = Main.RateLimitConfig;
31
+ export import RateLimitStore = Main.RateLimitStore;
30
32
  export import AuthConfig = Main.AuthConfig;
31
33
  export import ValidationConfig = Main.ValidationConfig;
32
34
  export import RpcEndpointOptions = Main.RpcEndpointOptions;
package/src/safe.js CHANGED
@@ -119,6 +119,7 @@ module.exports.RpcError = Main.RpcError;
119
119
  module.exports.RpcHttpError = Main.RpcHttpError;
120
120
  module.exports.Logger = Main.Logger;
121
121
  module.exports.MiddlewareManager = Main.MiddlewareManager;
122
+ module.exports.MemoryRateLimitStore = Main.MemoryRateLimitStore;
122
123
  module.exports.builtInMiddlewares = Main.builtInMiddlewares;
123
124
  module.exports.SchemaValidator = Main.SchemaValidator;
124
125
  module.exports.commonSchemas = Main.commonSchemas;
package/src/validation.js CHANGED
File without changes