mastercontroller 1.2.14 → 1.3.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.
@@ -0,0 +1,332 @@
1
+ /**
2
+ * MasterTimeout - Professional timeout system for MasterController
3
+ *
4
+ * Provides per-request timeout tracking with configurable options:
5
+ * - Global timeout (all requests)
6
+ * - Route-specific timeouts
7
+ * - Controller-level timeouts
8
+ * - Graceful cleanup on timeout
9
+ * - Detailed timeout logging
10
+ *
11
+ * Inspired by Rails ActionController::Timeout and Django MIDDLEWARE_TIMEOUT
12
+ *
13
+ * @version 1.0.0
14
+ */
15
+
16
+ var master = require('./MasterControl');
17
+ const { logger } = require('./error/MasterErrorLogger');
18
+
19
+ class MasterTimeout {
20
+ constructor() {
21
+ this.globalTimeout = 120000; // 120 seconds default
22
+ this.routeTimeouts = new Map();
23
+ this.activeRequests = new Map();
24
+ this.timeoutHandlers = [];
25
+ this.enabled = true;
26
+ }
27
+
28
+ /**
29
+ * Initialize timeout system
30
+ *
31
+ * @param {Object} options - Configuration options
32
+ * @param {Number} options.globalTimeout - Default timeout in ms (default: 120000)
33
+ * @param {Boolean} options.enabled - Enable/disable timeouts (default: true)
34
+ * @param {Function} options.onTimeout - Custom timeout handler
35
+ */
36
+ init(options = {}) {
37
+ if (options.globalTimeout) {
38
+ this.globalTimeout = options.globalTimeout;
39
+ }
40
+
41
+ if (options.enabled !== undefined) {
42
+ this.enabled = options.enabled;
43
+ }
44
+
45
+ if (options.onTimeout && typeof options.onTimeout === 'function') {
46
+ this.timeoutHandlers.push(options.onTimeout);
47
+ }
48
+
49
+ logger.info({
50
+ code: 'MC_TIMEOUT_INIT',
51
+ message: 'Timeout system initialized',
52
+ globalTimeout: this.globalTimeout,
53
+ enabled: this.enabled
54
+ });
55
+
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Set timeout for specific route pattern
61
+ *
62
+ * @param {String} routePattern - Route pattern (e.g., '/api/*', '/admin/reports')
63
+ * @param {Number} timeout - Timeout in milliseconds
64
+ *
65
+ * @example
66
+ * master.timeout.setRouteTimeout('/api/*', 30000); // 30 seconds for APIs
67
+ * master.timeout.setRouteTimeout('/admin/reports', 300000); // 5 minutes for reports
68
+ */
69
+ setRouteTimeout(routePattern, timeout) {
70
+ if (typeof timeout !== 'number' || timeout <= 0) {
71
+ throw new Error('Timeout must be a positive number in milliseconds');
72
+ }
73
+
74
+ this.routeTimeouts.set(routePattern, timeout);
75
+
76
+ logger.info({
77
+ code: 'MC_TIMEOUT_ROUTE_SET',
78
+ message: 'Route timeout configured',
79
+ routePattern,
80
+ timeout
81
+ });
82
+
83
+ return this;
84
+ }
85
+
86
+ /**
87
+ * Get timeout for request based on route
88
+ * Priority: Route-specific > Global
89
+ *
90
+ * @param {String} path - Request path
91
+ * @returns {Number} - Timeout in milliseconds
92
+ */
93
+ getTimeoutForPath(path) {
94
+ // Check route-specific timeouts
95
+ for (const [pattern, timeout] of this.routeTimeouts.entries()) {
96
+ if (this._pathMatches(path, pattern)) {
97
+ return timeout;
98
+ }
99
+ }
100
+
101
+ // Return global timeout
102
+ return this.globalTimeout;
103
+ }
104
+
105
+ /**
106
+ * Start timeout tracking for request
107
+ *
108
+ * @param {Object} ctx - Request context
109
+ * @returns {String} - Request ID
110
+ */
111
+ startTracking(ctx) {
112
+ if (!this.enabled) {
113
+ return null;
114
+ }
115
+
116
+ const requestId = this._generateRequestId();
117
+ const timeout = this.getTimeoutForPath(ctx.pathName || ctx.request.url);
118
+ const startTime = Date.now();
119
+
120
+ const timer = setTimeout(() => {
121
+ this._handleTimeout(requestId, ctx, startTime);
122
+ }, timeout);
123
+
124
+ this.activeRequests.set(requestId, {
125
+ timer,
126
+ timeout,
127
+ startTime,
128
+ path: ctx.pathName || ctx.request.url,
129
+ method: ctx.type || ctx.request.method.toLowerCase()
130
+ });
131
+
132
+ // Attach cleanup to response finish
133
+ ctx.response.once('finish', () => {
134
+ this.stopTracking(requestId);
135
+ });
136
+
137
+ ctx.response.once('close', () => {
138
+ this.stopTracking(requestId);
139
+ });
140
+
141
+ return requestId;
142
+ }
143
+
144
+ /**
145
+ * Stop timeout tracking for request
146
+ *
147
+ * @param {String} requestId - Request ID
148
+ */
149
+ stopTracking(requestId) {
150
+ const tracked = this.activeRequests.get(requestId);
151
+
152
+ if (tracked) {
153
+ clearTimeout(tracked.timer);
154
+ this.activeRequests.delete(requestId);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Handle request timeout
160
+ *
161
+ * @private
162
+ */
163
+ _handleTimeout(requestId, ctx, startTime) {
164
+ const tracked = this.activeRequests.get(requestId);
165
+
166
+ if (!tracked) {
167
+ return; // Already cleaned up
168
+ }
169
+
170
+ const duration = Date.now() - startTime;
171
+
172
+ // Log timeout
173
+ logger.error({
174
+ code: 'MC_REQUEST_TIMEOUT',
175
+ message: 'Request timeout exceeded',
176
+ requestId,
177
+ path: tracked.path,
178
+ method: tracked.method,
179
+ timeout: tracked.timeout,
180
+ duration
181
+ });
182
+
183
+ // Call custom handlers
184
+ for (const handler of this.timeoutHandlers) {
185
+ try {
186
+ handler(ctx, {
187
+ requestId,
188
+ path: tracked.path,
189
+ method: tracked.method,
190
+ timeout: tracked.timeout,
191
+ duration
192
+ });
193
+ } catch (err) {
194
+ logger.error({
195
+ code: 'MC_TIMEOUT_HANDLER_ERROR',
196
+ message: 'Timeout handler threw error',
197
+ error: err.message
198
+ });
199
+ }
200
+ }
201
+
202
+ // Send timeout response if not already sent
203
+ if (!ctx.response.headersSent) {
204
+ ctx.response.statusCode = 504; // Gateway Timeout
205
+ ctx.response.setHeader('Content-Type', 'application/json');
206
+ ctx.response.end(JSON.stringify({
207
+ error: 'Request Timeout',
208
+ message: 'The server did not receive a complete request within the allowed time',
209
+ code: 'MC_REQUEST_TIMEOUT',
210
+ timeout: tracked.timeout
211
+ }));
212
+ }
213
+
214
+ // Cleanup
215
+ this.stopTracking(requestId);
216
+ }
217
+
218
+ /**
219
+ * Get middleware function for pipeline
220
+ *
221
+ * @returns {Function} - Middleware function
222
+ */
223
+ middleware() {
224
+ const $that = this;
225
+
226
+ return async (ctx, next) => {
227
+ if (!$that.enabled) {
228
+ await next();
229
+ return;
230
+ }
231
+
232
+ const requestId = $that.startTracking(ctx);
233
+ ctx.requestId = requestId;
234
+
235
+ try {
236
+ await next();
237
+ } catch (err) {
238
+ // Stop tracking on error
239
+ $that.stopTracking(requestId);
240
+ throw err;
241
+ }
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Disable timeouts (useful for debugging)
247
+ */
248
+ disable() {
249
+ this.enabled = false;
250
+ logger.info({
251
+ code: 'MC_TIMEOUT_DISABLED',
252
+ message: 'Timeout system disabled'
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Enable timeouts
258
+ */
259
+ enable() {
260
+ this.enabled = true;
261
+ logger.info({
262
+ code: 'MC_TIMEOUT_ENABLED',
263
+ message: 'Timeout system enabled'
264
+ });
265
+ }
266
+
267
+ /**
268
+ * Get current timeout statistics
269
+ *
270
+ * @returns {Object} - Timeout stats
271
+ */
272
+ getStats() {
273
+ return {
274
+ enabled: this.enabled,
275
+ globalTimeout: this.globalTimeout,
276
+ routeTimeouts: Array.from(this.routeTimeouts.entries()).map(([pattern, timeout]) => ({
277
+ pattern,
278
+ timeout
279
+ })),
280
+ activeRequests: this.activeRequests.size,
281
+ requests: Array.from(this.activeRequests.entries()).map(([id, data]) => ({
282
+ requestId: id,
283
+ path: data.path,
284
+ method: data.method,
285
+ timeout: data.timeout,
286
+ elapsed: Date.now() - data.startTime,
287
+ remaining: data.timeout - (Date.now() - data.startTime)
288
+ }))
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Check if path matches pattern
294
+ *
295
+ * @private
296
+ */
297
+ _pathMatches(path, pattern) {
298
+ if (typeof pattern === 'string') {
299
+ // Normalize paths
300
+ const normalizedPath = '/' + path.replace(/^\/|\/$/g, '');
301
+ const normalizedPattern = '/' + pattern.replace(/^\/|\/$/g, '');
302
+
303
+ // Wildcard support
304
+ if (normalizedPattern.endsWith('/*')) {
305
+ const prefix = normalizedPattern.slice(0, -2);
306
+ return normalizedPath === prefix || normalizedPath.startsWith(prefix + '/');
307
+ }
308
+
309
+ // Exact match
310
+ return normalizedPath === normalizedPattern;
311
+ }
312
+
313
+ if (pattern instanceof RegExp) {
314
+ return pattern.test(path);
315
+ }
316
+
317
+ return false;
318
+ }
319
+
320
+ /**
321
+ * Generate unique request ID
322
+ *
323
+ * @private
324
+ */
325
+ _generateRequestId() {
326
+ return `req_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
327
+ }
328
+ }
329
+
330
+ master.extend("timeout", MasterTimeout);
331
+
332
+ module.exports = MasterTimeout;
package/MasterTools.js CHANGED
@@ -51,21 +51,48 @@ class MasterTools{
51
51
  };
52
52
 
53
53
  encrypt(payload, secret){
54
- let iv = crypto.randomBytes(16).toString('hex').slice(0, 16);
55
- let key = crypto.createHash('sha256').update(String(secret)).digest('base64').substr(0, 32);
56
- crypto.createCipheriv('aes-256-cbc', key, iv);
57
- var cipher = crypto.createCipher(algorithm, key);
58
- var crypted = cipher.update(payload,'utf8','hex');
59
- crypted += cipher.final('hex');
60
- return crypted;
54
+ // Generate random IV (16 bytes for AES)
55
+ const iv = crypto.randomBytes(16);
56
+
57
+ // Create 256-bit key from secret
58
+ const key = crypto.createHash('sha256').update(String(secret)).digest();
59
+
60
+ // Create cipher with AES-256-CBC
61
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
62
+
63
+ // Encrypt payload
64
+ let encrypted = cipher.update(String(payload), 'utf8', 'hex');
65
+ encrypted += cipher.final('hex');
66
+
67
+ // Prepend IV to encrypted data (IV is not secret, needed for decryption)
68
+ return iv.toString('hex') + ':' + encrypted;
61
69
  }
62
-
70
+
63
71
  decrypt(encryption, secret){
64
- var key = crypto.createHash('sha256').update(String(secret)).digest('base64').substr(0, 32);
65
- var decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
66
- var dec = decipher.update(encryption,'hex','utf8');
67
- dec += decipher.final('utf8');
68
- return dec;
72
+ try {
73
+ // Split IV and encrypted data
74
+ const parts = encryption.split(':');
75
+ if (parts.length !== 2) {
76
+ throw new Error('Invalid encrypted data format');
77
+ }
78
+
79
+ const iv = Buffer.from(parts[0], 'hex');
80
+ const encryptedData = parts[1];
81
+
82
+ // Create 256-bit key from secret
83
+ const key = crypto.createHash('sha256').update(String(secret)).digest();
84
+
85
+ // Create decipher
86
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
87
+
88
+ // Decrypt
89
+ let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
90
+ decrypted += decipher.final('utf8');
91
+
92
+ return decrypted;
93
+ } catch (error) {
94
+ throw new Error('Decryption failed: ' + error.message);
95
+ }
69
96
  }
70
97
 
71
98
  generateRandomKey(hash){