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/LICENSE +0 -0
- package/README.md +135 -9
- package/examples/example.js +0 -0
- package/examples/introspection/client.js +68 -0
- package/examples/introspection/server.js +89 -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 +124 -126
- package/src/batch.js +95 -56
- package/src/index.d.ts +33 -63
- package/src/index.js +357 -46
- package/src/logger.js +17 -5
- package/src/middleware.js +0 -0
- package/src/protocol.js +97 -0
- package/src/safe.d.ts +0 -0
- package/src/safe.js +0 -0
- package/src/validation.js +1 -0
- package/src/clients/rpc-client.js +0 -403
- package/src/clients/rpc-client.min.js +0 -1
- package/src/clients/rpc-client.min.mjs +0 -1
- package/src/clients/rpc-client.mjs +0 -393
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)
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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:
|
|
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 {
|
|
100
|
+
const { method, params, id } = request || {};
|
|
101
|
+
const envelope = validateEnvelope(request);
|
|
117
102
|
|
|
118
|
-
|
|
119
|
-
if (jsonrpc !== '2.0') {
|
|
103
|
+
if (!envelope.valid) {
|
|
120
104
|
return {
|
|
121
105
|
jsonrpc: '2.0',
|
|
122
|
-
id:
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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 =
|
|
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
|
|
274
|
+
id,
|
|
234
275
|
error: {
|
|
235
276
|
code: error.code || -32603,
|
|
236
277
|
message: error.message || 'Internal error',
|
|
237
|
-
data:
|
|
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
|
|
86
|
+
* Authentication verifier. Return a truthy value to allow the call.
|
|
77
87
|
*/
|
|
78
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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;
|