rpc-express-toolkit 2.0.0

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/logger.js ADDED
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Simple structured logger for JSON-RPC operations
5
+ */
6
+ class Logger {
7
+ constructor(options = {}) {
8
+ this.level = options.level || 'info';
9
+ this.enabled = options.enabled !== false;
10
+ this.prefix = options.prefix || '[JSON-RPC]';
11
+ this.customLogger = options.logger || null;
12
+
13
+ this.levels = {
14
+ error: 0,
15
+ warn: 1,
16
+ info: 2,
17
+ debug: 3,
18
+ trace: 4
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Check if the given level should be logged
24
+ * @param {string} level
25
+ * @returns {boolean}
26
+ */
27
+ shouldLog(level) {
28
+ return this.enabled && this.levels[level] <= this.levels[this.level];
29
+ }
30
+
31
+ /**
32
+ * Format log message with metadata
33
+ * @param {string} level
34
+ * @param {string} message
35
+ * @param {Object} meta
36
+ * @returns {Object}
37
+ */
38
+ formatMessage(level, message, meta = {}) {
39
+ return {
40
+ timestamp: new Date().toISOString(),
41
+ level: level.toUpperCase(),
42
+ message,
43
+ ...meta
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Generic log method
49
+ * @param {string} level
50
+ * @param {string} message
51
+ * @param {Object} meta
52
+ */
53
+ log(level, message, meta = {}) {
54
+ if (!this.shouldLog(level)) return;
55
+
56
+ const logData = this.formatMessage(level, message, meta);
57
+
58
+ if (this.customLogger) {
59
+ this.customLogger[level](logData);
60
+ } else {
61
+ const logMessage = `${this.prefix} ${logData.timestamp} [${logData.level}] ${message}`;
62
+ const metaStr = Object.keys(meta).length > 0 ? ` | ${JSON.stringify(meta)}` : '';
63
+ console[level === 'debug' || level === 'trace' ? 'log' : level](`${logMessage}${metaStr}`);
64
+ }
65
+ }
66
+
67
+ error(message, meta) { this.log('error', message, meta); }
68
+ warn(message, meta) { this.log('warn', message, meta); }
69
+ info(message, meta) { this.log('info', message, meta); }
70
+ debug(message, meta) { this.log('debug', message, meta); }
71
+ trace(message, meta) { this.log('trace', message, meta); }
72
+
73
+ /**
74
+ * Log RPC call start
75
+ * @param {string} method
76
+ * @param {any} params
77
+ * @param {string|number} id
78
+ * @param {Object} req
79
+ */
80
+ rpcCall(method, params, id, req) {
81
+ this.info('RPC call started', {
82
+ method,
83
+ id,
84
+ params: this.sanitizeParams(params),
85
+ userAgent: req.headers['user-agent'],
86
+ ip: req.ip || req.connection?.remoteAddress
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Log RPC call success
92
+ * @param {string} method
93
+ * @param {string|number} id
94
+ * @param {number} duration
95
+ * @param {any} result
96
+ */
97
+ rpcSuccess(method, id, duration, result) {
98
+ this.info('RPC call completed', {
99
+ method,
100
+ id,
101
+ duration: `${duration}ms`,
102
+ resultType: typeof result,
103
+ resultSize: JSON.stringify(result).length
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Log RPC call error
109
+ * @param {string} method
110
+ * @param {string|number} id
111
+ * @param {number} duration
112
+ * @param {Error} error
113
+ */
114
+ rpcError(method, id, duration, error) {
115
+ this.error('RPC call failed', {
116
+ method,
117
+ id,
118
+ duration: `${duration}ms`,
119
+ error: error.message,
120
+ code: error.code,
121
+ stack: error.stack
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Sanitize parameters for logging (remove sensitive data)
127
+ * @param {any} params
128
+ * @returns {any}
129
+ */
130
+ sanitizeParams(params) {
131
+ if (!params || typeof params !== 'object') return params;
132
+
133
+ const sensitiveKeys = ['password', 'token', 'key', 'secret', 'auth', 'authorization'];
134
+ const sanitized = Array.isArray(params) ? [...params] : { ...params };
135
+
136
+ const sanitizeObj = (obj) => {
137
+ if (typeof obj !== 'object' || obj === null) return obj;
138
+
139
+ for (const [key, value] of Object.entries(obj)) {
140
+ if (sensitiveKeys.some(sensitive => key.toLowerCase().includes(sensitive))) {
141
+ obj[key] = '[REDACTED]';
142
+ } else if (typeof value === 'object') {
143
+ sanitizeObj(value);
144
+ }
145
+ }
146
+ return obj;
147
+ };
148
+
149
+ return sanitizeObj(sanitized);
150
+ }
151
+ }
152
+
153
+ module.exports = Logger;
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Middleware manager for JSON-RPC operations
5
+ */
6
+ class MiddlewareManager {
7
+ constructor() {
8
+ this.middlewares = {
9
+ beforeCall: [],
10
+ afterCall: [],
11
+ onError: [],
12
+ beforeValidation: [],
13
+ afterValidation: []
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Add middleware for a specific hook
19
+ * @param {string} hook - The hook name (beforeCall, afterCall, onError, etc.)
20
+ * @param {Function} middleware - The middleware function
21
+ */
22
+ use(hook, middleware) {
23
+ if (!this.middlewares[hook]) {
24
+ throw new Error(`Unknown middleware hook: ${hook}`);
25
+ }
26
+
27
+ if (typeof middleware !== 'function') {
28
+ throw new Error('Middleware must be a function');
29
+ }
30
+
31
+ this.middlewares[hook].push(middleware);
32
+ }
33
+
34
+ /**
35
+ * Execute middlewares for a specific hook
36
+ * @param {string} hook
37
+ * @param {Object} context
38
+ * @returns {Promise<Object>}
39
+ */
40
+ async execute(hook, context) {
41
+ const middlewares = this.middlewares[hook] || [];
42
+ let result = context;
43
+
44
+ for (const middleware of middlewares) {
45
+ try {
46
+ const middlewareResult = await middleware(result);
47
+
48
+ // If middleware returns an object, merge it with context
49
+ if (middlewareResult && typeof middlewareResult === 'object') {
50
+ result = { ...result, ...middlewareResult };
51
+ }
52
+
53
+ // If middleware explicitly returns false, stop execution
54
+ if (middlewareResult === false) {
55
+ throw new Error('Middleware execution stopped');
56
+ }
57
+ } catch (error) {
58
+ // For error hooks, continue even if middleware fails
59
+ if (hook === 'onError') {
60
+ console.error('Error middleware failed:', error);
61
+ continue;
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Get all middlewares for a hook
72
+ * @param {string} hook
73
+ * @returns {Function[]}
74
+ */
75
+ getMiddlewares(hook) {
76
+ return this.middlewares[hook] || [];
77
+ }
78
+
79
+ /**
80
+ * Remove all middlewares for a hook
81
+ * @param {string} hook
82
+ */
83
+ clear(hook) {
84
+ if (hook) {
85
+ this.middlewares[hook] = [];
86
+ } else {
87
+ // Clear all hooks
88
+ Object.keys(this.middlewares).forEach(h => {
89
+ this.middlewares[h] = [];
90
+ });
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Remove a specific middleware from a hook
96
+ * @param {string} hook
97
+ * @param {Function} middleware
98
+ */
99
+ remove(hook, middleware) {
100
+ if (!this.middlewares[hook]) return;
101
+
102
+ const index = this.middlewares[hook].indexOf(middleware);
103
+ if (index > -1) {
104
+ this.middlewares[hook].splice(index, 1);
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Built-in middleware functions
111
+ */
112
+ const builtInMiddlewares = {
113
+ /**
114
+ * Rate limiting middleware
115
+ * @param {Object} options
116
+ * @returns {Function}
117
+ */
118
+ rateLimit(options = {}) {
119
+ const windowMs = options.windowMs || 60000; // 1 minute
120
+ const max = options.max || 100; // 100 requests per window
121
+ const message = options.message || 'Too many requests';
122
+
123
+ const requests = new Map();
124
+
125
+ return async (context) => {
126
+ const { req } = context;
127
+ const key = req.ip || req.connection?.remoteAddress || 'unknown';
128
+ const now = Date.now();
129
+
130
+ // Clean old entries
131
+ if (requests.has(key)) {
132
+ const userRequests = requests.get(key);
133
+ const validRequests = userRequests.filter(time => now - time < windowMs);
134
+ requests.set(key, validRequests);
135
+ }
136
+
137
+ const userRequests = requests.get(key) || [];
138
+
139
+ if (userRequests.length >= max) {
140
+ const error = new Error(message);
141
+ error.code = -32000; // Custom JSON-RPC error code
142
+ throw error;
143
+ }
144
+
145
+ userRequests.push(now);
146
+ requests.set(key, userRequests);
147
+
148
+ return context;
149
+ };
150
+ },
151
+
152
+ /**
153
+ * Authentication middleware
154
+ * @param {Function} authFunction
155
+ * @returns {Function}
156
+ */
157
+ auth(authFunction) {
158
+ return async (context) => {
159
+ const { req } = context;
160
+ const isAuthenticated = await authFunction(req);
161
+
162
+ if (!isAuthenticated) {
163
+ const error = new Error('Authentication required');
164
+ error.code = -32001;
165
+ throw error;
166
+ }
167
+
168
+ return context;
169
+ };
170
+ },
171
+
172
+ /**
173
+ * CORS middleware
174
+ * @param {Object} options
175
+ * @returns {Function}
176
+ */
177
+ cors(options = {}) {
178
+ const origin = options.origin || '*';
179
+ const methods = options.methods || ['POST'];
180
+ const headers = options.headers || ['Content-Type'];
181
+
182
+ return async (context) => {
183
+ const { res } = context;
184
+
185
+ res.header('Access-Control-Allow-Origin', origin);
186
+ res.header('Access-Control-Allow-Methods', methods.join(', '));
187
+ res.header('Access-Control-Allow-Headers', headers.join(', '));
188
+
189
+ return context;
190
+ };
191
+ },
192
+
193
+ /**
194
+ * Request timing middleware
195
+ * @returns {Function}
196
+ */
197
+ timing() {
198
+ return async (context) => {
199
+ context.startTime = Date.now();
200
+ return context;
201
+ };
202
+ },
203
+
204
+ /**
205
+ * Method whitelist middleware
206
+ * @param {string[]} allowedMethods
207
+ * @returns {Function}
208
+ */
209
+ methodWhitelist(allowedMethods) {
210
+ return async (context) => {
211
+ const { method } = context;
212
+
213
+ if (!allowedMethods.includes(method)) {
214
+ const error = new Error(`Method '${method}' is not allowed`);
215
+ error.code = -32601;
216
+ throw error;
217
+ }
218
+
219
+ return context;
220
+ };
221
+ }
222
+ };
223
+
224
+ module.exports = { MiddlewareManager, builtInMiddlewares };
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+
3
+ const Ajv = require('ajv');
4
+ const addFormats = require('ajv-formats');
5
+
6
+ /**
7
+ * Schema validator for JSON-RPC methods
8
+ */
9
+ class SchemaValidator {
10
+ constructor(options = {}) {
11
+ this.ajv = new Ajv({
12
+ allErrors: true,
13
+ removeAdditional: options.removeAdditional || false,
14
+ useDefaults: options.useDefaults !== false,
15
+ ...options.ajvOptions
16
+ });
17
+
18
+ addFormats(this.ajv);
19
+
20
+ // Add custom formats
21
+ this.addCustomFormats();
22
+ }
23
+
24
+ /**
25
+ * Add custom format validators
26
+ */
27
+ addCustomFormats() {
28
+ // BigInt format
29
+ this.ajv.addFormat('bigint', {
30
+ type: 'string',
31
+ validate: (data) => /^-?\d+n?$/.test(data)
32
+ });
33
+
34
+ // ObjectId format (MongoDB style)
35
+ this.ajv.addFormat('objectid', {
36
+ type: 'string',
37
+ validate: (data) => /^[0-9a-fA-F]{24}$/.test(data)
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Validate parameters against a schema
43
+ * @param {any} params
44
+ * @param {Object} schema
45
+ * @returns {Object} validation result
46
+ */
47
+ validate(params, schema) {
48
+ const validator = this.ajv.compile(schema);
49
+ const valid = validator(params);
50
+
51
+ return {
52
+ valid,
53
+ errors: valid ? null : validator.errors,
54
+ data: params
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Create a validation middleware
60
+ * @param {Object} schema
61
+ * @returns {Function}
62
+ */
63
+ middleware(schema) {
64
+ return async (context) => {
65
+ const { params } = context;
66
+ const result = this.validate(params, schema);
67
+
68
+ if (!result.valid) {
69
+ const error = new Error('Validation failed');
70
+ error.code = -32602; // Invalid params
71
+ error.data = {
72
+ validationErrors: result.errors.map(err => ({
73
+ field: err.instancePath || err.schemaPath,
74
+ message: err.message,
75
+ value: err.data
76
+ }))
77
+ };
78
+ throw error;
79
+ }
80
+
81
+ // Update params with validated/coerced data
82
+ context.params = result.data;
83
+ return context;
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Add a custom keyword
89
+ * @param {string} name
90
+ * @param {Object} definition
91
+ */
92
+ addKeyword(name, definition) {
93
+ this.ajv.addKeyword(name, definition);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Common schema definitions
99
+ */
100
+ const commonSchemas = {
101
+ // Pagination parameters
102
+ pagination: {
103
+ type: 'object',
104
+ properties: {
105
+ page: { type: 'integer', minimum: 1, default: 1 },
106
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
107
+ sort: { type: 'string' },
108
+ order: { type: 'string', enum: ['asc', 'desc'], default: 'asc' }
109
+ }
110
+ },
111
+
112
+ // User identification
113
+ userId: {
114
+ oneOf: [
115
+ { type: 'string', format: 'objectid' },
116
+ { type: 'string', format: 'uuid' },
117
+ { type: 'integer', minimum: 1 }
118
+ ]
119
+ },
120
+
121
+ // Email validation
122
+ email: {
123
+ type: 'string',
124
+ format: 'email',
125
+ maxLength: 255
126
+ },
127
+
128
+ // Password requirements
129
+ password: {
130
+ type: 'string',
131
+ minLength: 8,
132
+ maxLength: 128,
133
+ pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)'
134
+ },
135
+
136
+ // BigInt validation
137
+ bigintString: {
138
+ type: 'string',
139
+ format: 'bigint'
140
+ },
141
+
142
+ // Date validation
143
+ dateString: {
144
+ type: 'string',
145
+ format: 'date-time'
146
+ }
147
+ };
148
+
149
+ /**
150
+ * Schema builder helper
151
+ */
152
+ class SchemaBuilder {
153
+ constructor() {
154
+ this.schema = {
155
+ type: 'object',
156
+ properties: {},
157
+ required: []
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Add a property to the schema
163
+ * @param {string} name
164
+ * @param {Object} definition
165
+ * @param {boolean} required
166
+ * @returns {SchemaBuilder}
167
+ */
168
+ property(name, definition, required = false) {
169
+ this.schema.properties[name] = definition;
170
+ if (required) {
171
+ this.schema.required.push(name);
172
+ }
173
+ return this;
174
+ }
175
+
176
+ /**
177
+ * Add multiple properties
178
+ * @param {Object} properties
179
+ * @returns {SchemaBuilder}
180
+ */
181
+ properties(properties) {
182
+ Object.entries(properties).forEach(([name, definition]) => {
183
+ this.property(name, definition);
184
+ });
185
+ return this;
186
+ }
187
+
188
+ /**
189
+ * Set required fields
190
+ * @param {string[]} fields
191
+ * @returns {SchemaBuilder}
192
+ */
193
+ required(fields) {
194
+ this.schema.required = [...new Set([...this.schema.required, ...fields])];
195
+ return this;
196
+ }
197
+
198
+ /**
199
+ * Add additional properties setting
200
+ * @param {boolean} allowed
201
+ * @returns {SchemaBuilder}
202
+ */
203
+ additionalProperties(allowed) {
204
+ this.schema.additionalProperties = allowed;
205
+ return this;
206
+ }
207
+
208
+ /**
209
+ * Build and return the schema
210
+ * @returns {Object}
211
+ */
212
+ build() {
213
+ return this.schema;
214
+ }
215
+ }
216
+
217
+ module.exports = {
218
+ SchemaValidator,
219
+ commonSchemas,
220
+ SchemaBuilder
221
+ };